1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
|
#!/bin/sh
# scripts/profile-tests.sh
# Generate timing reports for the trbldoc test suite.
#
# Two levels of detail:
# 1. Per-test wall-clock timings from bats (--timing + JUnit report). Always
# produced. Answers "which tests take a long time".
# 2. Per-line hotspots inside trbldoc.sh (optional, --hotspots). Uses the
# BASH_ENV xtrace hook to time individual source lines across the whole
# run. Answers "which code takes a long time".
#
# POSIX sh; works under mingw bash, WSL bash and Alpine/busybox ash.
#
# Usage:
# scripts/profile-tests.sh [-s all|unit|integration] [-o OUTDIR]
# [-n TOPN] [-p] [-t "file.sh ..."]
#
# -s SUITE suite to run (default: integration)
# -o OUTDIR output directory (default: reports/profile-<timestamp>)
# -n TOPN number of rows in the printed "slowest" summary (default: 20)
# -p also collect per-line hotspots inside the target script(s)
# -t LIST space-separated target source files for -p (default: trbldoc.sh)
set -u
SELF="$0"
. "$(dirname -- "$0")/lib/common.sh"
tb_resolve_paths "$SELF"
SUITE=integration
OUTDIR=""
TOPN=20
HOTSPOTS=0
TARGETS="trbldoc.sh"
while getopts ":s:o:n:pt:h" opt; do
case "$opt" in
s) SUITE="$OPTARG" ;;
o) OUTDIR="$OPTARG" ;;
n) TOPN="$OPTARG" ;;
p) HOTSPOTS=1 ;;
t) TARGETS="$OPTARG" ;;
h) sed -n '2,30p' "$SELF"; exit 0 ;;
:) tb_die "option -$OPTARG requires an argument" ;;
\?) tb_die "unknown option -$OPTARG" ;;
esac
done
tb_require awk
tb_require sort
[ -n "$OUTDIR" ] || OUTDIR="$REPO_ROOT/reports/profile-$(tb_timestamp)"
case "$OUTDIR" in
/*|[A-Za-z]:[\\/]*)
;;
*)
OUTDIR="$REPO_ROOT/$OUTDIR"
;;
esac
mkdir -p "$OUTDIR" || tb_die "cannot create output dir: $OUTDIR"
JUNIT_DIR="$OUTDIR/junit"
mkdir -p "$JUNIT_DIR"
SUITE_DIRS="$(tb_suite_dirs "$SUITE")"
SUITE_DIRS_REL="$(tb_suite_dirs_rel "$SUITE")"
# ── 1. per-test timing via bats JUnit ───────────────────────────────────────
tb_info "running bats ($SUITE) with timing..."
# --timing adds per-test durations; the junit report captures them in XML.
# We do not abort on test failures: timing is still useful.
if tb_bats_usable "$SUITE"; then
# shellcheck disable=SC2086
bats --timing --report-formatter junit --output "$JUNIT_DIR" $SUITE_DIRS \
> "$OUTDIR/bats-output.txt" 2>&1 || tb_info "bats reported failures (see bats-output.txt)"
BATS_MODE="local"
elif command -v wsl >/dev/null 2>&1; then
tb_info "local bats appears unusable; retrying via WSL bats..."
WSL_REPO="$(tb_to_wsl_path "$REPO_ROOT")"
WSL_JUNIT="$(tb_to_wsl_path "$JUNIT_DIR")"
wsl -- bash -lc \
"cd '$WSL_REPO' && bats --timing --report-formatter junit --output '$WSL_JUNIT' $SUITE_DIRS_REL" \
> "$OUTDIR/bats-output.txt" 2>&1 || tb_info "bats (WSL) reported failures (see bats-output.txt)"
BATS_MODE="wsl"
else
tb_die "local bats command appears unusable and no WSL fallback is available"
fi
# Collect all JUnit XML the formatter produced.
: > "$OUTDIR/timings.tsv"
for xml in "$JUNIT_DIR"/*.xml; do
[ -f "$xml" ] || continue
awk -f "$LIB_DIR/timing-extract.awk" "$xml" >> "$OUTDIR/timings.tsv"
done
if [ ! -s "$OUTDIR/timings.tsv" ]; then
if grep -qi "bash\\\\r" "$OUTDIR/bats-output.txt" 2>/dev/null; then
tb_die "bats appears broken (CRLF shebang: 'bash\\r'). Reinstall bats or run dos2unix on the bats scripts, then retry."
fi
tb_die "no per-test timings parsed (check $OUTDIR/bats-output.txt)"
fi
# Sort slowest first.
sort -t "$(printf '\t')" -k1 -rn "$OUTDIR/timings.tsv" > "$OUTDIR/timings.sorted.tsv"
# CSV summary.
{
printf 'seconds,suite,test\n'
awk -F '\t' '{ gsub(/"/,"\"\"",$3); printf "%s,%s,\"%s\"\n",$1,$2,$3 }' \
"$OUTDIR/timings.sorted.tsv"
} > "$OUTDIR/timings.csv"
# HTML report.
awk -f "$LIB_DIR/render-timing.awk" \
-v TITLE="trbldoc test timings ($SUITE)" \
"$OUTDIR/timings.sorted.tsv" > "$OUTDIR/timings.html"
# ── 2. optional per-line hotspots ────────────────────────────────────────────
if [ "$HOTSPOTS" -eq 1 ]; then
tb_info "collecting per-line hotspots (this reruns the suite under xtrace)..."
TRACE_DIR="$OUTDIR/trace"
rm -rf "$TRACE_DIR"; mkdir -p "$TRACE_DIR"
# Build the trace-target basename filter.
TRACE_TARGETS=""
for tf in $TARGETS; do
TRACE_TARGETS="$TRACE_TARGETS ${tf##*/}"
done
BASH_ENV="$LIB_DIR/trace-init.sh"
TRBLDOC_TRACE_DIR="$TRACE_DIR"
TRBLDOC_TRACE_MODE="time"
TRBLDOC_TRACE_TARGETS="$TRACE_TARGETS"
if [ "$BATS_MODE" = "local" ]; then
export BASH_ENV TRBLDOC_TRACE_DIR TRBLDOC_TRACE_MODE TRBLDOC_TRACE_TARGETS
# shellcheck disable=SC2086
bats --report-formatter junit --output "$JUNIT_DIR" $SUITE_DIRS \
> "$OUTDIR/bats-hotspots-output.txt" 2>&1 \
|| tb_info "bats reported failures during hotspot run"
unset BASH_ENV TRBLDOC_TRACE_DIR TRBLDOC_TRACE_MODE TRBLDOC_TRACE_TARGETS
else
WSL_REPO="$(tb_to_wsl_path "$REPO_ROOT")"
WSL_LIB="$(tb_to_wsl_path "$LIB_DIR")"
WSL_TRACE="$(tb_to_wsl_path "$TRACE_DIR")"
WSL_JUNIT="$(tb_to_wsl_path "$JUNIT_DIR")"
wsl -- bash -lc \
"cd '$WSL_REPO' && export BASH_ENV='$WSL_LIB/trace-init.sh' TRBLDOC_TRACE_DIR='$WSL_TRACE' TRBLDOC_TRACE_MODE='time' TRBLDOC_TRACE_TARGETS='$TRACE_TARGETS'; bats --report-formatter junit --output '$WSL_JUNIT' $SUITE_DIRS_REL" \
> "$OUTDIR/bats-hotspots-output.txt" 2>&1 \
|| tb_info "bats (WSL) reported failures during hotspot run"
fi
if ls "$TRACE_DIR"/trace.*.log >/dev/null 2>&1; then
awk -f "$LIB_DIR/trace-extract.awk" -v MODE=time "$TRACE_DIR"/trace.*.log \
| sort -t "$(printf '\t')" -k1 -rn > "$OUTDIR/hotspots.tsv"
{
printf 'total_ms,hits,file,line\n'
awk -F '\t' '{ printf "%s,%s,%s,%s\n",$1,$2,$3,$4 }' "$OUTDIR/hotspots.tsv"
} > "$OUTDIR/hotspots.csv"
# Resolve first target path for source display in the HTML.
FIRST_TARGET="${TARGETS%% *}"
case "$FIRST_TARGET" in
/*) SRC_PATH="$FIRST_TARGET" ;;
*) SRC_PATH="$REPO_ROOT/$FIRST_TARGET" ;;
esac
awk -f "$LIB_DIR/render-hotspots.awk" \
-v TITLE="trbldoc line hotspots ($SUITE)" \
-v SRC="$SRC_PATH" \
"$OUTDIR/hotspots.tsv" > "$OUTDIR/hotspots.html"
else
if grep -qi "bash\\\\r" "$OUTDIR/bats-hotspots-output.txt" 2>/dev/null; then
tb_die "bats appears broken (CRLF shebang: 'bash\\r') during hotspot run. Reinstall bats or run dos2unix on the bats scripts."
fi
tb_info "no trace data captured; skipping hotspots report"
HOTSPOTS=0
fi
fi
# ── printed summary ──────────────────────────────────────────────────────────
printf '\n==== slowest %s tests ====\n' "$TOPN"
awk -F '\t' -v n="$TOPN" 'NR<=n { printf "%8.3fs %s\n", $1, $3 }' \
"$OUTDIR/timings.sorted.tsv"
printf '\nReports written to: %s\n' "$OUTDIR"
printf ' per-test timing : %s\n' "$OUTDIR/timings.html"
[ "$HOTSPOTS" -eq 1 ] && printf ' line hotspots : %s\n' "$OUTDIR/hotspots.html"
|