Problem
bashunit doc takes ~5.1s (doc equals: ~3.6s) — for printing a text file. Found while auditing the acceptance suite for #831: tests/acceptance/bashunit_test.sh invokes the doc command ~7 times (each assert_match_snapshot + assert_successful_code pair re-runs it, plus built-binary comparisons), making that one file 29.7s of the 94s sequential acceptance suite.
Root cause (verified)
bashunit::doc::print_asserts (src/doc.sh:12) loops over all 1580 lines of docs/assertions.md and forks per line:
- src/doc.sh:20 —
fn=$(echo "$line" | sed -n 's/^## \([A-Za-z0-9_]*\).*/\1/p') → a $() subshell + echo | sed pipe for every line, including the ~1400 non-heading lines. ~3200 forks per invocation.
- src/doc.sh:62 —
line="$(sed -E 's/ *\(#[-a-z0-9]+\)//g' <<<"$line")" → another fork per printed docstring line.
~3200+ forks × ~1.5ms ≈ the measured 5s (2.2-3.1s of it sys time).
Fix
Rewrite print_asserts as a single awk pass over the embedded docs. Per .claude/rules/perf-fork-budget.md: awk wins over pure-bash while read at ≥100 lines (this is 1580), and one awk fork replaces ~3200. The awk program carries the same logic: match ^## (assert*|bashunit*) headings, apply the substring filter, strip [/] and (#anchor) tags, buffer the docstring until the code fence, emit -------------- separator.
A pure-bash variant (case-glob heading match + parameter-expansion stripping) would also drop to milliseconds, but per the perf rules the single-awk version is both simpler and faster at this input size. Either is acceptable if output is byte-identical.
Expected: ~5.1s → 60-80ms per invocation; acceptance suite sequential −25s; bashunit_test.sh 29.7s → ~5s.
Constraints (important)
- Output must be byte-identical —
bashunit_test.sh snapshots ./bashunit doc and ./bashunit doc equals, and compares built-binary output against dev output. Do not regenerate snapshots to make a diff pass; a changed byte is a bug in the rewrite.
- Do not touch
bashunit::doc::get_embedded_docs (src/doc.sh:6) — build.sh swaps its body between the __BASHUNIT_EMBEDDED_DOCS_START__/END__ markers to embed the docs in the single-file artifact. Only print_asserts changes.
- Bash 3.0+ (
.claude/rules/bash-style.md); awk is already a core dependency (provider map, duplicate check).
- TDD: RED first — e.g. a fork-census check that
bashunit doc runs at most N sed/echo forks (pattern: tests/acceptance/bashunit_coldstart_forks_test.sh), or a timing-free unit test pinning print_asserts output for a small fixture doc plus the census assertion.
- CHANGELOG entry under
## Unreleased (user-facing perf).
Acceptance criteria
Verification commands
time ./bashunit doc >/dev/null
./bashunit doc | shasum # compare before/after
./bashunit tests/acceptance/bashunit_test.sh
./bashunit tests/ && ./bashunit --parallel tests/ && make sa && make lint
Problem
bashunit doctakes ~5.1s (doc equals: ~3.6s) — for printing a text file. Found while auditing the acceptance suite for #831:tests/acceptance/bashunit_test.shinvokes the doc command ~7 times (eachassert_match_snapshot+assert_successful_codepair re-runs it, plus built-binary comparisons), making that one file 29.7s of the 94s sequential acceptance suite.Root cause (verified)
bashunit::doc::print_asserts(src/doc.sh:12) loops over all 1580 lines ofdocs/assertions.mdand forks per line:fn=$(echo "$line" | sed -n 's/^## \([A-Za-z0-9_]*\).*/\1/p')→ a$()subshell +echo | sedpipe for every line, including the ~1400 non-heading lines. ~3200 forks per invocation.line="$(sed -E 's/ *\(#[-a-z0-9]+\)//g' <<<"$line")"→ another fork per printed docstring line.~3200+ forks × ~1.5ms ≈ the measured 5s (2.2-3.1s of it sys time).
Fix
Rewrite
print_assertsas a single awk pass over the embedded docs. Per.claude/rules/perf-fork-budget.md: awk wins over pure-bashwhile readat ≥100 lines (this is 1580), and one awk fork replaces ~3200. The awk program carries the same logic: match^## (assert*|bashunit*)headings, apply the substring filter, strip[/]and(#anchor)tags, buffer the docstring until the code fence, emit--------------separator.A pure-bash variant (case-glob heading match + parameter-expansion stripping) would also drop to milliseconds, but per the perf rules the single-awk version is both simpler and faster at this input size. Either is acceptable if output is byte-identical.
Expected: ~5.1s →
60-80ms per invocation; acceptance suite sequential −25s;bashunit_test.sh29.7s → ~5s.Constraints (important)
bashunit_test.shsnapshots./bashunit docand./bashunit doc equals, and compares built-binary output against dev output. Do not regenerate snapshots to make a diff pass; a changed byte is a bug in the rewrite.bashunit::doc::get_embedded_docs(src/doc.sh:6) — build.sh swaps its body between the__BASHUNIT_EMBEDDED_DOCS_START__/END__markers to embed the docs in the single-file artifact. Onlyprint_assertschanges..claude/rules/bash-style.md); awk is already a core dependency (provider map, duplicate check).bashunit docruns at most N sed/echo forks (pattern:tests/acceptance/bashunit_coldstart_forks_test.sh), or a timing-free unit test pinningprint_assertsoutput for a small fixture doc plus the census assertion.## Unreleased(user-facing perf).Acceptance criteria
time ./bashunit doc< 0.2s (baseline ~5.1s)./bashunit doc/doc equalsoutput byte-identical to baseline (existing snapshots pass unchanged)./build.sh bin) doc output still matches dev output (existing test)./bashunit tests/,--parallel,--parallel --simple --strict;make sa;make lintVerification commands