The promotion lock exceeds the ceiling plugin enable discloses to operators — 6,297–6,543 ns/row against 6,000 at the 100k shape, reproduced on two machines, and it is a REGRESSION #208

Closed
opened 2026-09-07 12:04:38 +02:00 by buildagent · 3 comments
Member

Found while clearing the nightly Plugin path cost + pool throughput job for v0.27.0. Three CI reds in a row, and it reproduces locally. This is not the contention it was first taken for.

Measured

bench_promotion_lock_at_the_hundred_thousand_file_shape, at 358e1ec:

where load generation rows promotion lock ns/row ceiling
CI runner (run 643) shared 2,300,002 15,048 ms 6,543 6,000
this box load1 5.06, 1 sibling cargo 2,300,002 14,482 ms 6,297 6,000

The failure message is the constant's own:

the promotion lock cost 6543 ns per generation row, above the 6000 ns/row that promotion::MEASURED_LOCK_NS_PER_GENERATION_ROW claims and that code-index plugin enable discloses to operators. Re-measure the table in crates/indexer/src/promotion.rs and move the constant with it — do NOT raise the constant alone.

The smaller arms all pass, and the rate rises with scale:

files generation rows ns/row
502 11,502 4,132
2,002 46,002 4,489
8,002 184,002 4,872
100,002 2,300,002 6,297

A flat ceiling over a rate that climbs 4,132 → 6,297 across a 200-fold row count. The 100k arm is the only one that crosses it, which is what the 100k arm exists for.

It is a REGRESSION, established rather than assumed

  • The 100k arm arrived in 1aa6514 (2026-09-04).
  • The last green nightly was 01a478b (2026-09-05), and git merge-base --is-ancestor 1aa6514 01a478b says the arm was present and passing there.
  • git log 01a478b..HEAD -- crates/indexer/src/promotion.rs crates/indexer/tests/bench_promotion_lock.rs is empty. Neither the ceiling nor its test moved.

So something else in that window made promotion measurably more expensive per row.

Candidate mechanism — NOT confirmed, and stated as such

Four migrations landed in the window: m0060 refs_rollup, m0061 file_refs_rollup, m0062, m0063. The first two install seven triggers between them, and four are on refs:

2 AFTER DELETE ON refs
2 AFTER INSERT ON refs

The 100k shape carries 1.1M refs, and the denominator (contributions + symbols + refs + imports) counts base rows only — so per-row trigger work would raise ns/row without moving the divisor. That is a plausible mechanism and it fits the timing.

I did not confirm it. I did not establish that generations::promote performs INSERTs or DELETEs on refs rather than an UPDATE of a generation pointer, and if it is an UPDATE these triggers never fire and the cause is elsewhere. Whoever takes this should settle that first — the wrong mechanism would send the fix into the wrong file.

Why this matters beyond a red nightly

The number is a product disclosure, not just a test bound. code-index plugin enable tells an operator how long the promotion lock will hold, derived from this constant, and it is currently 5–9% optimistic at the shape where it matters most.

It is also the class this repo has recorded paying for: a correctness suite cannot see a slowdown. ~3,400 workspace tests pass, CI is green on all five REQUIRED_JOBS, and only this nightly wall-clock ceiling saw it.

What must NOT be done

  • Do not raise the constant alone. The failure message forbids it in terms, and the neighbouring MEASURED_COLLECT_NS_PER_CASCADED_ROW records what happens when a ceiling gets raised reflexively until it bounds nothing.
  • Do not close it as contention. It reproduces on an idle-ish box at load 5 and on the CI runner, and three of the four reds were on runs with no concurrent workload.
  • Do not re-record the table until the cause is known. If the triggers are the cause, the honest record says so and the disclosure moves with a reason; if they are not, moving it hides whatever actually did.
  • Do not delete the 100k arm to make the nightly green. It is the only arm that crosses the ceiling and therefore the only one doing any work.

Release position

This does not mechanically block v0.27.0 — the job is deliberately not in release.yml's REQUIRED_JOBS, by its own comment, because it is a ratio measurement on a shared runner. But the shipped plugin enable disclosure is wrong by 5–9% at the 100k shape, and the release notes should not claim promotion-lock precision.

#41 (scale ceilings — this is one), #161 (the disk pre-flight on the same job), and MEASURED_COLLECT_NS_PER_CASCADED_ROW, whose idle re-measurement on 2026-09-07 found the same scale-dependent shape and was deliberately left alone for a related reason.

Found while clearing the nightly `Plugin path cost + pool throughput` job for v0.27.0. **Three CI reds in a row, and it reproduces locally.** This is not the contention it was first taken for. ## Measured `bench_promotion_lock_at_the_hundred_thousand_file_shape`, at `358e1ec`: | where | load | generation rows | promotion lock | **ns/row** | ceiling | |---|---|---:|---:|---:|---| | CI runner (run 643) | shared | 2,300,002 | 15,048 ms | **6,543** | 6,000 | | this box | load1 5.06, 1 sibling cargo | 2,300,002 | 14,482 ms | **6,297** | 6,000 | The failure message is the constant's own: > the promotion lock cost 6543 ns per generation row, above the 6000 ns/row that `promotion::MEASURED_LOCK_NS_PER_GENERATION_ROW` claims **and that `code-index plugin enable` discloses to operators**. Re-measure the table in `crates/indexer/src/promotion.rs` and move the constant with it — do NOT raise the constant alone. **The smaller arms all pass, and the rate rises with scale:** | files | generation rows | ns/row | |---:|---:|---:| | 502 | 11,502 | 4,132 | | 2,002 | 46,002 | 4,489 | | 8,002 | 184,002 | 4,872 | | **100,002** | **2,300,002** | **6,297** | A flat ceiling over a rate that climbs 4,132 → 6,297 across a 200-fold row count. The 100k arm is the only one that crosses it, which is what the 100k arm exists for. ## It is a REGRESSION, established rather than assumed - The 100k arm arrived in `1aa6514` (2026-09-04). - The last **green** nightly was `01a478b` (2026-09-05), and `git merge-base --is-ancestor 1aa6514 01a478b` says **the arm was present and passing there**. - `git log 01a478b..HEAD -- crates/indexer/src/promotion.rs crates/indexer/tests/bench_promotion_lock.rs` is **empty**. Neither the ceiling nor its test moved. So something else in that window made promotion measurably more expensive per row. ## Candidate mechanism — NOT confirmed, and stated as such Four migrations landed in the window: **m0060 `refs_rollup`**, **m0061 `file_refs_rollup`**, m0062, m0063. The first two install **seven triggers between them**, and four are on `refs`: ``` 2 AFTER DELETE ON refs 2 AFTER INSERT ON refs ``` The 100k shape carries 1.1M refs, and the denominator (`contributions + symbols + refs + imports`) counts base rows only — so per-row trigger work would raise ns/row without moving the divisor. That is a plausible mechanism and it fits the timing. **I did not confirm it.** I did not establish that `generations::promote` performs INSERTs or DELETEs on `refs` rather than an UPDATE of a generation pointer, and if it is an UPDATE these triggers never fire and the cause is elsewhere. Whoever takes this should settle that first — the wrong mechanism would send the fix into the wrong file. ## Why this matters beyond a red nightly **The number is a product disclosure**, not just a test bound. `code-index plugin enable` tells an operator how long the promotion lock will hold, derived from this constant, and it is currently 5–9% optimistic at the shape where it matters most. It is also the class this repo has recorded paying for: **a correctness suite cannot see a slowdown.** ~3,400 workspace tests pass, CI is green on all five `REQUIRED_JOBS`, and only this nightly wall-clock ceiling saw it. ## What must NOT be done - **Do not raise the constant alone.** The failure message forbids it in terms, and the neighbouring `MEASURED_COLLECT_NS_PER_CASCADED_ROW` records what happens when a ceiling gets raised reflexively until it bounds nothing. - **Do not close it as contention.** It reproduces on an idle-ish box at load 5 and on the CI runner, and three of the four reds were on runs with no concurrent workload. - **Do not re-record the table until the cause is known.** If the triggers are the cause, the honest record says so and the disclosure moves with a reason; if they are not, moving it hides whatever actually did. - **Do not delete the 100k arm to make the nightly green.** It is the only arm that crosses the ceiling and therefore the only one doing any work. ## Release position **This does not mechanically block v0.27.0** — the job is deliberately not in `release.yml`'s `REQUIRED_JOBS`, by its own comment, because it is a ratio measurement on a shared runner. But the shipped `plugin enable` disclosure is wrong by 5–9% at the 100k shape, and the release notes should not claim promotion-lock precision. ## Related #41 (scale ceilings — this is one), #161 (the disk pre-flight on the same job), and `MEASURED_COLLECT_NS_PER_CASCADED_ROW`, whose idle re-measurement on 2026-09-07 found the same scale-dependent shape and was deliberately left alone for a related reason.
Author
Member

