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