GeezerVisonThe World Wide Web, restored to 800 pixels of glory. | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Home|About|Gaming cabinet|Archives|How it works|Starter sites | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
GeezerVison is showing github.com — perf(thrift): decode inline Arrow results in one call, into memory returned on free by vuanhphung · Pull Request #1037 · databricks/databricks-sql-python Conversation
|
| Result | Peak: main / decode only / this PR |
After free: main / decode only / this PR |
Fetch time: main / this PR |
|---|---|---|---|
| 110 MB | 122 / 135 / 122 MB | 82 / 108 / 5 MB | 3.03 / 2.80 s |
| 562 MB | 601 / 602 / 585 MB | 516 / 583 / 14 MB | 14.23 / 14.34 s |
| 1.7 GB | 1810 / 2909 / 1758 MB | 1637 / 2859 / 26 MB | 42.53 / 42.84 s |
Throughput, 64 threads, a connection and a cursor per statement per thread, 1,000-row / 72-batch inline result, mock with 50 ms RTT, 3 interleaved runs: 528 → 1109 statements/s and p50 120 → 56 ms on main → this PR (decode only: 1113 statements/s; the memory map doesn't affect small results).
Results from the decode commits, measured with the original harness (median of 5 interleaved runs, 1,000-row result). The small-result path they exercise is unchanged by the memory-map commit.
| Setup | Threads | Fetch | Statements/s | p50 (ms) | p99 (ms) | CPU/statement (ms) |
|---|---|---|---|---|---|---|
| mock | 16 | fetchall_arrow |
283 → 294 | 53 → 53 | 86 → 69 | 2.2 → 1.5 |
| mock | 64 | fetchall_arrow |
393 → 748 | 161 → 85 | 194 → 129 | 5.3 → 1.8 |
| mock | 200 | fetchall_arrow |
386 → 696 | 503 → 277 | 625 → 482 | 5.5 → 2.1 |
| mock | 64 | fetchall |
175 → 215 | 360 → 289 | 450 → 415 | 9.1 → 5.7 |
| live | 200 | fetchall_arrow |
380 → 499 | 487 → 359 | 877 → 1073 | 5.4 → 2.1 |
| live | 64 | fetchall |
175 → 204 | 355 → 302 | 548 → 500 | 8.9 → 6.0 |
On the live warehouse, 64-thread fetchall_arrow and all 1- and 16-thread runs were capped by the shared warehouse, and the higher live p99 at 200 threads coincides with ~30% more load on it. SELECT 1 (one batch, unchanged path) and the kernel backend measured the same on both sides.
Supersedes #1036.
This pull request and its description were written by Isaac.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
The warehouse returns small inline results as dozens of LZ4 frames (72 for both 100 and 1,000 rows of range()). Decompressing each frame with lz4.frame.decompress releases and reacquires the GIL per frame, so with many busy threads a 1,000-row statement made ~600 context switches and spent most of its time waiting for the GIL. For three or more compressed batches, decode the concatenated frames with one pyarrow CompressedInputStream read. One- and two-batch results keep the python-lz4 path, as does any pyarrow build without the lz4 codec. Co-authored-by: Isaac <[email protected]> Signed-off-by: Vu Anh Phung <[email protected]>
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Verdict: 1 Low · 1 Nit
Looks good: the change is well-scoped and well-tested, with two low-severity points. One is that the exception type for corrupt payloads now depends on how many batches the result has; the other is a test that assumes the environment's pyarrow has the lz4 codec.
test_convert_arrow_based_set_decodes_many_lz4_batches_in_one_call expects (72, 0) decompress calls without checking utils._PYARROW_HAS_LZ4. On a pyarrow build without the lz4 codec (exactly the case the fallback handles), this test fails instead of skipping. Pin the flag with patch.object(utils, "_PYARROW_HAS_LZ4", True) plus a skip when pyarrow.Codec.is_available("lz4") is False, or just skip when the codec is missing.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
pyarrow raises OSError where python-lz4 raises RuntimeError, so the error type depended on how many batches a result had. Also skip the one-call test on pyarrow builds without the lz4 codec. Co-authored-by: Isaac <[email protected]> Signed-off-by: Vu Anh Phung <[email protected]>
|
Nit from the review body (one-call test assumes the lz4 codec): fixed in 2140f9d. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Verdict: 2 Low
Looks good overall. The batched LZ4 path is correct and tested with real compressed data, and the fallback paths are unchanged. I have two low concerns: real out-of-memory errors get reported as decompression failures, and corruption in a middle frame isn't tested.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
ArrowMemoryError subclasses both MemoryError and ArrowException, so an allocation failure was reported as an LZ4 decompression error. Also cover truncated and corrupted middle frames, with checksums as the warehouse sets them. Co-authored-by: Isaac <[email protected]> Signed-off-by: Vu Anh Phung <[email protected]>
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Verdict: 3 Low
No blocking issues: the single-call decode is correctly limited to 3+ LZ4 batches and falls back to the old path when pyarrow lacks the lz4 codec. The error types match the old path (MemoryError is re-raised unwrapped; other decode errors become RuntimeError). I found 3 low-severity items: a test that will fail on pyarrow builds without lz4, a decode stream that is never closed, and a difference from main on malformed frames.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| ) | ||
|
|
||
| def test_convert_arrow_based_set_lz4_memory_error_is_not_wrapped(self): | ||
| _, arrow_batches, schema = self._arrow_set(72, 14, True) |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
🔵 Low — test_convert_arrow_based_set_lz4_memory_error_is_not_wrapped patches pyarrow.CompressedInputStream, but that call only happens when _PYARROW_HAS_LZ4 is true. On a pyarrow build without the lz4 codec, the function uses the lz4.frame.decompress fallback and returns normally, so assertRaises(MemoryError) fails. Your PR description lists this setup as supported. test_convert_arrow_based_set_decodes_many_lz4_batches_in_one_call is already guarded; this test needs the same @unittest.skipUnless(utils._PYARROW_HAS_LZ4, ...) decorator.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Fixed in c6891ff: test_convert_arrow_based_set_lz4_memory_error_is_not_wrapped now has the same @unittest.skipUnless(utils._PYARROW_HAS_LZ4, ...) guard.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
I checked the current head and the fix isn't there yet. The only @unittest.skipUnless(utils._PYARROW_HAS_LZ4, ...) in tests/unit/test_thrift_backend.py is at line 2238, which guards test_convert_arrow_based_set_decodes_many_lz4_batches_in_one_call. Line 2291, just above test_convert_arrow_based_set_lz4_memory_error_is_not_wrapped at line 2292, has no guard. Maybe c6891ff hasn't been pushed to this branch, or the decorator ended up on a different test. Once the guard is directly above that test on the PR head, this thread is resolved.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
Close the CompressedInputStream with a with block so its buffers are freed even when an exception traceback keeps the frame alive. Skip the memory error test without the lz4 codec, test truncated frames without checksums, and note that each batch is one LZ4 frame. Co-authored-by: Isaac <[email protected]> Signed-off-by: Vu Anh Phung <[email protected]>
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Verdict: 2 Low
Looks good: I found two low-severity notes and no correctness issues. The mmap buffer's grow, shrink and fallback paths stay consistent in every error branch I traced, errors are converted the same way on both decode paths, and the tests cover these paths thoroughly.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
…the map On platforms without the memory map (non-Linux), decoding a large result through Arrow into a heap bytearray is the combination that used more memory than main with glibc, and other allocators are unmeasured. Use the one-call decode there only for results whose compressed size is under the map floor, where the throughput gain comes from; large results decode per batch as on main. Note the temporary copy of the compressed row set in the one-call path. Signed-off-by: Vu Anh Phung <[email protected]> Co-authored-by: Isaac <[email protected]>
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Verdict: 1 Medium
Looks good overall, with one medium concern: on Linux the decode path is chosen from the platform flag, not from whether the memory map was actually created. If mmap or mremap fails, a large result is decoded through Arrow into the heap, which is the high-memory case this PR is meant to avoid. The buffer management, error wrapping and tests otherwise look correct and thorough.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
The one-call Arrow decode for large results was gated on the platform flag, so on Linux a failed mmap (for example under strict overcommit, where the reservation is charged up front) still decoded a large result through Arrow into the heap, the case that used more memory than main. Gate it on the buffer actually being mapped instead: large results without a map use the per-batch decode, as on main. Signed-off-by: Vu Anh Phung <[email protected]> Co-authored-by: Isaac <[email protected]>
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Verdict: 2 Low
Looks good. The decode and mmap logic holds up, including growth, shrink and heap fallbacks, error mapping and map lifetime, and the tests cover it well. Two low findings: a missing codec-availability skip in two new tests, and a gate comment that doesn't match the compressed-size check it describes.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
Two tests assert that lz4.frame.decompress isn't called, which only holds when pyarrow has the lz4 codec; skip them otherwise, like the sibling tests. Reword the gate comment: without a map it limits the one-call decode by compressed size, so a highly compressible result can still decode into the heap. Signed-off-by: Vu Anh Phung <[email protected]> Co-authored-by: Isaac <[email protected]>
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Verdict: 1 Low
Looks good, with one low edge case. The mmap-backed stream buffer, the concatenated-frame Arrow decode (Arrow resets its decompressor between frames, and the pyarrow>=14 floor covers Codec.is_available and readinto), the error wrapping and the fallbacks all check out, and the unit tests are thorough. Nit: _decompress_lz4_frames_into hardcodes 4 * for the first read size instead of reusing _LZ4_RATIO_ESTIMATE, so the two estimates can drift apart.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
When the decoded stream filled the reservation exactly, append_stream grew the map before seeing EOF: a wasted mremap that, under memory pressure, could push a finished result onto the heap. Probe with a one-byte read and grow only if more data follows. Reuse _LZ4_RATIO_ESTIMATE for the first read size. Signed-off-by: Vu Anh Phung <[email protected]> Co-authored-by: Isaac <[email protected]>
|
Review-body nit, done in ccdbbf4: |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Verdict: 1 Low
Looks good, with one low concern. The mmap-backed stream buffer, the single-call LZ4 decode and the fallbacks are careful and well tested. The error types (RuntimeError, MemoryError) and lifetime handling (the table holding the map) keep matching the old behaviour.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
ccdbbf4c
Deployed
There was an error while loading. Please reload this page.
Successfully merging this pull request may close these issues.
There was an error while loading. Please reload this page.