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 DEBUG trap re-mangles the file path twice per executed line #1102
With the report phase down to ~1s (#1088, #1090) and the buffer gone (#1101),
capture is what a --coverage run costs. record_line runs per executed line
and does two cache lookups — the tracked decision and the normalized path — and
each one rebuilds its key with
That pattern substitution is the whole cost, and it is not linear in path
length. Measured on Bash 3.2 arm64, 5000 mangles:
path length
time
7 chars
39 ms
59 chars
653 ms
121 chars
2746 ms
So record_line costs 402 µs per line for a real path
(/Users/…/src/coverage/engine.sh), of which 328 µs is the two lookups and
only 29 µs the actual append. A project whose sources sit deeper pays
quadratically more, for nothing it can control.
Proposal
A one-entry memo for the last file seen. Consecutive executed lines come from
the same file almost always — that is what a function body is — so a single
string compare replaces both key rebuilds and both indirect reads:
10,000 iterations
mangle per call
1393 ms
same-file memo
56 ms
Expected: ~402 µs to ~75 µs per line, and the path-length sensitivity mostly
gone. The existing per-path caches stay for the miss path.
coverage::init has to clear the memo along with the other in-memory caches,
or a second run in the same shell inherits a stale decision.
Measured the proposal properly and the premise above is wrong. Recording
the corrected picture.
The memo barely helps. Implemented, it hits 75% of the time (4291 of 5718 record_line calls) and buys ~4%: 7019ms → 6751ms. Not worth the state.
Why the estimate was wrong. The 402 µs/line came from a micro-benchmark
using a 59-character absolute path. The trap actually sees what bash sourced — ./src/config/env.sh, about 20 characters — where the mangle is far cheaper.
Path length still matters super-linearly, but not at the lengths that occur.
Where the time really goes. For tests/unit/assert/basic_test.sh against --coverage-paths src:
no coverage
566 ms
coverage, nothing tracked (trap fires, rejects)
612 ms
coverage, src tracked
6716 ms
So trap dispatch costs 46 ms — #1060 already fixed that — and ~5.1s is
recording. Instrumenting it: normalize_path is called 494 times per run,
and its body is
which is four forks — roughly 2000 forks per run. It recurs because the
caches live in shell variables and the test body runs in a subshell, so every
test re-pays for the files it touches.
The fix that pays. Same semantics, one fork:
local dir="${file%/*}" base="${file##*/}"printf'%s/%s'"$(cd "$dir"2>/dev/null && pwd)""$base"
500 calls
four forks (today)
2081 ms
one fork
424 ms
Identical output on relative, absolute and /tmp/../tmp/./x.sh — the cd+pwd
stays, so symlink resolution is unchanged. Pure-bash string normalisation is not an option for exactly that reason: it would report /tmp/... where the
current code reports /private/tmp/....
Retitling this to the normalization fork; the memo is dropped.
Activity
Chemaclass commentedon Aug 13, 2026
Measured the proposal properly and the premise above is wrong. Recording
the corrected picture.
The memo barely helps. Implemented, it hits 75% of the time (4291 of 5718
record_linecalls) and buys ~4%: 7019ms → 6751ms. Not worth the state.Why the estimate was wrong. The 402 µs/line came from a micro-benchmark
using a 59-character absolute path. The trap actually sees what bash sourced —
./src/config/env.sh, about 20 characters — where the mangle is far cheaper.Path length still matters super-linearly, but not at the lengths that occur.
Where the time really goes. For
tests/unit/assert/basic_test.shagainst--coverage-paths src:srctrackedSo trap dispatch costs 46 ms — #1060 already fixed that — and ~5.1s is
recording. Instrumenting it:
normalize_pathis called 494 times per run,and its body is
which is four forks — roughly 2000 forks per run. It recurs because the
caches live in shell variables and the test body runs in a subshell, so every
test re-pays for the files it touches.
The fix that pays. Same semantics, one fork:
Identical output on relative, absolute and
/tmp/../tmp/./x.sh— thecd+pwdstays, so symlink resolution is unchanged. Pure-bash string normalisation is
not an option for exactly that reason: it would report
/tmp/...where thecurrent code reports
/private/tmp/....Retitling this to the normalization fork; the memo is dropped.
Chemaclass commentedon Aug 13, 2026
Done in #1105 (merged). The memo the issue originally proposed was measured and dropped; the fork reduction is what landed.