aboutsummaryrefslogtreecommitdiff
path: root/scripts/profile-tests.sh
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/profile-tests.sh')
-rw-r--r--scripts/profile-tests.sh184
1 files changed, 184 insertions, 0 deletions
diff --git a/scripts/profile-tests.sh b/scripts/profile-tests.sh
new file mode 100644
index 0000000..498ae25
--- /dev/null
+++ b/scripts/profile-tests.sh
@@ -0,0 +1,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"