Skip to content

Commit 062014b

Browse files
authored
perf(doc): parse assertion docs in a single awk pass (#833)
1 parent dd722c3 commit 062014b

3 files changed

Lines changed: 64 additions & 49 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
- `--jobs auto` / `-j auto` caps parallel concurrency at the CPU core count (portable across Linux/macOS/BSD); the default stays unlimited (#766)
1717

1818
### Changed
19+
- `bashunit doc` no longer forks an `echo | sed` pipe per line of the assertion docs: a single awk pass prints the same bytes in ~50ms instead of ~5s (#832)
1920
- Multi-file runs are no longer quadratic in file count: each file's test functions are unset once the file has been processed, so test subshells stop forking an ever-growing shell. bashunit's own 63-file unit suite: ~64s -> ~22s sequential, ~26s -> ~7s parallel (#829)
2021
- Major performance work with no behaviour change: assertions, per-test execution, per-file discovery, cold start and parallel result publishing are now (near) fork-free, snapshots and `--tag` scans are cached, and quadratic failure rendering is single-pass. Benchmarks on bash 3.2: 100x10 `assert_equals` ~1.50s -> ~0.76s, 500 snapshot assertions ~7.5s -> ~3.0s, 100 tagged tests ~2.92s -> ~0.68s, and bashunit's own acceptance suite ~61s -> ~17s (#761-#764, #772-#775, #798, #801-#807, #809, #810, #813, #817)
2122
- Per-test timing now defaults to `auto` (`BASHUNIT_SHOW_EXECUTION_TIME=true|false|auto`): shown only when the clock is fork-free, avoiding `perl` forks on bash 3.2; `--profile`/`--verbose`/reports still measure (see `adrs/adr-008-auto-skip-per-test-timing.md`) (#765)

src/doc.sh

Lines changed: 40 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -9,58 +9,49 @@ function bashunit::doc::get_embedded_docs() {
99
# __BASHUNIT_EMBEDDED_DOCS_END__
1010
}
1111

12+
# Single awk pass over the embedded docs: the previous line-by-line shell loop
13+
# forked an `echo | sed` pipe per line (~3.2k forks, ~5s for the ~1.6k-line
14+
# docs page); one awk fork does the same work in milliseconds (#832).
1215
function bashunit::doc::print_asserts() {
1316
local filter="${1:-}"
14-
local docstring=""
15-
local fn=""
16-
local should_print=0
1717

18-
local line
19-
while IFS='' read -r line || [ -n "$line" ]; do
20-
fn=$(echo "$line" | sed -n 's/^## \([A-Za-z0-9_]*\).*/\1/p')
21-
# Only treat assertion headings as doc entries; skip prose sections
22-
# like "## Related" that are part of the documentation page but not asserts.
23-
case "$fn" in
24-
assert* | bashunit*) ;;
25-
*) fn="" ;;
26-
esac
27-
if [ -n "$fn" ]; then
28-
local _match=0
29-
if [ -z "$filter" ]; then
30-
_match=1
31-
else
32-
case "$fn" in *"$filter"*) _match=1 ;; esac
33-
fi
34-
if [ "$_match" -eq 1 ]; then
35-
should_print=1
36-
echo "$line"
37-
docstring=""
38-
else
39-
should_print=0
40-
fi
41-
continue
42-
fi
18+
bashunit::doc::get_embedded_docs | awk -v filter="$filter" '
19+
{
20+
if ($0 ~ /^## /) {
21+
# Heading word: the leading [A-Za-z0-9_]* run after "## ". Only
22+
# assert*/bashunit* headings are doc entries; prose headings like
23+
# "## Related" fall through and are treated as regular content.
24+
fn = substr($0, 4)
25+
sub(/[^A-Za-z0-9_].*$/, "", fn)
26+
if (fn ~ /^(assert|bashunit)/) {
27+
if (filter == "" || index(fn, filter) > 0) {
28+
should_print = 1
29+
print $0
30+
doc = ""
31+
} else {
32+
should_print = 0
33+
}
34+
next
35+
}
36+
}
4337
44-
if ((should_print)); then
45-
# Check for code fence using pattern matching instead of regex
46-
# Avoids backtick escaping issues in Bash 3.0
47-
case "$line" in
48-
'```'*)
49-
echo "--------------"
50-
echo "$docstring"
51-
should_print=0
52-
continue
53-
;;
54-
esac
38+
if (should_print) {
39+
if ($0 ~ /^```/) {
40+
print "--------------"
41+
print doc
42+
should_print = 0
43+
next
44+
}
45+
if ($0 ~ /^::: code-group/) next
5546
56-
case "$line" in
57-
"::: code-group"*) continue ;;
58-
esac
59-
60-
# Remove markdown link brackets and anchor tags
61-
line="${line//[\[\]]/}"
62-
line="$(sed -E 's/ *\(#[-a-z0-9]+\)//g' <<<"$line")"
63-
docstring="$docstring$line"$'\n'
64-
fi
65-
done <<<"$(bashunit::doc::get_embedded_docs)"
47+
# Remove markdown link brackets and anchor tags. The bracket class
48+
# uses the POSIX []][ idiom: busybox awk (Alpine) rejects
49+
# backslash-escaped brackets inside a bracket expression.
50+
line = $0
51+
gsub(/[][]/, "", line)
52+
gsub(/ *\(#[-a-z0-9]+\)/, "", line)
53+
doc = doc line "\n"
54+
}
55+
}
56+
'
6657
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
# Regression guard for #832. `bashunit doc` used to fork an `echo | sed` pipe
5+
# for every line of the ~1.6k-line assertions.md (plus another `sed` per
6+
# printed docstring line): ~3200 forks and ~5s wall to print a text file. The
7+
# doc path must parse the embedded docs in a single awk pass instead.
8+
function test_doc_command_does_not_fork_sed_per_line() {
9+
if bashunit::check_os::is_windows; then
10+
bashunit::skip "process tracing is unreliable under Git Bash" && return
11+
fi
12+
13+
local trace
14+
trace="$(PS4='+ ' bash -x ./bashunit doc equals 2>&1 >/dev/null)"
15+
16+
# Count real `sed` process executions. The doc path invokes `sed` via PATH
17+
# (not a pinned absolute binary), so the traced command token is a bare
18+
# `sed` — match both forms; `command -v sed` is a builtin and never shows.
19+
local sed_forks
20+
sed_forks="$(printf '%s\n' "$trace" | grep -cE '^\++ +(/[^ ]*/)?sed ' || true)"
21+
22+
assert_equals 0 "$sed_forks"
23+
}

0 commit comments

Comments
 (0)