aboutsummaryrefslogtreecommitdiff
path: root/scripts/profile-run.sh
blob: 74ab425b576ba487761797a148e38d93bb674ab0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
#!/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-<timestamp>)
#   -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