Correcting my own filing on two counts, and the mechanism is now read rather than guessed.

1. I checked the wrong file

The issue says "git log 01a478b..HEAD on promotion.rs and its test is empty." True, and irrelevant: MEASURED_LOCK_NS_PER_GENERATION_ROW lives in promotion.rs, but promote() — the thing being timed — lives in generations.rs, which did change in the window (c3fc6d6).

I re-checked properly, by comparing the function body itself rather than the file:

promote_in_transaction @ HEAD     md5 0e048dd83e4c6ee5b9760e72357783d4
promote_in_transaction @ 01a478b  md5 0e048dd83e4c6ee5b9760e72357783d4

Byte-identical. The conclusion survives — the timed code did not change — but it was resting on the wrong evidence, and c3fc6d6's 96 added lines to that file are elsewhere in it.

2. My trigger hypothesis was right about the family and wrong about the event

I wrote that m0060/m0061 install AFTER INSERT/AFTER DELETE ON refs triggers and that the 100k shape carries 1.1M refs. Both true. But promote never inserts or deletes refs, so those triggers cannot fire during promotion. Enumerated, every SQL verb in promote_in_transaction:

UPDATE file_contributions   UPDATE files      UPDATE plugin_generations ×2
UPDATE refs                 UPDATE symbols ×2
DELETE FROM resolver_health DELETE FROM symbol_edges
INSERT OR IGNORE INTO resolve_scope_files ×2 / stale_outline_names / stale_path_evidence

I had characterised only 4 of the 7 triggers. The missing three include the ones that matter:

m0060: AFTER UPDATE OF target_id, kind, qualifier, reclassified, lang, generation_id ON refs   (×2)

And promote_in_transaction runs exactly:

UPDATE refs SET target_id = NULL, resolved_by = NULL
 WHERE id IN (SELECT r.id FROM refs r WHERE {crossing})

target_id is the first column in the trigger's OF list. So promotion's own ref update fires two rollup triggers per affected row, on a statement whose code is unchanged, under a schema that gained those triggers inside the regression window. That is a mechanism that explains a per-row cost rise with no code change — which is exactly the shape observed.

What is still not established

How many rows the {crossing} predicate actually touches at the 100k shape. If it is a small fraction of the 1.1M refs, two triggers over that subset may not account for 297–543 ns/row across the whole denominator, and something else is also contributing. Whoever takes this should measure the row count that statement updates before assuming it is the whole story — and note there are also two UPDATE symbols statements, one of which recomputes ref_count through a correlated SELECT COUNT(*) FROM refs (index-served by idx_refs_target, present since m0001).

What does not change

The breach itself: 6,543 ns/row on CI, 6,297 on a second machine, against a 6,000 ceiling, with the smaller arms passing and the rate climbing 4,132 → 6,297. Still a regression, still in a number plugin enable discloses to operators, and the constant is still untouched.

The lesson I would carry out of my own error: "the file that declares a constant is not the file that does the work it bounds." I let the constant's location stand in for the code's, and only caught it by re-deriving the check.

**Correcting my own filing on two counts, and the mechanism is now read rather than guessed.** ## 1. I checked the wrong file The issue says *"`git log 01a478b..HEAD` on `promotion.rs` and its test is empty."* True, and irrelevant: `MEASURED_LOCK_NS_PER_GENERATION_ROW` lives in `promotion.rs`, but **`promote()` — the thing being timed — lives in `generations.rs`**, which did change in the window (`c3fc6d6`). I re-checked properly, by comparing the function body itself rather than the file: ``` promote_in_transaction @ HEAD md5 0e048dd83e4c6ee5b9760e72357783d4 promote_in_transaction @ 01a478b md5 0e048dd83e4c6ee5b9760e72357783d4 ``` **Byte-identical.** The conclusion survives — the timed code did not change — but it was resting on the wrong evidence, and `c3fc6d6`'s 96 added lines to that file are elsewhere in it. ## 2. My trigger hypothesis was right about the family and wrong about the event I wrote that m0060/m0061 install `AFTER INSERT`/`AFTER DELETE ON refs` triggers and that the 100k shape carries 1.1M refs. Both true. **But `promote` never inserts or deletes refs**, so those triggers cannot fire during promotion. Enumerated, every SQL verb in `promote_in_transaction`: ``` UPDATE file_contributions UPDATE files UPDATE plugin_generations ×2 UPDATE refs UPDATE symbols ×2 DELETE FROM resolver_health DELETE FROM symbol_edges INSERT OR IGNORE INTO resolve_scope_files ×2 / stale_outline_names / stale_path_evidence ``` I had characterised only **4 of the 7** triggers. The missing three include the ones that matter: ``` m0060: AFTER UPDATE OF target_id, kind, qualifier, reclassified, lang, generation_id ON refs (×2) ``` And `promote_in_transaction` runs exactly: ```sql UPDATE refs SET target_id = NULL, resolved_by = NULL WHERE id IN (SELECT r.id FROM refs r WHERE {crossing}) ``` `target_id` is **the first column in the trigger's `OF` list**. So promotion's own ref update fires two rollup triggers per affected row, on a statement whose code is unchanged, under a schema that gained those triggers inside the regression window. That is a mechanism that explains a per-row cost rise with no code change — which is exactly the shape observed. ## What is still not established **How many rows the `{crossing}` predicate actually touches at the 100k shape.** If it is a small fraction of the 1.1M refs, two triggers over that subset may not account for 297–543 ns/row across the whole denominator, and something else is also contributing. Whoever takes this should measure the row count that statement updates before assuming it is the whole story — and note there are also two `UPDATE symbols` statements, one of which recomputes `ref_count` through a correlated `SELECT COUNT(*) FROM refs` (index-served by `idx_refs_target`, present since m0001). ## What does not change The breach itself: **6,543 ns/row on CI, 6,297 on a second machine, against a 6,000 ceiling**, with the smaller arms passing and the rate climbing 4,132 → 6,297. Still a regression, still in a number `plugin enable` discloses to operators, and the constant is still untouched. The lesson I would carry out of my own error: *"the file that declares a constant is not the file that does the work it bounds."* I let the constant's location stand in for the code's, and only caught it by re-deriving the check.
Author
Member

Fixed in caa62fe (master). The mechanism this issue proposed is refuted, and the real one was found by removal.

The {crossing} row count — this issue's own open question

Zero. At the 100k shape the crossing predicate matches 800,000 refs before the carry and 0 after it. The carry runs first and moves every ref to the incoming generation, so nothing crosses by the time the unbind runs; Switch::unbound == 0, confirmed at 8k and 100k.

So m0060's AFTER UPDATE OF target_id doors never fire during a promotion. UPDATE refs SET target_id = NULL … WHERE {crossing} costs 157 ms of a 13.94 s profiled transaction (1.1%) as a pure scan. The correlated ref_count recount — the other candidate named here — does not appear in the profile at all: temp.promotion_recount is empty for the same reason the unbind is a no-op.

The trigger whose OF list names your column is not necessarily the trigger your statement fires.

The real door: the carry

m0060's D4 and m0061's D3 both watch generation_id, and the carry is what moves it — via m0052's D2 cascade, once per ref, 1.1M times. Measured by dropping the seven rollup triggers on the same fixture, in the same process, immediately before promote:

files rollup triggers ns/row carry vm_step lock
8,002 7 (as shipped) 4,944 38,824,036 909.7 ms
8,002 0 (counterfactual) 3,037 16,680,247 558.8 ms
100,002 7 (as shipped) 6,251 485,300,036 14,376.6 ms
100,002 0 (counterfactual) 4,038 208,500,247 9,287.7 ms

2,213 ns/row = 35% of the lock, 57% of the carry's VDBE work. The counterfactual arm reads 4,038 against the 3,915 recorded before m0060 existed — 3% agreement across a fixture rebuilt from scratch. That closes the causal loop; nothing else in the window contributed measurably.

Per-statement attribution (sqlite3_trace_v2, 100k, idle): carry 89.9% (12.53 s of 13.94 s), promotion_recount 4.9%, promotion_scope 1.4%, unbind 1.1%, COMMIT 1.0%, DELETE symbol_edges 0.9%, UPDATE symbols SET ref_count = 0 0.7%.

