Skip to content

perf(bit-buffer): fix pre-sync per-code-block allocation in track!#194

Merged
zsoerenm merged 2 commits into
ss/two-bit-downconvert-and-correlatefrom
ss/fix-presync-bit-search-allocation
Jul 8, 2026
Merged

perf(bit-buffer): fix pre-sync per-code-block allocation in track!#194
zsoerenm merged 2 commits into
ss/two-bit-downconvert-and-correlatefrom
ss/fix-presync-bit-search-allocation

Conversation

@zsoerenm

@zsoerenm zsoerenm commented Jul 8, 2026

Copy link
Copy Markdown
Member

Summary

track! allocates in proportion to the signal length during acquisition (before bit sync), instead of being per-iteration allocation-free after warmup as the docstring promises. Confirmed by measurement on a single GPS L1CA sat with the correlator hoisted (the recommended real-time pattern), after a warmup call:

Signal length Before After
1 code period 352 B 272 B
10 code periods 1072 B 272 B

Root cause

The allocation profiler pinned the growth to _buffer_find_bit in src/bit_buffer.jl: ~80 B per code block = 2×SyncResult (boxed, 64 B) + 1×Core.Box (16 B).

It's a closure-capture box. The local sync is assigned in two branches (soft vs. hard detector), then captured by the trailing reduce closure via Int(sync.polarity). Julia boxes any variable that is both assigned in more than one place and captured by a closure — producing a Core.Box per call plus heap-boxing of each SyncResult. The box exists regardless of runtime path, so it fired on every code block during acquisition.

Fix

Hoist sync_polarity = Int(sync.polarity) into a plain local before the closure, so the closure captures a plain Int and sync stays unboxed. One line, no behavior change — the full test suite passes (including the bit-decode / L1CA sync tests that exercise this path). Acquisition is now flat at 272 B regardless of length; the post-sync locked steady state was already allocation-free.

Benchmark guard

Adds a multi-code-period track! allocation guard to benchmark/benchmarks.jl (presync and synced states at 2 and 20 code blocks). setup builds a fresh state and warms it with one call so the measured call is warm — flat memory ⇒ correct, scaling memory ⇒ regression. The benchmark comment's memory table shows the fix on the presync 20 blk row (was ~1920 B / 67 allocs, now ~320 B / 7 allocs; the 320 B residual is a constant BenchmarkTools kwarg-harness artifact).

🤖 Generated with Claude Code

…ind_bit

`_buffer_find_bit` assigns the local `sync` in two branches (soft vs. hard
detector) and then captures it in the trailing `reduce` closure via
`Int(sync.polarity)`. Julia boxes any variable that is both assigned in
more than one place and captured by a closure, so this produced a per-call
`Core.Box` plus heap-boxing of each `SyncResult` (~80 B). The box exists
regardless of runtime path, so it fired on every code block during
acquisition, making `track!` allocate in proportion to the signal length
(352 B at 1 code period → 1072 B at 10) rather than staying allocation-free
after warmup as the docstring claims.

Hoist `sync.polarity` into a plain `Int` local before the closure so the
closure captures the `Int` and `sync` stays unboxed. Acquisition is now
per-code-block allocation-free (flat 272 B regardless of length); the
post-sync locked steady state was already clean. No behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Benchmark Results (minimum time) — macos-14

Reporting the minimum over all samples (robust to shared-runner contention), not the median.

Alternative backends vs Float32 (track!, PR head)

Legend — backends: F32 Float32 (default) · I16 Int16 · 1b OneBit · 2b TwoBit (2-bit measurement + 2-bit carrier). Time columns are the minimum track! time; ×B = F32 / B (so >1 ⇒ backend B is faster than Float32), ✅ ≥ 5 % faster, ⚠️ ≥ 5 % slower. 1b/2b are BPSK-only, so their cells are blank for CBOC (Galileo E1B) scenarios.

