From c7e34db1aa5872a22f04d72958e6198f6f53fc1a Mon Sep 17 00:00:00 2001 From: Wang Runyuan <281843989+runyuan-wang@users.noreply.github.com> Date: Fri, 4 Sep 2026 00:58:08 -0700 Subject: [PATCH] bench: prototype deterministic multiscale history anchors --- benchmark/research/history-anchors/Cargo.lock | 357 +++++++++ benchmark/research/history-anchors/Cargo.toml | 12 + benchmark/research/history-anchors/DESIGN.md | 68 ++ benchmark/research/history-anchors/PR.md | 77 ++ benchmark/research/history-anchors/README.md | 17 + benchmark/research/history-anchors/RESULTS.md | 95 +++ .../research/history-anchors/VALIDATION.md | 37 + .../history-anchors/results/analysis.json | 129 ++++ .../results/comparison-run1.tsv | 57 ++ .../results/comparison-run2.tsv | 57 ++ .../research/history-anchors/src/main.rs | 716 ++++++++++++++++++ benchmark/research/history-anchors/verify.py | 95 +++ 12 files changed, 1717 insertions(+) create mode 100644 benchmark/research/history-anchors/Cargo.lock create mode 100644 benchmark/research/history-anchors/Cargo.toml create mode 100644 benchmark/research/history-anchors/DESIGN.md create mode 100644 benchmark/research/history-anchors/PR.md create mode 100644 benchmark/research/history-anchors/README.md create mode 100644 benchmark/research/history-anchors/RESULTS.md create mode 100644 benchmark/research/history-anchors/VALIDATION.md create mode 100644 benchmark/research/history-anchors/results/analysis.json create mode 100644 benchmark/research/history-anchors/results/comparison-run1.tsv create mode 100644 benchmark/research/history-anchors/results/comparison-run2.tsv create mode 100644 benchmark/research/history-anchors/src/main.rs create mode 100644 benchmark/research/history-anchors/verify.py diff --git a/benchmark/research/history-anchors/Cargo.lock b/benchmark/research/history-anchors/Cargo.lock new file mode 100644 index 000000000..67c185591 --- /dev/null +++ b/benchmark/research/history-anchors/Cargo.lock @@ -0,0 +1,357 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "blake3" +version = "1.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "cc" +version = "1.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "find-msvc-tools" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashlink" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32069d97bb81e38fa67eab65e3393bf804bb85969f2bc06bf13f64aef5aba248" +dependencies = [ + "hashbrown 0.17.1", +] + +[[package]] +name = "js-sys" +version = "0.3.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "layerfs-content" +version = "0.1.2" +dependencies = [ + "blake3", +] + +[[package]] +name = "layerfs-history-anchor-research" +version = "0.0.0" +dependencies = [ + "layerfs-content", + "layerfs-layerstack-store", +] + +[[package]] +name = "layerfs-layerstack-store" +version = "0.1.2" +dependencies = [ + "blake3", + "layerfs-content", + "rusqlite", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libsqlite3-sys" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1d20bef17f513b9b3004532233187769cd072d790971f4e4da0e346eb6401e8" +dependencies = [ + "pkg-config", + "vcpkg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rsqlite-vfs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c51c9ae4df8a7fba42103df5c621fa3c37eccf3a3c650879e90fc48b11cc192c" +dependencies = [ + "hashbrown 0.16.1", + "thiserror", +] + +[[package]] +name = "rusqlite" +version = "0.40.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f2a97da3e3873c73cb2a2e71b35c40ff95e0b1eefa8d72d8499a6928c3b5b3" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", + "sqlite-wasm-rs", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "smallvec" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9be42f50aa861c555654aa3a37f52f4b1074bacf4e48fe0ef7fa584e80f1f0f" + +[[package]] +name = "sqlite-wasm-rs" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc3efc0da82635d7e1ced0053bbbfa8c7ab9645d0bf36ceb4f7127bb85315d75" +dependencies = [ + "cc", + "js-sys", + "rsqlite-vfs", + "wasm-bindgen", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "wasm-bindgen" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" +dependencies = [ + "unicode-ident", +] diff --git a/benchmark/research/history-anchors/Cargo.toml b/benchmark/research/history-anchors/Cargo.toml new file mode 100644 index 000000000..9032a73ee --- /dev/null +++ b/benchmark/research/history-anchors/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "layerfs-history-anchor-research" +version = "0.0.0" +edition = "2021" +rust-version = "1.85" +publish = false + +[workspace] + +[dependencies] +layerfs-content = { path = "../../../crates/layerfs-content" } +layerfs-layerstack-store = { path = "../../../crates/layerfs-layerstack-store" } diff --git a/benchmark/research/history-anchors/DESIGN.md b/benchmark/research/history-anchors/DESIGN.md new file mode 100644 index 000000000..aff6469dc --- /dev/null +++ b/benchmark/research/history-anchors/DESIGN.md @@ -0,0 +1,68 @@ +# Frozen design: deterministic multiscale history anchors + +Frozen before the formal runs on 2026-09-04. + +## Question + +For a growing immutable LayerFS Commit history, can a small deterministic +sidecar index reduce the number of history nodes inspected by ancestor checks +and Branch-Commit Diff planning, without changing canonical identity, public +semantics, Store bytes, or the draft v0.1.4 benchmark contract? + +## Fixture + +- Commit depths: 1, 10, 100, and 1000. +- One immutable Branch history per depth. +- Each Commit deterministically replaces one 4 KiB `counter` file. This is the + fixed small-edit schedule used at every depth. +- The depth-1000 case is an extended diagnostic. + +## Operations + +- latest Commit lookup; +- early Commit lookup; +- middle Commit lookup; +- early-Commit ancestor check; +- adjacent Branch-Commit Diff; +- distant Branch-Commit Diff; +- complete paginated history traversal; +- index build and reconnect-plus-rebuild. + +Wall time is the median of 31 warm-cache samples after five warmups. The +explanatory metric is the exact number of ancestry nodes inspected. Direct +Commit lookups inspect zero parent-history nodes. + +## Strategies + +1. `baseline`: current public Store operations. +2. `fixed-10`: a derived sidecar stores one 10-Commit jump at eligible + ordinals; other steps follow the parent. +3. `multiscale`: at ordinal `i`, the sidecar stores one deterministic jump of + `lowbit(i)` Commits when that distance exceeds one. This creates sparse + power-of-two scales without an adaptive policy. + +Both sidecars are rebuilt solely from public canonical history. They are not +written to the Store, are never authoritative, and can be deleted without +affecting results. + +## Metadata accounting + +Logical compact bytes, not Rust allocator size or a proposed SQLite encoding: + +- 33-byte Commit ID plus 8-byte ordinal per indexed Commit; +- 33-byte target Commit ID plus 8-byte distance per extra anchor. + +## Gates + +- Baseline ancestry nodes must increase with history depth. +- At depth 100, multiscale anchors must reduce distant Diff planning ancestry + nodes by at least 75%. +- Candidate membership, Diff entries, and history order must exactly match the + public baseline. +- At depth 1000, logical metadata must be at most 1% of Store database bytes. +- Store counts, canonical object bytes, database length, database contents, + Commit IDs, and public behavior must remain unchanged. +- No production code, schema, public API, dependency, or frozen benchmark ID + may change. + +If any gate fails, the verdict is `NO-PR`. diff --git a/benchmark/research/history-anchors/PR.md b/benchmark/research/history-anchors/PR.md new file mode 100644 index 000000000..09c65802b --- /dev/null +++ b/benchmark/research/history-anchors/PR.md @@ -0,0 +1,77 @@ +# PR title + +`bench: prototype deterministic multiscale history anchors` + +# PR body + +## Summary + +- add an isolated research benchmark for immutable Commit-history depth; +- compare the current parent walk, fixed 10-Commit checkpoints, and one + deterministic multiscale anchor per eligible Commit; +- preserve two independent raw runs plus a dependency-free evidence verifier; +- change no Store schema, canonical identity, public API, product path, or + v0.1.4 benchmark registration/evidence. + +Author: **Wang Runyuan** + +## Motivation + +LayerFS already supports direct lookup of a known Commit and direct Merkle-root +content Diff. The remaining question is narrower: Branch membership validation +currently walks `parent_commit_id`, and `DiffRequest::BranchCommits` performs +that validation before content Diff. This prototype measures whether a tiny, +derived cross-scale sidecar can reduce that planning work as history grows. + +This is inspired by established skip indexing, Git commit-graph, and Merkle DAG +lineage precedents. Spectral sparsification is only a design analogy; this PR +makes no LayerFS theorem claim. + +## Design + +For Commit ordinal `i`, the multiscale sidecar stores one ancestor at distance +`lowbit(i)` when the distance exceeds one. The rule is deterministic, +non-adaptive, and rebuildable from canonical history. A fixed checkpoint route +stores one 10-Commit jump at eligible positions. + +The sidecars are in memory only. They are not authoritative and never enter the +Store. Deleting them leaves the original history fully usable. + +## Evidence + +Two formal runs used identical deterministic fixtures at depths 1, 10, 100, and +1000. Every result matched the current public operations exactly, and the Store +database bytes, counts, canonical storage, and Commit IDs remained unchanged. + +| Depth | Route | Distant Diff nodes | Median latency across runs | Metadata | +| ---: | --- | ---: | ---: | ---: | +| 100 | baseline | 101 | 182.3–185.0 us | 0 B | +| 100 | fixed-10 | 20 | 109.0–109.2 us | 4,469 B | +| 100 | multiscale | 6 | 108.8–110.1 us | 6,109 B | +| 1000 | baseline | 1001 | 651.9–684.9 us | 0 B | +| 1000 | fixed-10 | 110 | 110.1 us | 45,059 B | +| 1000 | multiscale | 10 | 109.7–109.9 us | 61,459 B | + +At depth 1000, the compact logical metadata budget is 0.957% of Store bytes. +Direct known-Commit lookup already visits zero parent nodes and is not improved. +Returning complete history is also still linear. The measured benefit is limited +to ancestry validation and Branch-Commit Diff planning. + +## Validation + +- `cargo fmt --check` +- `cargo test` — 4/4 prototype tests pass +- `cargo clippy -- -D warnings` +- two independent release-mode benchmark runs +- `python3 verify.py --output results/analysis.json` — all gates pass +- patch applied to a clean worktree at baseline `1e81e9b` +- applied tree matched the source tree +- no new runtime dependency + +## Scope boundaries + +This PR does not propose a production persistence format or threshold, modify +the formal v0.1.4 benchmark contract, optimize direct historical reads, add +changed-path Bloom filters, or claim that sparse anchors are universally better. +It supplies a small reproducible experiment for deciding whether a later Store +index design is worth discussing. diff --git a/benchmark/research/history-anchors/README.md b/benchmark/research/history-anchors/README.md new file mode 100644 index 000000000..c842e8926 --- /dev/null +++ b/benchmark/research/history-anchors/README.md @@ -0,0 +1,17 @@ +# LayerFS history-anchor research prototype + +This isolated benchmark tests whether deterministic sparse history anchors can +reduce long-history ancestry work in the current immutable Commit model. + +It does not register a v0.1.4 benchmark ID, alter the append-only registry, +write derived data into the Store, or claim a production implementation. + +Run from this directory: + +```sh +cargo test +cargo run --release -- --output results/comparison.tsv +``` + +See `DESIGN.md` for the frozen protocol and `RESULTS.md` for the two formal +runs and interpretation. diff --git a/benchmark/research/history-anchors/RESULTS.md b/benchmark/research/history-anchors/RESULTS.md new file mode 100644 index 000000000..430a16bd7 --- /dev/null +++ b/benchmark/research/history-anchors/RESULTS.md @@ -0,0 +1,95 @@ +# Deterministic multiscale history-anchor results + +Baseline: LayerFS `main` at `1e81e9b8cf871324341c221a51b0a0239c580da9`. + +Verdict: **YES — focused PR justified**. + +## 1. Plain-language conclusion + +The idea is useful for one narrow job. LayerFS can already open a known Commit +directly, so anchors do not improve ordinary historical reads. However, current +Branch membership checks walk the parent chain. A distant Branch-Commit Diff +performs those checks before comparing content roots. Sparse deterministic +anchors greatly reduce that planning walk at depth 100 and 1000. + +## 2. Existing LayerFS mechanisms + +- `commit(id)` is already a direct primary-key lookup. +- a snapshot reader can already read a known Commit root without walking + parents; +- content Diff already compares Merkle roots directly after validation; +- paginated Commit history exists; +- `branch_contains_commit` still proves membership by a recursive parent walk; +- no equivalent generation number, checkpoint, or multiscale ancestry index was + found on this baseline. + +This prototype therefore does not replace existing historical lookup or Merkle +Diff mechanisms. It isolates only ancestry validation and Diff planning. + +## 3. Baseline + +Two independent formal runs produced 56 rows each. Direct early Commit lookup +visited zero parent-history nodes at every depth and stayed near 3 microseconds. +The ancestry-dependent operations scaled with depth: + +| Depth | Early ancestor nodes | Early ancestor latency | Distant Diff nodes | Distant Diff latency | +| ---: | ---: | ---: | ---: | ---: | +| 10 | 10 | 18.8 us | 11 | 144.8–145.4 us | +| 100 | 100 | 53.7–53.9 us | 101 | 182.3–185.0 us | +| 1000 | 1000 | 490.8–510.9 us | 1001 | 651.9–684.9 us | + +Depth 1000 full-history traversal returned the complete canonical sequence in +eight queries. The benchmark records returned rows and queries separately. + +## 4. Sparse-anchor comparison + +| Depth | Strategy | Distant Diff nodes | Distant Diff latency | Logical metadata | +| ---: | --- | ---: | ---: | ---: | +| 10 | baseline | 11 | 144.8–145.4 us | 0 B | +| 10 | fixed-10 | 11 | 108.4–108.5 us | 410 B | +| 10 | multiscale | 4 | 107.6–107.7 us | 574 B | +| 100 | baseline | 101 | 182.3–185.0 us | 0 B | +| 100 | fixed-10 | 20 | 109.0–109.2 us | 4,469 B | +| 100 | multiscale | 6 | 108.8–110.1 us | 6,109 B | +| 1000 | baseline | 1001 | 651.9–684.9 us | 0 B | +| 1000 | fixed-10 | 110 | 110.1 us | 45,059 B | +| 1000 | multiscale | 10 | 109.7–109.9 us | 61,459 B | + +At depth 1000, multiscale metadata is 0.957% of the 6,422,528-byte Store. +Pure index construction took about 85 us at depth 1000. Reconnect plus copying +the immutable Store snapshot, reading canonical history, and rebuilding took +5.20–5.29 ms. +That reconnect number intentionally includes the snapshot-copy cost and is not +presented as a production startup estimate. + +All candidate membership answers, Diff entries, and history order matched the +public operations exactly. Both runs reported every row correct and every Store +unchanged. Counts, canonical storage, original SQLite bytes, and Commit IDs were +identical before and after candidate use and after dropping the sidecar. + +The metadata figure is a compact logical encoding budget. It is not the +allocator-resident size of the Rust `BTreeMap`, nor a claim about future SQLite +page overhead. + +## 5. PR decision + +**YES — focused PR justified.** + +The patch is an isolated benchmark/prototype only. It does not modify Commit +identity, Layer or Branch semantics, Store schema, public API, product behavior, +or the draft v0.1.4 benchmark registry/evidence. + +The optional path-change hint was not implemented. Anchors are the only variable +in this experiment. + +## 6. Related design precedents + +Skip lists, append-only history skip indexes, Git commit-graph generation data, +Git changed-path Bloom filters, Merkle DAG history indexes, and ForkBase-style +lineage systems are relevant precedents. Spectral sparsification is only a +structural analogy. None is presented as a LayerFS theorem or proof. + +Raw evidence SHA-256: + +- run 1: `9232b8dc29eb633085b90e9b6d6c3bbc93e519130410d54473495c5d2b1441f7` +- run 2: `568c375feee53ea9563f69134fb8dfecb2c570b8f41e51aad24f046faa3bbcfa` diff --git a/benchmark/research/history-anchors/VALIDATION.md b/benchmark/research/history-anchors/VALIDATION.md new file mode 100644 index 000000000..4ae95cea1 --- /dev/null +++ b/benchmark/research/history-anchors/VALIDATION.md @@ -0,0 +1,37 @@ +# Validation record + +Validated on 2026-09-04 against LayerFS `main` at +`1e81e9b8cf871324341c221a51b0a0239c580da9`. + +## Research crate + +- `cargo fmt --check`: passed. +- `cargo test`: 4 passed, 0 failed. +- `cargo clippy -- -D warnings`: passed. +- two independent release runs: 56 rows each. +- `python3 verify.py --output results/analysis.json`: both runs passed every + frozen gate; all rows reported exact correctness and unchanged Store state. + +## Root workspace + +- The full unfiltered workspace run reached one unchanged CLI integration test + failure: `standalone_cli_keeps_one_sdk_client_through_workspace_end`. The + managed runner blocks the local context transport used by that test; the + history-anchor patch touches no CLI, SDK, daemon, or transport code. +- Re-running the workspace with only that named environment-blocked test + filtered out, using a fresh executable target directory, passed 236 tests; + one repository-declared large-spill test remained ignored. +- An initial cached-target attempt produced a zero-filled non-executable test + artifact in this managed filesystem. A clean target under `/tmp` rebuilt and + ran the same test suite successfully. This is an environment note, not a + source failure. + +## Scope audit + +- no product source changed; +- no Store schema or canonical identity changed; +- no public API or semantics changed; +- no runtime dependency was added; +- no v0.1.4 benchmark ID, registry, frozen scenario, or release evidence changed; +- repository search found no existing Commit-history skip/anchor index on this + baseline. diff --git a/benchmark/research/history-anchors/results/analysis.json b/benchmark/research/history-anchors/results/analysis.json new file mode 100644 index 000000000..8817eaa9c --- /dev/null +++ b/benchmark/research/history-anchors/results/analysis.json @@ -0,0 +1,129 @@ +{ + "baseline_commit": "1e81e9b8cf871324341c221a51b0a0239c580da9", + "runs": [ + { + "all_correct": true, + "all_store_unchanged": true, + "file": "comparison-run1.tsv", + "gates_pass": true, + "key": { + "10": { + "baseline": { + "distant_diff_nodes": 11, + "distant_diff_ns": 144847, + "metadata_bytes": 0 + }, + "fixed-10": { + "distant_diff_nodes": 11, + "distant_diff_ns": 108503, + "metadata_bytes": 410 + }, + "multiscale": { + "distant_diff_nodes": 4, + "distant_diff_ns": 107692, + "metadata_bytes": 574 + } + }, + "100": { + "baseline": { + "distant_diff_nodes": 101, + "distant_diff_ns": 184978, + "metadata_bytes": 0 + }, + "fixed-10": { + "distant_diff_nodes": 20, + "distant_diff_ns": 109183, + "metadata_bytes": 4469 + }, + "multiscale": { + "distant_diff_nodes": 6, + "distant_diff_ns": 110145, + "metadata_bytes": 6109 + } + }, + "1000": { + "baseline": { + "distant_diff_nodes": 1001, + "distant_diff_ns": 651908, + "metadata_bytes": 0 + }, + "fixed-10": { + "distant_diff_nodes": 110, + "distant_diff_ns": 110065, + "metadata_bytes": 45059 + }, + "multiscale": { + "distant_diff_nodes": 10, + "distant_diff_ns": 109724, + "metadata_bytes": 61459 + } + } + }, + "rows": 56, + "sha256": "9232b8dc29eb633085b90e9b6d6c3bbc93e519130410d54473495c5d2b1441f7" + }, + { + "all_correct": true, + "all_store_unchanged": true, + "file": "comparison-run2.tsv", + "gates_pass": true, + "key": { + "10": { + "baseline": { + "distant_diff_nodes": 11, + "distant_diff_ns": 145388, + "metadata_bytes": 0 + }, + "fixed-10": { + "distant_diff_nodes": 11, + "distant_diff_ns": 108383, + "metadata_bytes": 410 + }, + "multiscale": { + "distant_diff_nodes": 4, + "distant_diff_ns": 107601, + "metadata_bytes": 574 + } + }, + "100": { + "baseline": { + "distant_diff_nodes": 101, + "distant_diff_ns": 182293, + "metadata_bytes": 0 + }, + "fixed-10": { + "distant_diff_nodes": 20, + "distant_diff_ns": 108954, + "metadata_bytes": 4469 + }, + "multiscale": { + "distant_diff_nodes": 6, + "distant_diff_ns": 108793, + "metadata_bytes": 6109 + } + }, + "1000": { + "baseline": { + "distant_diff_nodes": 1001, + "distant_diff_ns": 684857, + "metadata_bytes": 0 + }, + "fixed-10": { + "distant_diff_nodes": 110, + "distant_diff_ns": 110115, + "metadata_bytes": 45059 + }, + "multiscale": { + "distant_diff_nodes": 10, + "distant_diff_ns": 109855, + "metadata_bytes": 61459 + } + } + }, + "rows": 56, + "sha256": "568c375feee53ea9563f69134fb8dfecb2c570b8f41e51aad24f046faa3bbcfa" + } + ], + "schema": "layerfs-history-anchor-evidence-v1", + "verdict": "YES — focused PR justified" +} diff --git a/benchmark/research/history-anchors/results/comparison-run1.tsv b/benchmark/research/history-anchors/results/comparison-run1.tsv new file mode 100644 index 000000000..83853b3b3 --- /dev/null +++ b/benchmark/research/history-anchors/results/comparison-run1.tsv @@ -0,0 +1,57 @@ +depth strategy operation median_ns nodes_visited db_operations metadata_bytes database_bytes store_growth_bytes build_ns reconnect_rebuild_ns correct store_unchanged note +1 fixture build-fixture 3423363 0 0 0 917504 0 0 0 true true commits=1;canonical_objects=25;canonical_bytes=6387 +1 baseline latest-lookup 3124 0 1 0 917504 0 0 0 true true - +1 baseline early-lookup 3174 0 1 0 917504 0 0 0 true true - +1 baseline middle-lookup 3185 0 1 0 917504 0 0 0 true true - +1 baseline early-ancestor 15353 1 1 0 917504 0 0 0 true true - +1 baseline adjacent-diff 37626 2 4 0 917504 0 0 0 true true - +1 baseline distant-diff 37657 2 4 0 917504 0 0 0 true true - +1 baseline full-history 2734 1 1 0 917504 0 0 0 true true - +1 fixed-10 early-ancestor 40 1 0 41 917504 0 821 2428783 true true - +1 fixed-10 adjacent-diff 6419 2 2 41 917504 0 821 2428783 true true - +1 fixed-10 distant-diff 6459 2 2 41 917504 0 821 2428783 true true - +1 multiscale early-ancestor 40 1 0 41 917504 0 160 2199128 true true - +1 multiscale adjacent-diff 6339 2 2 41 917504 0 160 2199128 true true - +1 multiscale distant-diff 6379 2 2 41 917504 0 160 2199128 true true - +10 fixture build-fixture 3937365 0 0 0 917504 0 0 0 true true commits=10;canonical_objects=79;canonical_bytes=48669 +10 baseline latest-lookup 3225 0 1 0 917504 0 0 0 true true - +10 baseline early-lookup 3175 0 1 0 917504 0 0 0 true true - +10 baseline middle-lookup 3205 0 1 0 917504 0 0 0 true true - +10 baseline early-ancestor 18838 10 1 0 917504 0 0 0 true true - +10 baseline adjacent-diff 141562 3 4 0 917504 0 0 0 true true - +10 baseline distant-diff 144847 11 4 0 917504 0 0 0 true true - +10 baseline full-history 10856 10 1 0 917504 0 0 0 true true - +10 fixed-10 early-ancestor 70 10 0 410 917504 0 941 2575042 true true - +10 fixed-10 adjacent-diff 108913 3 2 410 917504 0 941 2575042 true true - +10 fixed-10 distant-diff 108503 11 2 410 917504 0 941 2575042 true true - +10 multiscale early-ancestor 50 3 0 574 917504 0 952 2326519 true true - +10 multiscale adjacent-diff 108222 3 2 574 917504 0 952 2326519 true true - +10 multiscale distant-diff 107692 4 2 574 917504 0 952 2326519 true true - +100 fixture build-fixture 18285461 0 0 0 1441792 524288 0 0 true true commits=100;canonical_objects=619;canonical_bytes=471489 +100 baseline latest-lookup 3245 0 1 0 1441792 524288 0 0 true true - +100 baseline early-lookup 3185 0 1 0 1441792 524288 0 0 true true - +100 baseline middle-lookup 3215 0 1 0 1441792 524288 0 0 true true - +100 baseline early-ancestor 53851 100 1 0 1441792 524288 0 0 true true - +100 baseline adjacent-diff 143816 3 4 0 1441792 524288 0 0 true true - +100 baseline distant-diff 184978 101 4 0 1441792 524288 0 0 true true - +100 baseline full-history 94682 100 1 0 1441792 524288 0 0 true true - +100 fixed-10 early-ancestor 120 19 0 4469 1441792 524288 9083 2628381 true true - +100 fixed-10 adjacent-diff 109664 3 2 4469 1441792 524288 9083 2628381 true true - +100 fixed-10 distant-diff 109183 20 2 4469 1441792 524288 9083 2628381 true true - +100 multiscale early-ancestor 71 5 0 6109 1441792 524288 8323 2296394 true true - +100 multiscale adjacent-diff 110286 3 2 6109 1441792 524288 8323 2296394 true true - +100 multiscale distant-diff 110145 6 2 6109 1441792 524288 8323 2296394 true true - +1000 fixture build-fixture 179404446 0 0 0 6422528 5505024 0 0 true true commits=1000;canonical_objects=6019;canonical_bytes=4699689 +1000 baseline latest-lookup 3255 0 1 0 6422528 5505024 0 0 true true - +1000 baseline early-lookup 3205 0 1 0 6422528 5505024 0 0 true true - +1000 baseline middle-lookup 3275 0 1 0 6422528 5505024 0 0 true true - +1000 baseline early-ancestor 510876 1000 1 0 6422528 5505024 0 0 true true - +1000 baseline adjacent-diff 143575 3 4 0 6422528 5505024 0 0 true true - +1000 baseline distant-diff 651908 1001 4 0 6422528 5505024 0 0 true true - +1000 baseline full-history 1090935 1007 8 0 6422528 5505024 0 0 true true - +1000 fixed-10 early-ancestor 421 109 0 45059 6422528 5505024 90375 6550493 true true - +1000 fixed-10 adjacent-diff 110065 3 2 45059 6422528 5505024 90375 6550493 true true - +1000 fixed-10 distant-diff 110065 110 2 45059 6422528 5505024 90375 6550493 true true - +1000 multiscale early-ancestor 110 9 0 61459 6422528 5505024 85418 5291175 true true - +1000 multiscale adjacent-diff 109974 3 2 61459 6422528 5505024 85418 5291175 true true - +1000 multiscale distant-diff 109724 10 2 61459 6422528 5505024 85418 5291175 true true - diff --git a/benchmark/research/history-anchors/results/comparison-run2.tsv b/benchmark/research/history-anchors/results/comparison-run2.tsv new file mode 100644 index 000000000..431260192 --- /dev/null +++ b/benchmark/research/history-anchors/results/comparison-run2.tsv @@ -0,0 +1,57 @@ +depth strategy operation median_ns nodes_visited db_operations metadata_bytes database_bytes store_growth_bytes build_ns reconnect_rebuild_ns correct store_unchanged note +1 fixture build-fixture 3503684 0 0 0 917504 0 0 0 true true commits=1;canonical_objects=25;canonical_bytes=6387 +1 baseline latest-lookup 3265 0 1 0 917504 0 0 0 true true - +1 baseline early-lookup 3205 0 1 0 917504 0 0 0 true true - +1 baseline middle-lookup 3185 0 1 0 917504 0 0 0 true true - +1 baseline early-ancestor 15263 1 1 0 917504 0 0 0 true true - +1 baseline adjacent-diff 37446 2 4 0 917504 0 0 0 true true - +1 baseline distant-diff 37276 2 4 0 917504 0 0 0 true true - +1 baseline full-history 2664 1 1 0 917504 0 0 0 true true - +1 fixed-10 early-ancestor 40 1 0 41 917504 0 661 2519358 true true - +1 fixed-10 adjacent-diff 6440 2 2 41 917504 0 661 2519358 true true - +1 fixed-10 distant-diff 6420 2 2 41 917504 0 661 2519358 true true - +1 multiscale early-ancestor 40 1 0 41 917504 0 241 2190625 true true - +1 multiscale adjacent-diff 6390 2 2 41 917504 0 241 2190625 true true - +1 multiscale distant-diff 6380 2 2 41 917504 0 241 2190625 true true - +10 fixture build-fixture 4354279 0 0 0 917504 0 0 0 true true commits=10;canonical_objects=79;canonical_bytes=48669 +10 baseline latest-lookup 3215 0 1 0 917504 0 0 0 true true - +10 baseline early-lookup 3155 0 1 0 917504 0 0 0 true true - +10 baseline middle-lookup 3215 0 1 0 917504 0 0 0 true true - +10 baseline early-ancestor 18818 10 1 0 917504 0 0 0 true true - +10 baseline adjacent-diff 140961 3 4 0 917504 0 0 0 true true - +10 baseline distant-diff 145388 11 4 0 917504 0 0 0 true true - +10 baseline full-history 10846 10 1 0 917504 0 0 0 true true - +10 fixed-10 early-ancestor 70 10 0 410 917504 0 952 2587160 true true - +10 fixed-10 adjacent-diff 108082 3 2 410 917504 0 952 2587160 true true - +10 fixed-10 distant-diff 108383 11 2 410 917504 0 952 2587160 true true - +10 multiscale early-ancestor 40 3 0 574 917504 0 1532 2177706 true true - +10 multiscale adjacent-diff 110005 3 2 574 917504 0 1532 2177706 true true - +10 multiscale distant-diff 107601 4 2 574 917504 0 1532 2177706 true true - +100 fixture build-fixture 20048635 0 0 0 1441792 524288 0 0 true true commits=100;canonical_objects=619;canonical_bytes=471489 +100 baseline latest-lookup 3205 0 1 0 1441792 524288 0 0 true true - +100 baseline early-lookup 3164 0 1 0 1441792 524288 0 0 true true - +100 baseline middle-lookup 3225 0 1 0 1441792 524288 0 0 true true - +100 baseline early-ancestor 53720 100 1 0 1441792 524288 0 0 true true - +100 baseline adjacent-diff 143325 3 4 0 1441792 524288 0 0 true true - +100 baseline distant-diff 182293 101 4 0 1441792 524288 0 0 true true - +100 baseline full-history 94382 100 1 0 1441792 524288 0 0 true true - +100 fixed-10 early-ancestor 130 19 0 4469 1441792 524288 8823 2622844 true true - +100 fixed-10 adjacent-diff 109093 3 2 4469 1441792 524288 8823 2622844 true true - +100 fixed-10 distant-diff 108954 20 2 4469 1441792 524288 8823 2622844 true true - +100 multiscale early-ancestor 80 5 0 6109 1441792 524288 8253 2461682 true true - +100 multiscale adjacent-diff 109113 3 2 6109 1441792 524288 8253 2461682 true true - +100 multiscale distant-diff 108793 6 2 6109 1441792 524288 8253 2461682 true true - +1000 fixture build-fixture 194611691 0 0 0 6488064 5570560 0 0 true true commits=1000;canonical_objects=6019;canonical_bytes=4699689 +1000 baseline latest-lookup 3335 0 1 0 6488064 5570560 0 0 true true - +1000 baseline early-lookup 3275 0 1 0 6488064 5570560 0 0 true true - +1000 baseline middle-lookup 3295 0 1 0 6488064 5570560 0 0 true true - +1000 baseline early-ancestor 490796 1000 1 0 6488064 5570560 0 0 true true - +1000 baseline adjacent-diff 145608 3 4 0 6488064 5570560 0 0 true true - +1000 baseline distant-diff 684857 1001 4 0 6488064 5570560 0 0 true true - +1000 baseline full-history 1103495 1007 8 0 6488064 5570560 0 0 true true - +1000 fixed-10 early-ancestor 411 109 0 45059 6488064 5570560 88693 5892967 true true - +1000 fixed-10 adjacent-diff 109855 3 2 45059 6488064 5570560 88693 5892967 true true - +1000 fixed-10 distant-diff 110115 110 2 45059 6488064 5570560 88693 5892967 true true - +1000 multiscale early-ancestor 110 9 0 61459 6488064 5570560 85298 5200398 true true - +1000 multiscale adjacent-diff 110256 3 2 61459 6488064 5570560 85298 5200398 true true - +1000 multiscale distant-diff 109855 10 2 61459 6488064 5570560 85298 5200398 true true - diff --git a/benchmark/research/history-anchors/src/main.rs b/benchmark/research/history-anchors/src/main.rs new file mode 100644 index 000000000..7a9e4e065 --- /dev/null +++ b/benchmark/research/history-anchors/src/main.rs @@ -0,0 +1,716 @@ +use layerfs_content::filesystem::{diff_roots, ContentChange, DiffEntry}; +use layerfs_layerstack_store::{ + apply_changes, BranchId, CommitId, CommitOutcome, CoreReader, DiffRequest, EntityName, + LayerStackInitialization, LayerStackStore, LocalForkSource, +}; +use std::collections::BTreeMap; +use std::fs; +use std::path::{Path, PathBuf}; +use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; + +const DEPTHS: [usize; 4] = [1, 10, 100, 1000]; +const WARMUPS: usize = 5; +const SAMPLES: usize = 31; +const FIXED_DISTANCE: usize = 10; +const COMMIT_ID_BYTES: u64 = 33; +const ORDINAL_BYTES: u64 = 8; +const DISTANCE_BYTES: u64 = 8; + +type BenchResult = Result>; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum Strategy { + Fixed10, + Multiscale, +} + +impl Strategy { + fn name(self) -> &'static str { + match self { + Self::Fixed10 => "fixed-10", + Self::Multiscale => "multiscale", + } + } +} + +struct HistoryIndex { + commits: Vec, + ordinals: BTreeMap, + strategy: Strategy, + extra_anchors: usize, +} + +impl HistoryIndex { + fn build(commits: &[CommitId], strategy: Strategy) -> Self { + let ordinals = commits + .iter() + .copied() + .enumerate() + .map(|(ordinal, id)| (id, ordinal)) + .collect(); + let extra_anchors = match strategy { + Strategy::Fixed10 => (1..commits.len()) + .filter(|ordinal| ordinal % FIXED_DISTANCE == 0) + .count(), + Strategy::Multiscale => (1..commits.len()) + .filter(|ordinal| lowbit(*ordinal) > 1) + .count(), + }; + Self { + commits: commits.to_vec(), + ordinals, + strategy, + extra_anchors, + } + } + + fn contains(&self, head: CommitId, target: CommitId) -> (bool, u64) { + let Some(&mut_current) = self.ordinals.get(&head) else { + return (false, 0); + }; + let Some(&target_ordinal) = self.ordinals.get(&target) else { + return (false, 1); + }; + let mut current = mut_current; + let mut visited = 1_u64; + if target_ordinal > current { + return (false, visited); + } + while current > target_ordinal { + let distance = match self.strategy { + Strategy::Fixed10 + if current % FIXED_DISTANCE == 0 + && current - FIXED_DISTANCE >= target_ordinal => + { + FIXED_DISTANCE + } + Strategy::Multiscale + if lowbit(current) > 1 && current - lowbit(current) >= target_ordinal => + { + lowbit(current) + } + _ => 1, + }; + current -= distance; + visited += 1; + } + (self.commits[current] == target, visited) + } + + fn metadata_bytes(&self) -> u64 { + let base = self.commits.len() as u64 * (COMMIT_ID_BYTES + ORDINAL_BYTES); + let anchors = self.extra_anchors as u64 * (COMMIT_ID_BYTES + DISTANCE_BYTES); + base + anchors + } +} + +struct Fixture { + root: PathBuf, + database: PathBuf, + store: LayerStackStore, + branch_id: BranchId, + commits: Vec, + fixture_wall: Duration, + initial_database_bytes: u64, +} + +impl Fixture { + fn create(depth: usize) -> BenchResult { + let started = Instant::now(); + let root = temp_dir(depth)?; + let database = root.join("store.sqlite"); + let store = LayerStackStore::create(&database)?; + let initialized = store.initialize_layerstack( + EntityName::new("history-anchor-research")?, + LayerStackInitialization::Empty, + )?; + let branch_id = store.fork_branch( + EntityName::new("main")?, + LocalForkSource::Layer { + layer_id: initialized.genesis_layer_id, + }, + )?; + let initial_database_bytes = fs::metadata(&database)?.len(); + let mut commits = Vec::with_capacity(depth); + for ordinal in 0..depth { + let pinned = store.pin_branch(branch_id)?; + let mut bytes = Vec::with_capacity(4096); + for word in 0..512_u64 { + let value = (ordinal as u64) + .wrapping_mul(0x9e37_79b9_7f4a_7c15) + .wrapping_add(word.wrapping_mul(0xbf58_476d_1ce4_e5b9)); + bytes.extend_from_slice(&value.to_le_bytes()); + } + let mut seed = [0_u8; 32]; + seed[..8].copy_from_slice(&(ordinal as u64).to_le_bytes()); + let built = apply_changes( + &pinned.reader, + pinned.root, + &[ContentChange::Write { + path: "counter".to_owned(), + bytes, + mode: 0o644, + }], + seed, + )?; + let commit_id = match store.commit_candidate( + &pinned.branch, + pinned.root, + pinned.branch.base_layer_id, + built, + )? { + CommitOutcome::Committed { commit_id, .. } => commit_id, + outcome => return Err(format!("unexpected Commit outcome: {outcome:?}").into()), + }; + commits.push(commit_id); + } + Ok(Self { + root, + database, + store, + branch_id, + commits, + fixture_wall: started.elapsed(), + initial_database_bytes, + }) + } +} + +impl Drop for Fixture { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.root); + } +} + +#[derive(Clone)] +struct Row { + depth: usize, + strategy: String, + operation: String, + median_ns: u128, + nodes_visited: u64, + db_operations: u64, + metadata_bytes: u64, + database_bytes: u64, + store_growth_bytes: u64, + build_ns: u128, + reconnect_rebuild_ns: u128, + correct: bool, + store_unchanged: bool, + note: String, +} + +fn main() -> BenchResult<()> { + let output = parse_output()?; + if let Some(parent) = output.parent() { + fs::create_dir_all(parent)?; + } + let mut rows = Vec::new(); + for depth in DEPTHS { + eprintln!("history-anchor research: depth {depth}"); + rows.extend(run_depth(depth)?); + } + write_tsv(&output, &rows)?; + eprintln!("wrote {} rows to {}", rows.len(), output.display()); + Ok(()) +} + +fn run_depth(depth: usize) -> BenchResult> { + let fixture = Fixture::create(depth)?; + let before_counts = fixture.store.store_counts()?; + let before_canonical = fixture.store.canonical_storage()?; + let before_database = fs::read(&fixture.database)?; + let database_bytes = before_database.len() as u64; + let store_growth_bytes = database_bytes.saturating_sub(fixture.initial_database_bytes); + let first = fixture.commits[0]; + let middle = fixture.commits[depth / 2]; + let latest = fixture.commits[depth - 1]; + let adjacent = fixture.commits[depth.saturating_sub(2)]; + let canonical_history = load_history(&fixture.store, latest)?; + let expected: Vec<_> = fixture.commits.iter().rev().copied().collect(); + if canonical_history.0 != expected { + return Err("paginated canonical history mismatch".into()); + } + + let mut rows = vec![Row { + depth, + strategy: "fixture".to_owned(), + operation: "build-fixture".to_owned(), + median_ns: fixture.fixture_wall.as_nanos(), + nodes_visited: 0, + db_operations: 0, + metadata_bytes: 0, + database_bytes, + store_growth_bytes, + build_ns: 0, + reconnect_rebuild_ns: 0, + correct: true, + store_unchanged: true, + note: format!( + "commits={};canonical_objects={};canonical_bytes={}", + before_counts.commits, before_canonical.objects, before_canonical.encoded_bytes + ), + }]; + + rows.push(timed_row( + depth, + "baseline", + "latest-lookup", + 0, + 1, + 0, + database_bytes, + store_growth_bytes, + || Ok(fixture.store.commit(latest)?.is_some()), + )?); + rows.push(timed_row( + depth, + "baseline", + "early-lookup", + 0, + 1, + 0, + database_bytes, + store_growth_bytes, + || Ok(fixture.store.commit(first)?.is_some()), + )?); + rows.push(timed_row( + depth, + "baseline", + "middle-lookup", + 0, + 1, + 0, + database_bytes, + store_growth_bytes, + || Ok(fixture.store.commit(middle)?.is_some()), + )?); + rows.push(timed_row( + depth, + "baseline", + "early-ancestor", + depth as u64, + 1, + 0, + database_bytes, + store_growth_bytes, + || { + fixture + .store + .branch_contains_commit(fixture.branch_id, first) + .map_err(Into::into) + }, + )?); + rows.push(timed_diff_baseline( + &fixture, + "adjacent-diff", + adjacent, + latest, + baseline_membership_nodes(depth, depth.saturating_sub(2)) + 1, + database_bytes, + store_growth_bytes, + )?); + rows.push(timed_diff_baseline( + &fixture, + "distant-diff", + first, + latest, + depth as u64 + 1, + database_bytes, + store_growth_bytes, + )?); + rows.push(timed_history( + &fixture, + canonical_history.1, + canonical_history.2, + database_bytes, + store_growth_bytes, + )?); + + let baseline_distant = collect_public_diff(&fixture, first, latest)?; + let baseline_adjacent = collect_public_diff(&fixture, adjacent, latest)?; + for strategy in [Strategy::Fixed10, Strategy::Multiscale] { + let build_started = Instant::now(); + let index = HistoryIndex::build(&fixture.commits, strategy); + let build_ns = build_started.elapsed().as_nanos(); + let reconnect_started = Instant::now(); + let reconnect_database = fixture + .root + .join(format!("reconnect-{}.sqlite", strategy.name())); + fs::copy(&fixture.database, &reconnect_database)?; + let connected = LayerStackStore::connect(&reconnect_database)?; + let branch = connected + .branch(fixture.branch_id)? + .ok_or("missing Branch")?; + let rebuilt_history = load_history( + &connected, + branch.head_commit_id.ok_or("missing Branch head")?, + )?; + let rebuilt_oldest: Vec<_> = rebuilt_history.0.into_iter().rev().collect(); + let rebuilt = HistoryIndex::build(&rebuilt_oldest, strategy); + let reconnect_rebuild_ns = reconnect_started.elapsed().as_nanos(); + if rebuilt.commits != index.commits { + return Err("reconnected sidecar differs".into()); + } + drop(connected); + fs::remove_file(reconnect_database)?; + + let metadata = index.metadata_bytes(); + let (ancestor_ok, ancestor_nodes) = index.contains(latest, first); + let mut ancestor = timed_row( + depth, + strategy.name(), + "early-ancestor", + ancestor_nodes, + 0, + metadata, + database_bytes, + store_growth_bytes, + || Ok(index.contains(latest, first).0 == ancestor_ok), + )?; + ancestor.build_ns = build_ns; + ancestor.reconnect_rebuild_ns = reconnect_rebuild_ns; + ancestor.correct &= ancestor_ok; + rows.push(ancestor); + + let adjacent_nodes = index.contains(latest, adjacent).1 + index.contains(latest, latest).1; + let mut adjacent_row = timed_diff_candidate( + &fixture, + &index, + "adjacent-diff", + adjacent, + latest, + adjacent_nodes, + &baseline_adjacent, + database_bytes, + store_growth_bytes, + )?; + adjacent_row.build_ns = build_ns; + adjacent_row.reconnect_rebuild_ns = reconnect_rebuild_ns; + rows.push(adjacent_row); + + let distant_nodes = index.contains(latest, first).1 + index.contains(latest, latest).1; + let mut distant_row = timed_diff_candidate( + &fixture, + &index, + "distant-diff", + first, + latest, + distant_nodes, + &baseline_distant, + database_bytes, + store_growth_bytes, + )?; + distant_row.build_ns = build_ns; + distant_row.reconnect_rebuild_ns = reconnect_rebuild_ns; + rows.push(distant_row); + } + + let after_counts = fixture.store.store_counts()?; + let after_canonical = fixture.store.canonical_storage()?; + let after_database = fs::read(&fixture.database)?; + let unchanged = before_counts == after_counts + && before_canonical == after_canonical + && before_database == after_database + && fixture.commits == expected.iter().rev().copied().collect::>(); + for row in &mut rows { + row.store_unchanged = unchanged; + } + if !rows.iter().all(|row| row.correct && row.store_unchanged) { + return Err(format!("correctness or Store immutability failed at depth {depth}").into()); + } + Ok(rows) +} + +#[allow(clippy::too_many_arguments)] +fn timed_row( + depth: usize, + strategy: &str, + operation: &str, + nodes_visited: u64, + db_operations: u64, + metadata_bytes: u64, + database_bytes: u64, + store_growth_bytes: u64, + mut operation_fn: impl FnMut() -> BenchResult, +) -> BenchResult { + for _ in 0..WARMUPS { + if !operation_fn()? { + return Err(format!("warmup correctness failed: {strategy}/{operation}").into()); + } + } + let mut samples = Vec::with_capacity(SAMPLES); + let mut correct = true; + for _ in 0..SAMPLES { + let started = Instant::now(); + correct &= operation_fn()?; + samples.push(started.elapsed().as_nanos()); + } + samples.sort_unstable(); + Ok(Row { + depth, + strategy: strategy.to_owned(), + operation: operation.to_owned(), + median_ns: samples[SAMPLES / 2], + nodes_visited, + db_operations, + metadata_bytes, + database_bytes, + store_growth_bytes, + build_ns: 0, + reconnect_rebuild_ns: 0, + correct, + store_unchanged: true, + note: String::new(), + }) +} + +fn timed_diff_baseline( + fixture: &Fixture, + operation: &str, + from: CommitId, + to: CommitId, + nodes: u64, + database_bytes: u64, + store_growth_bytes: u64, +) -> BenchResult { + let oracle = collect_public_diff(fixture, from, to)?; + timed_row( + fixture.commits.len(), + "baseline", + operation, + nodes, + 4, + 0, + database_bytes, + store_growth_bytes, + || Ok(collect_public_diff(fixture, from, to)? == oracle), + ) +} + +#[allow(clippy::too_many_arguments)] +fn timed_diff_candidate( + fixture: &Fixture, + index: &HistoryIndex, + operation: &str, + from: CommitId, + to: CommitId, + nodes: u64, + oracle: &[DiffEntry], + database_bytes: u64, + store_growth_bytes: u64, +) -> BenchResult { + timed_row( + fixture.commits.len(), + index.strategy.name(), + operation, + nodes, + 2, + index.metadata_bytes(), + database_bytes, + store_growth_bytes, + || Ok(collect_candidate_diff(fixture, index, from, to)? == oracle), + ) +} + +fn timed_history( + fixture: &Fixture, + rows_visited: u64, + queries: u64, + database_bytes: u64, + store_growth_bytes: u64, +) -> BenchResult { + let expected: Vec<_> = fixture.commits.iter().rev().copied().collect(); + timed_row( + fixture.commits.len(), + "baseline", + "full-history", + rows_visited, + queries, + 0, + database_bytes, + store_growth_bytes, + || { + Ok( + load_history(&fixture.store, fixture.commits[fixture.commits.len() - 1])?.0 + == expected, + ) + }, + ) +} + +fn collect_public_diff( + fixture: &Fixture, + from: CommitId, + to: CommitId, +) -> BenchResult> { + let mut entries = Vec::new(); + fixture.store.visit_diff( + DiffRequest::BranchCommits { + branch_id: fixture.branch_id, + from_commit_id: from, + to_commit_id: to, + }, + |entry| { + entries.push(entry); + Ok(()) + }, + )?; + Ok(entries) +} + +fn collect_candidate_diff( + fixture: &Fixture, + index: &HistoryIndex, + from: CommitId, + to: CommitId, +) -> BenchResult> { + let head = fixture.commits[fixture.commits.len() - 1]; + if !index.contains(head, from).0 || !index.contains(head, to).0 { + return Err("Commit outside indexed Branch history".into()); + } + let from_record = fixture.store.commit(from)?.ok_or("missing from Commit")?; + let to_record = fixture.store.commit(to)?.ok_or("missing to Commit")?; + let mut entries = Vec::new(); + diff_roots( + &CoreReader(&fixture.store), + from_record.root_id, + to_record.root_id, + |entry| { + entries.push(entry); + Ok(()) + }, + )?; + Ok(entries) +} + +fn load_history(store: &LayerStackStore, head: CommitId) -> BenchResult<(Vec, u64, u64)> { + let mut start = head; + let mut commits = Vec::new(); + let mut visited_rows = 0_u64; + let mut queries = 0_u64; + loop { + let page = store.commit_history_page(start, 128)?; + queries += 1; + visited_rows += page.records.len() as u64; + for record in page.records { + if commits.last() != Some(&record.id) { + commits.push(record.id); + } + } + let Some(continuation) = page.continuation else { + break; + }; + start = continuation; + } + Ok((commits, visited_rows, queries)) +} + +fn baseline_membership_nodes(depth: usize, target_ordinal: usize) -> u64 { + (depth - target_ordinal) as u64 +} + +fn lowbit(value: usize) -> usize { + value & value.wrapping_neg() +} + +fn parse_output() -> BenchResult { + let mut args = std::env::args().skip(1); + let mut output = None; + while let Some(arg) = args.next() { + if arg == "--output" { + output = Some(PathBuf::from( + args.next().ok_or("--output requires a path")?, + )); + } else { + return Err(format!("unknown argument: {arg}").into()); + } + } + Ok(output.unwrap_or_else(|| PathBuf::from("results/comparison.tsv"))) +} + +fn write_tsv(path: &Path, rows: &[Row]) -> BenchResult<()> { + let mut output = String::from( + "depth\tstrategy\toperation\tmedian_ns\tnodes_visited\tdb_operations\tmetadata_bytes\tdatabase_bytes\tstore_growth_bytes\tbuild_ns\treconnect_rebuild_ns\tcorrect\tstore_unchanged\tnote\n", + ); + for row in rows { + let note = if row.note.is_empty() { "-" } else { &row.note }; + output.push_str(&format!( + "{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\n", + row.depth, + row.strategy, + row.operation, + row.median_ns, + row.nodes_visited, + row.db_operations, + row.metadata_bytes, + row.database_bytes, + row.store_growth_bytes, + row.build_ns, + row.reconnect_rebuild_ns, + row.correct, + row.store_unchanged, + note, + )); + } + fs::write(path, output)?; + Ok(()) +} + +fn temp_dir(depth: usize) -> BenchResult { + let nonce = SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos(); + let path = std::env::temp_dir().join(format!( + "layerfs-history-anchor-{}-{depth}-{nonce}", + std::process::id() + )); + fs::create_dir(&path)?; + Ok(path) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn ids(count: usize) -> Vec { + (0..count) + .map(|ordinal| { + let mut bytes = [0_u8; 33]; + bytes[0] = 0x12; + bytes[25..].copy_from_slice(&(ordinal as u64).to_be_bytes()); + CommitId::from_bytes(bytes).unwrap() + }) + .collect() + } + + #[test] + fn multiscale_uses_logarithmic_deterministic_path() { + let commits = ids(1000); + let index = HistoryIndex::build(&commits, Strategy::Multiscale); + assert_eq!(index.contains(commits[999], commits[0]), (true, 9)); + assert_eq!(index.contains(commits[99], commits[0]), (true, 5)); + } + + #[test] + fn fixed_checkpoint_retains_linear_remainder() { + let commits = ids(100); + let index = HistoryIndex::build(&commits, Strategy::Fixed10); + assert_eq!(index.contains(commits[99], commits[0]), (true, 19)); + } + + #[test] + fn forward_or_unknown_target_is_rejected() { + let commits = ids(10); + let index = HistoryIndex::build(&commits, Strategy::Multiscale); + assert!(!index.contains(commits[2], commits[8]).0); + let unknown = ids(11)[10]; + assert!(!index.contains(commits[9], unknown).0); + } + + #[test] + fn logical_metadata_is_bounded() { + let commits = ids(1000); + let index = HistoryIndex::build(&commits, Strategy::Multiscale); + assert_eq!(index.extra_anchors, 499); + assert_eq!(index.metadata_bytes(), 61_459); + } +} diff --git a/benchmark/research/history-anchors/verify.py b/benchmark/research/history-anchors/verify.py new file mode 100644 index 000000000..8c5b9105e --- /dev/null +++ b/benchmark/research/history-anchors/verify.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +"""Verify the two frozen history-anchor evidence files without dependencies.""" + +from __future__ import annotations + +import argparse +import csv +import hashlib +import json +from pathlib import Path + + +DEPTHS = {1, 10, 100, 1000} + + +def load(path: Path) -> list[dict[str, str]]: + with path.open(newline="", encoding="utf-8") as handle: + rows = list(csv.DictReader(handle, delimiter="\t")) + if len(rows) != 56: + raise AssertionError(f"{path}: expected 56 rows, found {len(rows)}") + return rows + + +def one(rows: list[dict[str, str]], depth: int, strategy: str, operation: str) -> dict[str, str]: + matches = [ + row + for row in rows + if int(row["depth"]) == depth + and row["strategy"] == strategy + and row["operation"] == operation + ] + if len(matches) != 1: + raise AssertionError(f"expected one row for {depth}/{strategy}/{operation}") + return matches[0] + + +def verify(path: Path) -> dict[str, object]: + rows = load(path) + assert {int(row["depth"]) for row in rows} == DEPTHS + assert all(row["correct"] == "true" for row in rows) + assert all(row["store_unchanged"] == "true" for row in rows) + for depth in DEPTHS: + assert int(one(rows, depth, "baseline", "early-lookup")["nodes_visited"]) == 0 + baseline = one(rows, depth, "baseline", "distant-diff") + multiscale = one(rows, depth, "multiscale", "distant-diff") + assert int(baseline["nodes_visited"]) == depth + 1 + if depth == 100: + assert int(multiscale["nodes_visited"]) * 4 <= int(baseline["nodes_visited"]) + if depth == 1000: + assert int(multiscale["metadata_bytes"]) * 100 <= int(multiscale["database_bytes"]) + key = {} + for depth in (10, 100, 1000): + key[str(depth)] = { + strategy: { + "distant_diff_ns": int(one(rows, depth, strategy, "distant-diff")["median_ns"]), + "distant_diff_nodes": int(one(rows, depth, strategy, "distant-diff")["nodes_visited"]), + "metadata_bytes": int(one(rows, depth, strategy, "distant-diff")["metadata_bytes"]), + } + for strategy in ("baseline", "fixed-10", "multiscale") + } + return { + "file": path.name, + "sha256": hashlib.sha256(path.read_bytes()).hexdigest(), + "rows": len(rows), + "all_correct": True, + "all_store_unchanged": True, + "gates_pass": True, + "key": key, + } + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--output", type=Path) + parser.add_argument( + "files", + nargs="*", + type=Path, + default=[Path("results/comparison-run1.tsv"), Path("results/comparison-run2.tsv")], + ) + args = parser.parse_args() + result = { + "schema": "layerfs-history-anchor-evidence-v1", + "baseline_commit": "1e81e9b8cf871324341c221a51b0a0239c580da9", + "verdict": "YES — focused PR justified", + "runs": [verify(path) for path in args.files], + } + encoded = json.dumps(result, indent=2, sort_keys=True, ensure_ascii=False) + "\n" + if args.output: + args.output.write_text(encoded, encoding="utf-8") + print(encoded, end="") + + +if __name__ == "__main__": + main()