Repository navigation
perf(kernel): convert small results to rows without pandas - #1048
vuanhphung wants to merge 3 commits into
Conversation
KernelResultSet built a pandas DataFrame for every fetch, which costs hundreds of microseconds of GIL-held CPU even for a one-row result. Results of up to 128 rows (4 with a tz-aware timestamp column) are now converted with to_pylist() when every column has a type for which that gives the same values as the pandas path; other results still go through pandas. Signed-off-by: Vu Anh Phung <[email protected]> Co-authored-by: Isaac <[email protected]>
e57d47f to
d965a66
Compare
to_pylist() costs more per value than pandas, so with only a 128-row cap wide or string/decimal/date-heavy results were slower than the pandas path (16 lineitem columns x 128 rows 0.92-1.15x, 64 strings x 128 rows up to 2.2x, 32 dates x 128 rows up to 4.2x). A result now skips pandas only while rows times the sum of per-type weights stays within 1024, which kept every admitted shape at most 0.67x of the pandas CPU on pandas 1.3-3.1. date64, time32 and time64 now take the pandas path, since to_pylist() rounds pre-1970 date64 values down and wraps out-of-range times where pandas raises. A to_pylist() error (e.g. a date past 9999-12-31) also falls back to pandas, so errors match the pandas path. Signed-off-by: Vu Anh Phung <[email protected]> Co-authored-by: Isaac <[email protected]>
There was a problem hiding this comment.
Verdict: 2 Low
Looks good: two low-severity notes, one on recomputing the per-schema row limit on every fetch and one on the broad except Exception in the fallback. The fast path is safe: it only takes types allowlisted by type ID, skips NaN floats and nanosecond units, and falls back to pandas on any error. Tests compare its rows against the pandas path for every allowlisted type, for chunked and duplicate-named columns, and at the budget boundary.
…rror Review feedback: a to_pylist() failure silently switched to the pandas path, so a pyarrow regression that broke it for a common type would only show up as a slowdown. Log the fallback at debug level with the traceback; the catch stays broad so pandas keeps deciding the result or error. Signed-off-by: Vu Anh Phung <[email protected]> Co-authored-by: Isaac <[email protected]>
There was a problem hiding this comment.
Verdict: 1 Low
Looks good: the fast path checks types conservatively, falls back to pandas, and is well tested against the pandas path. One low concern: tz-aware timestamps may come back with a different tzinfo type when pandas ≥3 and pytz are both installed.
What type of PR is this?
Description
On the kernel backend every
fetchall()/fetchmany()/fetchone()turns the Arrow result into rows through a pandas DataFrame (ResultSet._convert_arrow_table). For a one-row result that round trip costs 600-700 µs of GIL-held CPU, about half of the Python CPU a query spends in the client. Under 200 threads sharing one process, a py-spy--gilprofile against a local mock put it at 33% of all GIL-held samples, the largest single item.KernelResultSetnow builds small results fromto_pylist()when every column has a type for which that returns the same Python values as the pandas path: integers, booleans, floats without NaN, decimals, strings, binary,date32, and timestamps not in nanoseconds.to_pylist()costs more per value than pandas, so "small" is a cost budget rather than a row count: rows × the sum of per-type weights must stay within 1024 (and rows ≤ 128). A weight is the type's per-valueto_pylist()cost relative to an integer: 1 for numbers, 3 for strings, 6 for binary and decimals, 12 for dates and timestamps, and 400 for tz-aware timestamps, which pyarrow converts slowly when pytz isn't installed. Everything else (NaN floats, nested, duration, interval,date64/time32/time64, nanosecond units,_disable_pandas, and results whoseto_pylist()raises, such as dates past year 9999) takes the existing path unchanged, so values and errors match the pandas path. The Thrift result set is not touched.How is this tested?
Unit tests compare rows from both paths (values, Python types, tzinfo) for every allowlisted type with nulls and edge values, chunked and duplicate-named columns, and check that excluded types, NaN, out-of-range values and results over the budget go through pandas. They pass with pandas 1.3.5, 1.5.3, 2.0.3, 2.2.3 and 3.1.1 on pyarrow 23.
The budget comes from a microbenchmark of single-thread CPU per conversion (13 column kinds, 1-300 columns, 1-128 rows, the five pandas versions above). The slowest result that now skips pandas costs 0.67× the pandas path; results that don't skip it cost the same as before (median 1.01×). Fast-path CPU relative to the pandas path, range over the five pandas versions:
With 16 threads converting concurrently, results at the budget's edge keep 2.6-8.9× the pandas path's throughput.
C800 benchmark (cold start, 4 processes × 200 sessions, each running TPC-H Q6, one row, back to back; 16 client cores; 3 interleaved rounds, medians; measured on the first revision, whose one-row path is unchanged here):
On the live warehouse QPS is bound by the warehouse today (avg 550 vs 515, peak 996 vs 1041, within run-to-run noise); the mock shows the driver's own capacity. With this revision, one process × 200 sessions against a local mock (20 ms server time, 4 interleaved rounds, medians): avg QPS 1510 → 3603, peak 1633 → 4199, query p50 101 → 33 ms, CPU per query 1.25 → 0.66 ms (the first revision measured 3498 / 4362 / 34 ms / 0.66 ms).
Live benchmark (staging Large Reyden warehouse)
TPC-H Q6 on a Large Real-Time warehouse, 800 sessions (4 processes x 200 threads), cold start, each session querying back to back for 120 s, 16 client cores. Isolated A/B: only this change vs connector main 0c1b9e8 + kernel 1.4.0, 5 interleaved rounds, nothing else on the warehouse. Measured at 51214d2. Median (min-max):
The warehouse is the bottleneck here (query latency is queueing at 800 sessions), so the ~35% lower client CPU per query shows up as headroom rather than QPS. One base and one PR round hit server-side 503/504s and are included.
SELECT 1 on an X-Large Real-Time warehouse (driver-bound). Q6 above is limited by the warehouse, so the same harness also ran
SELECT 1, where the client is the limit: X-Large warehouse, 4 processes x 200 threads, 120 s, 16 client cores, this change vs connector main 0c1b9e8 + kernel 1.4.0, measured at 51214d2, run in rotation with the other factory PRs, 2 rounds. Median (min-max):Average QPS change per round: +8.3%, +13.6%.
Related Tickets & Documents
This pull request and its description were written by Isaac.