Scenario F32 I16 1b 2b ×I16 ×1b ×2b
4-antenna @ 5 MHz 11.6 μs 8.1 μs 4.35 μs 8.56 μs 1.43 ✅ 2.67 ✅ 1.35 ✅
GPS L1CA, 8 sats @ 40 MHz 381.0 μs 145.0 μs 69.5 μs 104.0 μs 2.62 ✅ 5.48 ✅ 3.67 ✅
GPS L1CA, 8 sats @ 5 MHz 52.8 μs 24.0 μs 16.5 μs 21.2 μs 2.2 ✅ 3.19 ✅ 2.49 ✅
Galileo E1B, 4 sats @ 25 MHz 146.0 μs 72.0 μs 2.03 ✅
dynamic taps @ 5 MHz (kernel) 6.68 μs 2.6 μs 1.65 μs 2.57 ✅ 4.06 ✅
multi-signal N=3 @ 5 MHz 10.8 μs 6.2 μs 4.05 μs 8.28 μs 1.75 ✅ 2.68 ✅ 1.31 ✅
Time benchmarks (base vs PR head)

Ratio = 81e5299… / c2c9b4e…: >1 means the PR is faster. ✅ ≥ 5 % faster, ⚠️ ≥ 5 % slower. A blank cell means the benchmark exists on only one revision (🆕 = new on the PR, 🗑 = removed).

81e5299 c2c9b4e 81e5299… / c2c9b4e
downconvert and correlate/CPU/Float32 2.5 μs 2.57 μs 0.969
downconvert and correlate/CPU/Float32 4ant 4.89 μs 5.08 μs 0.964
downconvert and correlate/CPU/Float64 2.8 μs 2.74 μs 1.02
downconvert and correlate/CPU/Int16 2.63 μs 2.74 μs 0.963
downconvert and correlate/CPU/Int16 4ant 4.86 μs 4.86 μs 1.0
downconvert and correlate/CPU/Int32 2.55 μs 2.55 μs 0.998
fused kernel/1-ant dynamic taps 2.55 μs 2.56 μs 0.996
fused kernel/1-ant static taps 2.06 μs 2.06 μs 0.995
fused kernel/4-ant dynamic taps 7.78 μs 7.78 μs 1.0
fused kernel/4-ant static taps 4.43 μs 4.43 μs 1.0
fused tuple kernel/1-ant N=2 3.27 μs 3.27 μs 1.0
fused tuple kernel/1-ant N=3 3.4 μs 3.4 μs 1.0
fused tuple kernel/2-ant N=2 6.3 μs 6.31 μs 0.999
fused tuple kernel/2-ant N=3 5.62 μs 5.62 μs 1.0
fused tuple kernel/4-ant N=2 11.5 μs 11.7 μs 0.989
fused tuple kernel/4-ant N=3 10.1 μs 10.1 μs 1.0
track/1. Float32/2K – track 2.67 μs 2.67 μs 1.0
track/1. Float32/2K – track! 2.72 μs 2.79 μs 0.977
track/2. L1 8sat/5K – track 49.6 μs 49.8 μs 0.996
track/2. L1 8sat/5K – track! 49.3 μs 49.7 μs 0.993
track/2. L1 8sat/5K – track!-threaded 49.7 μs 49.9 μs 0.996
track/2. L1 8sat/5K – track-threaded 49.9 μs 49.9 μs 1.0
track/3. E1B 4sat/25K – track 136.0 μs 136.0 μs 1.0
track/3. E1B 4sat/25K – track! 135.0 μs 135.0 μs 1.0
track/3. E1B 4sat/25K – track!-threaded 135.0 μs 136.0 μs 1.0
track/3. E1B 4sat/25K – track-threaded 136.0 μs 136.0 μs 1.0
track/4. 8L1+8E1B/25K – track 490.0 μs 490.0 μs 1.0
track/4. 8L1+8E1B/25K – track! 489.0 μs 489.0 μs 1.0
track/4. 8L1+8E1B/25K – track!-threaded 488.0 μs 488.0 μs 1.0
track/4. 8L1+8E1B/25K – track-threaded 490.0 μs 490.0 μs 1.0
track/5. multi-signal N=1/5K – track 6.48 μs 6.63 μs 0.977
track/5. multi-signal N=1/5K – track! 6.46 μs 6.41 μs 1.01
track/6. multi-signal N=2/5K – track 11.0 μs 10.3 μs 1.06 ✅
track/6. multi-signal N=2/5K – track! 10.5 μs 10.8 μs 0.977
track/7. multi-signal N=3/5K – track 11.8 μs 12.0 μs 0.983
track/7. multi-signal N=3/5K – track! 11.8 μs 11.7 μs 1.01
track/8. L1CA presync 2 blk – track! 12.5 μs 12.4 μs 1.01
track/8. L1CA presync 20 blk – track! 122.0 μs 120.0 μs 1.01
track/8. L1CA synced 2 blk – track! 12.2 μs 12.3 μs 0.993
track/8. L1CA synced 20 blk – track! 118.0 μs 119.0 μs 0.997
time_to_load 161.0 μs 197.0 μs 0.816 ⚠️
Memory benchmarks (base vs PR head)
81e5299 c2c9b4e
downconvert and correlate/CPU/Float32 2 allocs: 576 B 2 allocs: 576 B
downconvert and correlate/CPU/Float32 4ant 2 allocs: 848 B 2 allocs: 848 B
downconvert and correlate/CPU/Float64 2 allocs: 576 B 2 allocs: 576 B
downconvert and correlate/CPU/Int16 2 allocs: 576 B 2 allocs: 576 B
downconvert and correlate/CPU/Int16 4ant 2 allocs: 848 B 2 allocs: 848 B
downconvert and correlate/CPU/Int32 2 allocs: 576 B 2 allocs: 576 B
fused kernel/1-ant dynamic taps 0 allocs: 0 B 0 allocs: 0 B
fused kernel/1-ant static taps 0 allocs: 0 B 0 allocs: 0 B
fused kernel/4-ant dynamic taps 0 allocs: 0 B 0 allocs: 0 B
fused kernel/4-ant static taps 0 allocs: 0 B 0 allocs: 0 B
fused tuple kernel/1-ant N=2 0 allocs: 0 B 0 allocs: 0 B
fused tuple kernel/1-ant N=3 0 allocs: 0 B 0 allocs: 0 B
fused tuple kernel/2-ant N=2 0 allocs: 0 B 0 allocs: 0 B
fused tuple kernel/2-ant N=3 0 allocs: 0 B 0 allocs: 0 B
fused tuple kernel/4-ant N=2 0 allocs: 0 B 0 allocs: 0 B
fused tuple kernel/4-ant N=3 0 allocs: 0 B 0 allocs: 0 B
track/1. Float32/2K – track 9 allocs: 944 B 9 allocs: 944 B
track/1. Float32/2K – track! 1 allocs: 26 B 0 allocs: 0 B
track/2. L1 8sat/5K – track 10 allocs: 4656 B 10 allocs: 4656 B
track/2. L1 8sat/5K – track! 0 allocs: 0 B 0 allocs: 0 B
track/2. L1 8sat/5K – track!-threaded 0 allocs: 0 B 0 allocs: 0 B
track/2. L1 8sat/5K – track-threaded 10 allocs: 4656 B 10 allocs: 4656 B
track/3. E1B 4sat/25K – track 10 allocs: 3056 B 10 allocs: 3056 B
track/3. E1B 4sat/25K – track! 0 allocs: 0 B 0 allocs: 0 B
track/3. E1B 4sat/25K – track!-threaded 0 allocs: 0 B 0 allocs: 0 B
track/3. E1B 4sat/25K – track-threaded 10 allocs: 3056 B 10 allocs: 3056 B
track/4. 8L1+8E1B/25K – track 26 allocs: 10464 B 26 allocs: 10464 B
track/4. 8L1+8E1B/25K – track! 0 allocs: 0 B 0 allocs: 0 B
track/4. 8L1+8E1B/25K – track!-threaded 0 allocs: 0 B 0 allocs: 0 B
track/4. 8L1+8E1B/25K – track-threaded 26 allocs: 10464 B 26 allocs: 10464 B
track/5. multi-signal N=1/5K – track 12 allocs: 1024 B 9 allocs: 944 B
track/5. multi-signal N=1/5K – track! 3 allocs: 80 B 0 allocs: 0 B
track/6. multi-signal N=2/5K – track 15 allocs: 1568 B 9 allocs: 1408 B
track/6. multi-signal N=2/5K – track! 6 allocs: 160 B 0 allocs: 0 B
track/7. multi-signal N=3/5K – track 18 allocs: 2000 B 9 allocs: 1760 B
track/7. multi-signal N=3/5K – track! 9 allocs: 240 B 0 allocs: 0 B
track/8. L1CA presync 2 blk – track! 13 allocs: 480 B 7 allocs: 320 B
track/8. L1CA presync 20 blk – track! 67 allocs: 1920 B 7 allocs: 320 B
track/8. L1CA synced 2 blk – track! 7 allocs: 320 B 7 allocs: 320 B
track/8. L1CA synced 20 blk – track! 7 allocs: 320 B 7 allocs: 320 B
time_to_load 196 allocs: 13984 B 196 allocs: 13984 B

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Benchmark Results (minimum time) — ubuntu-latest

