Skip to content

FEAT: Profiler - #552

Merged
Gaurav Sharma (bewithgaurav) merged 39 commits into
mainfrom
bewithgaurav/profiling
Sep 10, 2026
Merged

FEAT: Profiler#552
Gaurav Sharma (bewithgaurav) merged 39 commits into
mainfrom
bewithgaurav/profiling

Conversation

@bewithgaurav

@bewithgaurav Gaurav Sharma (bewithgaurav) commented Apr 30, 2026

Copy link
Copy Markdown
Collaborator

Work Item / Issue Reference

ADO Work Item: Fixed AB#44819


Summary

Adds a performance profiler for diagnosing where time goes inside a database call, split across the two layers the driver is built from: the Python layer (cursor.py) and the native C++ layer (ddbc_bindings). A normal Python profiler sees the entire native layer as one opaque block; this instruments both layers with named timers so a slow call can be attributed to, for example, native parameter binding rather than Python.

This is an internal development tool. The C++ instrumentation is compiled out of released wheels (see below), so the native driver carries no profiler code. The Python-side phase markers are lightweight and remain in the shipped cursor.py; when profiling is disabled they are a no-op branch whose measured end-to-end cost is within run-to-run noise versus a no-profiler build.

How it is built in

  • The C++ instrumentation is gated at compile time by an ENABLE_PROFILING CMake option, off by default. Normal builds expand every PERF_TIMER to a no-op and do not register the native ddbc_bindings.profiling submodule, so there is no native profiler code, no unwind tables, and no runtime branch in the released binary. Internal/dev builds opt in with ENABLE_PROFILING=1 bash build.sh (or set ENABLE_PROFILING=1 + build.bat on Windows).
  • The Python instrumentation (perf_timer.py and the perf_phase(...) markers in cursor.py) cannot be compiled out the same way, so it ships. When profiling is disabled, perf_phase returns a shared no-op context manager (measured at ~110 ns per marker in isolation). Some markers do sit on per-row paths (e.g. three in the fetchone loop), but that per-marker cost is orders of magnitude below per-row DB latency, so measured end-to-end overhead on execute/fetch workloads stays within run-to-run noise versus a no-profiler build.

Components

  • C++ layer: performance_counter.hpp provides a PERF_TIMER("name") RAII macro and instruments ddbc_bindings.cpp and the connection sources, exposed to Python as the ddbc_bindings.profiling submodule.
  • Python layer: perf_timer.py provides perf_phase("name") context managers around the execute/fetch phases in cursor.py. The ddbc:: and py:: prefixes identify which layer a timer belongs to.
  • Runner: the profiler/ CLI (python -m profiler) with built-in scenarios plus aggregate and timeline reports, and a --script flag for profiling an arbitrary workload. profiler/ is dev-only and excluded from the shipped wheel.

Controlled use and reentrancy

Use controlled diagnostic workloads with one controller owning the process-wide measurement windows: enable, run the workload, wait for worker threads, then collect. Recording supports worker threads; independent concurrent profiling sessions and always-on production profiling are not supported.

Reports use detached snapshots. Native counters release their mutex before allocating Python report objects. Python bookkeeping uses immutable records and omits only same-thread recursively triggered samples; application cleanup, ordinary nested timers and other threads still run.

Enabled instrumentation adds bookkeeping overhead. Compare runs using the same profiling configuration; the disabled Python fast path and native compile-out remain unchanged.

Tests

Existing runtime-instrumentation tests are retained. Native cases require a profiling-enabled build; tests importing the dev-only profiler package skip when it is absent from an installed wheel. Broader profiler testing and profiling-enabled CI remain follow-up work.

Follow-up work (intentionally out of scope for this PR)

This PR ships the internal profiler tooling only. The following are deliberately deferred to follow-up PRs and are not oversights:

  • CI does not build the profiling configuration. No pipeline currently sets ENABLE_PROFILING, so the profiling build and its native-layer tests are not exercised in CI. A follow-up will add an ENABLE_PROFILING build/test leg (Linux + Windows). This is tied to the planned work to drive CI benchmarks from profiler data, where these builds and the scenario set become the fixed workloads a regression gate runs.
  • End-user profiling experience. A minimal enable/dump API and a separate profiling wheel (so a customer can install one artifact, reproduce, and send a dump) are left for a follow-up designed with the team.
  • Cross-layer timeline ordering is approximate. Python and native epochs are initialized separately, so cross-layer ordering and nesting remain approximate. This clock-origin mismatch does not affect aggregate durations. A shared origin or measured cross-layer offset remains follow-up work.

Tasks 1, 2, 3: Update profiler, add new profiling points, expand benchmarks

Phase 1: Core Infrastructure (COMPLETE)
- Add performance_counter.hpp with thread-safe RAII profiling
- Integrate profiling submodule into ddbc_bindings.cpp
- Port run_profiler.py and profiling_results.md from old branch
- Support for enable/disable/get_stats/reset via Python API

Phase 2: Documentation (COMPLETE)
- PROFILER_SUMMARY.md: Executive summary and quick reference
- PERF_TIMER_LOCATIONS.md: All 43 timer locations with code snippets
- ENHANCED_PROFILING_PLAN.md: New profiling points and benchmarks
- PROFILER_UPGRADE_STATUS.md: Status tracker and phases

Phase 3: Implementation (TODO)
- 43 PERF_TIMER calls need to be added (documented in detail)
- New profiling points for types, transactions, pool, memory
- Comprehensive benchmark suite (8 new categories)

Key Features:
- Platform detection (Windows/Linux/macOS)
- Per-function timing with min/max/avg
- Granular timers for construct_rows bottleneck
- Designed for Windows vs Linux performance analysis

