Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
258 changes: 144 additions & 114 deletions mssql_python/cursor.py

Large diffs are not rendered by default.

134 changes: 134 additions & 0 deletions mssql_python/perf_timer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# 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
from contextlib import contextmanager

_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
_timeline_enabled = True
_epoch_ns = time.perf_counter_ns()


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():
out[name] = {
"calls": s["calls"],
"total_us": s["total_ns"] // 1000,
"min_us": s["min_ns"] // 1000,
"max_us": s["max_ns"] // 1000,
}
return out


@contextmanager
def perf_phase(name: str):
if not _enabled:
yield
return
t0 = time.perf_counter_ns()
yield
elapsed = time.perf_counter_ns() - t0
_record(name, elapsed, t0)


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)


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 @@ -318,6 +322,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
12 changes: 10 additions & 2 deletions mssql_python/pybind/build.bat
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,17 @@ if errorlevel 1 (
exit /b 1
)

REM Optional performance profiling instrumentation (off by default).
REM Enable with: set ENABLE_PROFILING=1 (before running build.bat)
set PROFILING_FLAG=
if defined ENABLE_PROFILING (
set PROFILING_FLAG=-DENABLE_PROFILING=ON
echo [MODE] Building WITH profiling instrumentation ^(ENABLE_PROFILING set^)
)

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
18 changes: 18 additions & 0 deletions mssql_python/pybind/connection/connection.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

// Logging uses LOG() macro for all diagnostic output
#include "logger_bridge.hpp"
#include "performance_counter.hpp"