Reporting the minimum over all samples (robust to shared-runner contention), not the median.

Alternative backends vs Float32 (track!, PR head)

Legend — backends: F32 Float32 (default) · I16 Int16 · 1b OneBit · 2b TwoBit (2-bit measurement + 2-bit carrier). Time columns are the minimum track! time; ×B = F32 / B (so >1 ⇒ backend B is faster than Float32), ✅ ≥ 5 % faster, ⚠️ ≥ 5 % slower. 1b/2b are BPSK-only, so their cells are blank for CBOC (Galileo E1B) scenarios.

Scenario F32 I16 1b 2b ×I16 ×1b ×2b
4-antenna @ 5 MHz 11.4 μs 6.51 μs 4.88 μs 8.29 μs 1.76 ✅ 2.34 ✅ 1.38 ✅
GPS L1CA, 8 sats @ 40 MHz 337.0 μs 133.0 μs 70.1 μs 105.0 μs 2.53 ✅ 4.8 ✅ 3.2 ✅
GPS L1CA, 8 sats @ 5 MHz 50.1 μs 27.3 μs 18.6 μs 25.2 μs 1.84 ✅ 2.69 ✅ 1.99 ✅
Galileo E1B, 4 sats @ 25 MHz 126.0 μs 60.2 μs 2.1 ✅
dynamic taps @ 5 MHz (kernel) 6.26 μs 3.49 μs 1.79 μs 1.8 ✅ 3.49 ✅
multi-signal N=3 @ 5 MHz 10.1 μs 5.59 μs 4.67 μs 8.27 μs 1.8 ✅ 2.16 ✅ 1.22 ✅
Time benchmarks (base vs PR head)

Ratio = 81e5299… / c2c9b4e…: >1 means the PR is faster. ✅ ≥ 5 % faster, ⚠️ ≥ 5 % slower. A blank cell means the benchmark exists on only one revision (🆕 = new on the PR, 🗑 = removed).

