aboutsummaryrefslogtreecommitdiff
path: root/scripts/profile-tests.sh
blob: 498ae2573c2c0862a2dd58b8316994aff3198de8 (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
181
182
183
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"