static SqlHandlePtr getEnvHandle() {
static SqlHandlePtr envHandle = []() -> SqlHandlePtr {
Expand Down Expand Up @@ -48,6 +49,7 @@ static SqlHandlePtr getEnvHandle() {
//-------------------------------------------------------------------------------------------------
Connection::Connection(const std::u16string& conn_str, bool use_pool)
: _connStr(conn_str), _autocommit(false), _fromPool(use_pool) {
PERF_TIMER("Connection::Connection");
allocateDbcHandle();
}

Expand All @@ -57,6 +59,7 @@ Connection::~Connection() {

// Allocates connection handle
void Connection::allocateDbcHandle() {
PERF_TIMER("Connection::allocateDbcHandle");
auto _envHandle = getEnvHandle();
SQLHANDLE dbc = nullptr;
LOG("Allocating SQL Connection Handle");
Expand All @@ -66,6 +69,7 @@ void Connection::allocateDbcHandle() {
}

void Connection::connect(const py::dict& attrs_before) {
PERF_TIMER("Connection::connect");
LOG("Connecting to database");
// Apply access token before connect
if (!attrs_before.is_none() && py::len(attrs_before) > 0) {
Expand All @@ -83,6 +87,7 @@ void Connection::connect(const py::dict& attrs_before) {
// and SQL Server authentication — all pure I/O that doesn't need the GIL.
// This allows other Python threads to run concurrently.
py::gil_scoped_release release;
PERF_TIMER("Connection::connect::SQLDriverConnect_call");
ret = SQLDriverConnect_ptr(_dbcHandle->get(), nullptr, connStrPtr, SQL_NTS, nullptr,
0, nullptr, SQL_DRIVER_NOPROMPT);
}
Expand All @@ -91,6 +96,7 @@ void Connection::connect(const py::dict& attrs_before) {
}

void Connection::disconnect() {
PERF_TIMER("Connection::disconnect");
if (_dbcHandle) {
LOG("Disconnecting from database");

Expand Down Expand Up @@ -185,6 +191,7 @@ void Connection::checkError(SQLRETURN ret) const {
}

void Connection::commit() {
PERF_TIMER("Connection::commit");
if (!_dbcHandle) {
ThrowStdException("Connection handle not allocated");
}
Expand All @@ -200,6 +207,7 @@ void Connection::commit() {
}

void Connection::rollback() {
PERF_TIMER("Connection::rollback");
if (!_dbcHandle) {
ThrowStdException("Connection handle not allocated");
}
Expand All @@ -215,6 +223,7 @@ void Connection::rollback() {
}

void Connection::setAutocommit(bool enable) {
PERF_TIMER("Connection::setAutocommit");
if (!_dbcHandle) {
ThrowStdException("Connection handle not allocated");
}
Expand Down Expand Up @@ -253,6 +262,7 @@ bool Connection::getAutocommit() const {
}

SqlHandlePtr Connection::allocStatementHandle() {
PERF_TIMER("Connection::allocStatementHandle");
if (!_dbcHandle) {
ThrowStdException("Connection handle not allocated");
}
Expand Down Expand Up @@ -516,6 +526,7 @@ std::chrono::steady_clock::time_point Connection::lastUsed() const {
ConnectionHandle::ConnectionHandle(const std::u16string& connStr, bool usePool,
const py::dict& attrsBefore)
: _usePool(usePool), _connStr(connStr) {
PERF_TIMER("ConnectionHandle::ConnectionHandle");
if (_usePool) {
_conn = ConnectionPoolManager::getInstance().acquireConnection(_connStr, attrsBefore);
} else {
Expand All @@ -531,6 +542,7 @@ ConnectionHandle::~ConnectionHandle() {
}

void ConnectionHandle::close() {
PERF_TIMER("ConnectionHandle::close");
if (!_conn) {
ThrowStdException("Connection object is not initialized");
}
Expand All @@ -543,20 +555,23 @@ void ConnectionHandle::close() {
}

void ConnectionHandle::commit() {
PERF_TIMER("ConnectionHandle::commit");
if (!_conn) {
ThrowStdException("Connection object is not initialized");
}
_conn->commit();
}

void ConnectionHandle::rollback() {
PERF_TIMER("ConnectionHandle::rollback");
if (!_conn) {
ThrowStdException("Connection object is not initialized");
}
_conn->rollback();
}

void ConnectionHandle::setAutocommit(bool enabled) {
PERF_TIMER("ConnectionHandle::setAutocommit");
if (!_conn) {
ThrowStdException("Connection object is not initialized");
}
Expand All @@ -571,6 +586,7 @@ bool ConnectionHandle::getAutocommit() const {
}

SqlHandlePtr ConnectionHandle::allocStatementHandle() {
PERF_TIMER("ConnectionHandle::allocStatementHandle");
if (!_conn) {
ThrowStdException("Connection object is not initialized");
}
Expand Down Expand Up @@ -635,13 +651,15 @@ py::object Connection::getInfo(SQLUSMALLINT infoType) const {
}

py::object ConnectionHandle::getInfo(SQLUSMALLINT infoType) const {
PERF_TIMER("ConnectionHandle::getInfo");
if (!_conn) {
ThrowStdException("Connection object is not initialized");
}
return _conn->getInfo(infoType);
}

void ConnectionHandle::setAttr(int attribute, py::object value) {
PERF_TIMER("ConnectionHandle::setAttr");
if (!_conn) {
ThrowStdException("Connection not established");
}
Expand Down
5 changes: 5 additions & 0 deletions mssql_python/pybind/connection/connection_pool.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@

// Logging uses LOG() macro for all diagnostic output
#include "logger_bridge.hpp"
#include "performance_counter.hpp"

ConnectionPool::ConnectionPool(size_t max_size, int idle_timeout_secs)
: _max_size(max_size), _idle_timeout_secs(idle_timeout_secs), _current_size(0) {}

std::shared_ptr<Connection> ConnectionPool::acquire(const std::u16string& connStr,
const py::dict& attrs_before) {
PERF_TIMER("ConnectionPool::acquire");
std::vector<std::shared_ptr<Connection>> to_disconnect;
std::shared_ptr<Connection> valid_conn = nullptr;
bool needs_connect = false;
Expand Down Expand Up @@ -120,6 +122,7 @@ std::shared_ptr<Connection> ConnectionPool::acquire(const std::u16string& connSt
}

void ConnectionPool::release(std::shared_ptr<Connection> conn) {
PERF_TIMER("ConnectionPool::release");
bool should_disconnect = false;
{
std::lock_guard<std::mutex> lock(_mutex);
Expand All @@ -145,6 +148,7 @@ void ConnectionPool::release(std::shared_ptr<Connection> conn) {
}

void ConnectionPool::close() {
PERF_TIMER("ConnectionPool::close");
std::vector<std::shared_ptr<Connection>> to_close;
{
std::lock_guard<std::mutex> lock(_mutex);
Expand All @@ -170,6 +174,7 @@ ConnectionPoolManager& ConnectionPoolManager::getInstance() {

std::shared_ptr<Connection> ConnectionPoolManager::acquireConnection(const std::u16string& connStr,
const py::dict& attrs_before) {
PERF_TIMER("ConnectionPoolManager::acquireConnection");
std::shared_ptr<ConnectionPool> pool;
{
std::lock_guard<std::mutex> lock(_manager_mutex);
Expand Down
Loading
Loading