81e5299 c2c9b4e 81e5299… / c2c9b4e
downconvert and correlate/CPU/Float32 2.36 μs 2.38 μs 0.993
downconvert and correlate/CPU/Float32 4ant 4.4 μs 4.4 μs 1.0
downconvert and correlate/CPU/Float64 2.75 μs 2.76 μs 0.996
downconvert and correlate/CPU/Int16 2.48 μs 2.47 μs 1.0
downconvert and correlate/CPU/Int16 4ant 4.88 μs 4.93 μs 0.991
downconvert and correlate/CPU/Int32 2.39 μs 2.42 μs 0.989
fused kernel/1-ant dynamic taps 2.3 μs 2.31 μs 0.994
fused kernel/1-ant static taps 1.91 μs 1.91 μs 1.0
fused kernel/4-ant dynamic taps 5.51 μs 5.57 μs 0.989
fused kernel/4-ant static taps 3.87 μs 3.89 μs 0.996
fused tuple kernel/1-ant N=2 2.57 μs 2.65 μs 0.969
fused tuple kernel/1-ant N=3 3.08 μs 3.08 μs 1.0
fused tuple kernel/2-ant N=2 3.92 μs 3.95 μs 0.991
fused tuple kernel/2-ant N=3 4.96 μs 4.98 μs 0.995
fused tuple kernel/4-ant N=2 6.48 μs 6.42 μs 1.01
fused tuple kernel/4-ant N=3 10.2 μs 10.2 μs 0.998
track/1. Float32/2K – track 2.64 μs 2.59 μs 1.02
track/1. Float32/2K – track! 2.68 μs 2.65 μs 1.01
track/2. L1 8sat/5K – track 48.3 μs 48.2 μs 1.0
track/2. L1 8sat/5K – track! 47.6 μs 47.6 μs 0.999
track/2. L1 8sat/5K – track!-threaded 47.8 μs 47.9 μs 0.997
track/2. L1 8sat/5K – track-threaded 48.3 μs 48.6 μs 0.995
track/3. E1B 4sat/25K – track 121.0 μs 121.0 μs 0.999
track/3. E1B 4sat/25K – track! 121.0 μs 121.0 μs 1.0
track/3. E1B 4sat/25K – track!-threaded 121.0 μs 120.0 μs 1.0
track/3. E1B 4sat/25K – track-threaded 121.0 μs 121.0 μs 0.999
track/4. 8L1+8E1B/25K – track 443.0 μs 443.0 μs 0.999
track/4. 8L1+8E1B/25K – track! 442.0 μs 441.0 μs 1.0
track/4. 8L1+8E1B/25K – track!-threaded 442.0 μs 442.0 μs 1.0
track/4. 8L1+8E1B/25K – track-threaded 445.0 μs 444.0 μs 1.0
track/5. multi-signal N=1/5K – track 6.27 μs 6.19 μs 1.01
track/5. multi-signal N=1/5K – track! 6.16 μs 6.15 μs 1.0
track/6. multi-signal N=2/5K – track 9.09 μs 9.1 μs 0.999
track/6. multi-signal N=2/5K – track! 9.23 μs 9.1 μs 1.01
track/7. multi-signal N=3/5K – track 12.0 μs 11.6 μs 1.04
track/7. multi-signal N=3/5K – track! 11.8 μs 11.6 μs 1.02
track/8. L1CA presync 2 blk – track! 11.8 μs 11.7 μs 1.01
track/8. L1CA presync 20 blk – track! 115.0 μs 113.0 μs 1.01
track/8. L1CA synced 2 blk – track! 11.6 μs 11.6 μs 0.998
track/8. L1CA synced 20 blk – track! 111.0 μs 112.0 μs 0.995
time_to_load 120.0 μs 125.0 μs 0.957
Memory benchmarks (base vs PR head)
81e5299 c2c9b4e
downconvert and correlate/CPU/Float32 2 allocs: 576 B 2 allocs: 576 B
downconvert and correlate/CPU/Float32 4ant 2 allocs: 848 B 2 allocs: 848 B
downconvert and correlate/CPU/Float64 2 allocs: 576 B 2 allocs: 576 B
downconvert and correlate/CPU/Int16 2 allocs: 576 B 2 allocs: 576 B
downconvert and correlate/CPU/Int16 4ant 2 allocs: 848 B 2 allocs: 848 B
downconvert and correlate/CPU/Int32 2 allocs: 576 B 2 allocs: 576 B
fused kernel/1-ant dynamic taps 0 allocs: 0 B 0 allocs: 0 B
fused kernel/1-ant static taps 0 allocs: 0 B 0 allocs: 0 B
fused kernel/4-ant dynamic taps 0 allocs: 0 B 0 allocs: 0 B
fused kernel/4-ant static taps 0 allocs: 0 B 0 allocs: 0 B
fused tuple kernel/1-ant N=2 0 allocs: 0 B 0 allocs: 0 B
fused tuple kernel/1-ant N=3 0 allocs: 0 B 0 allocs: 0 B
fused tuple kernel/2-ant N=2 0 allocs: 0 B 0 allocs: 0 B
fused tuple kernel/2-ant N=3 0 allocs: 0 B 0 allocs: 0 B
fused tuple kernel/4-ant N=2 0 allocs: 0 B 0 allocs: 0 B
fused tuple kernel/4-ant N=3 0 allocs: 0 B 0 allocs: 0 B
track/1. Float32/2K – track 9 allocs: 944 B 9 allocs: 944 B
track/1. Float32/2K – track! 1 allocs: 26 B 0 allocs: 0 B
track/2. L1 8sat/5K – track 10 allocs: 4536 B 10 allocs: 4536 B
track/2. L1 8sat/5K – track! 0 allocs: 0 B 0 allocs: 0 B
track/2. L1 8sat/5K – track!-threaded 0 allocs: 0 B 0 allocs: 0 B
track/2. L1 8sat/5K – track-threaded 10 allocs: 4536 B 10 allocs: 4536 B
track/3. E1B 4sat/25K – track 10 allocs: 2808 B 10 allocs: 2808 B
track/3. E1B 4sat/25K – track! 0 allocs: 0 B 0 allocs: 0 B
track/3. E1B 4sat/25K – track!-threaded 0 allocs: 0 B 0 allocs: 0 B
track/3. E1B 4sat/25K – track-threaded 10 allocs: 2808 B 10 allocs: 2808 B
track/4. 8L1+8E1B/25K – track 26 allocs: 10352 B 26 allocs: 10352 B
track/4. 8L1+8E1B/25K – track! 0 allocs: 0 B 0 allocs: 0 B
track/4. 8L1+8E1B/25K – track!-threaded 0 allocs: 0 B 0 allocs: 0 B
track/4. 8L1+8E1B/25K – track-threaded 26 allocs: 10352 B 26 allocs: 10352 B
track/5. multi-signal N=1/5K – track 12 allocs: 1024 B 9 allocs: 944 B
track/5. multi-signal N=1/5K – track! 3 allocs: 80 B 0 allocs: 0 B
track/6. multi-signal N=2/5K – track 15 allocs: 1568 B 9 allocs: 1408 B
track/6. multi-signal N=2/5K – track! 6 allocs: 160 B 0 allocs: 0 B
track/7. multi-signal N=3/5K – track 18 allocs: 2000 B 9 allocs: 1760 B
track/7. multi-signal N=3/5K – track! 9 allocs: 240 B 0 allocs: 0 B
track/8. L1CA presync 2 blk – track! 13 allocs: 480 B 7 allocs: 320 B
track/8. L1CA presync 20 blk – track! 67 allocs: 1920 B 7 allocs: 320 B
track/8. L1CA synced 2 blk – track! 7 allocs: 320 B 7 allocs: 320 B
track/8. L1CA synced 20 blk – track! 7 allocs: 320 B 7 allocs: 320 B
time_to_load 145 allocs: 11216 B 145 allocs: 11216 B