Decision: honest re-record, not a fix

The 2,213 ns/row buys the rollups being maintained by the writes — m0060's argument for triggers over a cache is that there is then no invalidation to get wrong, and a promotion is precisely the writer most likely to forget. SQLite cannot disarm a row trigger for one statement, so paying it set-wise means either taking generation_id out of the rollup keys (which makes the per-generation reads those tables exist for impossible) or moving maintenance back into Rust (the design m0060 argues against by name). Buying a wall-clock ceiling with either is a fix-per-case, in a release that is otherwise ready.

MEASURED_LOCK_NS_PER_GENERATION_ROW 6,000 → 9,000, by the same method the 6,000 was set by: 1.43× the worst idle reading (6,298) and 1.38× the worst reading under any ordinary load (6,543, shared CI runner) — the old value stood at 1.53× and 1.36× of its own two.

The finding worth more than the constant

No test in this tree had ever driven a promotion through either rollup trigger. The cheapest way to reclaim that 35% is to narrow an OF list — which would leave both rollups describing the outgoing generation: fast, wrong, and silent. Both suites now carry the_carry_of_a_promotion_moves_every_bucket, driving a real generations::promote and comparing each rollup against a live scan.

Mutations run, RED pasted in the commit: dropping generation_id from m0060's D3/D4 OF lists, and from m0061's WATCHED, both fail with the buckets left on generation 1 where the scan says 2. The ceiling's own mutation is reading 1 below — at 6,000 the bench panicked at 6,298.

Standing weakness, named rather than hidden

A flat constant over a rate that climbs 4,213 → 6,298 across a 200-fold row count now over-predicts by 2.3× at the 11.5k-row arm. Over-prediction is the direction this disclosure is allowed to be wrong in (plugin enable prints it as PREDICTED), but it is an error; the fix if ever wanted is a two-term estimate, and lock_estimate is the single function that would change. Recorded in the module doc so the next lane does not rediscover it.

Also recorded: the 100k arm is the stable one across three readings (6,298 / 6,214 / 6,261) while the small arms move up to 18% on a box under load 1.4 — the opposite of what a reader would guess from their run times.

Gates: fmt 0 · clippy 0 · doc 0 · cargo test --workspace --no-fail-fast 0 (317 suites) · bench_promotion_lock --ignored --test-threads=1 0. plugin-path-cost is dispatch/schedule-only, so it is being dispatched separately to verify the new ceiling on the runner that breached the old one.

