Repository navigation
perf(storage): memoize and cheapen the open-path generation inventory sweep - #1425
DecisionNerd wants to merge 1 commit into
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Advanced Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Warning Billing warning: we have not been able to collect payment for this subscription for more than 72 hours. Please update the payment method or pay any pending invoices in Billing to avoid service interruption. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
… sweep graph_files_inventory() re-ran a full manifest decode plus a streamed SHA-256 over every declared graph payload object on every call, and was called at least four times inside a single GraphForge::open for a V2/mapped-root project. Cut this to at most one computation per resolved generation and to a presence-and-length admission instead of a byte-for-byte hash sweep (O1, O3 of #1388's open-path redesign; O3 approved by the maintainer as an in-scope extension during implementation, tradeoff recorded below). O1 — memoize graph_files_inventory() on ResolvedProjectGeneration via Arc<OnceLock<..>>. A resolved generation is immutable by construction (CURRENT is never re-consulted after resolution), so every call site — and every clone of a ResolvedProjectGeneration for the same generation — now shares one computation instead of independently re-running the full sweep. Zero behavior change: the cached value is exactly what every call computed before. O3 — delete the payload verify_graph_object call inside the V2 loop, keeping the existing presence-and-length check. This is the step that actually removes the O(E) term: every real consumer of a graph object's bytes already re-authenticates lazily on first touch via open_graph_object_by_digest/read_graph_object_by_digest, so the eager inventory-wide sweep was provably redundant for every object a session actually reads, and simply undetected-until-next-read for objects it does not. Maintainer-approved tradeoff: corruption in an object nothing in the session reads is no longer caught at open; it surfaces on first real read, or via the administrative verify command (#1384 decision 4). This mirrors the presence-only pattern ShardedCsrIndex::open already uses for adjacency shards (#1094). O2 — replace materialize_from_cas's post-hardlink re-verify (graph_object_store/materialization.rs) with an identity, length and link-count check. link_child_into already confirms the newly-linked directory entry resolves to the same inode as the CAS source; nothing between the link syscall and the check can have altered those bytes within the same open, so re-reading and re-hashing the whole object was pure duplicate work. Verified: cargo fmt --all -- --check, cargo clippy --workspace -- -D warnings (clean), full openCypher TCK corpus (3,897/3,897 passing, identical before and after — confirmed via TCK_DUMP_FAILURES, which wrote 0 failing-scenario records in both runs), and the corruption / crash-recovery / cancellation test suites (identical pass/fail sets before and after, run from an ext4-backed worktree after the session scratchpad's tmpfs was found to fail graphforge-storage's filesystem- class gate independently of this change). Part of #1388 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
cd964b6 to
d36c06f
Compare
Blocking: this silently accepts corrupted Topology dataConverted to draft. An independent review with a working mutation test found that a same-inode, same-length one-byte corruption of Reproduced against two worktrees at
The refusal that disappears is Why the identity check cannot cover this
The cited precedent, Why lazy re-authentication on first read does not hold hereThe commit message's central justification is that every real consumer re-authenticates lazily via After Properties survive only by accident, and that is the #1392 pattern again
That asymmetry also explains why The
|
|
Found 1 test failure on Blacksmith runners: Failure
|
recovery_reauthentication.read_calls jittered non-monotonically ([69, 68, 70] on the edge-axis fixture) after #1392 removed the full-artifact SHA-256 rehash that used to dominate this phase and make its call count scale with bytes incidentally. Traced with targeted instrumentation: the residual call total is the sum of three always-present, byte-independent contributors (current-session supersession-read accounting, authenticating a published successor's manifest, and the interrupted-import recovery drill's bounded package materialization). The +/-1 jitter comes from compact_parent_inventory's content-addressed manifest-tree packing moving by one page between rungs; the same 18 files are authenticated every time, read_bytes still scales correctly, and no coverage was lost (confirmed A, not B or C). Add PhaseMetricPolicy::BoundedObjectCalls: per-rung buffered bounds stay as before, but instead of requiring non-decreasing values it bounds the cross-rung spread to a percentage of the minimum (tight enough that a regression back to per-byte authentication, which would move calls roughly in step with bytes, still blows the envelope) and requires every rung to clear an absolute floor (catching a component being silently dropped from every rung alike, which a spread-only check cannot see). Both are exercised by mutation in lifecycle_metric_policy_accepts_bounded_fixed_protocol_and_rejects_false_growth. Verified #1413 and #1425 do not hit this same check - both fail this target on different metrics for their own reasons (cas_install_read_write .write_bytes and hydration_verification.read_bytes going flat, respectively), so this change does not depend on or paper over either. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…le graph objects #1425/cd964b69 (O1/O2/O3 of #1388's open-path redesign) memoized graph_files_inventory() (O1, kept as-is -- it is sound and independently verified) but also deleted the full SHA-256 admission of every declared V2 payload object (O3) and replaced materialize_from_cas's post-hardlink re-verify with an identity/length/link-count check (O2). Both changes were justified by the same claim: every real consumer re-authenticates a graph object's bytes lazily on first touch via open_graph_object_by_digest/read_graph_object_by_digest. That claim is false for Topology-role objects. After hydrate_graph_workspace materializes (hardlinks) a compact graph into a private workspace, the query engine (PersistentAdjacencyProvider, Parquet reads) opens those files by path, not by CAS digest lookup -- neither digest-verifying function is ever called on them again. Confirmed by mutation test: flipping one byte in place (same inode, same declared length) in a hardlinked topology/nodes.parquet was accepted at open and an ordinary `MATCH (n) RETURN count(n)` query silently returned a result over the corrupted data. Properties-role objects happened to stay covered, but only via property_overlay::inventory's independent, unrelated re-hash of every property/edge_properties route file -- a mechanism this PR never touched and that does not reach Topology. Fix: restore the verify_graph_object call in graph_files_inventory()'s V2 loop. O1's memoization means this now runs once per resolved generation instead of the 3-4 times per open it ran before #1425 (the redundancy that motivated #1425 in the first place), with no format change and no new risk. O2's cheap post-hardlink check is left in place and is safe given this restored invariant: every real caller of materialize_graph_objects obtains its inventory from graph_files_inventory() immediately beforehand, so the content is already authenticated, in this same open, before the hardlink runs. Its doc comment is corrected to state this rather than the disproven "lazy re-authentication on first read" claim. entry.content_sha256 is this object's content-addressed CAS lookup key, not merely a corruption checksum, so it stays a full cryptographic digest here. A fast-checksum alternative that avoids the SHA-256 cost entirely would need a durable inventory format change and belongs to #1417, not this fix. Adds hardlinked_topology_payload_corruption_is_refused (workspace_hydration/tests.rs), which fails against #1425/cd964b69 unmodified and passes with this restoration. Part of #1388 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Superseded by #1435. #1435 carries both commits — the O1 memoization from this PR ( Why this one is not landingAn independent review with a working mutation test showed that after this PR, a one-byte corruption of The justification in the commit message — that every real consumer re-authenticates lazily on first touch via Properties-role objects stayed covered only by accident, via What #1435 does differentlyIt restores
Ships with Part of #1388. |
…le graph objects #1425/cd964b69 (O1/O2/O3 of #1388's open-path redesign) memoized graph_files_inventory() (O1, kept as-is -- it is sound and independently verified) but also deleted the full SHA-256 admission of every declared V2 payload object (O3) and replaced materialize_from_cas's post-hardlink re-verify with an identity/length/link-count check (O2). Both changes were justified by the same claim: every real consumer re-authenticates a graph object's bytes lazily on first touch via open_graph_object_by_digest/read_graph_object_by_digest. That claim is false for Topology-role objects. After hydrate_graph_workspace materializes (hardlinks) a compact graph into a private workspace, the query engine (PersistentAdjacencyProvider, Parquet reads) opens those files by path, not by CAS digest lookup -- neither digest-verifying function is ever called on them again. Confirmed by mutation test: flipping one byte in place (same inode, same declared length) in a hardlinked topology/nodes.parquet was accepted at open and an ordinary `MATCH (n) RETURN count(n)` query silently returned a result over the corrupted data. Properties-role objects happened to stay covered, but only via property_overlay::inventory's independent, unrelated re-hash of every property/edge_properties route file -- a mechanism this PR never touched and that does not reach Topology. Fix: restore the verify_graph_object call in graph_files_inventory()'s V2 loop. O1's memoization means this now runs once per resolved generation instead of the 3-4 times per open it ran before #1425 (the redundancy that motivated #1425 in the first place), with no format change and no new risk. O2's cheap post-hardlink check is left in place and is safe given this restored invariant: every real caller of materialize_graph_objects obtains its inventory from graph_files_inventory() immediately beforehand, so the content is already authenticated, in this same open, before the hardlink runs. Its doc comment is corrected to state this rather than the disproven "lazy re-authentication on first read" claim. entry.content_sha256 is this object's content-addressed CAS lookup key, not merely a corruption checksum, so it stays a full cryptographic digest here. A fast-checksum alternative that avoids the SHA-256 cost entirely would need a durable inventory format change and belongs to #1417, not this fix. Adds hardlinked_topology_payload_corruption_is_refused (workspace_hydration/tests.rs), which fails against #1425/cd964b69 unmodified and passes with this restoration. Part of #1388 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ry (CurateLabs#1412) * fix(storage): refuse completed-shape payload corruption at the boundary Same-inode, same-length corruption of a completed shape output was accepted at its own trust boundary. `authenticate_shaped_output` took the writer-receipt fast path and compared only inode, link count, length and a stored digest, none of which can see a payload mutation. CurateLabs#1269's regression test still failed the open, but only incidentally: `reclaim_superseded_payloads` -> `retire_payload` -> `authenticate_payload` streamed and fully SHA-256'd every superseded payload before unlinking it, and tripped over the mutation on the way to deleting the file. That pass is the largest single line in CurateLabs#1384's removal target. Establish the refusal deliberately, then remove the pass it used to depend on. * `ArtifactReceipt` gains `xxh64`, an inline corruption checksum over the same payload, produced by the same pass that writes the bytes. No pass reads a payload back in order to compute it. * `authenticate_shaped_output` streams the retained payload once and refuses it unless length and checksum match the receipt. `authenticate_shaped_output_identity` keeps the old metadata-only check for the deletion paths, where the bytes are never consumed again. * The verification runs once per process-open: `recover_shape_intent` performs it at session open, and the completed-shape replay in `shape_canonical_inner` skips it when this process already has. `prepare_canonical_encoding` no longer opts out of authenticating a replayed shape. * `retire_payload` and the completed-shape pass in `reclaim_superseded_payloads` drop their full SHA-256 re-reads and keep identity, link count and length. The primitive is XXH64, implemented in a new `corruption_checksum` module that records, beside the code, the two assumptions permitting a non-cryptographic checksum here: ADR 0013's threat model excludes an active same-identity adversary, and ext4 on mdadm RAID 1 does not checksum file data. Content- addressed naming is untouched and stays SHA-256. The checksum is serialised as fixed-width hex so a receipt's byte length does not depend on its payload. Measured on a repeatable fixture, 65,536 and 262,144 edges: | | 65,536 edges | 262,144 edges | |---|---:|---:| | bytes read per edge, before | 1,878.1 | 1,998.0 | | bytes read per edge, after | 1,133.6 | 1,253.6 | | | -39.6% | -37.3% | | CPU us per edge, before | 119.0 | 128.2 | | CPU us per edge, after | 104.7 | 114.4 | | | -12.1% | -10.7% | | recovery reauthentication bytes, before | 53,712,534 | 216,236,929 | | recovery reauthentication bytes, after | 4,918,631 | 21,103,274 | | | -90.8% | -90.2% | Peak RSS moved by less than 0.3% in either direction (79,148 -> 78,892 KiB and 83,916 -> 84,100 KiB), which is noise: this step deletes read passes and adds no buffer. One behavioural change, and it is the one CurateLabs#1384 accepts by name: `canonical_encoder_uses_owned_spool_after_source_mutation` asserted that a shaped source mutated *after* the pass that consumed it made encoding fail. It now succeeds, because the encoder's owned spool holds the correct bytes and the mutated file is only being deleted. A mutation that reaches the consuming read still fails closed, unchanged, and that is what the test now asserts alongside. Closes CurateLabs#1392 Part of CurateLabs#1384 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(g500-ladder): bound recovery-reauthentication calls, not monotonic recovery_reauthentication.read_calls jittered non-monotonically ([69, 68, 70] on the edge-axis fixture) after CurateLabs#1392 removed the full-artifact SHA-256 rehash that used to dominate this phase and make its call count scale with bytes incidentally. Traced with targeted instrumentation: the residual call total is the sum of three always-present, byte-independent contributors (current-session supersession-read accounting, authenticating a published successor's manifest, and the interrupted-import recovery drill's bounded package materialization). The +/-1 jitter comes from compact_parent_inventory's content-addressed manifest-tree packing moving by one page between rungs; the same 18 files are authenticated every time, read_bytes still scales correctly, and no coverage was lost (confirmed A, not B or C). Add PhaseMetricPolicy::BoundedObjectCalls: per-rung buffered bounds stay as before, but instead of requiring non-decreasing values it bounds the cross-rung spread to a percentage of the minimum (tight enough that a regression back to per-byte authentication, which would move calls roughly in step with bytes, still blows the envelope) and requires every rung to clear an absolute floor (catching a component being silently dropped from every rung alike, which a spread-only check cannot see). Both are exercised by mutation in lifecycle_metric_policy_accepts_bounded_fixed_protocol_and_rejects_false_growth. Verified CurateLabs#1413 and CurateLabs#1425 do not hit this same check - both fail this target on different metrics for their own reasons (cas_install_read_write .write_bytes and hydration_verification.read_bytes going flat, respectively), so this change does not depend on or paper over either. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…opology-role graph objects (CurateLabs#1435) * perf(storage): memoize and cheapen the open-path generation inventory sweep graph_files_inventory() re-ran a full manifest decode plus a streamed SHA-256 over every declared graph payload object on every call, and was called at least four times inside a single GraphForge::open for a V2/mapped-root project. Cut this to at most one computation per resolved generation and to a presence-and-length admission instead of a byte-for-byte hash sweep (O1, O3 of CurateLabs#1388's open-path redesign; O3 approved by the maintainer as an in-scope extension during implementation, tradeoff recorded below). O1 — memoize graph_files_inventory() on ResolvedProjectGeneration via Arc<OnceLock<..>>. A resolved generation is immutable by construction (CURRENT is never re-consulted after resolution), so every call site — and every clone of a ResolvedProjectGeneration for the same generation — now shares one computation instead of independently re-running the full sweep. Zero behavior change: the cached value is exactly what every call computed before. O3 — delete the payload verify_graph_object call inside the V2 loop, keeping the existing presence-and-length check. This is the step that actually removes the O(E) term: every real consumer of a graph object's bytes already re-authenticates lazily on first touch via open_graph_object_by_digest/read_graph_object_by_digest, so the eager inventory-wide sweep was provably redundant for every object a session actually reads, and simply undetected-until-next-read for objects it does not. Maintainer-approved tradeoff: corruption in an object nothing in the session reads is no longer caught at open; it surfaces on first real read, or via the administrative verify command (CurateLabs#1384 decision 4). This mirrors the presence-only pattern ShardedCsrIndex::open already uses for adjacency shards (CurateLabs#1094). O2 — replace materialize_from_cas's post-hardlink re-verify (graph_object_store/materialization.rs) with an identity, length and link-count check. link_child_into already confirms the newly-linked directory entry resolves to the same inode as the CAS source; nothing between the link syscall and the check can have altered those bytes within the same open, so re-reading and re-hashing the whole object was pure duplicate work. Verified: cargo fmt --all -- --check, cargo clippy --workspace -- -D warnings (clean), full openCypher TCK corpus (3,897/3,897 passing, identical before and after — confirmed via TCK_DUMP_FAILURES, which wrote 0 failing-scenario records in both runs), and the corruption / crash-recovery / cancellation test suites (identical pass/fail sets before and after, run from an ext4-backed worktree after the session scratchpad's tmpfs was found to fail graphforge-storage's filesystem- class gate independently of this change). Part of CurateLabs#1388 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(storage): restore the only content check that covered Topology-role graph objects CurateLabs#1425/cd964b69 (O1/O2/O3 of CurateLabs#1388's open-path redesign) memoized graph_files_inventory() (O1, kept as-is -- it is sound and independently verified) but also deleted the full SHA-256 admission of every declared V2 payload object (O3) and replaced materialize_from_cas's post-hardlink re-verify with an identity/length/link-count check (O2). Both changes were justified by the same claim: every real consumer re-authenticates a graph object's bytes lazily on first touch via open_graph_object_by_digest/read_graph_object_by_digest. That claim is false for Topology-role objects. After hydrate_graph_workspace materializes (hardlinks) a compact graph into a private workspace, the query engine (PersistentAdjacencyProvider, Parquet reads) opens those files by path, not by CAS digest lookup -- neither digest-verifying function is ever called on them again. Confirmed by mutation test: flipping one byte in place (same inode, same declared length) in a hardlinked topology/nodes.parquet was accepted at open and an ordinary `MATCH (n) RETURN count(n)` query silently returned a result over the corrupted data. Properties-role objects happened to stay covered, but only via property_overlay::inventory's independent, unrelated re-hash of every property/edge_properties route file -- a mechanism this PR never touched and that does not reach Topology. Fix: restore the verify_graph_object call in graph_files_inventory()'s V2 loop. O1's memoization means this now runs once per resolved generation instead of the 3-4 times per open it ran before CurateLabs#1425 (the redundancy that motivated CurateLabs#1425 in the first place), with no format change and no new risk. O2's cheap post-hardlink check is left in place and is safe given this restored invariant: every real caller of materialize_graph_objects obtains its inventory from graph_files_inventory() immediately beforehand, so the content is already authenticated, in this same open, before the hardlink runs. Its doc comment is corrected to state this rather than the disproven "lazy re-authentication on first read" claim. entry.content_sha256 is this object's content-addressed CAS lookup key, not merely a corruption checksum, so it stays a full cryptographic digest here. A fast-checksum alternative that avoids the SHA-256 cost entirely would need a durable inventory format change and belongs to CurateLabs#1417, not this fix. Adds hardlinked_topology_payload_corruption_is_refused (workspace_hydration/tests.rs), which fails against CurateLabs#1425/cd964b69 unmodified and passes with this restoration. Part of CurateLabs#1388 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * test(g500-ladder): declare hydration_verification.read_bytes bounded, not proportional (CurateLabs#1433) The `hydration_verification` row asserted positive 1x->2x->4x growth of read_bytes. That growth was `materialize_from_cas` re-hashing every hard-linked object after linking it: the fourth full sweep of the same immutable generation per open. With CurateLabs#1435 content admission happens once, in the memoized `graph_files_inventory()`, which is not this phase's I/O, and the row went flat (87,475 / 87,479 / 87,479 on the edge axis) - so the row was encoding the redundant sweep as expected behaviour. Replace SCALE with `HydrationReadReconciliation`: what remains in this phase is exactly the single-link copy protocol, so read_bytes reconciles to `2 * write_bytes + route_table` (each private copy is read once to copy and once to verify the installed file; the one non-copy read is the bounded route-table authentication; hard-linked objects contribute zero). `write_bytes` is already policed exactly by `NodeBearingBytes`, and the residual is bounded by the same 2 KiB control bound the UUID controls use. Proven by mutation on both axes: reads that additionally track retained logical bytes (the removed sweep coming back) exceed the route-table bound; reads that drop the copy verification re-read, all reads, or the route-table read underflow the reconciliation and fail. Measured on the edge axis, main -> CurateLabs#1435: read_bytes [173,351, 193,618, 230,050] -> [87,475, 87,479, 87,479]; write_bytes unchanged at [43,649, 43,651, 43,651]; residual 177 bytes at every rung. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Summary
Part of #1388. Implements steps O1, O2, and O3 of the open-path redesign (design doc reviewed at session start; O3 explicitly approved by the maintainer mid-implementation as an in-scope extension — see reasoning below). Does not implement O4 (XXH3 swap in
ordinal_identity_v4.rs) or anything past O3 — those are owned by other agents / require separate sign-off.ResolvedProjectGeneration::graph_files_inventory()decoded the manifest and, for the V2/mapped-root participant, ran a full streamed SHA-256 over every declared graph payload object. This function was called at least four times inside a singleGraphForge::open, so a V2/mapped-root open paid for four-plus full-byte-range SHA-256 passes over the same graph payload.graph_files_inventory()onResolvedProjectGenerationviaArc<OnceLock<Result<Option<Arc<GraphFilesInventory>>, GfError>>>. A resolved generation is immutable by construction (CURRENTis never re-consulted after resolution), so every call site, and every clone of aResolvedProjectGenerationfor the same generation, now shares one computation. Zero behavior change to what gets verified.verify_graph_objectcall insidegraph_files_inventory()'s V2 loop; keep the existing presence-and-declared-length check. This is the step that removes the O(E) term. Every real consumer of a graph object's bytes already re-authenticates lazily on first touch viaopen_graph_object_by_digest/read_graph_object_by_digest, so the eager sweep was provably redundant for objects a session reads, and simply undetected-until-next-read for objects it does not. Mirrors the presence-only patternShardedCsrIndex::openalready uses for adjacency shards (fix(exec): plateau S18→S19 recount/query RSS so host ladder admits S20 #1094).materialize_from_cas's post-hardlink re-verify (graph_object_store/materialization.rs) with an identity, length, and link-count check.link_child_intoalready confirms the newly linked directory entry resolves to the same inode as the CAS source; nothing between the link syscall and the check can have altered those bytes within the same open, so re-reading and re-hashing the whole object was pure duplicate work.The tradeoff O3 makes (maintainer-approved)
Before:
GraphForge::openproved every declared graph object was byte-for-byte intact before returning — including objects no query in the session ever reads. After: open proves every declared object is present at its declared length; content authentication is deferred to first real read (which already happens unconditionally) or to the explicit administrative verify command (#1384 decision 4). A corrupted object that a session never reads is no longer caught at open — it surfaces on the next read of that object, or on an explicit verify run. Corruption in an object a query does read is still caught, exactly as before, because the lazy per-object authentication path is untouched. The maintainer's framing, recorded here per the coordinator's request: the verify command covers whole-store integrity, this path maximizes usable speed, and this is an analyst bench rather than a forensic system.One correction to the design doc
The design cited
GraphObjectReadLease::open_for_construction(graph_object_store.rs:959-1054) as an existing precedent for "trust identity + length without re-hashing." Reading it directly: it still streams the whole file through SHA-256 and compares the digest — it does not skip hashing. The real precedent for O2's metadata-only check isStableDirectory::link_child_intoitself (graphforge-filesystem/src/lib.rs), which already assertsfile_identity(installed) == file_identity(source)as part of completing the hard link, plus the identity/link-count revalidation pattern already used elsewhere ingraph_object_store.rs(e.g.revalidate_lifecycle,replace_child). O2 mirrors that actual pattern, notopen_for_construction.Verified
All runs below compare this branch against
origin/mainat the commit this branch is based on, back to back, on the same host.cargo fmt --all -- --check— clean.cargo clippy --workspace -- -D warnings— clean, zero warnings, full workspace.cargo test -p graphforge-api --test bdd --release): 3,897/3,897 scenarios passing, identical before and after (verified via the harness's structuredreport.json, not just the summary line).UnsupportedFilesystem/filesystem_class_unproven— the CI/dev sandbox's/tmpis tmpfs, whichfilesystem_admission.rscorrectly refuses to classify as durable storage), not caused by this change.graphforge-storage --lib(1,128 tests, clean rerun on ext4-backed worktrees after tmpfs was found to be the confound above): 850 passed / 278 failed, identical failing test set before and after. Every sampled failure traces to the samefilesystem_class_unprovencause infilesystem_admission.rs, a module this PR does not touch.graphforge-api --lib(742 tests — includesdurability_certification_tests,composite_recovery_tests,mutation_transaction_fault_tests,stream_cancellation_isolation_tests,workspace_hydrationtests, corruption/crash-recovery/cancellation coverage), run withTMPDIRpointed at an ext4-backed directory to remove the tmpfs confound entirely: 742 passed / 0 failed, both before and after this change.Measured: open cost per edge, before vs. after
Two graph sizes (8x range), each opened repeatedly (10-15 iterations) from a durably-published generation, wall-clock measured around
GraphForge::new(Some(path)), on the same ext4-backed host, back to back:At 400,000 edges: 1.93x wall-clock speedup, per-edge cost drops 48% (3.79 → 1.96 µs/edge). The qualitative shape matches the design's prediction: before, per-edge cost grows with graph size (3.79 vs 2.16 µs/edge, 8x range) — the O(bytes) signature; after, per-edge cost is flat-to-decreasing (2.21 vs 1.96 µs/edge) — the O(files) signature. At 50,000 edges the two are within noise of each other because fixed per-open overhead (DataFusion context, ordinal-identity open, etc.) dominates at that size; the linear term only becomes visible once it outweighs the fixed cost, exactly as in the epic's own ladder data.
Peak resident memory (
VmHWMafter the open phase) at 400,000 edges: before 187.0 MB, after 161.6 MB — no regression; if anything a modest reduction, consistent with fewer full-file read buffers allocated during the removed verification passes. No new O(E) resident-memory term is introduced by this change; the bounded-memory property from #1094 is untouched (adjacency shard opens are unaffected).I could not reproduce the exact multi-million-edge, sixteen-fold ladder rungs from #1388's own table within this session (that ladder is a separate, long-running, dedicated fixture); the numbers above are from an independently constructed fixture at a smaller but still meaningfully O(E)-dominated scale, intended to demonstrate the mechanism and its direction/magnitude, not to replace #1388's own ladder re-run (tracked as O6 in the design doc, owned separately).
Out of scope (unchanged in this PR)
ordinal_identity_v4.rs) — different agent, file untouched here.supersession.rs,shaping_merge.rs,installation.rs,benches/, per-phase I/O attribution, the CLI verify command — untouched, per the brief's file-ownership boundaries. Confirmed no open PR currently touchesmaterialization.rsorproject_generation.rsbefore starting.Part of #1388
🤖 Generated with Claude Code
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.