#!/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-) # -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"