Reference PR: #147 (original profiler branch)
Based on analysis showing 2.3x Linux slowdown (now 16% after optimizations)
- perf_timer.py: Python phase-level profiling (perf_phase, perf_start/perf_stop)
- performance_counter.hpp: C++ timeline recording, ddbc:: prefix via macro
- cursor.py: Phase timers on execute, fetch*, executemany
- ddbc_bindings.cpp, connection.cpp, connection_pool.cpp: PERF_TIMER calls
- profiler/: CLI package (python -m profiler) with scenarios, timeline mode,
  custom script support (--script), aggregate + waterfall reporters
- my_bench.py: Example custom profiling script
@bewithgaurav Gaurav Sharma (bewithgaurav) changed the title Bewithgaurav/profiling FEAT: Profiler May 7, 2026
Re-apply py:: and ddbc:: profiling instrumentation on top of main's u16string signature migration, GIL-release changes, and the issue #531 charCtype fetch path. No behavior change to profiling; timers preserved across the refactored execute/fetch/connect paths.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Drop the root-level planning/status notes (ENHANCED_PROFILING_PLAN, LATEST_UPDATE, PERF_TIMER_LOCATIONS, PROFILER_SUMMARY, PROFILER_UPGRADE_STATUS, profiling_results) and my_bench.py. These were working scratch from building the profiler and shouldn't ship. The profiler tooling itself (profiler/, mssql_python/perf_timer.py, performance_counter.hpp) stays.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The merge resolution accidentally scoped main's 'skip detect_and_convert_parameters when re-executing the same SQL' fast path to only the multi-arg branch. Main applies it to both the single-container (execute(sql, (a, b))) and multi-arg forms. Restore main's structure: compute actual_params in the if/else, then run the same-SQL shortcut once for both, still inside the py::execute::param_unpack timer.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread mssql_python/cursor.py Dismissed
Comment thread profiler/core.py Dismissed
@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

📊 Code Coverage Report

🔥 Diff Coverage

69%


🎯 Overall Coverage

82%


📈 Total Lines Covered: 8191 out of 9889
📁 Project: mssql-python


Diff Coverage

Diff: main...HEAD, staged and unstaged changes

  • mssql_python/cursor.py (89.2%): Missing lines 2507,2509,2526-2530,2533-2535,2539,2551,2620-2621,2894,2896
  • mssql_python/perf_timer.py (98.0%): Missing lines 240,277,279
  • mssql_python/pybind/connection/connection.cpp (73.3%): Missing lines 216,232,248,287
  • mssql_python/pybind/connection/connection_pool.cpp (100%)
  • mssql_python/pybind/ddbc_bindings.cpp (82.5%): Missing lines 863-865,871-872,1733,2807,3003,4171,4201,4207-4209,4218-4220,4245,4263-4265,4270,4304-4306
  • mssql_python/pybind/performance_counter.hpp (0.8%): Missing lines 77-80,82-94,96-109,112-130,132-159,161-168,170-174,176-191,210-215,217-233

Summary

  • Total: 586 lines
  • Missing: 177 lines
  • Coverage: 69%

mssql_python/cursor.py

Lines 2503-2513

  2503 
  2504                     # Check if this should be a DAE (data at execution) parameter based on column size
  2505                     if sample_value is not None:
  2506                         if isinstance(sample_value, str) and column_size > MAX_INLINE_CHAR:
! 2507                             is_dae = True
  2508                         elif isinstance(sample_value, (bytes, bytearray)) and column_size > 8000:
! 2509                             is_dae = True
  2510 
  2511                     # Sanitize precision/scale for numeric types
  2512                     if sql_type in (
  2513                         ddbc_sql_const.SQL_DECIMAL.value,

Lines 2522-2543

  2522                         ddbc_sql_const.SQL_VARBINARY.value,
  2523                         ddbc_sql_const.SQL_LONGVARBINARY.value,
  2524                     ):
  2525                         # Find the maximum size needed for any row's binary data
! 2526                         max_binary_size = 0
! 2527                         for row in seq_of_parameters:
! 2528                             value = row[col_index]
! 2529                             if value is not None and isinstance(value, (bytes, bytearray)):
! 2530                                 max_binary_size = max(max_binary_size, len(value))
  2531 
  2532                         # For SQL Server VARBINARY(MAX), we need to use large object binding
! 2533                         if column_size > 8000 or max_binary_size > 8000:
! 2534                             sql_type = ddbc_sql_const.SQL_LONGVARBINARY.value
! 2535                             is_dae = True
  2536 
  2537                         # Update column_size to actual maximum size if it's larger
  2538                         # Always ensure at least a minimum size of 1 for empty strings
! 2539                         column_size = max(max_binary_size, 1)
  2540 
  2541                     paraminfo = param_info()
  2542                     paraminfo.paramCType = c_type
  2543                     paraminfo.paramSQLType = sql_type

Lines 2547-2555

  2547                     paraminfo.isDAE = is_dae
  2548 
  2549                     # Ensure we never have SQL_C_DEFAULT (0) for C-type
  2550                     if paraminfo.paramCType == 0:
! 2551                         paraminfo.paramCType = ddbc_sql_const.SQL_C_DEFAULT.value
  2552 
  2553                     parameters_type.append(paraminfo)
  2554                 else:
  2555                     # Use auto-detection for columns without explicit types

Lines 2616-2625

  2616                                 max_binary_size = max(max_binary_size, len(value))
  2617 
  2618                         # For SQL Server VARBINARY(MAX), we need to use large object binding
  2619                         if max_binary_size > 8000:
! 2620                             paraminfo.paramSQLType = ddbc_sql_const.SQL_LONGVARBINARY.value
! 2621                             paraminfo.isDAE = True
  2622 
  2623                         # Update column_size to actual maximum size
  2624                         # Always ensure at least a minimum size of 1 for empty strings
  2625                         paraminfo.columnSize = max(max_binary_size, 1)

Lines 2890-2900

  2890                         column_map_lower=column_map_lower,
  2891                     )
  2892                     for row_data in rows_data
  2893                 ]
! 2894         except Exception:
  2895             # On error, don't increment rownumber - rethrow the error
