Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
5e15451
FEAT: Add performance profiling infrastructure and documentation
bewithgaurav Feb 4, 2026
1eb6650
DOC: Add latest status update for profiler branch
bewithgaurav Apr 8, 2026
2a87987
Add profiling infrastructure with Python + C++ instrumentation
bewithgaurav Apr 8, 2026
0a23cf9
my_bench and delete run_profiler
bewithgaurav Apr 10, 2026
11a3c44
Merge origin/main into bewithgaurav/profiling
bewithgaurav Jul 6, 2026
dcb5537
CHORE: remove profiler dev scratch docs and bench script
bewithgaurav Jul 6, 2026
321a22c
FIX: restore main's same-SQL param shortcut for single-container params
bewithgaurav Jul 6, 2026
417a8c7
Merge origin/main into bewithgaurav/profiling
bewithgaurav Jul 14, 2026
cd498fa
FIX: exclude profiler/ dev tooling from the packaged wheel
bewithgaurav Jul 14, 2026
da6bf23
CHORE: add tests for the performance profiler
bewithgaurav Jul 14, 2026
31ef66e
DOC: document the profiler counter's single-mutex design decision
bewithgaurav Jul 14, 2026
b837868
CHORE: gate profiler behind ENABLE_PROFILING build flag, off by default
bewithgaurav Jul 15, 2026
0dbba47
DOC: rewrite profiler README as minimal from-basics docs
bewithgaurav Jul 15, 2026
99e8502
Merge origin/main into bewithgaurav/profiling
bewithgaurav Sep 8, 2026
1b22b8c
FIX: make profiler counter config atomic and capture timer active-state
bewithgaurav Sep 8, 2026
c72232c
FIX: isolate profiler measurement windows so scenarios don't contamin…
bewithgaurav Sep 8, 2026
0abc283
FIX: address Copilot review — header includes, perf_stop guard, excep…
bewithgaurav Sep 8, 2026
f902803
FIX: address second Copilot review — timeline epoch, build.bat flag, …
bewithgaurav Sep 8, 2026
d3e6723
FIX: address external review — arrow scenario, scenario isolation, pe…
bewithgaurav Sep 8, 2026
bb75af0
FIX: make profiler timer destructor non-throwing and broaden CLI erro…
bewithgaurav Sep 8, 2026
c068e0e
FIX: close cursor in run_script even when the user script fails to co…
bewithgaurav Sep 8, 2026
0d33882
FIX: script wall-time accuracy, narrow arrow catch, measure executema…
bewithgaurav Sep 8, 2026
320f639
FIX: nanosecond accumulation + metadata timer scope in profiler
bewithgaurav Sep 8, 2026
243fe03
FIX: preserve original traceback in profiler error paths
bewithgaurav Sep 8, 2026
f5acae9
DOCS: link the profiler guide from the top-level README
bewithgaurav Sep 8, 2026
d639bc9
FIX: correct README overhead claim, drop unused import, sync timeline…
bewithgaurav Sep 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ omit =
main.py
setup.py
bcp_options.py
profiler/*
tests/*

[report]
Expand Down
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,17 @@ provided by the bot. You will only need to do this once across all repos using o
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.


### Performance profiling (internal)

The driver ships with an optional, compile-time-gated profiler that times both the
Python and native (C++) layers of a query. The **native (C++) instrumentation is off
by default and compiled out of released wheels**; the thin Python-layer markers
(`perf_timer.py` and the `perf_phase(...)` calls) do ship, but are a no-op unless
profiling is explicitly enabled. It is intended for contributors diagnosing where time
goes on the execute/fetch paths. See [`profiler/README.md`](profiler/README.md) for how
to build with profiling enabled and run it.

## License
The mssql-python driver for SQL Server is licensed under the MIT license, except the dynamic-link libraries (DLLs) in the [libs](https://github.com/microsoft/mssql-python/tree/main/mssql_python_odbc/libs) folder
that are licensed under MICROSOFT SOFTWARE LICENSE TERMS.
Expand Down
271 changes: 151 additions & 120 deletions mssql_python/cursor.py

Large diffs are not rendered by default.

181 changes: 181 additions & 0 deletions mssql_python/perf_timer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.
"""
Lightweight phase-level profiling for the Python layer.

Usage in cursor.py:
from mssql_python.perf_timer import perf_phase

with perf_phase("py::execute::param_type_detection"):
...

Control from profiler script:
from mssql_python.perf_timer import enable, disable, get_stats, reset

Stats dict matches the C++ profiling format so both layers can be
printed with the same reporter. Entries use a "py::" prefix to
distinguish from C++ timers.
"""

import time

_enabled = False
_stats: dict[str, dict] = {}
_timeline: list[dict] = []
_timeline_enabled = False
_epoch_ns: int = 0


def enable():
global _enabled
_enabled = True


def disable():
global _enabled
_enabled = False


def is_enabled() -> bool:
return _enabled


def reset():
_stats.clear()
_timeline.clear()


def reset_stats_only():
_stats.clear()


def enable_timeline():
global _timeline_enabled, _epoch_ns
# Clear any previously recorded events when (re)setting the epoch, so every
# event in _timeline shares the current epoch. Otherwise a second
# enable_timeline() without an intervening reset() would leave stale events
# whose offsets were computed from an older epoch, corrupting the sort.
_timeline.clear()
_epoch_ns = time.perf_counter_ns()
_timeline_enabled = True


def disable_timeline():
global _timeline_enabled
_timeline_enabled = False


def get_timeline() -> list[dict]:
return [
{
"name": ev["name"],
"start_us": ev["start_ns"] // 1000,
"duration_us": ev["duration_ns"] // 1000,
}
for ev in _timeline
]


def get_stats() -> dict:
out = {}
for name, s in _stats.items():
# Divide accumulated ns to us only here (never per-sample) and keep
# fractional us so sub-microsecond phases do not truncate to zero.
out[name] = {
"calls": s["calls"],
"total_us": s["total_ns"] / 1000.0,
"min_us": s["min_ns"] / 1000.0,
"max_us": s["max_ns"] / 1000.0,
}
return out


class _NullPhase:
"""No-op context manager returned by perf_phase when profiling is disabled.

A single shared instance is reused so the disabled path costs only a function
call plus two slot method calls, avoiding the generator + _GeneratorContextManager
allocation of an @contextmanager that every instrumented call site would
otherwise pay even when profiling is off.
"""

__slots__ = ()

def __enter__(self):
return None

def __exit__(self, *exc):
return False


class _Phase:
"""Times one phase and records it on exit (used only when enabled).

Recording happens in __exit__, which always runs even if the wrapped block
raises, so an exception can't silently drop the sample and desync the Python
call counts from the C++ ones.
"""

__slots__ = ("_name", "_t0")

def __init__(self, name: str):
self._name = name

def __enter__(self):
self._t0 = time.perf_counter_ns()
return None

def __exit__(self, *exc):
_record(self._name, time.perf_counter_ns() - self._t0, self._t0)
return False


_NULL_PHASE = _NullPhase()


def perf_phase(name: str):
if not _enabled:
return _NULL_PHASE
return _Phase(name)


def perf_start() -> int:
if not _enabled:
return 0
return time.perf_counter_ns()


def perf_stop(name: str, t0: int):
# t0 == 0 means perf_start() ran while disabled (or was never called); a
# falsy start has no valid interval, so record nothing rather than a bogus
# "now - 0" duration.
if not _enabled or not t0:
return
_record(name, time.perf_counter_ns() - t0, t0)


def _record(name: str, elapsed: int, start_ns: int = 0):
entry = _stats.get(name)
if entry is None:
_stats[name] = {
"calls": 1,
"total_ns": elapsed,
"min_ns": elapsed,
"max_ns": elapsed,
}
else:
entry["calls"] += 1
entry["total_ns"] += elapsed
if elapsed < entry["min_ns"]:
entry["min_ns"] = elapsed
if elapsed > entry["max_ns"]:
entry["max_ns"] = elapsed

if _timeline_enabled and start_ns:
_timeline.append(
{
"name": name,
"start_ns": start_ns - _epoch_ns,
"duration_ns": elapsed,
}
)
10 changes: 10 additions & 0 deletions mssql_python/pybind/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ project(ddbc_bindings)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# Performance profiling instrumentation. OFF by default so released wheels ship
# with zero profiler code. Turn on with -DENABLE_PROFILING=ON (dev/internal builds).
option(ENABLE_PROFILING "Build with performance profiling instrumentation" OFF)

# Enable verbose output to see actual compiler/linker commands
set(CMAKE_VERBOSE_MAKEFILE ON CACHE BOOL "Verbose output" FORCE)

Expand Down Expand Up @@ -354,6 +358,12 @@ target_compile_definitions(ddbc_bindings PRIVATE
NOMINMAX
)

# Emit ENABLE_PROFILING to the compiler only when the option is on.
if(ENABLE_PROFILING)
message(STATUS "Building WITH performance profiling instrumentation")
target_compile_definitions(ddbc_bindings PRIVATE ENABLE_PROFILING)
endif()

# Add warning level flags for MSVC
if(MSVC)
target_compile_options(ddbc_bindings PRIVATE /W4 /WX)
Expand Down
15 changes: 13 additions & 2 deletions mssql_python/pybind/build.bat
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,20 @@ if errorlevel 1 (
exit /b 1
)

REM Optional performance profiling instrumentation (off by default).
REM Enable with: set ENABLE_PROFILING=1 (or ON) before running build.bat.
REM Only "1"/"ON" enable it — a stray "set ENABLE_PROFILING=0" must NOT produce a
REM profiling build (matches build.sh and avoids accidentally shipping one).
set PROFILING_FLAG=
if /I "%ENABLE_PROFILING%"=="1" set "PROFILING_FLAG=-DENABLE_PROFILING=ON"
if /I "%ENABLE_PROFILING%"=="ON" set "PROFILING_FLAG=-DENABLE_PROFILING=ON"
if defined PROFILING_FLAG (
echo [MODE] Building WITH profiling instrumentation ^(ENABLE_PROFILING=%ENABLE_PROFILING%^)
)

REM Now invoke CMake with correct source path (options first, path last!)
echo [DIAGNOSTIC] Running CMake configure with: cmake -A %PLATFORM_NAME% -DARCHITECTURE=%ARCH% "%SOURCE_DIR:~0,-1%"
cmake -A %PLATFORM_NAME% -DARCHITECTURE=%ARCH% "%SOURCE_DIR:~0,-1%"
echo [DIAGNOSTIC] Running CMake configure with: cmake -A %PLATFORM_NAME% -DARCHITECTURE=%ARCH% %PROFILING_FLAG% "%SOURCE_DIR:~0,-1%"
cmake -A %PLATFORM_NAME% -DARCHITECTURE=%ARCH% %PROFILING_FLAG% "%SOURCE_DIR:~0,-1%"
echo [DIAGNOSTIC] CMake configure exit code: %errorlevel%
if errorlevel 1 (
echo [ERROR] CMake configuration failed
Expand Down
13 changes: 11 additions & 2 deletions mssql_python/pybind/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,14 @@ mkdir -p "${BUILD_DIR}"
cd "${BUILD_DIR}"
echo "[DIAGNOSTIC] Changed to build directory: ${BUILD_DIR}"

# Optional performance profiling instrumentation (off by default).
# Enable with: ENABLE_PROFILING=1 bash build.sh
PROFILING_FLAG=""
if [[ "${ENABLE_PROFILING:-}" == "1" || "${ENABLE_PROFILING:-}" == "ON" ]]; then
PROFILING_FLAG="-DENABLE_PROFILING=ON"
echo "[MODE] Building WITH profiling instrumentation (ENABLE_PROFILING=ON)"
fi

# Configure CMake (with Clang coverage instrumentation on Linux only - codecov is not supported for macOS)
echo "[DIAGNOSTIC] Running CMake configure"
if [[ "$COVERAGE_MODE" == "true" && "$OS" == "Linux" ]]; then
Expand All @@ -108,14 +116,15 @@ if [[ "$COVERAGE_MODE" == "true" && "$OS" == "Linux" ]]; then
-DCMAKE_CXX_COMPILER=clang++ \
-DCMAKE_CXX_FLAGS="-fprofile-instr-generate -fcoverage-mapping" \
-DCMAKE_C_FLAGS="-fprofile-instr-generate -fcoverage-mapping" \
$PROFILING_FLAG \
"${SOURCE_DIR}"
else
if [[ "$OS" == "macOS" ]]; then
echo "[ACTION] Configuring for macOS (default build)"
cmake -DMACOS_STRING_FIX=ON "${SOURCE_DIR}"
cmake -DMACOS_STRING_FIX=ON $PROFILING_FLAG "${SOURCE_DIR}"
else
echo "[ACTION] Configuring for Linux with architecture: $DETECTED_ARCH"
cmake -DARCHITECTURE="$DETECTED_ARCH" "${SOURCE_DIR}"
cmake -DARCHITECTURE="$DETECTED_ARCH" $PROFILING_FLAG "${SOURCE_DIR}"
fi
fi

Expand Down
Loading
Loading