perf: guard loggerExternal.entering/exiting with isLoggable in SQLServerResultSet#2945
Closed
saurabh500 wants to merge 2 commits into
Closed
perf: guard loggerExternal.entering/exiting with isLoggable in SQLServerResultSet#2945saurabh500 wants to merge 2 commits into
saurabh500 wants to merge 2 commits into
Conversation
Wrap 396 unguarded loggerExternal.entering() and exiting() calls in SQLServerResultSet with isLoggable(Level.FINER) checks. When logging is disabled (the common production case), this eliminates: - Autoboxing of primitive parameters (int -> Integer) for varargs - Object[] array creation for multi-arg entering/exiting calls - getClassNameLogging() call overhead Benchmarked with 10M iterations on JDK 21: Unguarded: 47.4ms Guarded: 24.2ms (~2x faster per getter call) For a typical query reading 1M rows x 10 columns, this eliminates ~20M unnecessary autoboxing allocations and array creations.
Single-file, zero-dependency benchmark that demonstrates the impact of guarding entering/exiting calls with isLoggable(FINER) in ResultSet getters. Compile and run: javac LoggerGuardBenchmark.java && java LoggerGuardBenchmark
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Guards 396 unguarded
loggerExternal.entering(...)/loggerExternal.exiting(...)calls inSQLServerResultSetwithisLoggable(Level.FINER)checks.When logging is disabled (the common production case), the unguarded variants still incur:
int→Integer) for the varargsObject...parameterObject[]array allocation on every call site that passes >1 arggetClassNameLogging()invocation overheadAfter this change, the guard short-circuits all of that when the logger level is above
FINER(the default).Change
Single file:
src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResultSet.java. Every existingloggerExternal.entering(...)/loggerExternal.exiting(...)is now wrapped inif (loggerExternal.isLoggable(Level.FINER)) { ... }. No behavioral change; logging output is identical whenFINERis enabled.Benchmarks
Real benchmarks against SQL Server 2022 using JMH (
org.openjdk.jmh:jmh-core:1.37).Hardware / software
mcr.microsoft.com/mssql/server:2022-latest(16.0.4250.1, Developer Edition on Ubuntu) running in Docker onlocalhost:1433mvn -Pjre17 -DskipTests -Dmaven.javadoc.skip=true -Dgpg.skip=true packagefrom this branch (AFTER) and from the parent commit (BEFORE)-Xms6g -Xmx6g, modeAverageTime, single client processBenchmark 1 — narrow result set, mixed types ✅ clear win
10 columns (4× INT, 3× NVARCHAR(50), DATETIME2, BIGINT, FLOAT) × 100 K rows. Table populated once, read once per
@Benchmarkop.JMH: 2 forks × (3 × 3 s warmup + 5 × 5 s measurement).
~1.82× faster (−45 %) — this is the realistic OLTP/reporting workload the patch was designed for. Mixed types with a few cols/row maximizes the relative weight of per-getter overhead.
Benchmark 2 — very wide result set, on-the-fly cross join (server-CPU-bound)
1000 INT columns × 1 M rows generated entirely from
sys.all_columns × sys.all_columnswith no table load:JMH: 1 fork × (1 warmup + 3 measurement).
Server-side row-construction dominates, total time stays ~110 s. Δ within noise.
Benchmark 3 — very wide result set, persisted table (pure I/O pull)
Same shape as Benchmark 2 but pre-loaded into a real table, so each iteration is essentially a TDS stream of pre-built rows:
One-time load: ~40 s. Per-iteration read drops from ~110 s (cross-join) to ~65 s, confirming the cross-join was server-CPU-bound.
JMH: 1 fork × (2 warmup + 6 measurement). Same connection re-used between forks (BEFORE run reuses the table created by AFTER run).
Δ ≈ 0.6 s on a 65 s mean (~0.9 %); CIs overlap heavily → statistically indistinguishable.
Interpretation
getIntwork in TDS column parsing / value materialization plus on-the-wire bytes completely dominate. TheInteger.valueOf+Object[]allocation removed by the guard is real GC pressure but small relative to that floor (Benchmarks 2 & 3).Object[]), which is an additional benefit not captured by wall-clock here but visible under allocation-rate profiling (see the standalone reproducerbenchmark/LoggerGuardBenchmark.javaintroduced in commit6e7d5d5b— ~2× faster per call, ~zero allocation guarded).Reproducer
The full JMH project used for the SQL Server runs (pom + 2 benchmark classes + log/JSON outputs) lives at
/tmp/jdbc-bench/on the build machine; happy to upstream it on request.Risk
FINERis enabled (the guard's predicate is exactly the level threshold the JUL handler would check anyway).