! 2896             raise
  2897 
  2898     def fetchall(self) -> List[Row]:
  2899         """
  2900         Fetch all (remaining) rows of a query result.

mssql_python/perf_timer.py

Lines 236-244

  236 
  237 
  238 def _record(name: str, elapsed: int, start_ns: int = 0):
  239     if getattr(_local, "depth", 0):
! 240         return
  241     with _bookkeeping():
  242         # Prebind the no-argument release: RLock.__exit__ and method lookup can
  243         # allocate before unlocking, which is unsafe around GC finalizers.
  244         release = _lock.release

Lines 273-283

  273             _lock.acquire()
  274             try:
  275                 # Revalidate after allocations and any callbacks they triggered.
  276                 if not _enabled or window != _window_start_ns or stats is not _stats:
! 277                     return
  278                 if stats.get(name) is not entry:
! 279                     continue
  280                 stats[name] = updated
  281                 # A timeline-only restart must not discard valid aggregate samples.
  282                 if event is not None and _timeline_enabled and timeline is _timeline:
  283                     timeline.append(event)

mssql_python/pybind/connection/connection.cpp

Lines 212-220

  212     }
  213 }
  214 
  215 void Connection::commit() {
! 216     PERF_TIMER("Connection::commit");
  217     if (!_dbcHandle) {
  218         ThrowStdException("Connection handle not allocated");
  219     }
  220     updateLastUsed();

Lines 228-236

  228     checkError(ret);
  229 }
  230 
  231 void Connection::rollback() {
! 232     PERF_TIMER("Connection::rollback");
  233     if (!_dbcHandle) {
  234         ThrowStdException("Connection handle not allocated");
  235     }
  236     updateLastUsed();

Lines 244-252

  244     checkError(ret);
  245 }
  246 
  247 void Connection::setAutocommit(bool enable) {
! 248     PERF_TIMER("Connection::setAutocommit");
  249     if (!_dbcHandle) {
  250         ThrowStdException("Connection handle not allocated");
  251     }
  252     SQLINTEGER value = enable ? SQL_AUTOCOMMIT_ON : SQL_AUTOCOMMIT_OFF;

Lines 283-291

  283     return value == SQL_AUTOCOMMIT_ON;
  284 }
  285 
  286 SqlHandlePtr Connection::allocStatementHandle() {
! 287     PERF_TIMER("Connection::allocStatementHandle");
  288     if (!_dbcHandle) {
  289         ThrowStdException("Connection handle not allocated");
  290     }
  291     updateLastUsed();

mssql_python/pybind/ddbc_bindings.cpp

Lines 859-869

  859                 ThrowStdException(errorString.str());
  860             }
  861         }
  862         assert(SQLBindParameter_ptr && SQLGetStmtAttr_ptr && SQLSetDescField_ptr);
! 863         RETCODE rc;
! 864         {
! 865             PERF_TIMER("BindParameters::SQLBindParameter_call");
  866             rc = SQLBindParameter_ptr(
  867                 hStmt, static_cast<SQLUSMALLINT>(paramIndex + 1), /* 1-based indexing */
  868                 static_cast<SQLUSMALLINT>(paramInfo.inputOutputType),
  869                 static_cast<SQLSMALLINT>(paramInfo.paramCType),

Lines 867-876

  867                 hStmt, static_cast<SQLUSMALLINT>(paramIndex + 1), /* 1-based indexing */
  868                 static_cast<SQLUSMALLINT>(paramInfo.inputOutputType),
  869                 static_cast<SQLSMALLINT>(paramInfo.paramCType),
  870                 static_cast<SQLSMALLINT>(paramInfo.paramSQLType), paramInfo.columnSize,
! 871                 paramInfo.decimalDigits, dataPtr, bufferLength, strLenOrIndPtr);
! 872         }
  873         if (!SQL_SUCCEEDED(rc)) {
  874             LOG("BindParameters: SQLBindParameter failed for param[%d] - "
  875                 "SQLRETURN=%d, C_Type=%d, SQL_Type=%d",
  876                 paramIndex, rc, paramInfo.paramCType, paramInfo.paramSQLType);

Lines 1729-1737

  1729     return rc;
  1730 }
  1731 
  1732 SQLRETURN SQLGetTypeInfo_Wrapper(SqlHandlePtr StatementHandle, SQLSMALLINT DataType) {
! 1733     PERF_TIMER("SQLGetTypeInfo_Wrapper");
  1734     if (!SQLGetTypeInfo_ptr) {
  1735         ThrowStdException("SQLGetTypeInfo function not loaded");
  1736     }

Lines 2803-2811

  2803             }
  2804             LOG("BindParameterArray: Calling SQLBindParameter - "
  2805                 "param_index=%d, buffer_length=%lld",
  2806                 paramIndex, static_cast<long long>(bufferLength));
! 2807             RETCODE rc;
  2808             {
  2809                 PERF_TIMER("BindParameterArray::SQLBindParameter_call");
  2810                 rc = SQLBindParameter_ptr(hStmt, static_cast<SQLUSMALLINT>(paramIndex + 1),
  2811                                          static_cast<SQLUSMALLINT>(info.inputOutputType),

Lines 2999-3007

  2999 }
  3000 
  3001 // Wrap SQLNumResultCols
  3002 SQLSMALLINT SQLNumResultCols_wrap(SqlHandlePtr statementHandle) {
! 3003     PERF_TIMER("SQLNumResultCols_wrap");
  3004     LOG("SQLNumResultCols: Getting number of columns in result set for "
  3005         "statement_handle=%p",
  3006         (void*)statementHandle->get());
  3007     if (!SQLNumResultCols_ptr) {

Lines 4167-4175

  4167     SQLRETURN ret;
  4168     {
  4169         // Release the GIL during the blocking ODBC fetch
  4170         py::gil_scoped_release release;
! 4171         PERF_TIMER("FetchBatchData::SQLFetchScroll_call");
  4172         ret = SQLFetchScroll_ptr(hStmt, SQL_FETCH_NEXT, 0);
  4173     }
  4174     if (ret == SQL_NO_DATA) {
  4175         LOG("FetchBatchData: No data to fetch");

Lines 4197-4205

  4197     const bool useWideChar = (charCtype == SQL_C_WCHAR);
  4198     std::vector<ColumnInfo> columnInfos(numCols);
  4199     // Performance: Build function pointer dispatch table (once per batch).
  4200     // This eliminates the switch statement from the hot loop - 10,000 rows × 10
! 4201     // cols reduces from 100,000 switch evaluations to just 10 switch evaluations.
  4202     std::vector<ColumnProcessor> columnProcessors(numCols);
  4203     std::vector<ColumnInfoExt> columnInfosExt(numCols);
  4204     // Compute effective char encoding once for the batch (same for all columns)
  4205     const std::string effectiveCharEnc = GetEffectiveCharDecoding(charEncoding);

Lines 4203-4213

  4203     std::vector<ColumnInfoExt> columnInfosExt(numCols);
  4204     // Compute effective char encoding once for the batch (same for all columns)
  4205     const std::string effectiveCharEnc = GetEffectiveCharDecoding(charEncoding);
  4206 
! 4207     {
! 4208         PERF_TIMER("FetchBatchData::cache_column_metadata");
! 4209         for (SQLUSMALLINT col = 0; col < numCols; col++) {
  4210             const auto& columnMeta = columnNames[col].cast<py::dict>();
  4211             columnInfos[col].dataType = columnMeta["DataType"].cast<SQLSMALLINT>();
  4212             columnInfos[col].columnSize = columnMeta["ColumnSize"].cast<SQLULEN>();
  4213             columnInfos[col].isLob =

Lines 4214-4224

  4214                 std::find(lobColumns.begin(), lobColumns.end(), col + 1) != lobColumns.end();
  4215             columnInfos[col].processedColumnSize = columnInfos[col].columnSize;
  4216             HandleZeroColumnSizeAtFetch(columnInfos[col].processedColumnSize);
  4217 
! 4218             SQLSMALLINT dt = columnInfos[col].dataType;
! 4219             bool isCharType = (dt == SQL_CHAR || dt == SQL_VARCHAR || dt == SQL_LONGVARCHAR);
! 4220 
  4221             if (isCharType && useWideChar) {
  4222                 // When VARCHAR is bound as SQL_C_WCHAR, buffer size is in SQLWCHAR
  4223                 // units (same as NVARCHAR). +1 for null terminator.
  4224                 columnInfos[col].fetchBufferSize = columnInfos[col].processedColumnSize + 1;

Lines 4241-4249

  4241         }
  4242 
  4243         for (SQLUSMALLINT col = 0; col < numCols; col++) {
  4244             // Populate extended column info for processors that need it
! 4245             columnInfosExt[col].dataType = columnInfos[col].dataType;
  4246             columnInfosExt[col].columnSize = columnInfos[col].columnSize;
  4247             columnInfosExt[col].processedColumnSize = columnInfos[col].processedColumnSize;
  4248             columnInfosExt[col].fetchBufferSize = columnInfos[col].fetchBufferSize;
  4249             columnInfosExt[col].isLob = columnInfos[col].isLob;

Lines 4259-4274

  4259             SQLSMALLINT dataType = columnInfos[col].dataType;
  4260             switch (dataType) {
  4261                 case SQL_INTEGER:
  4262                     columnProcessors[col] = ColumnProcessors::ProcessInteger;
! 4263                     break;
! 4264                 case SQL_SMALLINT:
! 4265                     columnProcessors[col] = ColumnProcessors::ProcessSmallInt;
  4266                     break;
  4267                 case SQL_BIGINT:
  4268                     columnProcessors[col] = ColumnProcessors::ProcessBigInt;
  4269                     break;
! 4270                 case SQL_TINYINT:
  4271                     columnProcessors[col] = ColumnProcessors::ProcessTinyInt;
  4272                     break;
  4273                 case SQL_BIT:
  4274                     columnProcessors[col] = ColumnProcessors::ProcessBit;

Lines 4300-4310

  4300                     // For complex types (Decimal, DateTime, Guid, etc.), set to
  4301                     // nullptr and handle via fallback switch in the hot loop
  4302                     columnProcessors[col] = nullptr;
  4303                     break;
! 4304             }
! 4305         }
! 4306     }  // end cache_column_metadata timer scope
  4307 
  4308     // Performance: Single-phase row creation pattern
  4309     // Create each row, fill it completely, then append to results list
  4310     // This prevents data corruption (no partially-filled rows) and simplifies

mssql_python/pybind/performance_counter.hpp

Lines 73-195

   73     // keep the aggregate window and are handled separately in record().
   74     std::atomic<uint64_t> generation_{0};
   75 
   76 public:
!  77     static PerformanceCounter& instance() {
!  78         static PerformanceCounter counter;
!  79         return counter;
!  80     }
   81 
!  82     void enable() {
!  83         std::lock_guard<std::mutex> lock(mutex_);
!  84         // New window: move the generation so any timer still in flight from a
!  85         // previous window is rejected by record() instead of landing here.
!  86         generation_.fetch_add(1, std::memory_order_relaxed);
!  87         enabled_ = true;
!  88     }
!  89     void disable() {
!  90         std::lock_guard<std::mutex> lock(mutex_);
!  91         enabled_ = false;
!  92     }
!  93     bool is_enabled() const { return enabled_; }
!  94     uint64_t current_generation() const { return generation_.load(std::memory_order_relaxed); }
   95 
!  96     void enable_timeline() {
!  97         std::lock_guard<std::mutex> lock(mutex_);
!  98         // Clear stale events when (re)setting the epoch so every event in
!  99         // timeline_ shares the current epoch; a second enable_timeline() without
! 100         // an intervening reset() would otherwise mix offsets from two epochs.
! 101         timeline_.clear();
! 102         epoch_ = std::chrono::steady_clock::now();
! 103         timeline_enabled_ = true;
! 104     }
! 105     void disable_timeline() {
! 106         std::lock_guard<std::mutex> lock(mutex_);
! 107         timeline_enabled_ = false;
! 108     }
! 109     bool is_timeline_enabled() const { return timeline_enabled_; }
  110 
  111     void record(const std::string& name, int64_t duration_ns,
! 112                 std::chrono::time_point<std::chrono::steady_clock> start, uint64_t generation) {
! 113         if (!enabled_) return;
! 114 
! 115         std::lock_guard<std::mutex> lock(mutex_);
! 116         // Check under the lock so disable(), enable() and resets cannot race the write.
! 117         if (!enabled_ || generation != generation_.load(std::memory_order_relaxed))
! 118             return;
! 119         auto& stats = counters_[name];
! 120         stats.total_time_ns += duration_ns;
! 121         stats.call_count++;
! 122         stats.min_time_ns = std::min(stats.min_time_ns, duration_ns);
! 123         stats.max_time_ns = std::max(stats.max_time_ns, duration_ns);
! 124 
! 125         // Keep aggregate samples even if their timeline epoch has been replaced.
! 126         if (timeline_enabled_ && start >= epoch_) {
! 127             auto offset = std::chrono::duration_cast<std::chrono::microseconds>(start - epoch_).count();
! 128             timeline_.push_back({name, offset, duration_ns / 1000});
! 129         }
! 130     }
  131 
! 132     py::dict get_stats() {
! 133         std::unordered_map<std::string, PerfStats> snapshot;
! 134         {
! 135             std::lock_guard<std::mutex> lock(mutex_);
! 136             snapshot = counters_;
! 137         }
! 138         // Python allocation may run a finalizer that re-enters native recording.
! 139         // Only native data is copied under the mutex; build the report after unlocking.
! 140         py::dict result;
! 141 
! 142         for (const auto& [name, stats] : snapshot) {
! 143             py::dict d;
! 144             // Convert accumulated nanoseconds to microseconds only here (never
! 145             // per-sample), keeping sub-microsecond precision as fractional us so
! 146             // high-frequency timers do not truncate to zero.
! 147             d["total_us"] = stats.total_time_ns / 1000.0;
! 148             d["calls"] = stats.call_count;
! 149             d["avg_us"] = stats.call_count > 0
! 150                               ? static_cast<double>(stats.total_time_ns) / stats.call_count / 1000.0
! 151                               : 0.0;
! 152             d["min_us"] = stats.min_time_ns == INT64_MAX ? 0.0 : stats.min_time_ns / 1000.0;
! 153             d["max_us"] = stats.max_time_ns / 1000.0;
! 154             d["platform"] = PROFILING_PLATFORM;
! 155             result[py::str(name)] = d;
! 156         }
! 157 
! 158         return result;
! 159     }
  160 
! 161     void reset() {
! 162         std::lock_guard<std::mutex> lock(mutex_);
! 163         // Counters are cleared, so any timer that started before now belongs to a
! 164         // window that no longer exists; move the generation to reject it.
! 165         generation_.fetch_add(1, std::memory_order_relaxed);
! 166         counters_.clear();
! 167         timeline_.clear();
! 168     }
  169 
! 170     void reset_stats_only() {
! 171         std::lock_guard<std::mutex> lock(mutex_);
! 172         generation_.fetch_add(1, std::memory_order_relaxed);
! 173         counters_.clear();
! 174     }
  175 
! 176     py::list get_timeline() {
! 177         std::vector<TimelineEvent> snapshot;
! 178         {
! 179             std::lock_guard<std::mutex> lock(mutex_);
! 180             snapshot = timeline_;
! 181         }
! 182         py::list result;
! 183         for (const auto& ev : snapshot) {
! 184             py::dict d;
! 185             d["name"] = ev.name;
! 186             d["start_us"] = ev.start_us;
! 187             d["duration_us"] = ev.duration_us;
! 188             result.append(d);
! 189         }
! 190         return result;
! 191     }
  192 };
  193 
  194 // RAII timer - automatically records on destruction
  195 class ScopedTimer {

Lines 206-237

  206     uint64_t startGeneration_{0};
  207 
  208 public:
  209     explicit ScopedTimer(const char* name)
! 210         : name_(name), active_(PerformanceCounter::instance().is_enabled()) {
! 211         if (active_) {
! 212             startGeneration_ = PerformanceCounter::instance().current_generation();
! 213             start_ = std::chrono::steady_clock::now();
! 214         }
! 215     }
  216 
! 217     ~ScopedTimer() {
! 218         if (active_) {
! 219             // A destructor is implicitly noexcept: if record() threw (its
! 220             // unordered_map insert / vector push_back can throw bad_alloc), the
! 221             // exception would call std::terminate and crash the driver — but only
! 222             // while profiling. Swallow any failure so profiling can never take the
! 223             // process down; a dropped sample is an acceptable cost under OOM.
! 224             try {
! 225                 auto end = std::chrono::steady_clock::now();
! 226                 auto duration_ns =
! 227                     std::chrono::duration_cast<std::chrono::nanoseconds>(end - start_).count();
! 228                 PerformanceCounter::instance().record(name_, duration_ns, start_, startGeneration_);
! 229             } catch (...) {
! 230                 // ignore: never let a profiling timer abort the process
! 231             }
! 232         }
! 233     }
  234 };
  235 
  236 } // namespace mssql_profiling


📋 Files Needing Attention

📉 Files with overall lowest coverage (click to expand)
mssql_python.pybind.performance_counter.hpp: 0.7%
mssql_python.pybind.logger_bridge.cpp: 57.9%
mssql_python.pybind.ddbc_bindings.h: 61.5%
mssql_python.pybind.logger_bridge.hpp: 70.8%
mssql_python.row.py: 77.6%
mssql_python.pybind.ddbc_bindings.cpp: 77.7%
mssql_python.pybind.connection.connection_pool.cpp: 81.8%
mssql_python.logging.py: 85.5%
mssql_python.helpers.py: 89.3%
mssql_python.pooling.py: 90.1%

🔗 Quick Links

⚙️ Build Summary 📋 Coverage Details

View Azure DevOps Build

Browse Full Coverage Report

Pick up 1.11.0 release, context-manager transaction (#639), bulkcopy timeout (#650), py-core 0.1.6, and macOS dylib config (#661). No profiler conflicts; auto-merge clean.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions github-actions Bot added the pr-size: large Substantial code update label Jul 14, 2026
find_packages() was picking up the top-level profiler/ package, so the internal benchmark CLI (and a generic 'profiler' top-level name) would ship to PyPI. Exclude it. The runtime instrumentation it drives (perf_timer.py, the ddbc_bindings profiling submodule) lives inside mssql_python and still ships.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Cover both layers: the python perf_timer (perf_phase/perf_start/perf_stop, enable/disable, stats, timeline, reset vs reset_stats_only) and the C++ ddbc_bindings.profiling submodule (toggle, live query capture, timeline, reset). Autouse fixture resets and disables both layers around every test so profiling state never leaks into the rest of the suite.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Note in performance_counter.hpp that the global mutex is taken only when profiling is enabled, targets single-threaded diagnostics where it is uncontended, and is a deliberate simplification (thread_local is the upgrade path if multithreaded profiling ever matters).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Profiling is now compiled out of normal builds. PERF_TIMER expands to a no-op and the ddbc_bindings.profiling submodule is not registered unless the C++ extension is built with -DENABLE_PROFILING (set ENABLE_PROFILING=1 for build.sh/build.bat). Released wheels therefore ship with zero profiler code. This replaces the old manual comment-toggle in performance_counter.hpp with a real CMake option. Internal/dev builds opt in to get the instrumentation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Explains what the two-layer profiler is, the py:: / ddbc:: prefixes, how to make a profiling build (ENABLE_PROFILING), how to run it (CLI --script or the runtime API), how to read the output, and how to add a timer. Drops the stale scenario-specific and internal references.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Re-apply py:: and ddbc:: profiling instrumentation on top of main's C++ execute-pipeline rewrite (SQLExecute_wrap single-pipeline, no ParamInfo across the pybind boundary; param detection moved to C++), the #671 GIL/env-handle and disconnect changes, identity-aware pool keying (pool_key/token_factory), the musl call_once safety in loadDriver, and the new mssql_python_odbc packaging split (combined the setup.py find_packages excludes).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI lite review requested due to automatic review settings September 8, 2026 04:54

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The new C++ profiling counter has confirmed undefined-behavior hazards (data races and an uninitialized timer start) that should be fixed before merging.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR introduces an internal, opt-in two-layer performance profiler for diagnosing latency across the Python DB-API layer and the native ddbc_bindings C++ layer, with a dev-only profiler/ runner and a new test suite covering both layers.

Changes:

  • Adds a Python profiling module (mssql_python/perf_timer.py) and instruments key phases in Cursor.execute()/executemany()/fetch*().
  • Adds a C++ RAII timing macro (PERF_TIMER) + global counter/timeline, and exposes it as mssql_python.ddbc_bindings.profiling in profiling-enabled builds.
  • Adds a dev-only profiler/ CLI + scenarios/reporting, and excludes it from the shipped wheel via setup.py.
File summaries
File Description
tests/test_025_profiler.py Adds tests for Python and (optional) C++ profiling enable/disable, stats, and timeline.
setup.py Excludes profiler/ from distribution packages while keeping runtime instrumentation shipped.
profiler/init.py Exposes Profiler API for the internal runner package.
profiler/main.py Implements python -m profiler CLI entrypoint and argument parsing.
profiler/core.py Orchestrates scenarios and collects/prints combined C++ + Python profiling output.
profiler/reporter.py Merges and formats stats/timeline from both layers for reporting.
profiler/scenarios.py Defines benchmark scenarios and test-data setup helpers.
profiler/README.md Documents how to build/run the profiler and interpret its output.
mssql_python/pybind/performance_counter.hpp Adds C++ profiling counter/timeline and PERF_TIMER macro implementation.
mssql_python/pybind/ddbc_bindings.cpp Adds C++ timers in key ODBC paths and exposes ddbc_bindings.profiling submodule in profiling builds.
mssql_python/pybind/connection/connection.cpp Adds C++ timers around connection lifecycle and transactional operations.
mssql_python/pybind/connection/connection_pool.cpp Adds C++ timers around pool acquire/release/close and pool-manager acquire.
mssql_python/pybind/CMakeLists.txt Adds ENABLE_PROFILING CMake option and defines ENABLE_PROFILING compile definition when enabled.
mssql_python/pybind/build.sh Passes -DENABLE_PROFILING=ON to CMake when ENABLE_PROFILING env var is set.
mssql_python/pybind/build.bat Passes -DENABLE_PROFILING=ON to CMake when ENABLE_PROFILING env var is set.
mssql_python/perf_timer.py Introduces Python-layer timing (stats + optional timeline) aligned with C++ schema.
mssql_python/cursor.py Instruments execute/fetch paths with Python timers and adds a measured param-type-detection section in executemany.
Review details

Suppressed comments (3)

mssql_python/pybind/performance_counter.hpp:59

  • enabled_ and timeline_enabled_ are read/written from multiple threads (timer scopes can run concurrently with enable/disable from Python) but are plain bools accessed without synchronization; this is a C++ data race (undefined behavior). Use std::atomic (or guard all reads/writes with the mutex) to make enable checks thread-safe.
    std::mutex mutex_;
    bool enabled_ = false;
    bool timeline_enabled_ = false;
    std::chrono::time_point<std::chrono::high_resolution_clock> epoch_;

mssql_python/pybind/performance_counter.hpp:75

  • enable_timeline() writes epoch_ without holding mutex_, but record() reads epoch_ while holding mutex_. Because the write is not synchronized with the read, this is also a data race; protect epoch_ updates with the same mutex (and ideally set epoch_ before flipping timeline_enabled_ on).
    void enable_timeline() {
        timeline_enabled_ = true;
        epoch_ = std::chrono::high_resolution_clock::now();
    }
    void disable_timeline() { timeline_enabled_ = false; }

mssql_python/pybind/performance_counter.hpp:149

  • ScopedTimer only initializes start_ when profiling is enabled at construction time, but the destructor checks is_enabled() again. If profiling is toggled on between construction and destruction, start_ is uninitialized and the duration calculation becomes undefined behavior. Capture an active_ flag at construction and use it in the destructor.
    explicit ScopedTimer(const char* name) : name_(name) {
        if (PerformanceCounter::instance().is_enabled()) {
            start_ = std::chrono::high_resolution_clock::now();
        }
    }
  • Files reviewed: 17/17 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread mssql_python/perf_timer.py
Comment thread mssql_python/pybind/performance_counter.hpp
Two thread-safety fixes in performance_counter.hpp, both only affecting profiling builds. (1) enabled_/timeline_enabled_ are now std::atomic and enable_timeline() writes epoch_ under the mutex, so enable/disable/enable_timeline are safe to call from a thread other than the one running timers (timers run with the GIL released). (2) ScopedTimer captures the enabled state once at construction into active_ and uses it in the destructor, instead of re-checking is_enabled(); this removes the window where profiling flipping on between ctor and dtor could read an uninitialized start_ or record a half-open interval.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 8, 2026 05:26

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

It contains a few correctness issues (missing standard includes in the new C++ header and missing cleanup on exceptions in Profiler.run_script()) that can break builds or leak resources.

Review details

Suppressed comments (4)

Previously missed (3) — in code that hasn't changed since the last review.

mssql_python/pybind/performance_counter.hpp:16

  • PerformanceCounter uses int64_t, INT64_MAX, and std::min/std::max but the header doesn’t include the standard headers that define them (<cstdint> / <stdint.h> and <algorithm>). This can cause build failures depending on transitive includes.
    profiler/core.py:199
  • Profiler.run_script() leaks the cursor (and leaves profiling enabled) if the user script fails to compile or raises during exec(), because cleanup only happens on the success path. Wrap execution in try/finally so cursor.close() and self._ctx.collect() always run.
    profiler/init.py:17
  • The module docstring shows p.report(), but Profiler doesn’t define a report method (and there’s no def report(...) anywhere under profiler/). This example will fail if copied.

mssql_python/perf_timer.py:108

  • perf_start() returns 0 when profiling is disabled, but perf_stop() will record a bogus duration if profiling gets enabled between start and stop (it will compute now - 0). Guarding against a zero start avoids corrupting stats in that edge case.
def perf_start() -> int:
    if not _enabled:
        return 0
    return time.perf_counter_ns()


def perf_stop(name: str, t0: int):
    if not _enabled:
        return
    _record(name, time.perf_counter_ns() - t0, t0)

  • Files reviewed: 17/17 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

…ate each other

The profiler CLI recorded across window boundaries: collect() snapshotted and reset the counters but left profiling enabled, so a scenario's teardown (commit/close), the next scenario's setup, and a fetch scenario's pre-enable cursor.execute all kept recording and bled into the next scenario's numbers. Profiler.close() also left profiling on for programmatic callers.

Make the window airtight in _ProfilingContext: enable() resets both layers before turning on (clean start, discards anything leaked between windows), collect() disables both layers after snapshotting (nothing outside a window is counted), and close() disables. Removed the now-redundant setup drain in _ensure_test_data. Added tests asserting collect() disables profiling and that work between two windows never appears in the next window's stats.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 8, 2026 05:31

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

There are correctness/robustness issues in the new profiling implementation (missing required C++ includes and cleanup gaps in the profiler runner/scenarios) that can cause build failures or leaked profiling state.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (1)

mssql_python/pybind/performance_counter.hpp:15

  • performance_counter.hpp uses std::min/std::max, int64_t, and INT64_MAX but does not include the standard headers that define them. This can fail to compile depending on transitive includes. Add the missing headers explicitly (e.g., and ).
#include <chrono>
#include <string>
#include <vector>
#include <unordered_map>
#include <mutex>
#include <atomic>
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
  • Files reviewed: 17/17 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment thread profiler/core.py Outdated
Comment thread profiler/scenarios.py
…tion-safe profiler runner

performance_counter.hpp: add the standard headers it actually uses (<algorithm>, <cstdint>, <limits>) instead of relying on transitive pybind/STL includes, which is not guaranteed across toolchains. perf_timer.perf_stop: ignore a falsy (t0==0) start so a perf_start() that ran while disabled cannot record a bogus now-minus-zero duration. profiler runner: guarantee profiling is disabled even when a scenario or a --script user script raises — one try/finally in run() covers every scenario (and future ones) rather than a guard in each scenario body, and run_script() closes its cursor and ends the window in a finally. Added a test asserting a raising script leaves profiling off.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Several profiler scenarios enable profiling but don’t reliably disable it on exception paths, risking process-wide profiling-state leaks in dev tooling/tests.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (4)

profiler/scenarios.py:197

  • fetchall() enables profiling but only closes the cursor in finally. If fetchall() raises before ctx.collect() runs, profiling can remain enabled and leak into subsequent work.
def fetchall(conn, table, ctx) -> dict:
    cursor = conn.cursor()
    try:
        cursor.execute(f"SELECT * FROM {table}")
        ctx.enable()
        t0 = time.perf_counter()
        rows = cursor.fetchall()
        wall_ms = (time.perf_counter() - t0) * 1000
        cpp, py = ctx.collect()
    finally:
        cursor.close()

profiler/scenarios.py:223

  • fetchone() enables profiling but doesn't disable it in finally. Any exception in the fetch loop before ctx.collect() will leave profiling enabled (global state leak).
def fetchone(conn, table, ctx, row_count: int = FETCHONE_ROWS) -> dict:
    cursor = conn.cursor()
    try:
        cursor.execute(f"SELECT TOP {row_count} * FROM {table}")
        ctx.enable()
        t0 = time.perf_counter()
        count = 0
        while True:
            row = cursor.fetchone()
            if row is None:
                break
            count += 1
        wall_ms = (time.perf_counter() - t0) * 1000
        cpp, py = ctx.collect()
    finally:
        cursor.close()
    return {

profiler/scenarios.py:248

  • fetchmany() enables profiling but doesn't disable it in finally. If cursor.fetchmany() or subsequent code raises before ctx.collect(), profiling can remain enabled and affect later measurements.
def fetchmany(conn, table, ctx, batch_size: int = FETCHMANY_SIZE) -> dict:
    cursor = conn.cursor()
    try:
        cursor.execute(f"SELECT * FROM {table}")
        ctx.enable()
        t0 = time.perf_counter()
        total = 0
        while True:
            batch = cursor.fetchmany(batch_size)
            if not batch:
                break
            total += len(batch)
        wall_ms = (time.perf_counter() - t0) * 1000
        cpp, py = ctx.collect()
    finally:
        cursor.close()
    return {

profiler/scenarios.py:314

  • fetch_arrow() enables profiling but only closes the cursor in finally. If arrow_batch() raises something other than ImportError (e.g., a driver error) before ctx.collect() runs, profiling can remain enabled and leak into later measurements.
    finally:
        cursor.close()
  • Files reviewed: 19/19 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment thread profiler/scenarios.py
Comment thread mssql_python/pybind/ddbc_bindings.cpp
Copilot AI review requested due to automatic review settings September 10, 2026 04:13

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The profiling paths are correctly gated (native compile-time, Python runtime no-op), and the PR includes targeted tests that validate enable/disable, window isolation, and cleanup behavior.

Review details
  • Files reviewed: 19/19 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Add a Python-only subprocess case that triggers cyclic GC during statistics collection and asserts completed finalizer cleanup and an unchanged snapshot. Bound the child to ten seconds so a regression fails rather than hanging CI.

The case requires neither SQL Server nor profiling-enabled native instrumentation. It detects the previous non-reentrant-lock deadlock and an incomplete RLock-only change.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 10, 2026 04:46

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The new python -m profiler CLI does not handle ImportError from missing/failed native extension imports, causing a traceback instead of the intended one-line error output.

Review details

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

profiler/main.py:68

  • The CLI only converts ValueError/RuntimeError/FileNotFoundError into a one-line message, but a missing/failed native extension import (ImportError from mssql_python.ddbc_bindings) will still produce a traceback. Since the CLI aims to surface setup problems cleanly, include ImportError in this handler (or catch it in Profiler construction) so a missing build shows as a single-line error.
  • Files reviewed: 19/19 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@gargsaumya gargsaumya left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The latest head addresses the earlier window-boundary, finalizer, cleanup, and test-packaging findings. I found one remaining clock-resolution edge case in the Python window identity logic.

Comment thread mssql_python/perf_timer.py

@gargsaumya gargsaumya left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’ve added a suggestion, but it is non-blocking, so you can consider addressing it as part of future scope.
Approving this PR.

@gargsaumya gargsaumya left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’ve added a suggestion, but it is non-blocking, so you can consider addressing it as part of future scope.
Approving this PR.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the PR for correctness, security, reliability, performance, test coverage, repository conventions, and applicable architecture and design specifications. No actionable issues were identified. The implementation is consistent with repository standards and the applicable approved design requirements.

Recommendation: Approve

The previously reported runtime issues and the requested bounded GC/finalizer regression test are addressed. An independent rubber-duck pass found no additional actionable issues. No separate approved profiler design was available; the documented internal diagnostic scope was used for this review.

At submission, 27 checks have succeeded; the macOS SQL2022 job and parent validation status remain pending. This approval does not bypass CI or authorize merging.

Copilot AI review requested due to automatic review settings September 10, 2026 06:31

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the PR! LGTM!

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The new shipped mssql_python.perf_timer module is not reflected in the package’s type stub surface, which can break typed imports (e.g., from mssql_python import perf_timer).

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 19/19 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread mssql_python/perf_timer.py
Copilot AI review requested due to automatic review settings September 10, 2026 08:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

There are test gating and script-compilation issues that can cause incorrect test execution and failures in some environments.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (1)

profiler/core.py:212

  • compile(path.read_bytes(), ...) decodes the source as UTF-8 and does not honor PEP 263 encoding cookies. That can make --script fail for valid Python files that declare a different encoding. Use tokenize.open() (or similar) to read the script with its declared encoding before compiling.
        path = Path(script_path).resolve()
        if not path.is_file():
            raise FileNotFoundError(f"Script not found: {script_path}")
        code = compile(path.read_bytes(), str(path), "exec", dont_inherit=True)

  • Files reviewed: 19/19 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread tests/test_025_profiler.py
Copilot AI review requested due to automatic review settings September 10, 2026 09:36

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

It introduces substantial cross-language instrumentation and concurrency-sensitive logic in the native extension that warrants final human review despite only minor issues found.

Review details
  • Files reviewed: 19/19 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment thread README.md
Comment thread mssql_python/cursor.py
@bewithgaurav
Gaurav Sharma (bewithgaurav) merged commit 9122376 into main Sep 10, 2026
37 of 39 checks passed
Gaurav Sharma (bewithgaurav) added a commit that referenced this pull request Sep 10, 2026
Merge main at 9122376, including profiler PR #552 and the other upstream updates. Preserve the cached-binding implementation and header extraction while resolving overlapping instrumentation in ddbc_bindings.cpp. Keep the upstream timers around the actual ODBC bind calls and retain the cache invalidation and handle guards.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr-size: large Substantial code update

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants