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): return inline Arrow result memory to the OS when freed by vuanhphung · Pull Request #1036 · databricks/databricks-sql-python Conversation
|
| Workload | Peak, main | Peak, this PR | After free, main | After free, this PR | Fetch time, main → PR |
|---|---|---|---|---|---|
fetchall_arrow() 110 MB |
126 MB | 118 MB | 104 MB | 6 MB | 2.90 → 2.72 s |
fetchall_arrow() 562 MB |
601 MB | 583 MB | 475 MB | 12 MB | 14.34 → 15.27 s |
fetchall_arrow() 1.7 GB |
1811 MB | 1758 MB | 1679 MB | 26 MB | 44.63 → 44.51 s |
fetchall_arrow() 573 MB, highly compressible |
612 MB | 590 MB | 540 MB | 11 MB | 8.20 → 8.61 s |
fetchmany_arrow(100k) loop, 562 MB |
31 MB | 17 MB | 15 MB | 5 MB | 14.35 → 14.72 s |
fetchall() rows, 110 MB |
1662 MB | 1654 MB | 162 MB | 74 MB | 8.43 → 8.61 s |
Fetch time is dominated by one ~0.1 s FetchResults round trip per 100k rows, and the differences above go both ways within noise. The conversion itself, timed in-process during the same runs, changes by at most ~0.7 ms per round trip (2.34 s → 2.29 s for the 1.7 GB result; 0.62 s → 0.73 s for the streaming loop). A single 51 MB row set (wide rows) converts with no transient memory on both main and this PR.
Same technique as #1032 (CloudFetch). Both edit CHANGELOG.md under Unreleased, so whichever merges second has a one-line conflict there.
This pull request and its description were written by Isaac.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
Each inline FetchResults row set was decompressed into a bytearray grown on the malloc heap. The Arrow table reads it zero-copy, so ~3.7 MB per round trip stayed live there, mixed with short-lived reply buffers; after a large result was released glibc kept almost all of it resident (a 1.7 GB inline fetchall_arrow left ~1.6 GB). Copy each set's stream into a private anonymous mmap instead (pre-faulted with MAP_POPULATE where available), which goes back to the OS when the table is freed. Sets under 128 KiB and mmap failures keep the heap 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 Low
Looks good. There is one low note: the lz4 path now briefly needs about twice the row set's memory while the batches are joined. The mmap handling is otherwise correct: anonymous MAP_PRIVATE with MAP_ANONYMOUS added implicitly by CPython, py_buffer keeps the mapping alive, and failures fall back to the heap. The tests cover the large, small, failure and schema-only paths.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
Holding every decompressed batch until the copy needed about twice the row set's memory, and releasing the parts did not help because glibc keeps freed chunks resident. Reserve the map from the compressed size instead (untouched pages cost nothing), copy each batch in as it is decompressed, and grow the map with mremap on overflow. Platforms without mremap continue on the heap. A 51 MB row set now converts with no transient, 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 change is correct and well contained. My two notes are both low severity: the tests for the grow and fallback paths don't check that those paths actually run, and the 4x over-reservation is never trimmed after the map is filled.
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.
Release the unused part of the size estimate once the stream is complete, so it no longer counts against a strict overcommit limit while the result is alive. Use the map only where MAP_PRIVATE exists: the heap retention is a glibc behaviour, and Windows commits anonymous maps up front and copies on resize. Tests now assert the growth, shrink, and heap-fallback paths are actually taken. 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 mmap-backed buffer is correct. Buffer lifetime is tied to the Arrow table through py_buffer, the fallbacks are complete, and the tests are thorough. Two low-severity concerns: each live row set holds its own mapping (mapping-count limit), and the heap fallback when the map can't grow makes extra transient copies.
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.
…a memoryview The heap retention this works around is glibc's, so other platforms keep the previous bytearray. When a map can't grow, copy what was written via a memoryview instead of a temporary bytes slice. Signed-off-by: Vu Anh Phung <[email protected]> Co-authored-by: Isaac <[email protected]>
The tests replaced mmap.mmap on the stdlib module for the whole process. Swap the mmap module seen by utils instead, and give the large-set fixture an explicit check that it stays above the mmap floor. 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 Nit
Looks good. No correctness concerns. _ArrowStreamBuffer falls back cleanly to the heap when mmap creation, growth or shrinking fails. The pyarrow.py_buffer export keeps the map alive for as long as the table lives. The unit tests cover the growth, shrink, fallback, platform and lifetime paths.
test_non_posix_platform_stays_on_the_heap only reaches the hasattr(mmap, "MAP_PRIVATE") guard on Linux. On other platforms _INLINE_ARROW_USE_MMAP already short-circuits first, so the test passes without exercising that guard. Adding @linux_only or renaming the test would make its intent clearer.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
It only reaches the MAP_PRIVATE guard where the map path is enabled. Signed-off-by: Vu Anh Phung <[email protected]> Co-authored-by: Isaac <[email protected]>
|
Round-8 nit, done in the latest commit: |
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-severity note: the 1 MiB mmap floor is checked against the 4x-inflated size estimate, so the comment's VMA-count bound is about 4x too generous. The mmap and mremap handling, the heap fallbacks and the lifetime of the buffer handed to pyarrow all look correct, and the unit tests cover each branch well.
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 1 MiB floor was compared with the 4x size estimate, so row sets with as little as ~256 KiB of compressed data still got their own map. Compare it with the batches' total size, a lower bound on the stream size, so every map really is at least ~1 MiB and the VMA bound in the comment holds. 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 concern. The mmap helper keeps the old bytearray path when map creation, growth or shrinking fails. Private-map flags, lifetime (the map is held by the pyarrow buffer) and the decompression/IPC behavior are correct, and the tests cover each fallback branch. The gap: highly compressible row sets can skip the map path because the 1 MiB floor is checked against the compressed size.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
… floor Row sets that compress very well could be under 1 MiB on the wire but much larger decompressed, so they stayed on the heap and kept the retention. Start such streams on the heap and move them to a map once the bytes written reach the floor (copying the < 1 MiB written so far). Every map still holds at least 1 MiB of stream data. 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.
✅ No issues identified by the review bot.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
Superseded by #1037, which now contains this PR's memory-map target (commit 8636c7f on top of #1037's single-stream LZ4 decode). The two had to land together: decoding through pyarrow alone made large inline results worse than main (1.7 GB fetchall_arrow: peak 2909 vs 1810 MB, 2859 vs 1637 MB resident after free). Combined, live with use_cloud_fetch=False: 1.7 GB peak 1810 -> 1758 MB and after free 1637 -> 26 MB vs main, with fetch time unchanged. At 64 threads on a 1,000-row inline result: 528 -> 1109 statements/s. Details and the full tables are in #1037's description. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
2e7600ad
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.