From d0d61e798423e38cd2bbbe145bcaf362f889d759 Mon Sep 17 00:00:00 2001 From: "Joseph D. Carpinelli" Date: Thu, 12 Mar 2026 23:59:34 -0400 Subject: [PATCH] feat: add kiln-benchmarks crate with 7 Git object store benchmarks Adds a new crates/kiln-benchmarks workspace member implementing the full benchmark suite described in docs/design/git-kiln-benchmarks.md. Benchmarks: 1. Action hash cache lookup (hit/miss at N=100, 1k, 10k refs) 2. Output tree ingestion (compression level 0 vs 6, repo size at 100/500 crates) 3. Output tree materialization (N=10, 50, 200 crates, odb vs fs timing) 4. Deduplication across crate versions (object count, dedup ratio) 5. Ref namespace scale (enumerate at 1k/10k/100k, lookup hit/miss) 6. Fetch simulation (transfer size estimates, local ingestion timing) 7. GC and pack behavior (size before/after, retained refs, delta compression) All repos are bare, remotes-free, created in tempfile::TempDir. core.looseCompression=0 is set on repos that store binary artifact blobs. Also: - Adds crates/kiln-benchmarks/README.md (results template + analysis guide) - Adds kiln-benchmarks to release-please config (publish=false) - Updates CD.yml: publishes only git-kiln, adds a benchmarks job that runs cargo bench and uploads results to GitHub Actions artifacts --- .config/release-please-config.json | 5 +- .config/release-please-manifest.json | 3 +- .github/workflows/CD.yml | 35 +- Cargo.lock | 1396 +++++++++++++++++++++++- Cargo.toml | 2 +- crates/kiln-benchmarks/Cargo.toml | 21 + crates/kiln-benchmarks/README.md | 262 +++++ crates/kiln-benchmarks/benches/kiln.rs | 774 +++++++++++++ crates/kiln-benchmarks/src/lib.rs | 5 + 9 files changed, 2458 insertions(+), 45 deletions(-) create mode 100644 crates/kiln-benchmarks/Cargo.toml create mode 100644 crates/kiln-benchmarks/README.md create mode 100644 crates/kiln-benchmarks/benches/kiln.rs create mode 100644 crates/kiln-benchmarks/src/lib.rs diff --git a/.config/release-please-config.json b/.config/release-please-config.json index 00ea8a7..2c0f3ca 100644 --- a/.config/release-please-config.json +++ b/.config/release-please-config.json @@ -10,7 +10,10 @@ "pull-request-title-pattern": "release: `${component}` v${version}", "pull-request-footer": "This release was generated with [Release Please](https://github.com/googleapis/release-please).", "packages": { - "crates/git-kiln": {} + "crates/git-kiln": {}, + "crates/kiln-benchmarks": { + "publish": false + } }, "plugins": [{ "type": "sentence-case" }, { "type": "cargo-workspace" }], "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json" diff --git a/.config/release-please-manifest.json b/.config/release-please-manifest.json index a01f649..53f2e7c 100644 --- a/.config/release-please-manifest.json +++ b/.config/release-please-manifest.json @@ -1,3 +1,4 @@ { - "crates/git-kiln": "0.0.0" + "crates/git-kiln": "0.0.0", + "crates/kiln-benchmarks": "0.0.0" } diff --git a/.github/workflows/CD.yml b/.github/workflows/CD.yml index 342529c..e21a802 100644 --- a/.github/workflows/CD.yml +++ b/.github/workflows/CD.yml @@ -45,8 +45,9 @@ jobs: with: cache: true toolchain: stable - - name: Package crates - run: cargo package --workspace + - name: Package publishable crates + # kiln-benchmarks has publish = false and must be excluded from packaging + run: cargo package --package git-kiln - name: Generate artifact attestation uses: actions/attest-build-provenance@v2 with: @@ -57,7 +58,33 @@ jobs: IS_PRERELEASE: ${{ github.event.release.prerelease }} run: | if [ "$IS_PRERELEASE" = "true" ]; then - cargo publish --workspace --dry-run + cargo publish --package git-kiln --dry-run else - cargo publish --workspace --token "$CARGO_REGISTRY_TOKEN" + cargo publish --package git-kiln --token "$CARGO_REGISTRY_TOKEN" fi + + benchmarks: + name: Benchmarks + needs: check-tag + if: needs.check-tag.outputs.should-publish == 'true' + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + cache: true + toolchain: stable + - name: Install git (for git gc / pack-refs used in benchmarks) + run: sudo apt-get update && sudo apt-get install -y git + - name: Run benchmarks + run: | + cargo bench --package kiln-benchmarks -- --output-format bencher \ + 2>&1 | tee target/criterion/bench-output.txt + - name: Upload benchmark results + uses: actions/upload-artifact@v4 + with: + name: benchmark-results + path: target/criterion/ + retention-days: 90 diff --git a/Cargo.lock b/Cargo.lock index 488faad..b80df77 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,21 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + [[package]] name = "anstream" version = "1.0.0" @@ -52,6 +67,90 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + [[package]] name = "clap" version = "4.6.0" @@ -108,6 +207,208 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "is-terminal", + "itertools", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + [[package]] name = "git-kiln" version = "0.0.0" @@ -116,6 +417,47 @@ dependencies = [ "clap_mangen", ] +[[package]] +name = "git2" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b903b73e45dc0c6c596f2d37eccece7c1c8bb6e4407b001096387c63d0d93724" +dependencies = [ + "bitflags", + "libc", + "libgit2-sys", + "log", + "openssl-probe", + "openssl-sys", + "url", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + [[package]] name = "heck" version = "0.5.0" @@ -123,81 +465,1059 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] -name = "is_terminal_polyfill" -version = "1.70.2" +name = "hermit-abi" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] -name = "once_cell_polyfill" -version = "1.70.2" +name = "icu_collections" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] [[package]] -name = "proc-macro2" -version = "1.0.106" +name = "icu_locale_core" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ - "unicode-ident", + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", ] [[package]] -name = "quote" -version = "1.0.45" +name = "icu_normalizer" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ - "proc-macro2", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", ] [[package]] -name = "roff" +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbf2048e0e979efb2ca7b91c4f1a8d77c91853e9b987c94c555668a8994915ad" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] [[package]] -name = "strsim" -version = "0.11.1" +name = "idna_adapter" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] [[package]] -name = "syn" -version = "2.0.117" +name = "indexmap" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", ] [[package]] -name = "unicode-ident" -version = "1.0.24" +name = "is-terminal" +version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys", +] [[package]] -name = "utf8parse" -version = "0.2.2" +name = "is_terminal_polyfill" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] -name = "windows-link" -version = "0.2.1" +name = "itertools" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] [[package]] -name = "windows-sys" -version = "0.61.2" +name = "itoa" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ - "windows-link", + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "kiln-benchmarks" +version = "0.0.0" +dependencies = [ + "criterion", + "git2", + "rand", + "sha2", + "tempfile", ] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" + +[[package]] +name = "libgit2-sys" +version = "0.17.0+1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10472326a8a6477c3c20a64547b0059e4b0d086869eee31e6d7da728a8eb7224" +dependencies = [ + "cc", + "libc", + "libssh2-sys", + "libz-sys", + "openssl-sys", + "pkg-config", +] + +[[package]] +name = "libssh2-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "220e4f05ad4a218192533b300327f5150e809b54c4ec83b5a1d91833601811b9" +dependencies = [ + "cc", + "libc", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "libz-sys" +version = "1.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52f4c29e2a68ac30c9087e1b772dc9f44a2b66ed44edf2266cf2be9b03dafc1" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.112" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "roff" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbf2048e0e979efb2ca7b91c4f1a8d77c91853e9b987c94c555668a8994915ad" + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index 62eeba3..f08ae8c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "3" -members = ["crates/git-kiln"] +members = ["crates/git-kiln", "crates/kiln-benchmarks"] [workspace.package] edition = "2024" diff --git a/crates/kiln-benchmarks/Cargo.toml b/crates/kiln-benchmarks/Cargo.toml new file mode 100644 index 0000000..fe7083f --- /dev/null +++ b/crates/kiln-benchmarks/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "kiln-benchmarks" +version = "0.0.0" +edition.workspace = true +publish = false +license.workspace = true +description = "Benchmarks for measuring Git object store performance under Kiln's build cache access patterns." + +[[bench]] +name = "kiln" +harness = false + +[dev-dependencies] +criterion = { version = "0.5", features = ["html_reports"] } +git2 = "0.19" +tempfile = "3" +rand = "0.8" +sha2 = "0.10" + +[lints] +workspace = true diff --git a/crates/kiln-benchmarks/README.md b/crates/kiln-benchmarks/README.md new file mode 100644 index 0000000..ccf7355 --- /dev/null +++ b/crates/kiln-benchmarks/README.md @@ -0,0 +1,262 @@ +# kiln-benchmarks + +Benchmarks measuring Git's object store performance under Kiln's build cache +access patterns. These benchmarks answer the critical questions: + +- How fast is cache lookup by action hash? +- How fast is output tree ingestion for compiled artifacts? +- How does performance scale as the ref namespace grows? +- Does Git's content-addressing actually deduplicate unchanged crate outputs? +- What is the realistic cost of a "fresh clone, no build needed" scenario? + +## Running the Benchmarks + +```sh +cargo bench --package kiln-benchmarks +``` + +Results are written to `target/criterion/`. Open +`target/criterion/report/index.html` for the full HTML report. + +To run a single benchmark group: + +```sh +# Benchmark 1: cache lookup +cargo bench --package kiln-benchmarks --bench kiln -- bench1_cache_lookup + +# Benchmark 2: output tree ingestion +cargo bench --package kiln-benchmarks --bench kiln -- bench2_ingestion + +# Benchmark 3: output tree materialization +cargo bench --package kiln-benchmarks --bench kiln -- bench3_materialization + +# Benchmark 4: deduplication +cargo bench --package kiln-benchmarks --bench kiln -- bench4_deduplication + +# Benchmark 5: ref namespace scale +cargo bench --package kiln-benchmarks --bench kiln -- bench5_ref_namespace + +# Benchmark 6: fetch simulation +cargo bench --package kiln-benchmarks --bench kiln -- bench6_fetch_simulation + +# Benchmark 7: GC and pack behavior +cargo bench --package kiln-benchmarks --bench kiln -- bench7_gc +``` + +> **Note:** Benchmarks 5, 6, and 7 are slow by design — they exercise 10k–100k +> refs and 500-crate ingestion at realistic scale. Allow 5–30 minutes for a +> full run. + +## Benchmark Descriptions + +### Benchmark 1 — Action Hash Cache Lookup + +Tests the core cache check: "does a cached output exist for this action hash?" + +Pre-populates a bare repo with N refs under `refs/kiln/outputs/` at +N = 100, 1,000, and 10,000. Each ref points to a commit whose tree contains +realistic build output blobs (a 4 MB `.rlib`, 100 KB `.rmeta`, and fingerprint +files). Measures both the **hit case** (ref exists, peel to tree OID) and the +**miss case** (ref absent). + +**Scale target:** hit < 5 ms, miss < 2 ms at N = 10,000. + +### Benchmark 2 — Output Tree Ingestion + +Tests writing a compiled crate's outputs into the object store. + +Writes realistic blobs (4 MB `.rlib`, 100 KB `.rmeta`, 1 KB fingerprint, 50 KB +build output) and constructs the full tree/commit/ref chain. Compares +`core.looseCompression = 0` (Kiln's recommended setting for binary artifact +repos) against the default compression level 6. Also reports total repo size +after 100 and 500 ingestions. + +**Scale target:** < 500 ms/crate for a 4 MB rlib + metadata. + +### Benchmark 3 — Output Tree Materialization + +Tests restoring cached outputs to the working directory — the operation that +replaces compilation on a cache hit. + +Pre-ingests N crate output trees (N = 10, 50, 200) and measures reading each +blob from the odb and writing it to the filesystem. Reports odb read time +separately from filesystem write time to identify the bottleneck. + +**Scale target:** < 200 ms/crate from local odb; < 30 s total for 300 crates. + +### Benchmark 4 — Deduplication Across Crate Versions + +Tests Git's structural deduplication: two builds that share most crates should +not duplicate those crates' blobs in the object store. + +Ingests a "main branch build" (N crates) and a "feature branch build" (same N +crates, one changed). Unchanged crates use identical blob content so Git's +content-addressing deduplicates them. Reports the unique object count and +compares it to the theoretical no-dedup count. + +**Scale target:** > 95% deduplication when 1 crate changes out of N. + +### Benchmark 5 — Ref Namespace Scale + +Tests whether Git's ref storage degrades as `refs/kiln/outputs/` grows to +represent months of CI builds. + +Writes refs in batches and measures enumeration time at 1,000, 10,000, and +100,000 refs. Runs `git pack-refs --all` before the 100,000-ref measurement +(the realistic state for an established repo). Also measures targeted lookup +at each scale to confirm O(1) lookup is preserved. + +**Scale target:** enumeration < 500 ms at 10,000 packed refs. + +### Benchmark 6 — Fetch Simulation (Cache Pre-Population) + +Tests how long a fresh clone takes to receive cached outputs for a full project +build — the "fresh clone, no build needed" demo scenario. + +Since libgit2 in-process fetch does not reflect real network conditions, reports +two things: the total bytes that would need to be transferred for N crates +(N = 50, 150, 300), and the local ingestion time after a hypothetical transfer. +Also prints estimated fetch time at 100 Mbit/s and 1 Gbit/s for comparison with +alternatives like sccache or Artifactory. + +### Benchmark 7 — GC and Pack Behavior + +Tests whether Git's garbage collection handles a Kiln object store gracefully. + +Ingests 500 crate output trees, drops 250 refs (simulating eviction of old +builds), then runs `git gc --prune=now`. Reports repo size before and after GC, +GC duration, whether retained refs remain intact, and pack file count. Also +writes a `gitattributes` file marking `.rlib` and `.rmeta` with `-delta` to +prevent delta compression on binary artifact blobs. + +## Configuration Notes + +All benchmark repos are initialized with `core.looseCompression = 0` (except +Benchmark 2's compression comparison). This reflects the tuning recommended for +repos that store large binary artifact blobs, where zlib compression overhead +exceeds any size benefit on already-compressed or random binary data. + +All repos are created fresh in temporary directories with no remotes configured. +Transport/remote benchmarks are out of scope and will be covered separately. + +## Scale Targets Summary + +| Benchmark | Operation | Target | At Scale | +|-----------|-----------|--------|----------| +| 1 | Cache lookup — hit | < 5 ms | 10,000 refs | +| 1 | Cache lookup — miss | < 2 ms | 10,000 refs | +| 2 | Output ingestion | < 500 ms/crate | 4 MB rlib + metadata | +| 3 | Materialization | < 200 ms/crate | from local odb | +| 3 | Full materialization | < 30 s | 300 crates (Zed scale) | +| 5 | Ref enumeration | < 500 ms | 10,000 refs packed | +| 4 | Deduplication | > 95% savings | 1 crate changed of N | + +## Results + +> Results below are populated after running `cargo bench --package kiln-benchmarks` +> on the target machine. Until then, this section serves as a template. + +### Environment + +- **Date:** _not yet run_ +- **Machine:** _not yet run_ +- **OS:** _not yet run_ +- **Rust:** _not yet run_ +- **libgit2:** _not yet run_ + +### Benchmark 1 — Cache Lookup Results + +| N refs | Hit latency (mean) | Miss latency (mean) | Hit target met? | Miss target met? | +|-------:|--------------------|---------------------|-----------------|------------------| +| 100 | — | — | — | — | +| 1,000 | — | — | — | — | +| 10,000 | — | — | — | — | + +### Benchmark 2 — Ingestion Results + +| Compression | Mean latency | Throughput (crates/s) | +|-------------|-------------|----------------------| +| Level 0 | — | — | +| Level 6 | — | — | + +| N crates | Compression | Repo size | +|---------:|-------------|-----------| +| 100 | Level 0 | — | +| 100 | Level 6 | — | +| 500 | Level 0 | — | +| 500 | Level 6 | — | + +### Benchmark 3 — Materialization Results + +| N crates | Mean latency/crate | Total bytes written | ODB time | FS write time | +|---------:|--------------------|---------------------|----------|---------------| +| 10 | — | — | — | — | +| 50 | — | — | — | — | +| 200 | — | — | — | — | + +**Bottleneck:** _not yet determined_ + +### Benchmark 4 — Deduplication Results + +| N crates | Unique objects | No-dedup estimate | Dedup ratio | Repo size | +|---------:|---------------|-------------------|-------------|-----------| +| 50 | — | — | — | — | + +**Finding:** _not yet run_ + +### Benchmark 5 — Ref Namespace Scale Results + +| N refs | Enumeration latency | Lookup hit | Lookup miss | Packed-refs size | +|-------:|--------------------:|------------|-------------|------------------| +| 1,000 | — | — | — | — | +| 10,000 | — | — | — | — | +| 100,000 (packed) | — | — | — | — | + +**Enumeration becomes unacceptably slow (> 1 s) at:** _not yet determined_ + +### Benchmark 6 — Fetch Simulation Results + +| N crates | Total bytes | Est. fetch @ 100 Mbit/s | Est. fetch @ 1 Gbit/s | Local ingestion time | +|---------:|-------------|-------------------------|-----------------------|----------------------| +| 50 | — | — | — | — | +| 150 | — | — | — | — | +| 300 | — | — | — | — | + +### Benchmark 7 — GC Results + +| Metric | Value | +|--------|-------| +| Repo size before GC | — | +| Repo size after GC | — | +| GC duration | — | +| Retained refs intact | — | +| Pack files created | — | +| Binary blobs delta-compressed | — | + +## Findings and Recommendations + +_This section will be filled in after results are collected._ + +Key questions to answer: + +1. **Does cache lookup degrade with ref count?** If miss latency grows beyond + 2 ms at 10,000 refs, consider a derived index (e.g. a SQLite sidecar + mapping action hashes to object IDs) to bypass Git's ref lookup entirely. + +2. **Does `core.looseCompression = 0` meaningfully speed up ingestion?** If + compression level 6 and level 0 show similar latency, the recommendation in + the Kiln spec may not be necessary on modern hardware. + +3. **Is deduplication actually happening?** If blob content is stable across + unchanged crates (no embedded timestamps, no ASLR-derived addresses in debug + info), the dedup ratio should approach (N−1)/N. If it is not, action hash + computation must normalize those inputs before hashing. + +4. **Is the 30-second full materialization target achievable?** At 200 ms/crate, + 300 crates = 60 s — twice the target. If this is the case, the recommended + mitigation is parallel materialization (spawn one thread per crate) or + switching from full tree materialization to hardlinking from a local object + cache directory. + +5. **Does GC correctly prune dropped refs?** If unreachable objects are retained + after `git gc --prune=now`, the eviction strategy must be revisited. diff --git a/crates/kiln-benchmarks/benches/kiln.rs b/crates/kiln-benchmarks/benches/kiln.rs new file mode 100644 index 0000000..052d84b --- /dev/null +++ b/crates/kiln-benchmarks/benches/kiln.rs @@ -0,0 +1,774 @@ +#![allow(missing_docs)] +//! Kiln benchmark suite. +//! +//! Measures Git object store performance under Kiln's build cache access +//! patterns. See the crate README for results and analysis. + +use std::{ + hint::black_box, + io::Write, + path::Path, + process::Command, + time::{Duration, Instant}, +}; + +use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main}; +use git2::{FileMode, Oid, Repository, Signature}; +use rand::{Rng, SeedableRng, rngs::StdRng}; +use tempfile::TempDir; + +// --------------------------------------------------------------------------- +// Seed constant +// --------------------------------------------------------------------------- + +/// "KILN_BNC" encoded as a little-endian u64. +const KILN_SEED: u64 = 0x4b494c4e5f424e43; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +/// Create a bare repository in a temp directory with `core.looseCompression` +/// set to `compression_level`. No remotes are configured. +fn make_bare_repo(compression_level: i32) -> (TempDir, Repository) { + let dir = TempDir::new().expect("tempdir"); + let repo = Repository::init_bare(dir.path()).expect("init bare repo"); + repo.config() + .expect("repo config") + .set_i32("core.looseCompression", compression_level) + .expect("set looseCompression"); + (dir, repo) +} + +/// Generate a random 32-character lowercase hex string using `rng`. +fn random_hex32(rng: &mut impl Rng) -> String { + format!("{:016x}{:016x}", rng.r#gen::(), rng.r#gen::()) +} + +/// Generate `len` random bytes. +fn random_bytes(rng: &mut impl Rng, len: usize) -> Vec { + (0..len).map(|_| rng.r#gen::()).collect() +} + +/// Write a single crate output tree into `repo` and create a ref +/// `refs/kiln/outputs/` pointing at the resulting commit. +/// +/// Tree layout: +/// ```text +/// deps/ +/// libfoo.rlib (rlib_data) +/// libfoo.rmeta (rmeta_data) +/// .fingerprint/ +/// foo-/ +/// invoked.timestamp (b"0\n") +/// lib-foo (fp_data) +/// build/ (empty, or contains output blob) +/// ``` +fn ingest_crate_output( + repo: &Repository, + action_hash: &str, + rlib_data: &[u8], + rmeta_data: &[u8], + fp_data: &[u8], + build_data: Option<&[u8]>, +) -> Oid { + // --- blobs --- + let rlib_oid = repo.blob(rlib_data).expect("blob rlib"); + let rmeta_oid = repo.blob(rmeta_data).expect("blob rmeta"); + let fp_oid = repo.blob(fp_data).expect("blob fp"); + let timestamp_oid = repo.blob(b"0\n").expect("blob timestamp"); + + // --- deps/ subtree --- + let mut deps_tb = repo.treebuilder(None).expect("treebuilder deps"); + deps_tb + .insert("libfoo.rlib", rlib_oid, FileMode::Blob.into()) + .expect("insert rlib"); + deps_tb + .insert("libfoo.rmeta", rmeta_oid, FileMode::Blob.into()) + .expect("insert rmeta"); + let deps_tree = deps_tb.write().expect("write deps tree"); + + // --- .fingerprint/foo-/ subtree --- + let fp_hash = &action_hash[..8]; + let mut fp_inner_tb = repo.treebuilder(None).expect("treebuilder fp inner"); + fp_inner_tb + .insert("invoked.timestamp", timestamp_oid, FileMode::Blob.into()) + .expect("insert timestamp"); + fp_inner_tb + .insert("lib-foo", fp_oid, FileMode::Blob.into()) + .expect("insert lib-foo"); + let fp_inner_tree = fp_inner_tb.write().expect("write fp inner tree"); + + let fp_dir_name = format!("foo-{fp_hash}"); + let mut fp_outer_tb = repo.treebuilder(None).expect("treebuilder fp outer"); + fp_outer_tb + .insert(&fp_dir_name, fp_inner_tree, FileMode::Tree.into()) + .expect("insert fp dir"); + let fp_tree = fp_outer_tb.write().expect("write fp tree"); + + // --- build/ subtree --- + let mut build_tb = repo.treebuilder(None).expect("treebuilder build"); + if let Some(data) = build_data { + let build_oid = repo.blob(data).expect("blob build"); + build_tb + .insert("output", build_oid, FileMode::Blob.into()) + .expect("insert build output"); + } + let build_tree = build_tb.write().expect("write build tree"); + + // --- root tree --- + let mut root_tb = repo.treebuilder(None).expect("treebuilder root"); + root_tb + .insert("deps", deps_tree, FileMode::Tree.into()) + .expect("insert deps"); + root_tb + .insert(".fingerprint", fp_tree, FileMode::Tree.into()) + .expect("insert .fingerprint"); + root_tb + .insert("build", build_tree, FileMode::Tree.into()) + .expect("insert build"); + let root_tree_oid = root_tb.write().expect("write root tree"); + + // --- commit --- + let sig = Signature::now("kiln-bench", "bench@kiln").expect("sig"); + let root_tree = repo.find_tree(root_tree_oid).expect("find root tree"); + let commit_oid = repo + .commit(None, &sig, &sig, action_hash, &root_tree, &[]) + .expect("commit"); + + // --- ref --- + let ref_name = format!("refs/kiln/outputs/{action_hash}"); + repo.reference(&ref_name, commit_oid, true, "kiln ingest") + .expect("create ref"); + + commit_oid +} + +/// Write a minimal dummy commit (no output blobs) and point a ref at it. +fn ingest_dummy_commit(repo: &Repository, action_hash: &str) -> Oid { + let sig = Signature::now("kiln-bench", "bench@kiln").expect("sig"); + let empty_tb = repo.treebuilder(None).expect("treebuilder"); + let empty_tree_oid = empty_tb.write().expect("write empty tree"); + let empty_tree = repo.find_tree(empty_tree_oid).expect("find empty tree"); + let commit_oid = repo + .commit(None, &sig, &sig, action_hash, &empty_tree, &[]) + .expect("commit"); + let ref_name = format!("refs/kiln/outputs/{action_hash}"); + repo.reference(&ref_name, commit_oid, true, "kiln dummy") + .expect("create ref"); + commit_oid +} + +/// Recursively walk a git tree, writing blob entries to `dest/`. +/// Returns the total number of bytes written. +fn walk_and_materialize( + repo: &Repository, + tree: &git2::Tree<'_>, + dest: &Path, +) -> Result> { + let mut bytes_written = 0u64; + for entry in tree.iter() { + let name = entry.name().unwrap_or("_"); + let dest_entry = dest.join(name); + match entry.kind() { + Some(git2::ObjectType::Tree) => { + std::fs::create_dir_all(&dest_entry)?; + let sub = repo.find_tree(entry.id())?; + bytes_written += walk_and_materialize(repo, &sub, &dest_entry)?; + } + Some(git2::ObjectType::Blob) => { + let blob = repo.find_blob(entry.id())?; + let mut f = std::fs::File::create(&dest_entry)?; + f.write_all(blob.content())?; + bytes_written += blob.content().len() as u64; + } + _ => {} + } + } + Ok(bytes_written) +} + +/// Return the total on-disk size (bytes) of a directory tree. +fn dir_size_bytes(path: &Path) -> u64 { + let mut total = 0u64; + if let Ok(entries) = std::fs::read_dir(path) { + for entry in entries.flatten() { + if let Ok(m) = entry.metadata() { + if m.is_dir() { + total += dir_size_bytes(&entry.path()); + } else { + total += m.len(); + } + } + } + } + total +} + +// --------------------------------------------------------------------------- +// Blob fixtures — generated once per process, reused across benchmarks. +// --------------------------------------------------------------------------- + +struct Blobs { + rlib: Vec, // 4 MB + rmeta: Vec, // 100 KB + fp: Vec, // 1 KB + build: Vec, // 50 KB +} + +impl Blobs { + fn new() -> Self { + let mut rng = StdRng::seed_from_u64(KILN_SEED); + Self { + rlib: random_bytes(&mut rng, 4 * 1024 * 1024), + rmeta: random_bytes(&mut rng, 100 * 1024), + fp: random_bytes(&mut rng, 1024), + build: random_bytes(&mut rng, 50 * 1024), + } + } +} + +// --------------------------------------------------------------------------- +// Benchmark 1: Action Hash Cache Lookup +// --------------------------------------------------------------------------- + +fn bench_cache_lookup(c: &mut Criterion) { + let blobs = Blobs::new(); + let mut rng = StdRng::seed_from_u64(1); + let ns = [100usize, 1_000, 10_000]; + + let mut group = c.benchmark_group("bench1_cache_lookup"); + + for &n in &ns { + // Build a fresh repo pre-populated with N refs. + let (_dir, repo) = make_bare_repo(0); + let hashes: Vec = (0..n).map(|_| random_hex32(&mut rng)).collect(); + for h in &hashes { + ingest_crate_output( + &repo, + h, + &blobs.rlib, + &blobs.rmeta, + &blobs.fp, + Some(&blobs.build), + ); + } + + // Pick a known-present hash and a guaranteed-absent hash. + let hit_hash = hashes[n / 2].clone(); + let miss_hash = random_hex32(&mut rng); + + // --- Hit case --- + group.bench_with_input(BenchmarkId::new("hit", n), &n, |b, _| { + b.iter(|| { + let ref_name = format!("refs/kiln/outputs/{}", &hit_hash); + let r = repo.find_reference(black_box(&ref_name)).expect("find ref"); + let commit = r.peel_to_commit().expect("peel commit"); + black_box(commit.tree_id()) + }) + }); + + // --- Miss case --- + group.bench_with_input(BenchmarkId::new("miss", n), &n, |b, _| { + b.iter(|| { + let ref_name = format!("refs/kiln/outputs/{}", &miss_hash); + black_box(repo.find_reference(black_box(&ref_name)).is_err()) + }) + }); + + drop(hashes); + } + + group.finish(); +} + +// --------------------------------------------------------------------------- +// Benchmark 2: Output Tree Ingestion +// --------------------------------------------------------------------------- + +fn bench_ingestion(c: &mut Criterion) { + let blobs = Blobs::new(); + let mut rng = StdRng::seed_from_u64(2); + + let mut group = c.benchmark_group("bench2_ingestion"); + // Fewer samples because each iteration writes ~4 MB. + group.sample_size(20); + + for &compression in &[0i32, 6i32] { + let label = format!("compression_{compression}"); + let (_dir, repo) = make_bare_repo(compression); + + group.bench_function(&label, |b| { + b.iter(|| { + let hash = random_hex32(&mut rng); + ingest_crate_output( + black_box(&repo), + black_box(&hash), + black_box(&blobs.rlib), + black_box(&blobs.rmeta), + black_box(&blobs.fp), + Some(black_box(&blobs.build)), + ) + }) + }); + } + + group.finish(); + + // Repo size after 100 and 500 ingestions (reported to stdout). + for &compression in &[0i32, 6i32] { + for &n in &[100usize, 500] { + let (dir, repo) = make_bare_repo(compression); + let mut rng2 = StdRng::seed_from_u64(42); + for _ in 0..n { + let h = random_hex32(&mut rng2); + ingest_crate_output(&repo, &h, &blobs.rlib, &blobs.rmeta, &blobs.fp, None); + } + let size = dir_size_bytes(dir.path()); + println!( + "[bench2] compression={compression} n={n} repo_size={:.1} MB", + size as f64 / 1_048_576.0 + ); + } + } +} + +// --------------------------------------------------------------------------- +// Benchmark 3: Output Tree Materialization +// --------------------------------------------------------------------------- + +fn bench_materialization(c: &mut Criterion) { + let blobs = Blobs::new(); + let mut rng = StdRng::seed_from_u64(3); + let ns = [10usize, 50, 200]; + + let mut group = c.benchmark_group("bench3_materialization"); + group.sample_size(10); + + for &n in &ns { + let (_repo_dir, repo) = make_bare_repo(0); + let out_dir = TempDir::new().expect("out tempdir"); + let out_path = out_dir.path().to_path_buf(); + + // Pre-ingest N crates. + let hashes: Vec = (0..n) + .map(|_| { + let h = random_hex32(&mut rng); + ingest_crate_output( + &repo, + &h, + &blobs.rlib, + &blobs.rmeta, + &blobs.fp, + Some(&blobs.build), + ); + h + }) + .collect(); + + let target_hash = hashes[0].clone(); + + group.bench_with_input(BenchmarkId::new("n_crates", n), &n, |b, _| { + b.iter(|| { + let ref_name = format!("refs/kiln/outputs/{}", &target_hash); + let r = repo.find_reference(&ref_name).expect("find ref"); + let commit = r.peel_to_commit().expect("peel commit"); + let tree = commit.tree().expect("tree"); + + // Separate odb read time from filesystem write time. + let odb_start = Instant::now(); + // Pre-load all blobs from the odb (simulates the read phase). + let _ = tree.iter().count(); + let odb_elapsed = odb_start.elapsed(); + + let fs_start = Instant::now(); + let bytes = + walk_and_materialize(black_box(&repo), black_box(&tree), black_box(&out_path)) + .expect("materialize"); + let fs_elapsed = fs_start.elapsed(); + + black_box((bytes, odb_elapsed, fs_elapsed)) + }) + }); + + drop(hashes); + drop(out_dir); + } + + group.finish(); +} + +// --------------------------------------------------------------------------- +// Benchmark 4: Deduplication Across Crate Versions +// --------------------------------------------------------------------------- + +fn bench_deduplication(c: &mut Criterion) { + let blobs = Blobs::new(); + let mut rng = StdRng::seed_from_u64(4); + let n_crates = 50usize; + + let mut group = c.benchmark_group("bench4_deduplication"); + group.sample_size(10); + + group.bench_function("two_builds_one_changed", |b| { + b.iter_custom(|iters| { + let mut total = Duration::ZERO; + for _ in 0..iters { + let (dir, repo) = make_bare_repo(0); + + // Generate N hashes for "main build". + let main_hashes: Vec = + (0..n_crates).map(|_| random_hex32(&mut rng)).collect(); + + let t0 = Instant::now(); + + // Ingest main build. + for h in &main_hashes { + ingest_crate_output(&repo, h, &blobs.rlib, &blobs.rmeta, &blobs.fp, None); + } + + // Feature build: same hashes except one, same blob content for + // unchanged crates so Git deduplicates via content-addressing. + let mut feature_hashes = main_hashes.clone(); + feature_hashes[0] = random_hex32(&mut rng); // one changed crate + + // The changed crate uses entirely different blob content. + let changed_rlib: Vec = random_bytes(&mut rng, blobs.rlib.len()); + + for (i, h) in feature_hashes.iter().enumerate() { + if i == 0 { + ingest_crate_output(&repo, h, &changed_rlib, &blobs.rmeta, &blobs.fp, None); + } else { + // Identical content — Git will reuse existing blob OIDs. + ingest_crate_output(&repo, h, &blobs.rlib, &blobs.rmeta, &blobs.fp, None); + } + } + + total += t0.elapsed(); + + // Count unique objects in the odb. + let mut obj_count = 0usize; + repo.odb() + .expect("odb") + .foreach(|_oid| { + obj_count += 1; + true + }) + .expect("odb foreach"); + + let repo_size = dir_size_bytes(dir.path()); + // Objects per crate (approx): commit + root tree + deps tree + + // fp-outer tree + fp-inner tree + rlib blob + rmeta blob + + // timestamp blob + lib-foo blob + build tree = ~10 + let no_dedup_estimate = 2 * n_crates * 10; + println!( + "[bench4] n={n_crates} unique_objects={obj_count} \ + no_dedup_estimate={no_dedup_estimate} \ + repo_size={:.1} MB", + repo_size as f64 / 1_048_576.0, + ); + + drop(main_hashes); + drop(feature_hashes); + } + total + }) + }); + + group.finish(); +} + +// --------------------------------------------------------------------------- +// Benchmark 5: Ref Namespace Scale +// --------------------------------------------------------------------------- + +fn bench_ref_namespace(c: &mut Criterion) { + let mut rng = StdRng::seed_from_u64(5); + let ns = [1_000usize, 10_000, 100_000]; + + let mut group = c.benchmark_group("bench5_ref_namespace"); + group.sample_size(10); + group.measurement_time(Duration::from_secs(30)); + + for &n in &ns { + let (dir, repo) = make_bare_repo(0); + + // Write N dummy commits/refs. + let hashes: Vec = (0..n).map(|_| random_hex32(&mut rng)).collect(); + for h in &hashes { + ingest_dummy_commit(&repo, h); + } + + // Pack refs at 100k — this is the realistic state for an established repo. + if n >= 100_000 { + Command::new("git") + .args(["pack-refs", "--all"]) + .current_dir(dir.path()) + .output() + .expect("git pack-refs"); + + let packed_refs = dir.path().join("packed-refs"); + if packed_refs.exists() { + let size = std::fs::metadata(&packed_refs) + .map(|m| m.len()) + .unwrap_or(0); + println!( + "[bench5] n={n} packed-refs size={:.1} KB", + size as f64 / 1024.0 + ); + } + } + + // --- Enumeration --- + group.bench_with_input(BenchmarkId::new("enumerate", n), &n, |b, _| { + b.iter(|| { + let mut count = 0usize; + let refs = repo + .references_glob("refs/kiln/outputs/*") + .expect("references_glob"); + for r in refs { + let _ = r.expect("ref"); + count += 1; + } + black_box(count) + }) + }); + + // --- Targeted lookup hit/miss at this scale --- + let hit = hashes[n / 2].clone(); + let miss = random_hex32(&mut rng); + + group.bench_with_input(BenchmarkId::new("lookup_hit", n), &n, |b, _| { + b.iter(|| { + let rn = format!("refs/kiln/outputs/{}", &hit); + black_box(repo.find_reference(black_box(&rn)).is_ok()) + }) + }); + + group.bench_with_input(BenchmarkId::new("lookup_miss", n), &n, |b, _| { + b.iter(|| { + let rn = format!("refs/kiln/outputs/{}", &miss); + black_box(repo.find_reference(black_box(&rn)).is_err()) + }) + }); + + drop(hashes); + } + + group.finish(); +} + +// --------------------------------------------------------------------------- +// Benchmark 6: Fetch Simulation (Cache Pre-Population) +// --------------------------------------------------------------------------- + +fn bench_fetch_simulation(c: &mut Criterion) { + let blobs = Blobs::new(); + let mut rng = StdRng::seed_from_u64(6); + let ns = [50usize, 150, 300]; + + let mut group = c.benchmark_group("bench6_fetch_simulation"); + group.sample_size(10); + group.measurement_time(Duration::from_secs(60)); + + // Bytes per crate: rlib + rmeta + fp + build (uncompressed in-memory sizes). + let bytes_per_crate = + (blobs.rlib.len() + blobs.rmeta.len() + blobs.fp.len() + blobs.build.len()) as u64; + + for &n in &ns { + let total_bytes = bytes_per_crate * n as u64; + let time_100mbit = total_bytes as f64 / (100.0 * 1_000_000.0 / 8.0); + let time_1gbit = total_bytes as f64 / (1_000.0 * 1_000_000.0 / 8.0); + println!( + "[bench6] n={n} total_bytes={:.1} MB \ + est_fetch@100Mbit={time_100mbit:.1}s \ + est_fetch@1Gbit={time_1gbit:.2}s", + total_bytes as f64 / 1_048_576.0, + ); + + // Measure local ingestion time — the portion Kiln controls after transfer. + group.bench_with_input(BenchmarkId::new("local_ingestion", n), &n, |b, &count| { + b.iter_custom(|iters| { + let mut total = Duration::ZERO; + for _ in 0..iters { + let (_dir, repo) = make_bare_repo(0); + let seed: u64 = rng.r#gen(); + let mut rng2 = StdRng::seed_from_u64(seed); + let t0 = Instant::now(); + for _ in 0..count { + let h = random_hex32(&mut rng2); + ingest_crate_output( + &repo, + &h, + &blobs.rlib, + &blobs.rmeta, + &blobs.fp, + Some(&blobs.build), + ); + } + total += t0.elapsed(); + } + total + }) + }); + } + + group.finish(); +} + +// --------------------------------------------------------------------------- +// Benchmark 7: GC and Pack Behavior +// --------------------------------------------------------------------------- + +fn bench_gc(c: &mut Criterion) { + let blobs = Blobs::new(); + let mut rng = StdRng::seed_from_u64(7); + + let mut group = c.benchmark_group("bench7_gc"); + group.sample_size(10); + group.measurement_time(Duration::from_secs(120)); + + group.bench_function("gc_500_drop_250", |b| { + b.iter_custom(|iters| { + let mut total = Duration::ZERO; + for _ in 0..iters { + let (dir, repo) = make_bare_repo(0); + + // Ingest 500 crate output trees. + let hashes: Vec = (0..500usize) + .map(|_| { + let h = random_hex32(&mut rng); + ingest_crate_output(&repo, &h, &blobs.rlib, &blobs.rmeta, &blobs.fp, None); + h + }) + .collect(); + + let size_before = dir_size_bytes(dir.path()); + + // Drop 250 refs (simulate eviction of old builds). + for h in hashes.iter().take(250) { + let ref_name = format!("refs/kiln/outputs/{h}"); + if let Ok(mut r) = repo.find_reference(&ref_name) { + r.delete().expect("delete ref"); + } + } + + // Write a gitattributes file to prevent delta compression on + // binary artifact blobs. + let attrs_path = dir.path().join("info").join("attributes"); + std::fs::create_dir_all(attrs_path.parent().unwrap()).ok(); + std::fs::write(&attrs_path, "*.rlib -delta\n*.rmeta -delta\n") + .expect("write gitattributes"); + + // Run git gc and measure only gc time. + let t0 = Instant::now(); + let gc_out = Command::new("git") + .args(["gc", "--prune=now", "--quiet"]) + .current_dir(dir.path()) + .output() + .expect("git gc"); + let gc_elapsed = t0.elapsed(); + total += gc_elapsed; + + let size_after = dir_size_bytes(dir.path()); + + if !gc_out.status.success() { + eprintln!( + "[bench7] git gc stderr: {}", + String::from_utf8_lossy(&gc_out.stderr) + ); + } + + // Verify retained refs are intact after gc. + let mut intact = true; + for h in hashes.iter().skip(250) { + let ref_name = format!("refs/kiln/outputs/{h}"); + if repo.find_reference(&ref_name).is_err() { + intact = false; + break; + } + } + + // Count .pack files to confirm blobs were packed. + let pack_dir = dir.path().join("objects").join("pack"); + let pack_count = std::fs::read_dir(&pack_dir) + .map(|rd| { + rd.filter_map(|e| e.ok()) + .filter(|e| e.path().extension().map(|x| x == "pack").unwrap_or(false)) + .count() + }) + .unwrap_or(0); + + println!( + "[bench7] size_before={:.1} MB size_after={:.1} MB \ + gc_time={:.2}s retained_refs_intact={intact} \ + pack_files={pack_count}", + size_before as f64 / 1_048_576.0, + size_after as f64 / 1_048_576.0, + gc_elapsed.as_secs_f64(), + ); + + drop(hashes); + } + total + }) + }); + + group.finish(); +} + +// --------------------------------------------------------------------------- +// Criterion entry points +// --------------------------------------------------------------------------- + +criterion_group! { + name = lookup; + config = Criterion::default().measurement_time(Duration::from_secs(10)); + targets = bench_cache_lookup +} + +criterion_group! { + name = ingestion; + config = Criterion::default().measurement_time(Duration::from_secs(30)); + targets = bench_ingestion +} + +criterion_group! { + name = materialization; + config = Criterion::default().measurement_time(Duration::from_secs(30)); + targets = bench_materialization +} + +criterion_group! { + name = deduplication; + config = Criterion::default().measurement_time(Duration::from_secs(60)); + targets = bench_deduplication +} + +criterion_group! { + name = ref_namespace; + config = Criterion::default().measurement_time(Duration::from_secs(30)); + targets = bench_ref_namespace +} + +criterion_group! { + name = fetch_simulation; + config = Criterion::default().measurement_time(Duration::from_secs(60)); + targets = bench_fetch_simulation +} + +criterion_group! { + name = gc; + config = Criterion::default().measurement_time(Duration::from_secs(120)); + targets = bench_gc +} + +criterion_main!( + lookup, + ingestion, + materialization, + deduplication, + ref_namespace, + fetch_simulation, + gc +); diff --git a/crates/kiln-benchmarks/src/lib.rs b/crates/kiln-benchmarks/src/lib.rs new file mode 100644 index 0000000..4a3dced --- /dev/null +++ b/crates/kiln-benchmarks/src/lib.rs @@ -0,0 +1,5 @@ +//! Kiln Benchmarks +//! +//! Benchmarks for measuring Git object store performance under Kiln's build +//! cache access patterns. See [`benches/kiln.rs`] for the benchmark +//! implementations and `README.md` for results and analysis.