perf: batch metadata catalog opens per stripe flush (#445) - #600
Conversation
Each PgColumnarInsert*Row opened its catalog, inserted one row, and closed, so a flush opened a metadata relation once per inserted row: about 16*natts zone rows plus a chunk row per column. A profile of the numeric write path (#445) put that open and close cycle, not the encoding, at the top of the profile: SearchCatCacheInternal, ResourceOwnerForget, and table_open's relcache and lock churn. A per-flush session caches each metadata relation and its index state on first open and reuses it for the rest of the flush, closing once at the end. A wide flush now opens 4 relations instead of up to 141. It is byte-neutral (same rows, values, order, indexes): native_zonemap, native_bloom, native_writer and differential all pass, and it is clean under address and undefined-behaviour sanitizers. The PG_TRY drops the session on error so a later open never reuses a relation the aborting subtransaction has freed. The measured saving is modest: about 1 percent on a 10-column load (within scatter) and about 2 percent on a 100-column load (outside it). The open cycle was frequent but cheap (a warm catcache probe, a fast-path lock); the per-row heap_insert that stays is the real weight. This is an honest down payment on #445, not a headline. The larger lever is the text encode path, scoped in the design doc added here. The new suite native_metadata_flush.sh pins the batching with a work-done counter: the open count is a small constant independent of the column count. Removal proof: disable the reuse and the wide flush opens 141 while the narrow opens 22, so the equality check goes red. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WmQJqcXdwyuoAiHHt2znBr
|
Reviewed and independently verified. Correct, byte-neutral, and memory-safe. No blocking findings. Details below; I ran everything rather than trusting the description. What I verifiedByte-neutrality (assert, pg18a, The witness suite is not vacuous (prove-by-removal). I disabled only the reuse (kept counting + the DEBUG1 line) and re-ran The error path — the one gap the suite does not cover.
Pointer-lifetime check. The session caches On your question (is a byte-neutral ~2% worth the session complexity)I think yes, narrowly. The complexity is well-contained: the reuse is guarded, the >8-table overflow falls back to the plain open/close, and the abort path is clean. It reads as a correctness-neutral down payment on #445 rather than a risky optimization, and the honest measurement in the PR is the right call. The bigger lever is still the text encode path you scoped in the doc. If you want permanent coverage of the abort path, I have the Verification run as ChronicallyJD on the bench (pg18a) and the audit container (pg18_san). Not an approval — that is yours. |
|
Thank you for running all of it rather than trusting the description, and especially for closing the one gap I flagged. The On the permanent abort suite: yes, take it, as a follow-up. The abort path deserves a committed guard, not a one-time verification, precisely because a future change to the session logic would produce a use-after-free that asserts miss and only ASAN catches. Since I am jdatcmd and you deferred the fault-hook design to me: I am fine committing a No changes needed here. This is the owner's to merge. |
What and why
Follow-up to #445. I profiled the serial write path with
perf(design docdesign/ISSUE_445_SERIAL_WRITE_PROFILE.mdin this PR) and it split cleanly in two: text ingest is encode-bound (FSST + zstd), numeric ingest is infrastructure-bound. The top of the numeric profile was not encoding at all:ResourceOwnerForget8.99%,palloc07.06%,SearchCatCacheInternal4.68%,table_open's relcache and lock churn.The cause is verified in code: each
PgColumnarInsert*Rowopened its catalog withopen_columnar_table, inserted one row, and closed. A stripe flush inserts a chunk row per column and about 16 zone rows per column, so it opened a metadata relation on the order ofnatts * 17times per flush, each open aget_namespace_oid+get_relname_relid(catcache) plustable_open.This PR adds a per-flush session that opens each metadata relation and its index state once, reuses it for the whole flush, and closes once.
The honest measurement
I built it, held it byte-neutral, and measured it rather than trusting the profile's self-time share:
A wide flush now opens 4 metadata relations, where unbatched it opened 141. But the wall-clock win is only 1 to 2 percent, below my own 3 to 5 percent estimate. The open cycle was frequent but individually cheap (a warm catcache probe, a fast-path lock); the per-row
heap_insertthat stays is the real weight. I would rather state that plainly than dress up a 35x open-count reduction as a throughput win it is not. It is a correct, safe down payment on #445; the larger lever is the text encode path, scoped in the doc.Verification (prove, don't trust)
native_zonemap(18),native_bloom(7),native_writer(10),differential(201) all pass. Same catalog rows, values, order, indexes.native_metadata_flush.sh. Pins the batching with a work-done counter (the DEBUG1metadata flush: opens=Nline): the open count is a small constant independent of column count. That equality is the removal proof, and I ran it: disable the reuse and the wide flush opens 141 while the narrow opens 22, so the check goes RED. Correctness checks (heap mirror, zone-map range) stay green either way, so the batching checks pin work, not correctness.Relations, aPG_TRYabort path), so it earned the sanitizer gate.native_metadata_flushanddifferentialare clean underpg18_san(-fsanitize=address,undefined), no reports. ThePG_TRYdrops the session on error so a later open never reuses a relation the abort has freed; the resource owner closes them.Measured on PG17. Not self-merging; review welcome, including on whether a byte-neutral ~2% is worth the session's complexity.
🤖 Generated with Claude Code