Fixed in `caa62fe` (master). **The mechanism this issue proposed is refuted, and the real one was found by removal.** ## The `{crossing}` row count — this issue's own open question **Zero.** At the 100k shape the crossing predicate matches **800,000** refs *before* the carry and **0** after it. The carry runs first and moves every ref to the incoming generation, so nothing crosses by the time the unbind runs; `Switch::unbound == 0`, confirmed at 8k and 100k. So m0060's `AFTER UPDATE OF target_id` doors **never fire during a promotion**. `UPDATE refs SET target_id = NULL … WHERE {crossing}` costs 157 ms of a 13.94 s profiled transaction (1.1%) as a pure scan. The correlated `ref_count` recount — the other candidate named here — does not appear in the profile at all: `temp.promotion_recount` is empty for the same reason the unbind is a no-op. *The trigger whose `OF` list names your column is not necessarily the trigger your statement fires.* ## The real door: the carry m0060's D4 and m0061's D3 both watch `generation_id`, and **the carry is what moves it** — via m0052's D2 cascade, once per ref, 1.1M times. Measured by dropping the seven rollup triggers on the same fixture, in the same process, immediately before `promote`: | files | rollup triggers | ns/row | carry vm_step | lock | |---:|---:|---:|---:|---:| | 8,002 | 7 (as shipped) | 4,944 | 38,824,036 | 909.7 ms | | 8,002 | 0 (counterfactual) | 3,037 | 16,680,247 | 558.8 ms | | 100,002 | 7 (as shipped) | 6,251 | 485,300,036 | 14,376.6 ms | | 100,002 | 0 (counterfactual) | 4,038 | 208,500,247 | 9,287.7 ms | **2,213 ns/row = 35% of the lock, 57% of the carry's VDBE work.** The counterfactual arm reads 4,038 against the **3,915** recorded before m0060 existed — 3% agreement across a fixture rebuilt from scratch. That closes the causal loop; nothing else in the window contributed measurably. Per-statement attribution (`sqlite3_trace_v2`, 100k, idle): **carry 89.9%** (12.53 s of 13.94 s), `promotion_recount` 4.9%, `promotion_scope` 1.4%, unbind 1.1%, `COMMIT` 1.0%, `DELETE symbol_edges` 0.9%, `UPDATE symbols SET ref_count = 0` 0.7%. ## Decision: honest re-record, not a fix The 2,213 ns/row buys the rollups being maintained **by the writes** — m0060's argument for triggers over a cache is that there is then no invalidation to get wrong, and a promotion is precisely the writer most likely to forget. SQLite cannot disarm a row trigger for one statement, so paying it set-wise means either taking `generation_id` out of the rollup keys (which makes the per-generation reads those tables exist for impossible) or moving maintenance back into Rust (the design m0060 argues against by name). Buying a wall-clock ceiling with either is a fix-per-case, in a release that is otherwise ready. `MEASURED_LOCK_NS_PER_GENERATION_ROW` **6,000 → 9,000**, by the same method the 6,000 was set by: 1.43× the worst idle reading (6,298) and 1.38× the worst reading under any ordinary load (6,543, shared CI runner) — the old value stood at 1.53× and 1.36× of its own two. ## The finding worth more than the constant **No test in this tree had ever driven a promotion through either rollup trigger.** The cheapest way to reclaim that 35% is to narrow an `OF` list — which would leave both rollups describing the *outgoing* generation: fast, wrong, and silent. Both suites now carry `the_carry_of_a_promotion_moves_every_bucket`, driving a real `generations::promote` and comparing each rollup against a live scan. Mutations run, RED pasted in the commit: dropping `generation_id` from m0060's D3/D4 `OF` lists, and from m0061's `WATCHED`, both fail with the buckets left on generation 1 where the scan says 2. The ceiling's own mutation is reading 1 below — at 6,000 the bench panicked at 6,298. ## Standing weakness, named rather than hidden A flat constant over a rate that climbs 4,213 → 6,298 across a 200-fold row count now **over-predicts by 2.3× at the 11.5k-row arm**. Over-prediction is the direction this disclosure is allowed to be wrong in (`plugin enable` prints it as PREDICTED), but it is an error; the fix if ever wanted is a two-term estimate, and `lock_estimate` is the single function that would change. Recorded in the module doc so the next lane does not rediscover it. Also recorded: the 100k arm is the **stable** one across three readings (6,298 / 6,214 / 6,261) while the small arms move up to 18% on a box under load 1.4 — the opposite of what a reader would guess from their run times. Gates: `fmt 0` · `clippy 0` · `doc 0` · `cargo test --workspace --no-fail-fast` **0** (317 suites) · `bench_promotion_lock --ignored --test-threads=1` **0**. `plugin-path-cost` is dispatch/schedule-only, so it is being dispatched separately to verify the new ceiling on the runner that breached the old one.
Author
Member

Verified on the runner that broke it. Closing.

plugin-path-cost is schedule/workflow_dispatch only, so the push CI structurally could not test this — a green push run says nothing about the ceiling. Dispatched run #651 on caa62fe to get the real verdict:

run 651 — 13/13 success
  Plugin path cost + pool throughput (nightly)       success   ← the job that went red three times
  Grammar rebuild from source (nightly)              success
  OSS corpus tier-3 scale (nightly)                  success
  OSS corpus (tier 1)                                success
  cargo test / test (daemon transport) / abi 32-bit+wasm32   success
  clippy / fmt / deny / doc / MSRV 1.98 / windows-gnu        success

The 9,000 ceiling holds where 6,000 breached at 6,543. Both rollup census tests (the_carry_of_a_promotion_moves_every_bucket and its file_refs_rollup twin) ran green in cargo test on the same run, so the door the cost buys is now graded on CI and not only locally.

