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
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
|
#!/bin/sh
# scripts/hotspot-tests.sh
# Repeatable hotspot analysis for the trbldoc test suite.
#
# Produces:
# - per-test timings (parsed from `bats --timing`)
# - line-level hotspots for target scripts from xtrace logs
# - optional phase hotspots for trbldoc.sh
#
# Usage:
# scripts/hotspot-tests.sh [-s all|unit|integration] [-o OUTDIR]
# [-n TOPN] [-t "file.sh ..."]
#
# -s SUITE suite to run (default: all)
# -o OUTDIR output directory (default: reports/hotspots-<timestamp>)
# -n TOPN number of slowest tests to print (default: 25)
# -t LIST space-separated target scripts for trace hotspots (default: trbldoc.sh)
set -u
SELF="$0"
. "$(dirname -- "$0")/lib/common.sh"
tb_resolve_paths "$SELF"
SUITE=all
OUTDIR=""
TOPN=25
TARGETS="trbldoc.sh"
while getopts ":s:o:n:t:h" opt; do
case "$opt" in
s) SUITE="$OPTARG" ;;
o) OUTDIR="$OPTARG" ;;
n) TOPN="$OPTARG" ;;
t) TARGETS="$OPTARG" ;;
h) sed -n '2,24p' "$SELF"; exit 0 ;;
:) tb_die "option -$OPTARG requires an argument" ;;
\?) tb_die "unknown option -$OPTARG" ;;
esac
done
tb_require awk
tb_require sort
case "$TOPN" in
''|*[!0-9]*|0) tb_die "-n TOPN must be a positive integer" ;;
esac
[ -n "$OUTDIR" ] || OUTDIR="$REPO_ROOT/reports/hotspots-$(tb_timestamp)"
case "$OUTDIR" in
/*|[A-Za-z]:[\\/]*) ;;
*) OUTDIR="$REPO_ROOT/$OUTDIR" ;;
esac
mkdir -p "$OUTDIR" || tb_die "cannot create output dir: $OUTDIR"
SUITE_DIRS="$(tb_suite_dirs "$SUITE")"
SUITE_DIRS_REL="$(tb_suite_dirs_rel "$SUITE")"
TIMING_RAW="$OUTDIR/bats-timing-output.txt"
TRACE_DIR="$OUTDIR/trace"
rm -rf "$TRACE_DIR"; mkdir -p "$TRACE_DIR"
# Build trace target filter and resolve first target source for HTML context.
TRACE_TARGETS=""
FIRST_TARGET=""
for tf in $TARGETS; do
TRACE_TARGETS="$TRACE_TARGETS ${tf##*/}"
if [ -z "$FIRST_TARGET" ]; then
FIRST_TARGET="$tf"
fi
done
tb_info "running bats ($SUITE) with --timing output..."
if tb_bats_usable "$SUITE"; then
# shellcheck disable=SC2086
bats --timing $SUITE_DIRS > "$TIMING_RAW" 2>&1 || tb_info "bats reported failures (timing output still collected)"
BATS_MODE="local"
elif command -v wsl >/dev/null 2>&1; then
tb_info "local bats appears unusable; retrying timing run via WSL bats..."
WSL_REPO="$(tb_to_wsl_path "$REPO_ROOT")"
wsl -- bash -lc \
"cd '$WSL_REPO' && bats --timing $SUITE_DIRS_REL" \
> "$TIMING_RAW" 2>&1 || tb_info "bats (WSL) reported failures (timing output still collected)"
BATS_MODE="wsl"
else
tb_die "local bats command appears unusable and no WSL fallback is available"
fi
# Parse bats timing lines: "ok N <name> in <ms>ms"
awk '
match($0, /^ok [0-9]+ (.*) in ([0-9]+)ms$/, m) {
name = m[1]
ms = m[2] + 0
suite = name
sub(/:.*/, "", suite)
printf "%.3f\t%s\t%s\t%d\n", ms / 1000.0, suite, name, ms
}
' "$TIMING_RAW" > "$OUTDIR/timings.tsv"
if [ ! -s "$OUTDIR/timings.tsv" ]; then
tb_die "no per-test timing rows parsed (check $TIMING_RAW)"
fi
sort -t "$(printf '\t')" -k1,1nr "$OUTDIR/timings.tsv" > "$OUTDIR/timings.sorted.tsv"
{
printf 'seconds,suite,test,milliseconds\n'
awk -F '\t' '{ gsub(/"/,"\"\"",$3); printf "%s,%s,\"%s\",%s\n",$1,$2,$3,$4 }' "$OUTDIR/timings.sorted.tsv"
} > "$OUTDIR/timings.csv"
awk -f "$LIB_DIR/render-timing.awk" \
-v TITLE="trbldoc test timings ($SUITE)" \
"$OUTDIR/timings.sorted.tsv" > "$OUTDIR/timings.html"
tb_info "running bats ($SUITE) under xtrace timing hook..."
if [ "$BATS_MODE" = "local" ]; then
BASH_ENV="$LIB_DIR/trace-init.sh"
TRBLDOC_TRACE_DIR="$TRACE_DIR"
TRBLDOC_TRACE_MODE="time"
TRBLDOC_TRACE_TARGETS="$TRACE_TARGETS"
export BASH_ENV TRBLDOC_TRACE_DIR TRBLDOC_TRACE_MODE TRBLDOC_TRACE_TARGETS
# shellcheck disable=SC2086
bats $SUITE_DIRS > "$OUTDIR/bats-trace-output.txt" 2>&1 || tb_info "bats reported failures during trace 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 -- 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 $SUITE_DIRS_REL" \
> "$OUTDIR/bats-trace-output.txt" 2>&1 || tb_info "bats (WSL) reported failures during trace 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"
case "$FIRST_TARGET" in
/*|[A-Za-z]:[\\/]*) 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
tb_info "no trace logs captured for target(s):$TRACE_TARGETS"
fi
# Optional phase hotspots when trbldoc.sh + helper scripts are available.
if [ -f "$OUTDIR/hotspots.tsv" ] && [ -f "$LIB_DIR/trace-phase-extract.awk" ] && [ -f "$LIB_DIR/render-phases.awk" ] && [ -f "$REPO_ROOT/trbldoc.sh" ]; then
_find_line() {
awk -v p="$1" 'index($0, p) { print NR; exit }' "$REPO_ROOT/trbldoc.sh"
}
L_DISCOVER_END="$(_find_line '> "$source_files_list"')"
L_EXTRACT_END="$(_find_line '# Resolve src_repo once before the chunk loop.')"
L_ORDER_END="$(_find_line 'done < "$sorted_chunk_order_file"')"
L_RENDER_END="$(_find_line 'done < "$chunk_render_records_file"')"
L_NAV_END="$(_find_line '# Second pass: resolve inline references now that global.meta is complete.')"
L_REFS_END="$(_find_line 'rm -f "$_ref_find_tmp"')"
L_TOC_END="$(_find_line '# Post-processing pass: substitute {{ toc }} in all rendered index.html files')"
L_ASSEMBLE_END="$(_find_line 'assemble_site "$cache_dir/namespace" "$out_dir"')"
if [ -n "$L_DISCOVER_END" ] && [ -n "$L_EXTRACT_END" ] && [ -n "$L_ORDER_END" ] \
&& [ -n "$L_RENDER_END" ] && [ -n "$L_NAV_END" ] && [ -n "$L_REFS_END" ] \
&& [ -n "$L_TOC_END" ] && [ -n "$L_ASSEMBLE_END" ]; then
awk -f "$LIB_DIR/trace-phase-extract.awk" \
-v SRC_BASE="trbldoc.sh" \
-v L_DISCOVER_END="$L_DISCOVER_END" \
-v L_EXTRACT_END="$L_EXTRACT_END" \
-v L_ORDER_END="$L_ORDER_END" \
-v L_RENDER_END="$L_RENDER_END" \
-v L_NAV_END="$L_NAV_END" \
-v L_REFS_END="$L_REFS_END" \
-v L_TOC_END="$L_TOC_END" \
-v L_ASSEMBLE_END="$L_ASSEMBLE_END" \
"$TRACE_DIR"/trace.*.log \
| sort -t "$(printf '\t')" -k1 -rn > "$OUTDIR/phase-hotspots.tsv"
{
printf 'total_ms,hits,phase\n'
awk -F '\t' '{ printf "%s,%s,%s\n",$1,$2,$3 }' "$OUTDIR/phase-hotspots.tsv"
} > "$OUTDIR/phase-hotspots.csv"
awk -f "$LIB_DIR/render-phases.awk" \
-v TITLE="trbldoc phase hotspots ($SUITE)" \
"$OUTDIR/phase-hotspots.tsv" > "$OUTDIR/phase-hotspots.html"
fi
fi
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"
if [ -f "$OUTDIR/hotspots.html" ]; then
printf ' line hotspots : %s\n' "$OUTDIR/hotspots.html"
else
printf ' line hotspots : not generated (target not exercised)\n'
fi
if [ -f "$OUTDIR/phase-hotspots.html" ]; then
printf ' phase hotspots : %s\n' "$OUTDIR/phase-hotspots.html"
fi
|