fix: a profile must not measure the assert build or its own fixture (#445) - #504
Conversation
…445) Two artifacts in bench/run_profile.sh, both found while profiling the load for #445, and both of the class the script already exists to prevent: output that looks like data. AN ASSERT BUILD IS NOT A SLOWER VERSION OF THE SAME PROFILE. --enable-cassert compiles in validation production never runs, and it does not distribute evenly. Profiled against the assert PG18 here, the largest single self-time entry of the ingest shape was verify_compact_attribute at 9.28%, a function that does not exist in a production build, with every other percentage diluted against it. A reader would reasonably have gone looking at tuple descriptor validation. That is the same category as an unopenable perf event, which this script already refuses on the grounds that profiling would report an empty profile as data, so it gets the same treatment: refuse, with PROFILE_ALLOW_CASSERT=1 to override, because an assert build is still worth profiling when it is the only one to hand and two assert builds compare fine with each other. The build is printed with the event and the unwind method, for the same reason those are. THE INGEST SHAPE PROFILED ITS OWN DATA GENERATOR. It was INSERT INTO pw SELECT g, md5(g::text) FROM generate_series(1,200000) g so md5_calc took 12.31% and pg_md5_hash 1.65%: about 14% of an "ingest" measurement was the fixture inventing strings inside the timed statement, inflating the denominator every write-path entry was measured against. The rows are now materialised into a heap table first, outside the profiled statement, which is also closer to what a load is: rows arriving from somewhere rather than computed per row by the insert. Proved by running it: the assert build now exits 1 with a FATAL naming the measured cost, PROFILE_ALLOW_CASSERT=1 lets it through and labels every percentage, and on a non-assert build md5_calc leaves the profile entirely. The numbers in #499 were taken through both artifacts, so its 33.9% and 14.6% are directionally right and quantitatively off. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ChronicallyJD
left a comment
There was a problem hiding this comment.
Approving. Both are real, both are my defects, and the second one invalidates numbers I published.
The assert-build argument is the one I should have made myself. The script already refuses an unopenable perf event on the grounds that it "would report an empty profile as data", and then does not apply that same standard to the build it is profiling. verify_compact_attribute at 9.28% self time — the largest single entry, a function that does not exist in production, diluting every other percentage — is exactly the failure the rest of the script is built around. Refusing with an override, and printing the build alongside the event and the unwind method, is the consistent treatment.
The fixture defect is worse and it is mine. INSERT ... SELECT md5(g::text) FROM generate_series(...) put the data generator inside the timed statement, so about 14% of an "ingest" profile was the fixture inventing strings. I chose that shape for convenience and never asked what was in the denominator.
So #499's ingest figures are wrong and I have quoted them repeatedly — on #472 when sizing the cache, and in the benchmark report. Directionally right, quantitatively off, and the corrected ~70% makes the case for #502 stronger than the number I gave it. I will correct both.
Checked
grep -c 'enable-cassert'does not false-positive on--disable-cassert: "disable" isd-i-s-a-b-l-e, soenable-cassertis not a substring of it. A build configured with an explicit--disable-cassertis correctly read as non-assert.grep -cprints0on no match and the|| trueabsorbs its exit 1 underpipefail, soCASSERTis always a number and the!= "0"test cannot fall through on an empty string.- The override warns per run and the tail labels the build, so a saved profile carries its own caveat rather than depending on someone remembering.
One residual, and one new interaction
The heap source is now in the denominator. Reading 200,000 rows from pw_src is far cheaper than computing md5 per row, and "rows arriving from somewhere" is the right model, so this is a large improvement rather than a trade. Worth one clause in the comment that the source scan is still measured, so the next person does not rediscover it as a third artifact.
More interesting: once #502 lands, the ingest number stops being window-independent. pw is never truncated between loop iterations, so each pass appends another ~200,000 rows and the table grows for the whole window. That is harmless today. With the FSST verdict cache it is not: the first row groups pay for the decision and later ones reuse it, so the measured FSST share falls as the window lengthens, and PROFILE_SECS becomes an input to the answer.
That does not affect your before/after here — both arms ran the same window, which is why the comparison holds. It affects anyone who later profiles ingest at a different PROFILE_SECS and compares against these numbers.
Cheapest fix is a TRUNCATE pw inside the loop before each insert, so every iteration profiles a load starting from the same state; that also removes the growing-relation effect on row-group flushing. It does put a truncate in the timed statement, which is a smaller artifact than the one being removed but is still one, so the alternative is to leave it and say in the comment that the ingest shape measures a sustained load rather than a cold one, and that its FSST share is window-dependent once the cache exists.
Either is fine. Silently window-dependent is the thing to avoid, and it is not obvious from reading the script.
On the correction itself
Recording that #499's numbers were taken through both artifacts, in the commit rather than leaving it to be rediscovered, is the right call. I would rather have my published figures corrected in the open than quietly superseded, and the same applies to the 48% to 11.7% on #445.
…mmandprompt#505) Every bench harness assumed an environment that existed only because someone built it by hand. The knowledge was spread across memories, a HANDOFF.md that is deliberately not in the repository, and issue comments, so a second machine -- or this one after a rebuild -- could not reproduce a number. This is the first half: the PostgreSQL builds, the contrib the suites need, and an audit that says whether a box can run bench/ at all. It does NOT install Citus, TimescaleDB, DuckDB or the ClickBench dataset, and says so in its own header rather than leaving that to be discovered. ## `check` is the load-bearing part It changes nothing, runs anywhere, and answers the question the issue actually asks: can this box reproduce a number? A provisioning script nobody can verify is how the undocumented box happened in the first place. It writes down what was written down nowhere: * **The naming convention is a contract.** pgNNa is an assert build for the matrix; pgNNn is NON-assert and is where every number in docs/benchmarks.md comes from; pgNN_san is clang with address and undefined sanitizers. Running a benchmark on an assert build is not a small error -- commandprompt#504 had to teach the profiler to refuse one because asserts put verify_compact_attribute at 9.28% of an ingest profile. * **The suffix is checked against `pg_config --configure`, not trusted.** The name is a claim; the flags are the fact. A pg18n accidentally built with cassert would produce quietly wrong benchmark numbers and nothing else on the box would notice. * **The privilege split**, which differs per script and fails in the worst direction: run_bench.sh, _fsst and _readstream install and want root; the installing scripts redirect make install to /dev/null, so run unprivileged they fail QUIETLY and measure the previously installed .so. run_bench_join.sh and run_clickbench.sh do not install and must not be run as root -- `check` warns when it is running as root for exactly that reason. * **initdb needs --locale=C explicitly**, because sudo and runuser do not carry LANG and initdb then leaves a directory that is not a cluster, which reads as a start failure several steps later. ## It found something on the box it was written from btree_gist is present on all five assert builds of the bench host and **absent from pg18n**, the non-assert build benchmarks actually run on. That install is a sixth prefix nobody had checked; since commandprompt#448 test/temporal.sh hard-fails rather than skipping when btree_gist is missing. ## Two traps this hit while being written, both now handled **`make install PG_CONFIG=... ` in a contrib directory inside a configured source tree ignores PG_CONFIG.** It builds in-tree and installs to that tree's own ./configure --prefix. The first version reported "installed btree_gist into pg18n" while the files went to an unrelated prefix -- exit 0, empty log, nothing where it was wanted. Fixed with USE_PGXS=1. **And it trusted the exit status.** A build system that installs the right files into the wrong prefix returns 0, so the installer now verifies the .control file exists at `pg_config --sharedir` afterwards and fails if it does not. ## Tested by running it on two boxes that disagree An audit that cannot report a problem is worth nothing, so it was run where the answers differ: bench host 19 ok, 0 warn, 1 missing (found the pg18n gap) container 13 ok, 4 warn, 4 missing (before), 17 ok, 4 warn, 0 missing (after) Every state -- ok, WARN and MISSING -- is produced by a real box, and the root-user warning fires on one and not the other. `contrib` is idempotent: the second run reports "already in" for all six prefixes and changes nothing. Refs commandprompt#505, commandprompt#504, commandprompt#448.
…mmandprompt#505) Every bench harness assumed an environment that existed only because someone built it by hand. The knowledge was spread across memories, a HANDOFF.md that is deliberately not in the repository, and issue comments, so a second machine -- or this one after a rebuild -- could not reproduce a number. This is the first half: the PostgreSQL builds, the contrib the suites need, and an audit that says whether a box can run bench/ at all. It does NOT install Citus, TimescaleDB, DuckDB or the ClickBench dataset, and says so in its own header rather than leaving that to be discovered. ## `check` is the load-bearing part It changes nothing, runs anywhere, and answers the question the issue actually asks: can this box reproduce a number? A provisioning script nobody can verify is how the undocumented box happened in the first place. It writes down what was written down nowhere: * **The naming convention is a contract.** pgNNa is an assert build for the matrix; pgNNn is NON-assert and is where every number in docs/benchmarks.md comes from; pgNN_san is clang with address and undefined sanitizers. commandprompt#504 had to teach the profiler to refuse an assert build because asserts put verify_compact_attribute at 9.28% of an ingest profile. * **The suffix is checked against `pg_config --configure`, not trusted.** The name is a claim; the flags are the fact. A pg18n accidentally built with cassert would produce quietly wrong benchmark numbers and nothing else on the box would notice. * **The privilege split**, which fails in the worst direction: the installing harnesses redirect make install to /dev/null, so run unprivileged they fail QUIETLY and measure the previously installed .so. `check` warns when it is running as root, because run_bench_join.sh and run_clickbench.sh must not be. * **initdb needs --locale=C explicitly**, because sudo and runuser do not carry LANG and initdb then leaves a directory that is not a cluster. ## What it found btree_gist is present on all five ASSERT builds of the bench host and **absent from pg18n and pg18_san** -- the two prefixes that are not assert builds, which are exactly the two nobody had enumerated. pg18n is where every published benchmark number comes from. Since commandprompt#448, test/temporal.sh hard-fails rather than skipping when btree_gist is missing. ## Three traps this hit while being written **`make install PG_CONFIG=...` inside a configured source tree ignores PG_CONFIG.** A contrib directory there builds in-tree and installs to the parent tree's ./configure --prefix. The first version reported "installed btree_gist into pg18n" while the files went to an unrelated prefix -- exit 0, empty log, nothing where it was wanted. USE_PGXS=1 makes PG_CONFIG authoritative. (An out-of-tree extension like this one is unaffected: its Makefile takes PG_CONFIG as authoritative already.) **It trusted the exit status.** A build system that installs the right files into the wrong prefix returns 0, so the installer now verifies the .control file exists at `pg_config --sharedir` afterwards and fails if it does not. That verification is what caught the third trap. **A sanitizer build's own pg_config is instrumented and leaks on exit**, so with leak detection on it exits NON-ZERO and every `$(pg_config --sharedir)` comes back empty -- the failure then reads as "the extension is missing" rather than "I could not ask where it goes". detect_leaks=0 is exported for that reason. ## And the auditor had the same blind spot it was written to catch The first version enumerated the benchmark and matrix prefixes and skipped the sanitizer one, so it could not have reported the pg18_san gap -- the same "checked five, missed the sixth" shape that left the gap in the first place. The contrib audit and installer now iterate every prefix the script knows about. ## Tested by running it on two boxes that disagree An audit that cannot report a problem is worth nothing, so it was run where the answers differ. Every state it can emit -- ok, WARN, MISSING -- is produced by a real machine, and the root-user warning fires on one and not the other: bench host 19 ok, 0 warn, 1 missing (found the pg18n gap; read-only) container 13 ok, 4 warn, 4 missing before container 18 ok, 4 warn, 0 missing after, all seven prefixes `contrib` is idempotent: a second run reports "already in" and writes nothing. Refs commandprompt#505, commandprompt#504, commandprompt#448.
…mmandprompt#505) Every bench harness assumed an environment that existed only because someone built it by hand. The knowledge was spread across memories, a HANDOFF.md that is deliberately not in the repository, and issue comments, so a second machine -- or this one after a rebuild -- could not reproduce a number. This is the first half: the PostgreSQL builds, the contrib the suites need, and an audit that says whether a box can run bench/ at all. It does NOT install Citus, TimescaleDB, DuckDB or the ClickBench dataset, and says so in its own header rather than leaving that to be discovered. ## `check` is the load-bearing part It changes nothing, runs anywhere, and answers the question the issue actually asks: can this box reproduce a number? A provisioning script nobody can verify is how the undocumented box happened in the first place. It writes down what was written down nowhere: * **The naming convention is a contract.** pgNNa is an assert build for the matrix; pgNNn is NON-assert and is where every number in docs/benchmarks.md comes from; pgNN_san is clang with address and undefined sanitizers. commandprompt#504 had to teach the profiler to refuse an assert build because asserts put verify_compact_attribute at 9.28% of an ingest profile. * **The suffix is checked against `pg_config --configure`, not trusted.** The name is a claim; the flags are the fact. A pg18n accidentally built with cassert would produce quietly wrong benchmark numbers and nothing else on the box would notice. * **The privilege split**, which fails in the worst direction: the installing harnesses redirect make install to /dev/null, so run unprivileged they fail QUIETLY and measure the previously installed .so. `check` warns when it is running as root, because run_bench_join.sh and run_clickbench.sh must not be. * **initdb needs --locale=C explicitly**, because sudo and runuser do not carry LANG and initdb then leaves a directory that is not a cluster. ## What it found btree_gist is present on all five ASSERT builds of the bench host and **absent from pg18n and pg18_san** -- the two prefixes that are not assert builds, which are exactly the two nobody had enumerated. pg18n is where every published benchmark number comes from. Since commandprompt#448, test/temporal.sh hard-fails rather than skipping when btree_gist is missing. ## Three traps this hit while being written **`make install PG_CONFIG=...` inside a configured source tree ignores PG_CONFIG.** A contrib directory there builds in-tree and installs to the parent tree's ./configure --prefix. The first version reported "installed btree_gist into pg18n" while the files went to an unrelated prefix -- exit 0, empty log, nothing where it was wanted. USE_PGXS=1 makes PG_CONFIG authoritative. (An out-of-tree extension like this one is unaffected: its Makefile takes PG_CONFIG as authoritative already.) **It trusted the exit status.** A build system that installs the right files into the wrong prefix returns 0, so the installer now verifies the .control file exists at `pg_config --sharedir` afterwards and fails if it does not. That verification is what caught the third trap. **A sanitizer build's own pg_config is instrumented and leaks on exit**, so with leak detection on it exits NON-ZERO and every `$(pg_config --sharedir)` comes back empty -- the failure then reads as "the extension is missing" rather than "I could not ask where it goes". detect_leaks=0 is exported for that reason. ## And the auditor had the same blind spot it was written to catch The first version enumerated the benchmark and matrix prefixes and skipped the sanitizer one, so it could not have reported the pg18_san gap -- the same "checked five, missed the sixth" shape that left the gap in the first place. The contrib audit and installer now iterate every prefix the script knows about. ## Tested by running it on two boxes that disagree An audit that cannot report a problem is worth nothing, so it was run where the answers differ. Every state it can emit -- ok, WARN, MISSING -- is produced by a real machine, and the root-user warning fires on one and not the other: bench host 19 ok, 0 warn, 1 missing (found the pg18n gap; read-only) container 13 ok, 4 warn, 4 missing before container 18 ok, 4 warn, 0 missing after, all seven prefixes `contrib` is idempotent: a second run reports "already in" and writes nothing. Refs commandprompt#505, commandprompt#504, commandprompt#448. ## Getting the source: a tarball is enough, and that is the safer shape `contrib` no longer needs a configured source tree. It resolves an extracted tree if one exists, otherwise extracts `postgresql-<version>.tar.*` to a scratch directory, and always builds with USE_PGXS=1 against the target prefix's own installed pgxs. That is jdatcmd's suggestion from doing it by hand on the bench, where no extracted tree existed at all, and it is better than what I had: it sidesteps the in-tree PG_CONFIG trap **by construction** instead of guarding against it. `check` now also reports whether a missing module is fixable on this box: MISSING btree_gist in pg16a (test/temporal.sh hard-fails; 'provision.sh contrib' can fix it) MISSING btree_gist in pg16a AND no contrib source for PostgreSQL 16.14 on this box Reporting a problem without saying whether it can be fixed here is half an answer, and the half that sends someone looking. Both branches were exercised by removing btree_gist from pg16a and re-running with and without a reachable source.
Two artifacts in
bench/run_profile.sh, found while profiling the load for #445. Both are the class that script already exists to prevent: output that looks like data.1. An assert build is not a slower version of the same profile
--enable-cassertcompiles in validation production never runs, and it does not distribute evenly. Profiled against the assert PG18 on this box, the top of the ingest profile was:The largest single self-time entry is a function that does not exist in a production build, with every other percentage diluted against it. A reader would reasonably have gone looking at tuple descriptor validation.
That is the same category as an unopenable perf event, which this script already refuses on the grounds that "profiling would report an empty profile as data". So it gets the same treatment: refuse, with
PROFILE_ALLOW_CASSERT=1to override, since an assert build is still worth profiling when it is the only one to hand and two assert builds compare fine with each other. The build is now printed with the event and the unwind method, for exactly the reason those are.2. The ingest shape profiled its own data generator
put
md5_calcat 12.31% andpg_md5_hashat 1.65%. About 14% of an "ingest" measurement was the fixture inventing strings inside the timed statement, inflating the denominator every write-path entry was measured against. Rows are now materialised into a heap table first, outside the profiled statement, which is also closer to what a load is: rows arriving from somewhere rather than computed per row by the insert.Proved by running it
PROFILE_ALLOW_CASSERT=1lets it through and labels every percentage, and the tail printsBuild: ASSERT (--enable-cassert)md5_calcleaves the profile entirelyWhy this matters immediately
With both artifacts removed, the ingest profile is a different document, and the #472 result is far larger than I had reported:
mainencode_fsst_sharedPgColumnarFsstBuildChunkTablefsst_count_addfsst_cand_cmpI had posted 48% to 11.7% on #445 from measurements taken through both artifacts. Same direction, wrong size, and the corrected figures are on that issue.
#499's own 33.9% and 14.6% were taken the same way, so they are directionally right and quantitatively off. Noted in the commit rather than left to be rediscovered.
🤖 Generated with Claude Code