You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
perf(coverage): the trap caches die with every test subshell and get rebuilt #1104
record_line caches two decisions per file — whether it is tracked, and its
normalized path — in shell variables. The test body runs in a subshell, so
those variables are a copy that dies with it, and the next test rebuilds them
for every file it touches.
Instrumenting one run of tests/unit/assert/basic_test.sh against --coverage-paths src: 5718 record_line calls, 1427 of them cache misses
over only 13 distinct files. Thirteen files, eleven hundred redundant
misses.
A miss is the expensive path (Bash 3.2 arm64, per 500 calls):
Warm the caches in the parent, before the first test subshell, from the
tracked list seed_tracked_files already builds (#1053). A subshell inherits
the parent's variables, so every worker starts with the decisions already
made and the miss path runs about thirteen times per run instead of 1427.
The spelling matters: the trap keys on whatever BASH_SOURCE holds, which is
how the file was sourced — often ./src/x.sh rather than the absolute path
the seed list carries. Warming both spellings covers the common cases, and a
miss still falls through to the current path, so nothing breaks when a file
arrives spelled some third way.
Related
#1102 makes each miss ~4x cheaper; this removes most of them entirely. The two
compose.
Found the concrete mechanism, and it is cheaper to fix than warming.
should_track already has a disk cache, and it reads it like this:
cached_decision=$(grep "^${file}:""$cache_file"2>/dev/null | head -1)||true
That is a command substitution plus grep plus head — three processes per
lookup — and it runs on every in-memory miss, which is 1427 times per run of
one test file because the in-memory cache dies with each test subshell.
Measured on Bash 3.2 arm64, a 13-entry cache, 500 lookups:
grep | head per lookup
1015 ms
read the file once, match in memory
32 ms
2 ms versus 0.064 ms. At 1427 lookups that is ~2.85s per run — essentially all
of the recording time left after #1102.
Same anti-pattern #1076 removed from the stats cache: a cache that costs more
than the computation it replaces.
Plan
Read cache.dat once per process into parallel arrays on first use, then match
in memory; append to the file as new decisions are made, as today. That keeps
the cross-process sharing that the file exists for (parallel workers), removes
the per-lookup forks, and needs no knowledge of the tracked list up front —
which matters because seeding happens at report time (#1053), so the parent has
no file list to warm from when the trap starts.
Activity
Chemaclass commentedon Aug 13, 2026
Found the concrete mechanism, and it is cheaper to fix than warming.
should_trackalready has a disk cache, and it reads it like this:That is a command substitution plus
grepplushead— three processes perlookup — and it runs on every in-memory miss, which is 1427 times per run of
one test file because the in-memory cache dies with each test subshell.
Measured on Bash 3.2 arm64, a 13-entry cache, 500 lookups:
grep | headper lookup2 ms versus 0.064 ms. At 1427 lookups that is ~2.85s per run — essentially all
of the recording time left after #1102.
Same anti-pattern #1076 removed from the stats cache: a cache that costs more
than the computation it replaces.
Plan
Read
cache.datonce per process into parallel arrays on first use, then matchin memory; append to the file as new decisions are made, as today. That keeps
the cross-process sharing that the file exists for (parallel workers), removes
the per-lookup forks, and needs no knowledge of the tracked list up front —
which matters because seeding happens at report time (#1053), so the parent has
no file list to warm from when the trap starts.
Chemaclass commentedon Aug 13, 2026
Done in #1109 (merged).