Recap of what this issue actually established, since the headline number is the least interesting part:

  • The filed mechanism was refuted by measurement. {crossing} matches 800,000 refs before the carry and zero after it, so m0060's AFTER UPDATE OF target_id doors never fire during a promotion. The unbind is 1.1% of the transaction as a pure scan; the correlated ref_count recount does not appear at all.
  • The real door was found by removal, not by reading. Dropping the seven rollup triggers takes the 100k arm from 6,251 to 4,038 ns/row — reproducing the pre-m0060 measurement of 3,915 within 3% on a fixture rebuilt from scratch. That is what closed the causal loop.
  • No test in this tree had ever driven a promotion through either rollup trigger, so the cheapest way to reclaim that 35% — narrowing an OF list — would have left both rollups describing the outgoing generation: fast, wrong, silent. That gate now exists and is the durable output of this issue.
  • The standing weakness is recorded rather than hidden: a flat constant over a rate climbing 4,213 → 6,298 across a 200-fold row count over-predicts by 2.3× at the 11.5k-row arm, and lock_estimate is named as the single function a two-term estimate would change.

A watcher note, because it nearly produced a false green here

My first CI watcher used "no rows are running" as its terminal condition. On a DAG that is ambiguous — it is true when the run has finished and when the next job has not been created yet. It polled in exactly that gap, after test-daemon-leg went green and before plugin-path-cost existed, and reported TERMINAL with 11 green rows. The run has 13; jobs are ADDED as they start.

Had that been believed, this issue would have been closed as verified by a run in which the only job that could verify it had not executed. The replacement waits on the named job reaching a terminal status and reports "row not created yet" distinctly from "running", so absence can never read as completion. Worth knowing for anyone else watching this job, since it is the last one in the graph by design and therefore the one most likely to be missed this way.

**Verified on the runner that broke it. Closing.** `plugin-path-cost` is `schedule`/`workflow_dispatch` only, so the push CI structurally could not test this — a green push run says nothing about the ceiling. Dispatched run **#651** on `caa62fe` to get the real verdict: ``` run 651 — 13/13 success Plugin path cost + pool throughput (nightly) success ← the job that went red three times Grammar rebuild from source (nightly) success OSS corpus tier-3 scale (nightly) success OSS corpus (tier 1) success cargo test / test (daemon transport) / abi 32-bit+wasm32 success clippy / fmt / deny / doc / MSRV 1.98 / windows-gnu success ``` The 9,000 ceiling holds where 6,000 breached at 6,543. Both rollup census tests (`the_carry_of_a_promotion_moves_every_bucket` and its `file_refs_rollup` twin) ran green in `cargo test` on the same run, so the door the cost buys is now graded on CI and not only locally. Recap of what this issue actually established, since the headline number is the least interesting part: * **The filed mechanism was refuted by measurement.** `{crossing}` matches 800,000 refs before the carry and **zero** after it, so m0060's `AFTER UPDATE OF target_id` doors never fire during a promotion. The unbind is 1.1% of the transaction as a pure scan; the correlated `ref_count` recount does not appear at all. * **The real door was found by removal, not by reading.** Dropping the seven rollup triggers takes the 100k arm from 6,251 to 4,038 ns/row — reproducing the pre-m0060 measurement of 3,915 within 3% on a fixture rebuilt from scratch. That is what closed the causal loop. * **No test in this tree had ever driven a promotion through either rollup trigger**, so the cheapest way to reclaim that 35% — narrowing an `OF` list — would have left both rollups describing the outgoing generation: fast, wrong, silent. That gate now exists and is the durable output of this issue. * The standing weakness is recorded rather than hidden: a flat constant over a rate climbing 4,213 → 6,298 across a 200-fold row count over-predicts by 2.3× at the 11.5k-row arm, and `lock_estimate` is named as the single function a two-term estimate would change. ## A watcher note, because it nearly produced a false green here My first CI watcher used "no rows are running" as its terminal condition. On a DAG that is ambiguous — it is true when the run has finished **and** when the next job has not been created yet. It polled in exactly that gap, after `test-daemon-leg` went green and before `plugin-path-cost` existed, and reported TERMINAL with 11 green rows. The run has 13; jobs are ADDED as they start. Had that been believed, this issue would have been closed as verified by a run in which the only job that could verify it had not executed. The replacement waits on the **named** job reaching a terminal status and reports "row not created yet" distinctly from "running", so absence can never read as completion. Worth knowing for anyone else watching this job, since it is the last one in the graph by design and therefore the one most likely to be missed this way.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
h-dv/code-index#208
No description provided.