fix: resolve delta trees lazily in a lock-free pool - #2852
Merged
Conversation
Sebastian Thiel (Byron)
force-pushed
the
delta-tree-parallelism
branch
4 times, most recently
from
July 29, 2026 19:05
b20d2d5 to
03cfb1e
Compare
Sebastian Thiel (Byron)
force-pushed
the
delta-tree-parallelism
branch
from
July 30, 2026 09:26
03cfb1e to
db891d1
Compare
Sebastian Thiel (Byron)
marked this pull request as ready for review
July 30, 2026 09:27
Sebastian Thiel (Byron)
enabled auto-merge
July 30, 2026 09:27
<!-- agent --> --- Summary of findings The lazy, stealable resolver fixes the pathological phpstan pack without regressing the Linux best case. At 16 threads, phpstan falls from the 98.38-second baseline resolver time to 22.70 seconds, while the Linux best-case resolver improves slightly from 11.81 to 11.33 seconds. Charged peak memory on phpstan falls from 13.14 GB to 2.16 GB, and the Linux best case falls from 1.96 GB to 1.76 GB. Across the measured packs, gix resolver speedup at 16 threads ranges from 10.80x to 12.48x over its new serial path. On phpstan, gix is 1.07x faster than Git at one thread and 2.31x faster at 16 threads. On the identical 10.9M-object Linux payload, gix's SHA-256 resolver is 2.72x to 2.99x faster than its SHA-1 resolver, confirming that the slow SHA-1 implementation dominates much of the remaining absolute cost. The Git SHA-256 comparison was initially unfair: the local Git build used the portable SHA256_BLK backend while RustCrypto selected ARMv8 SHA-2 instructions. Rebuilding Git with OpenSSL reduced its eight-thread wall time from 144.15 to 40.45 seconds and user CPU time from 471.94 to 99.81 seconds. All final Git SHA-256 comparisons use that faster backend. --- Pathological phpstan pack The pack contains 100.7k objects but expands to 174.6 GB because one root ends in a deep, expensive delta tree. The previous resolver takes 98.53 seconds wall clock and has a 13.14 GB peak memory footprint. The new resolver takes 283.41, 71.94, 36.42, and 22.83 seconds wall clock at 1, 4, 8, and 16 threads. Its resolver speedups are 3.94x, 7.81x, and 12.48x relative to one thread, remaining nearly linear through eight threads. Peak memory footprint is 0.68, 0.75, 1.17, and 2.16 GB respectively. Thus the fastest run is 4.31x faster than the old wall time while using 84% less charged peak memory. Git was measured at 1, 2, 4, 8, and 16 threads while keeping its aggregate delta-base cache allowance at least as large as the default eight-thread allowance of 8 times 96 MiB. Git takes 304.51, 168.06, 98.80, 65.41, and 52.79 seconds, for 5.77x one-to-sixteen-thread scaling. At equal thread counts, gix is 1.07x, 1.37x, 1.80x, and 2.31x faster at 1, 4, 8, and 16 threads. At the nearest wall-time operating points, gix at four threads takes 71.94 seconds with a 0.75 GB peak footprint, while Git at eight threads takes 65.41 seconds with a 1.90 GB peak footprint. gix therefore uses 61% less charged working memory near Git-matching throughput. --- Linux best-case pack The 7.6M-object fixture expands to 95.6 GB and guards the already-friendly case. The old implementation takes 13.68 seconds wall clock and 11.81 seconds in the resolver. The new implementation takes 137.48, 36.75, 19.81, and 13.16 seconds wall clock at 1, 4, 8, and 16 threads; resolver scaling reaches 11.97x at 16 threads. The 16-thread result is slightly faster than the old implementation, with 11.33 seconds in the resolver, and lowers peak footprint from 1.96 to 1.76 GB. Peak footprint remains effectively constant from one through 16 threads. Git takes 156.65, 60.48, 45.48, and 42.97 seconds at the same thread counts, so gix's advantage grows from 1.14x to 3.27x. --- SHA-1 and SHA-256 sibling packs The sibling packs contain 10.9M objects, expand to 146.7 GB, and preserve the same compressed payload and delta topology. This isolates hashing from work distribution. For SHA-1, gix takes 212.57, 56.58, 30.60, and 20.74 seconds wall clock at 1, 4, 8, and 16 threads. Resolver scaling reaches 11.36x at 16 threads. The default eight-thread Git run takes 125.11 seconds, while gix takes 30.60 seconds at eight threads and 20.74 seconds at 16. For SHA-256, gix takes 72.47, 21.83, 12.58, and 8.67 seconds wall clock. Resolver scaling reaches 10.80x at 16 threads. Against OpenSSL-backed Git, gix is 1.21x, 2.02x, 3.22x, and 5.61x faster at equal thread counts. Git improves from 87.85 seconds at one thread to 40.45 seconds at eight, then regresses to 48.68 seconds at 16 with 204.81 seconds of system CPU. --- Hasher and scheduler interpretation On identical pack data, gix SHA-256 resolver time is 70.31 seconds at one thread versus 210.35 seconds for SHA-1, and 6.51 seconds at 16 threads versus 18.51 seconds for SHA-1. SHA-256 is therefore 2.99x faster serially and 2.84x faster at 16 threads, with a 2.72x to 2.99x advantage throughout the measured range. The similar 10.80x to 12.48x gix scaling across pathological, best-case, SHA-1, and SHA-256 packs argues against lock contention being the main high-thread limitation. The remaining flattening is consistent with finite parallel work, scheduling overhead, memory bandwidth, and hashing cost. --- Memory interpretation The resolver no longer materializes the unresolved internal-node frontier. It resolves a child only when a worker starts it, shares immutable bases between sibling tasks, and recycles the final base reference. A linear chain therefore needs roughly two object buffers per active worker instead of retaining every intermediate base. macOS max RSS includes clean file-backed pages. gix maps the pack, so its reported RSS can include most of a multi-gigabyte pack even when those pages are reclaimable; Git reads through bounded pread buffers. Peak memory footprint better represents charged working memory here. On phpstan, gix max RSS grows from 7.20 to 8.90 GB across 1 to 16 threads, but charged footprint grows from only 0.68 to 2.16 GB. On the Linux best case, footprint stays at 1.76 GB across the same range. --- Implementation Use lock-free local deques and a shared root injector so idle workers can steal branches from the last expensive root without every worker hoarding roots. Enable this resolver through the gix parallel feature. Regression tests prove that internal siblings are not all materialized before descent and that two workers can concurrently resolve children from one remaining root. The scheduler follows the proven Git index-pack principles of bounded live delta bases and keeping independent delta work available, informed by builtin/index-pack.c at cf5497b14c.
Only then will it use its parallel implementation.
Sebastian Thiel (Byron)
force-pushed
the
delta-tree-parallelism
branch
from
July 30, 2026 09:29
51db931 to
79e3478
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Tasks
This section is maintained by humans. Do not edit it automatically.
Everything below this line was generated by
Codex GPT-5.Created by Codex on behalf of Byron. Byron will review before this is ready to merge.
Summary
gix's parallel feature to the newgix-packschedulerRegression coverage
Benchmarks
Git index-pack thread selection
Git
index-packaccepts--threads=<n>even though the option is omitted from its short usage text. With no explicit value orpack.threadsconfiguration, it starts from the online CPU count, uses all CPUs below four, uses three threads for four or five CPUs, halves the count from six through 39 CPUs, and caps it at 20 from 40 CPUs onward. This 16-core machine therefore defaults to eight resolver threads. Existing unqualified Git measurements below have been relabeled as-t8 (default). Git multiplies its delta-base cache limit by the selected thread count.phpstan pathological pack
The pathological phpstan pack contains 100.7k objects and decodes to 174.6 GB.
For this comparison Git's aggregate delta-base cache allowance is kept at or above the default eight-thread allowance of 8 × 96 MiB. Because Git multiplies
core.deltaBaseCacheLimitby its worker count, the per-thread limits are 768, 384, 192, 96, and 96 MiB for 1, 2, 4, 8, and 16 threads respectively. The 16-thread run therefore retains Git's default 96 MiB per worker and has a 1.5 GiB aggregate allowance.Git
index-packscaling:-t1-t2for 2x threads-t4for 2x threads-t8for 2x threadsSame-thread wall-time comparison:
gix results:
-t1-t4-t8-t16gix resolver scaling and memory growth:
-t1-t1-t1-t4for 2x threads-t8for 2x threadsgix scaling is nearly linear through eight workers. Doubling from eight to sixteen workers yields another 1.60x speedup at 79.9% incremental efficiency, while max RSS rises by 13% and peak footprint by 85%.
At the nearest wall-time operating points, gix
-t4takes 71.94 s versus Git-t8at 65.41 s. gix reports 7.42 GB max RSS versus Git's 1.91 GB because its RSS includes clean file-backed pages from the 6.9 GB pack mapping, while Git reads through boundedpread()buffers. Charged peak footprint is 0.75 GB for gix versus 1.90 GB for Git, 61% lower.Commands:
Linux best-case pack:
Git
index-packon the Linux best-case pack:-t1-t4for 2x threads-t8for 2x threadsSame-thread wall-time comparison:
Commands:
-t1-t4-t8-t16Resolver scaling and memory growth:
-t1-t1-t1-t4for 2× threads-t8for 2× threadsScaling is nearly linear through eight workers. Doubling from eight to sixteen yields another 1.58× speedup at 79.1% incremental efficiency. Across the entire one-to-sixteen-thread range, max RSS rises by 9% while peak footprint remains effectively unchanged.
At
-t16, the best-case pack is slightly faster than baseline: 13.16 s versus 13.68 s wall time and 11.33 s versus 11.81 s resolver time. It also uses less memory: 2.97 GB versus 3.54 GB max RSS and 1.76 GB versus 1.96 GB peak footprint.Commands:
SHA-1 sibling-pack comparison
This is the original SHA-1 sibling of the 10.9M-object SHA-256 benchmark pack below, with the same compressed payload and delta topology and 146.7 GB of decoded data. It is distinct from the 7.6M-object
tests/fixtures/repos/linux.gitpack above, whose-t16wall time is 13.16 s.-t8(default)-t1-t4-t8-t16Resolver scaling:
-t1-t4for 2x threads-t8for 2x threadsCommands:
SHA-256 hasher comparison
To isolate object hashing from scheduling, the 10.9M-object Linux pack was converted by preserving its compressed payload and delta topology byte-for-byte, replacing the SHA-1 pack trailer with SHA-256, and regenerating the index in a SHA-256 repository. It decodes to 146.7 GB. This is a pack-verification fixture rather than a valid converted repository: commit and tree payloads still contain SHA-1 references.
The original Git build used the portable
SHA256_BLKbackend, while gix's RustCrypto SHA-256 dispatches to ARMv8 SHA-2 instructions on this machine. To avoid disadvantaging Git, the comparison was rerun with Gitcf5497b14cbuilt withSHA1_DCfor SHA-1 andSHA256_OPENSSLfrom OpenSSL 3.6.3 for SHA-256. At-t8, changing only Git's SHA-256 backend reduced wall time from 144.15 s to 40.45 s (3.56x faster) and user CPU time from 471.94 s to 99.81 s (4.73x less). All Git SHA-256 comparison values below use the OpenSSL build; the SHA-1 measurements above remain from the originalSHA1_DCbuild.Git
index-packSHA-256 scaling:-t1-t4for 2x threads-t8for 2x threadsSame-thread wall-time comparison:
gix SHA-256 results:
-t1-t4-t8-t16gix resolver scaling:
-t1-t4for 2x threads-t8for 2x threadsSHA-1 versus SHA-256 resolver time on the identical compressed pack:
SHA-256 is 2.72x to 2.99x faster across the measured thread counts with identical work distribution and delta data, showing that the slow SHA-1 implementation, rather than the scheduler, dominates the absolute regression.
Commands:
Validation
cargo test -p gix-packcargo clippy -p gix-pack --all-targets --all-features -- -D warningscargo check -p gix-pack --no-default-features --features sha1cargo check -p gix --no-default-features --features parallel,sha1cargo fmt --all -- --checkThe scheduling and bounded-live-base approach was informed by Git's
builtin/index-pack.catcf5497b14c; the implementation is independent and lock-free.Fixes #2424.