The package path costs ~8x the builtin path in SQLite work for byte-identical facts, and its vm_step is non-deterministic #113
Labels
No labels
code-review
correctness
dos
performance
security
severity/high
severity/low
severity/medium
tech-debt
Kind/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
h-dv/code-index#113
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Two findings from #84 phase 4's cost leg (
crates/indexer/tests/ruby_package_cost.rs, baselinetests/corpus/ruby-package-cost.json). Both are recorded rather than fixed. Full context:_prdoc/records/84-P4-parity-deltas.md§4.The setup
Two legs over the SAME 156
ruby-sinatrafiles, both throughindex_path_with_packages, in one process, back to back, median of 3 warmed-up passes:crates/plugins/src/ruby.rs, no packages;tests/packages/rubywith all six pool capabilities granted.The two produce 1260 identical symbols, 18,450 identical ref sites and 231 identical imports. The only content differences are the five pinned deltas of #112 and #84.
Finding 1 — 8x the SQLite work for the same rows
vm_stepfullscan_stepsortautoindextemp.pool_admit's admission JOINs are not the explanation:PoolAdmit::prepareturns on from the SCHEMA (symbols.contribution_id+component_capabilitiesexist), not from whether a package is installed, so both legs carry every admission join. The cause is not yet known.For scale: the 2026-08-29 regression that motivated
corpus_cost.rswas 3.2x wall for byte-identical content, and was treated as a defect rather than blessed.corpus_costcannot see this one — its own doc says why:Finding 2 — the package leg's
vm_stepis not deterministicSeven consecutive medians-of-three on one box:
Single readings without the warm-up were worse: 101.5M..111.2M, 9.6%.
The content is deterministic — two package-leg indexes of the same tree produce byte-identical symbol and ref projections — so this is cost non-determinism only.
corpus_cost's premise is that these counters are "properties of the STATEMENTS and the DATA, not of the machine", proven over eleven passes at 0.02%; that premise does not hold on the package path.The direct consequence:
ruby_package_cost.rs'sBAND_UP_PCTis 8 rather thancorpus_cost's 5, i.e. the gate can see a 10% regression in SQLite work and not a 5% one. Tightening it needs this understood first.Also worth knowing
The blessed
wall_ratio_pct(350, ceiling 525, floor 227) was measured on a box at load average 6..12 with twelve cores. It should be re-blessed on an isolated runner before it is relied on to catch anything below ~1.5x.The cause is found, and this issue's own framing is what hid it
Correct that it is not
pool_admit. The reason it stayed unknown is the sentence beside it — "~8x the SQLite work for byte-identical facts". That reads as the same work, done more expensively. It is not. The package leg runs a whole pass the builtin leg does not run at all.Measured, not inferred
New probe:
crates/indexer/tests/package_cost_attribution.rs. It installs the sameSQLITE_TRACE_PROFILEhookcorpus::work::measureuses, but readssqlite3_sql()off the statement pointer first, so the same total is bucketed by SQL text instead of summed. Both legs, one process, the same 156ruby-sinatrafiles, same fixture asruby_package_cost.Top statements by
(package - builtin)vm_step:UPDATE refs SET influence = 2, influence_component_id = COALESCE(…)—influence::apply_generalstep 3, the COMPETED armUPDATE refs SET influence = 2, …(the anchor/bridge follow-up)UPDATE refs SET influence = 1, …— step 2, ENDPOINTUPDATE refs SET influence = 0, influence_component_id = NULL …— step 1, the re-derive CLEARUPDATE refs SET (target_id, resolved_by) = …— the resolver properExcess vm_step 94,901,010; the top five account for 102% of it (over 100% because a few statements are marginally cheaper on the package leg). The four
influencestatements alone are 96,942,182 — everything.Note the
execscolumn.builtin 0.Influence::is_builtin_only_indexshort-circuits when the grant fingerprint is empty, which is every index with no dynamic contribution holding a capability. So on the builtin leginfluence::apply_generaldoes not run, and on the package leg it runs once over all 18,450 refs.What that means for both findings
Finding 1 (the 8x) is INHERENT, and should be ratcheted and named rather than optimised away. Classifying every ref against every dynamic producer's grant is what
refs.influenceIS — #77's disclosure that a package could have changed this edge. A package-bearing index does strictly more work because it has strictly more to disclose. Comparing it to a builtin leg that skips the pass entirely is not a like-for-like ratio and never was.But one number in there is not inherent. 3,838,889 fullscan steps on step 3, over 18,450 refs, is ~208 scan steps per ref.
apply_generalinlinesdynamic_admit_selectas a correlated subquery — three times in step 2, twice more insidecompetitors(…)in step 3 — so SQLite re-evaluates thefile_contributions ⋈ component_capabilities ⋈ extraction_componentsrelation once per ref row rather than once per pass. Materialising it into an indexed temp table the wayPoolAdmit::preparealready materialisestemp.pool_admitis a migration-free change to one function, and 97% of the excessfullscan_stepsits behind it.Finding 2 (the non-determinism) is almost certainly the same statement. It is the only one in the pass with a multi-million-step scan whose plan SQLite is free to re-cost between runs; the other three move 18,449 fullscan steps each and the builtin leg holds 0.04%. That is a prediction this probe can settle: re-run it seven times and see whether the 3.3% spread lives in the top bucket. I have not done that — stated rather than implied.
Recommended disposition
dynamic_admit_selectonce per pass ininfluence::apply_general, mirroringPoolAdmit::prepare. Then re-measure — if the excessfullscan_stepcollapses and the spread with it,BAND_UP_PCTcan come down from 8 towardcorpus_cost's 5 and this gate stops being blind to a 5% regression.ruby_package_cost's doc — which it already does, honestly, and which is now backed by a cause rather than by an open question.Not changed by #112
ruby_package_costpasses unmodified with #112's language-profile gate in place (the_package_leg_cost_stays_within_the_blessed_bandgreen,tests/corpus/ruby-package-cost.jsonuntouched). #112 adds twoGLOBdisjuncts to statements that are not in the top fifteen here.Caveat on the instrument, since #158 is the adjacent lesson
This probe measures a cold index pass. Like
cost-baseline.jsonit issues no daemon read, so it cannot see a read-path change. Anything about query cost at read time needs a different instrument, and reading this file's silence as approval would be the same mistake.🤖 Generated with Claude Code
https://claude.ai/code/session_01K1zj5VcFJvJt3pQxe9259K
BUILD IT — built, measured, and BOTH findings are closed by the one change
The previous comment recommended materialising
dynamic_admit_selectand said "then re-measure — if the excessfullscan_stepcollapses and the spread with it,BAND_UP_PCTcan come down." Done, and it did both.influence::apply_generalnow buildstemp.influence_dyn_admitonce per pass, with indexes oncontribution_idandfile_id, and the eight correlated-subquery sites read the table. That mirrorscapabilities::PoolAdmit::preparefor the reason that function already gives: an admission relation is a property of the GENERATION, not of the row being classified. Migration-free, one function, and the relation it re-derived per ref holds six rows.Finding 1 — the excess
Measured by this issue's own probe (
crates/indexer/tests/package_cost_attribution.rs), same 156ruby-sinatrafiles, same fixture, one process, both legs:vm_stepfullscan_stepvm_step(pkg/builtin)fullscan_stepvm_stepfullscan_stepvm_stepThe builtin leg is unmoved. That is what says this is the package pass and not the machine.
The remaining 1.66x is the inherent part this issue's previous comment identified and which should stay:
apply_generalruns once on the package leg and zero times on the builtin one, becauseInfluence::is_builtin_only_indexshort-circuits on an empty grant fingerprint. A package-bearing index has strictly more to disclose.Finding 2 — the non-determinism went with it, which is what identifies the cause
The prediction was "almost certainly the same statement … that is a prediction this probe can settle." Settled. Four consecutive SINGLE readings — no median, no warm-up, the harshest case, the one that used to spread 9.6%:
against the BUILTIN leg's 0.04% in the same runs. The 3.3% spread lived in the multi-million-step scan, and it is gone because the scan is.
So
BAND_UP_PCTcomes down from 8 to 5,corpus_cost's value: 5% is now a 135x margin over measured noise, the same shapecorpus_costhas, and this gate can see a 5% SQLite regression again instead of only a 10% one.The mutations, RUN
Materialisation drops rows — build the table with
WHERE 0. RESULT: RED, 10 of 12influence_classificationtests, e.g.left: (Some(0), None) right: (Some(2), Some(9)).The rebuild — replace
DROP TABLE IF EXISTSwithCREATE TEMP TABLE IF NOT EXISTS. RESULT: SURVIVOR — 12 passed, 0 failed.That survivor is a finding about the change itself and it is worth stating plainly: materialising buys a staleness hazard that the subquery form could not have. A temp table belongs to the CONNECTION, not the transaction, so a second pass on one connection reads the first pass's grants unless the build drops it — and every pass in that file opened a fresh connection, so nothing in the repository graded it.
a_revoked_grant_does_not_survive_in_the_materialised_admission_setruns two passes on ONE connection with the grant revoked between them; under that mutation it is RED and it is the only test that is.And a gate died in the process — recorded, not papered over
ruby-package-cost.json'swall_ratio_pctfell from 350 to 142, andRATIO_DOWN_PCT's own doc says what the floor is for: "a package leg that silently ran the BUILTIN pass instead reads as ~100." At 35% the floor is now 92 — below the very reading it exists to refuse. A bound that cannot reach its own case is not a bound.Narrowing it to 25 (floor ~107) is not available either, because the ratio's noise grew as the ratio shrank: two median-of-3 runs minutes apart on this box, at load average 22..29, read 101 and 142 — a 40% swing on a quantity whose whole spread used to be 17%. A floor at 107 would go red on an honest run.
So the floor is widened to 55 and demoted in writing: it is a coarse tripwire, and the job of proving the two legs are two belongs to the four structural controls that run BEFORE any bound is read — equal symbol counts, non-zero package-language files, zero builtin-ruby files on the package leg, both legs executing SQLite work. Those are contention-immune, which a wall clock never was. All four held across every measurement above.
On the re-frame
Agreed and applied where it counts:
BAND_UP_PCT's doc,ruby-package-cost.json's bless reason and_prdoc/records/84-P4-parity-deltas.md§4.1 now all say the 8x was a fifth pass rather than a slower one. The issue title is still the old framing; renaming it is the owner's call.Caveat on the instrument, unchanged
This is still a COLD INDEX measurement. It issues no daemon read and cannot see a read-path change. Nothing here says anything about query cost at read time.
Commit
5876b74onwip/rubypkg, based onintegrationata33210d.🤖 Generated with Claude Code
https://claude.ai/code/session_01K1zj5VcFJvJt3pQxe9259K
Triage 2026-09-06: CLOSING. Both findings — the 8× cost and the non-determinism — fell to one change, and the band was tightened rather than widened.
Verified against master. Landed as
09cc387, re-measured inf6a878a.The change
const DYN_ADMIT: &str = "temp.influence_dyn_admit"—crates/indexer/src/influence.rs:410;materialize_dyn_admitat:417, building the table with indexes oncontribution_idandfile_id, rebuilt per pass (staleness reasoning at:405-409). The admission relation is derived once per pass instead of once per ref.The measurement, on this box, this run
vm_stepratio (pkg/builtin)fullscan_stepratiof6a878a's own record puts the corpus-side figures atvm_step109,095,227 → 25,865,288 (−76.3%) andfullscan_step4,045,989 → 541,182 (−86.6%), against a prediction of −75.9% / −86.4% made before the run. A prediction that lands that close is what separates a real attribution from a harness artifact.The band went DOWN, which is the part that makes this a close
BAND_UP_PCT: i64 = 5—crates/indexer/tests/ruby_package_cost.rs:102, down from 8 to matchcorpus_cost. Finding 2's residual non-determinism now has to fit inside a tighter gate than the one it was filed against. A cost issue closed by widening its own ratchet would not be closed at all.The staleness hazard the materialisation introduces is graded:
a_revoked_grant_does_not_survive_in_the_materialised_admission_set—crates/indexer/tests/influence_classification.rs. That is the right test to have written, because caching an admission relation is exactly how a revoked grant survives.Runs (exit 0, corpus mounted)
Non-zero
executed=on both — not theexecuted=0 unavailable=1false green.Residual, named
The remaining 1.66× is the inherent extra
influence::apply_generalpass. It is not a defect and should stay; what it should not do is drift, which the 5% band now enforces.f6a878a's record also notes the condition gate refused the old band by name (0.3.0 against a leg running 0.4.0) and demanded a re-measure rather than a re-bless — so this number is attached to the conditions it was taken under.🤖 Triage lane, 2026-09-06, master
45cf6e4