Skip to content

perf: skip latency timing nothing records; build rows in fewer calls (stacked on #1059) - #1062

Open
vuanhphung wants to merge 4 commits into
perf/rd-py-overhead-3from
perf/rd-py-overhead-4
Open

vuanhphung wants to merge 4 commits into
perf/rd-py-overhead-3from
perf/rd-py-overhead-4

Conversation

@vuanhphung

@vuanhphung vuanhphung commented Nov 14, 2026 •

Copy link
Copy Markdown
Collaborator

What type of PR is this?

  • Other (performance)

Stacked on #1059 (base perf/rd-py-overhead-3); review only the top commit.

Description

Two pieces of per-call Python work that run on every query, on every backend.

log_latency (on execute(), execute_async() and the metadata calls) timed each call and then threw the timing away when the connection's Python telemetry was off and DEBUG logging was disabled. That is always the case on kernel connections, which export telemetry from the kernel. The wrapper now checks both first and calls the method directly when nothing would record the latency. With telemetry on, or DEBUG enabled, it behaves as before, including exporting on exceptions.

Each fetched row was built through Row.__call__, _create_row, Row.__new__ and Row.__setattr__. Row.__call__ and _create_row now build the tuple and set __fields__ directly, so each row takes fewer Python calls. The resulting object is the same: a Row whose __dict__ holds __fields__. Pickling, repr, asDict, attribute and key access, and the too-many-values error are unchanged.

How is this tested?

  • Unit tests
  • Manually

New unit tests cover the decorator (no timing, export or log line when telemetry and DEBUG are off, a debug line when DEBUG is on, export with telemetry on even when the call raises, objects without a connection) and Row (factory rows, fewer values, too many values, read-only, _create_row, pickle round trip). test_row.py also passes against the base of this stack. The decorator tests patch the clock and check that nothing is timed on the fast path, so two of them fail against the base. The full unit suite passes (-m "not realkernel", and test_session.py -m realkernel); mypy and black (22.3.0 and 26.3.1) are clean.

Single-thread benchmark: a real use_kernel connection with the kernel session faked, so only Python runs. Each query is a fresh cursor, execute("SELECT 1"), fetchall() and close. 6 interleaved rounds, medians:

#1059 this PR
Python time per one-row query 17.22 µs 16.23 µs (-5.7%)
Row factory call, 5 columns 402 ns 288 ns

Rows matter most on larger results. ResultSet._convert_arrow_table (the Thrift/SEA row path) on a 100,000 x 5 table, 3 interleaved rounds, best of 7:

#1059 this PR
default (pandas) path 174-178 ms 151-153 ms (-13%)
_disable_pandas=True path 301-307 ms 275-280 ms (-9%)

On one-row queries the saving does not show in QPS. A one-process client running SELECT 1 on 800 threads is limited by GIL hand-offs: each query releases the GIL 7 times, 6 of them in pyarrow's C-data-interface imports. That makes 1 µs of GIL-held Python worth about 0.7% QPS. Measured with the full set of open kernel-path PRs (#1059, #1052, #1050, #1051, #1053) and the matching kernel build, 3 interleaved rounds each:

1 x 800 sessions, SELECT 1 base this PR
Rust mock, 30 ms server time, avg QPS 5519 / 4885 / 5023 5083 / 5412 / 5428
Staging X-Large Real-Time warehouse, 60 s, avg QPS 4506 / 4590 / 4527 4545 / 4529 / 4520
same, peak QPS 4993 / 4946 / 4970 5074 / 5076 / 5106
same, client CPU per query 0.76 ms 0.77 ms

All within run-to-run noise. The client ran on 8 cores.

#1061 (with databricks/databricks-sql-kernel#467) cuts the hand-offs to one per query, which makes the GIL-held Python matter more. On top of it (same PR set plus #1061, the kernel PRs plus #467), Rust mock, 30 ms server time, 7 client cores, 20 s, 3 interleaved rounds:

SELECT 1, one process base this PR
800 sessions, avg QPS 11178 / 11129 / 9793 10212 / 11616 / 11418
800 sessions, client CPU per query 0.345 / 0.345 / 0.346 ms 0.324 / 0.335 / 0.330 ms (-4%)
1600 sessions, avg QPS 8452 / 9365 / 9631 10055 / 10792 / 10364 (+11% median)
1600 sessions, client CPU per query 0.449 / 0.390 / 0.383 ms 0.363 / 0.366 / 0.374 ms (-6%)
one session, 1,000-row x 5-column result, time per query 2593 / 2324 / 2366 µs 2382 / 2124 / 2134 µs (-10%)

Live on the same stack (staging X-Large, 1,600 sessions, 60 s, 8 client cores), the results are within noise: base 6503 / 6489 avg QPS vs 6984 / 6669 / 6222. A third base round hit a cold warehouse and is left out. The client is CPU-bound at that load.

Related Tickets & Documents

Stacked on #1059.

This pull request and its description were written by Isaac.

log_latency timed every decorated call (execute, execute_async, metadata
calls) and then dropped the result when telemetry was off and DEBUG
logging was disabled, which is always the case on kernel connections.
It now checks first and calls the method directly. Row factories built
each row through Row.__call__, _create_row, Row.__new__ and
Row.__setattr__; they now build the tuple and set __fields__ directly,
producing the same object.

Single-thread benchmark of one-row kernel queries with the kernel
faked: 17.2 -> 16.2 us of Python per query; a 5-column row factory
call drops from 402 to 288 ns.

Signed-off-by: Vu Anh Phung <[email protected]>
Co-authored-by: Isaac <[email protected]>

@peco-review-bot peco-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Verdict: 1 Low

Looks good. I found 1 low-severity issue: a unit test asserts a timing-dependent log message and could fail intermittently on CI. Both production changes behave the same as before. The early return in log_latency gives the same telemetry and debug-log results, because ResultSetDownloadHandler has no connection and so was never exported anyway. Building rows through tuple.__new__ plus a direct write to __dict__ gives the same object that Row.__new__ and Row.__setattr__ produced. Nit: the log_latency docstring ("measures the execution time…") and the leftover "Fast check" comment don't mention that nothing is timed when telemetry and DEBUG logging are both off.

Comment thread tests/unit/test_latency_logger.py Resolved
Match "execute completed in <n>ms" instead of 0ms, so a slow CI runner
can't fail it, and say in the log_latency docstring and comment that
nothing is timed when telemetry and DEBUG logging are both off.

Signed-off-by: Vu Anh Phung <[email protected]>
Co-authored-by: Isaac <[email protected]>

@peco-review-bot peco-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Verdict: 1 Nit

Looks good: I found no correctness issues. The early return in log_latency keeps the previous behaviour whenever telemetry or DEBUG logging is on. ResultSetDownloadHandler has no connection, so it never exported before this change either. The inlined Row construction produces the same object Row.__setattr__ would have built, and the new tests cover the changed branches.

Other findings

  • ⚪ Nit — log_latency now reads connection.telemetry_enabled before the wrapped call rather than after it. That is equivalent today because Connection.telemetry_enabled is only set in Connection.__init__ (client.py:474). A short note in the wrapper comment that the flag is read once, before the call, would keep a future change that toggles it mid-call from quietly diverging.

@peco-review-bot peco-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Verdict: 1 Low

Looks good. Both changes keep the existing behaviour: the latency wrapper still exports when telemetry is on (including on exceptions), and the inlined Row construction produces the same object as Row.__new__ + __setattr__ did. One low finding: the fast-path test can't fail if the optimization regresses.

Comment thread tests/unit/test_latency_logger.py Resolved
Patch the module's clock and assert monotonic() is not called with
telemetry and DEBUG logging off (or without a connection), and is called
when either is on. The fake clock also makes the logged and exported
latency exact.

Signed-off-by: Vu Anh Phung <[email protected]>
Co-authored-by: Isaac <[email protected]>

@peco-review-bot peco-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ No issues identified by the review bot.

This branch was previously deployed

1 inactive deployment
azure-prod — d8212f32 Deployed Nov 14, 2026 by peco-review-bot[bot] via followup #2367
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant