#!/bin/sh # scripts/profile-run.sh # Profile direct trbldoc invocations (outside bats) using the existing xtrace # hook and report pipeline. # # Outputs: # - run-summary.tsv/csv wall-clock per run # - hotspots.tsv/csv/html per-line cumulative self-time # - phase-hotspots.tsv/csv/html per-phase cumulative self-time # # Usage: # scripts/profile-run.sh [-o OUTDIR] [-n RUNS] [-C] [-t "file.sh ..."] -- [trbldoc args...] # # -o OUTDIR output directory (default: reports/profile-run-) # -n RUNS number of repeated runs (default: 1) # -C force clean rebuild (-C) on every run # -t LIST trace target script basenames (default: trbldoc.sh) set -u SELF="$0" . "$(dirname -- "$0")/lib/common.sh" tb_resolve_paths "$SELF" OUTDIR="" RUNS=1 CLEAN=0 TARGETS="trbldoc.sh" while getopts ":o:n:Ct:h" opt; do case "$opt" in o) OUTDIR="$OPTARG" ;; n) RUNS="$OPTARG" ;; C) CLEAN=1 ;; 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 shift $((OPTIND - 1)) [ "${1:-}" = "--" ] && shift tb_require awk tb_require sort tb_require bash case "$RUNS" in ''|*[!0-9]*|0) tb_die "-n RUNS must be a positive integer" ;; esac [ -n "$OUTDIR" ] || OUTDIR="$REPO_ROOT/reports/profile-run-$(tb_timestamp)" case "$OUTDIR" in /*|[A-Za-z]:[\\/]*) ;; *) OUTDIR="$REPO_ROOT/$OUTDIR" ;; esac mkdir -p "$OUTDIR" || tb_die "cannot create output dir: $OUTDIR" TRACE_DIR="$OUTDIR/trace" RUN_DIR="$OUTDIR/runs" mkdir -p "$TRACE_DIR" "$RUN_DIR" TRBLDOC_PATH="$REPO_ROOT/trbldoc.sh" [ -f "$TRBLDOC_PATH" ] || tb_die "trbldoc source not found: $TRBLDOC_PATH" TRACE_TARGETS="" for tf in $TARGETS; do TRACE_TARGETS="$TRACE_TARGETS ${tf##*/}" done # Resolve line anchors once for phase aggregation. If any anchor is missing, # skip phase aggregation rather than failing the whole profiling run. tb_find_line() { _pat="$1" awk -v p="$_pat" 'index($0, p) { print NR; exit }' "$TRBLDOC_PATH" } L_DISCOVER_END="$(tb_find_line '> "$source_files_list"')" L_EXTRACT_END="$(tb_find_line '# Resolve src_repo once before the chunk loop.')" L_ORDER_END="$(tb_find_line 'done < "$sorted_chunk_order_file"')" L_RENDER_END="$(tb_find_line 'done < "$chunk_render_records_file"')" L_NAV_END="$(tb_find_line '# Second pass: resolve inline references now that global.meta is complete.')" L_REFS_END="$(tb_find_line 'rm -f "$_ref_find_tmp"')" L_TOC_END="$(tb_find_line '# Post-processing pass: substitute {{ toc }} in all rendered index.html files')" L_ASSEMBLE_END="$(tb_find_line 'assemble_site "$cache_dir/namespace" "$out_dir"')" : > "$OUTDIR/run-summary.tsv" run_idx=1 while [ "$run_idx" -le "$RUNS" ]; do tb_info "profiling run $run_idx/$RUNS..." run_out="$RUN_DIR/run-$run_idx" mkdir -p "$run_out" run_stdout="$run_out/stdout.txt" run_stderr="$run_out/stderr.txt" run_start_s="$(date +%s)" 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 if [ "$CLEAN" -eq 1 ]; then (cd "$REPO_ROOT" && bash "$TRBLDOC_PATH" -C "$@") >"$run_stdout" 2>"$run_stderr" else (cd "$REPO_ROOT" && bash "$TRBLDOC_PATH" "$@") >"$run_stdout" 2>"$run_stderr" fi run_exit=$? unset BASH_ENV TRBLDOC_TRACE_DIR TRBLDOC_TRACE_MODE TRBLDOC_TRACE_TARGETS run_end_s="$(date +%s)" run_ms=$(( (run_end_s - run_start_s) * 1000 )) printf '%s\t%s\t%s\n' "$run_idx" "$run_ms" "$run_exit" >> "$OUTDIR/run-summary.tsv" run_idx=$((run_idx + 1)) done sort -t "$(printf '\t')" -k2 -rn "$OUTDIR/run-summary.tsv" > "$OUTDIR/run-summary.sorted.tsv" { printf 'run,total_ms,exit_code\n' awk -F '\t' '{ printf "%s,%s,%s\n",$1,$2,$3 }' "$OUTDIR/run-summary.sorted.tsv" } > "$OUTDIR/run-summary.csv" 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" FIRST_TARGET="${TARGETS%% *}" 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 (direct runs)" \ -v SRC="$SRC_PATH" \ "$OUTDIR/hotspots.tsv" > "$OUTDIR/hotspots.html" else tb_die "no trace logs captured (check run stderr logs under $RUN_DIR)" fi 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 (direct runs)" \ "$OUTDIR/phase-hotspots.tsv" > "$OUTDIR/phase-hotspots.html" else tb_info "phase anchor discovery incomplete; skipping phase-hotspots report" fi printf '\n==== slowest runs ====\n' awk -F '\t' '{ printf "run %s %8.1f ms exit=%s\n",$1,$2,$3 }' "$OUTDIR/run-summary.sorted.tsv" printf '\nReports written to: %s\n' "$OUTDIR" printf ' run summary : %s\n' "$OUTDIR/run-summary.csv" printf ' line hotspots : %s\n' "$OUTDIR/hotspots.html" if [ -f "$OUTDIR/phase-hotspots.html" ]; then printf ' phase hotspots : %s\n' "$OUTDIR/phase-hotspots.html" fi