@zsoerenm

zsoerenm commented Jul 8, 2026

Copy link
Copy Markdown
Member Author

Heads-up: the Julia 1.10 CI failure is pre-existing and not caused by this PR

The Julia 1.10 - ubuntu-latest job fails during codegen with:

error: couldn't allocate output register for constraint 'x'

This is an LLVM 15 register-allocation crash while compiling the Int16 SIMD downconvert kernels — not a test failure (every testset that runs passes).

I verified it is not caused by this PR:

  • The base branch (ss/two-bit-downconvert-and-correlate, commit 81e5299) passed all CI including Julia 1.10 on 2026-07-04.
  • I opened a throwaway probe PR ([CI PROBE - do not merge] base on Julia 1.10 with today's deps #195) from the unmodified base (a README no-op only) today. It failed Julia 1.10 with the identical crash, then I closed it.

So the breakage is branch-wide and appeared between 2026-07-04 and today — consistent with a freshly-resolved SIMD-stack dependency (Pkg.test re-resolves per run) introducing the LLVM 1.10 codegen bug. It reproduces on the plain base with none of this PR's changes.

This PR itself only touches scalar code in bit_buffer.jl (plus the benchmark file, which Pkg.test doesn't run), and does not make 1.10 any worse than base. The other CI jobs (Format, Docs, Buildkite, Julia 1 / 1.11) are green, and the benchmark comment shows the intended allocation win with no regressions.

Fixing the 1.10 dependency/toolchain crash is a separate, branch-wide concern (e.g. a [compat] pin on the offending SIMD dependency, or an upstream report) and is out of scope for this allocation fix — happy to take it on separately if you'd like.

@zsoerenm

zsoerenm commented Jul 8, 2026

Copy link
Copy Markdown
Member Author

Update: the Julia 1.10 crash is flaky, not deterministic — it passed on re-run

Correcting my earlier comment: the couldn't allocate output register for constraint 'x' crash is nondeterministic. The exact same commit that failed 1.10 twice earlier just passed 1.10 on a fresh run (Julia 1.10 is now ✅ on this PR). LLVM 15's register allocator for these SIMD kernels is evidently sensitive to compilation nondeterminism, so it crashes on some runs and succeeds on others.

So this is a flaky, branch-wide CI issue (it also hit the unmodified base in probe #195), not caused by this PR and not a hard blocker — a re-run clears it. If the flakiness proves annoying, it may be worth a separate look (a [compat] pin on the SIMD dependency that started tripping it, or an upstream report), but that's out of scope here.

This PR is now green across the board.

…sions

Adds a testset that tracks noise (which never bit-syncs, so the per-code-block
bit-edge search `_buffer_find_bit` runs on every block) and asserts `track!` is
allocation-free after warmup at both a short and a long chunk length. The
existing per-stage allocation test misses this: it tracks a real signal that
reaches bit sync within the warmup, so it only ever measures the post-sync path.

With the closure-capture box this branch removes, the pre-sync search allocated
~80 B per code block, so the test fails on the old implementation (160 B at 2
blocks, 1600 B at 20 — the 20-block assertion catches the scaling) and passes
now.

Gated to Julia >= 1.11: on 1.10 the compiler leaves a ~770 B/block allocation in
the pre-sync soft-bit path (unrelated to the box) that 1.11+ elides, so the
`== 0` assertion only holds on 1.11+. The regression still shows on 1.11+ and in
the benchmark suite's memory table.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@zsoerenm
zsoerenm force-pushed the ss/fix-presync-bit-search-allocation branch from f10e3a7 to c2c9b4e Compare July 8, 2026 19:21
@codecov

codecov Bot commented Jul 8, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.78%. Comparing base (81e5299) to head (c2c9b4e).

Additional details and impacted files
@@                           Coverage Diff                            @@
##           ss/two-bit-downconvert-and-correlate     #194      +/-   ##
========================================================================
- Coverage                                 98.05%   97.78%   -0.28%     
========================================================================
  Files                                        29       29              
  Lines                                      2933     2934       +1     
========================================================================
- Hits                                       2876     2869       -7     
- Misses                                       57       65       +8     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@zsoerenm
zsoerenm merged commit 62b2425 into ss/two-bit-downconvert-and-correlate Jul 8, 2026
12 checks passed
@zsoerenm
zsoerenm deleted the ss/fix-presync-bit-search-allocation branch July 8, 2026 19:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant