aboutsummaryrefslogtreecommitdiff
path: root/scripts/hotspot-tests.sh
diff options
context:
space:
mode:
authorAlexander M Pickering <alex@cogarr.net>2026-07-21 20:19:46 -0500
committerAlexander M Pickering <alex@cogarr.net>2026-07-21 20:19:46 -0500
commit657fb0007f39f07cc0401e0c5d03e25df6234aa4 (patch)
treef73fe23232dc80802f39caed738c38464a60ec6d /scripts/hotspot-tests.sh
downloadtrbldoc-657fb0007f39f07cc0401e0c5d03e25df6234aa4.tar.gz
trbldoc-657fb0007f39f07cc0401e0c5d03e25df6234aa4.tar.bz2
trbldoc-657fb0007f39f07cc0401e0c5d03e25df6234aa4.zip
Inital commit.
Diffstat (limited to 'scripts/hotspot-tests.sh')
-rw-r--r--scripts/hotspot-tests.sh204
1 files changed, 204 insertions, 0 deletions
diff --git a/scripts/hotspot-tests.sh b/scripts/hotspot-tests.sh
new file mode 100644
index 0000000..8973e6b
--- /dev/null
+++ b/scripts/hotspot-tests.sh
@@ -0,0 +1,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