Skip to content

perf(doc): bashunit doc forks echo|sed per line of assertions.md — ~5s to print a file (single awk pass) #832

Description

@Chemaclass

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-identicalbashunit_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

  • time ./bashunit doc < 0.2s (baseline ~5.1s)
  • ./bashunit doc / doc equals output byte-identical to baseline (existing snapshots pass unchanged)
  • Built binary (./build.sh bin) doc output still matches dev output (existing test)
  • Fork census: no per-line sed/echo forks in the doc path
  • Full suite green: ./bashunit tests/, --parallel, --parallel --simple --strict; make sa; make lint

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

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Fields

No fields configured for issues without a type.

Projects

Status
Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions