Skip to content

v0.1.7: the whole-file lane's chain charge counts its group, and history-stride10 no longer completes (#190) #228

Description

@yifanxuaaa

Status: defect on main at eb2c06e2; diagnosis, mechanism and fix measured. Filed 2026-09-22 from the run that found it. history-stride10 is a registered admission row outside the 217 (#190's lane), so this blocks that lane's evidence on this tree.

1. What fails

history-stride10 no longer completes:

$ LAYERFS_CONSTRUCTION_WORKERS=1 LAYERFS_HISTORY_ADVISORY=1 LAYERFS_HISTORY_DEPTH_LIMIT=7 \
    fs-bench-storage-content --case history-stride10 --corpus <corpus> --out /tmp/x
history-stride10 INCOMPLETE gates=1

g1.o1-chain-complete  INCOMPLETE  product error: Integrity("dependency encoded work")

Deterministic: three fresh runs, ~3.5–5.3 s wall, abort at 17.1 MB of Store (4,041 pages, apparent_bytes 16,551,936 in all three). Also reproduced through runner.py perf (benchmark-results/history-stride10/stride10-20260922T121313Z, repeat-20260922T121442Z, stride10-l7-20260922T121536Z).

It is not the budget, not the corpus (history_corpus.self_check clean: 17 states / 101,477 path states / 561,010,345 B), and not the history switches: LAYERFS_HISTORY_ADVISORY=1 LAYERFS_HISTORY_DEPTH_LIMIT=7 and LAYERFS_HISTORY_CHUNK_PREDECESSORS=0 all abort. The harness's own documented refusal (advisory depth u8::MAX, "the product currently rejects") is a different one.

2. It passes on every archived build

Same corpus, same declared switches:

build result operation Store
stage-6-history-190-opt-20260919T232858Z/binaries/baseline-c5826d20… (Sep 19) PASS 17/17 46.70 s 49.4 MB
stage-6-history-209-merged-20260920T225623Z/binary-archive/merged-main (Sep 20) PASS 17/17 23.54 s 52.2 MB
main @ eb2c06e2 (today) INCOMPLETE aborts at 17.1 MB

3. Mechanism

record_width charges a chain read the group's width for the whole-file lane (core/crates/layerfs-storage/src/encoding/delta/read.rs:527), while the budget it is charged against is per chainCHAIN_ENCODED_LIMIT = 256 KiB (policy.rs:183). That early return was written when the lane sealed one record per group, so group width ≡ record width. a1faf957e ("group the compact whole-file lane") gave the lane whole groups — its own census puts it at 519 groups instead of 9,444 — so every chain read is now charged the group's membership, and a depth-8 chain asks for roughly eight group targets (~384 KiB) against 256 KiB.

Verified by construction: restoring the pre-a1faf957e seal rule (PackLane::WholeFile => occupied) in a temporary diagnostic build makes the same case pass — 15.63 s, 17/17 states, 71.1 MB. The edit was reverted immediately; the tree is clean and the binary hashes back to 90a04b44….

Secondary: on the abort path phases-perf.json reports preparation + verification larger than the invocation wall (8.75 s inside 5.34 s), so g7.phase-reconciliation fires. The gate is right; the document is wrong when the driver aborts.

4. Fix (owner-authorized option A), implemented and measured

record_width no longer special-cases the lane: it takes the Raw arm every other lane takes and charges the record its locator names. The lane seals GroupCodec::Raw by construction (pack/assemble.rs:185), so that arm is exact for it.

  • Production LOC 97,249 → 97,246 (delta −3); the added lines are the explaining comment.
  • history-stride10: PASS 17/17, operation 15.06 s, and all 17 state roots identical to the Sep-19, Sep-20 and diagnostic runs.
  • history-stride3: PASS 53/53, 37.10 s, 106.7 MB.
  • pipeline-namespace-100000: 15/15 gates, 14/14 pinned counters, digest 2412681d… unchanged (statements 12,206 / commits 388 / inserted 113,635 / content_bytes 502,914,928), i.e. no other row's pin moves.
  • 110 test binaries, 631 passed / 0 failed; clippy clean; fmt clean; core/tools/check_product_boundary.py PASS.

The cost, stated plainly. Nine objects change representation — delta.prefix_selected 38,117 → 38,108 and delta.full_records 12,998 → 13,007 — because the reader's charge feeds the selection's own chain budget. The row is therefore not byte-identical to the pre-grouping arm; its 17 roots are.

Option B (charge only what was fetched) is not taken. A alone cleared the lane, and B changes more semantics ("encoded bytes this wave read") for no measured need. It stays in the priced list rather than in the diff.

5. Why the crate tests did not catch it

chain_encoded_limit is a constant with no test-visible override, the whole-file chain depth caps at 8, and the closest expressible in-crate shape charges 240,607 of 262,144 — 8 % short of the refusal. So the lane is the only oracle that reaches this boundary, and that is recorded rather than papered over. A future round that wants a unit test here needs a test-visible capacity override first.

6. Not in this issue

  • The store-size axis of grouping on this lane (63.3 MB with the fix, 71.1 MB with one-record groups on the same tree, 52.2 MB on the Sep-20 build) is measured but not attributed: the three differ in more than the seal rule.
  • history-stride1 was not run (157 states, the campaign's own budget ruling keeps it out of these rounds).
  • Raising CHAIN_ENCODED_LIMIT is not proposed: it is a bounded-work guarantee with its own test (tests/delta_chains.rs:83).

Evidence: docs/roadmap/0.1/0.1.7/evidence/issue228-stride10-chain-charge-20260922T124500Z/report.md (runs under benchmark-results/history-stride10/ and benchmark-results/postfix/).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions