diff options
| author | Alexander M Pickering <alex@cogarr.net> | 2026-07-21 20:19:46 -0500 |
|---|---|---|
| committer | Alexander M Pickering <alex@cogarr.net> | 2026-07-21 20:19:46 -0500 |
| commit | 657fb0007f39f07cc0401e0c5d03e25df6234aa4 (patch) | |
| tree | f73fe23232dc80802f39caed738c38464a60ec6d /scripts/lib | |
| download | trbldoc-657fb0007f39f07cc0401e0c5d03e25df6234aa4.tar.gz trbldoc-657fb0007f39f07cc0401e0c5d03e25df6234aa4.tar.bz2 trbldoc-657fb0007f39f07cc0401e0c5d03e25df6234aa4.zip | |
Inital commit.
Diffstat (limited to 'scripts/lib')
| -rw-r--r-- | scripts/lib/common.sh | 105 | ||||
| -rw-r--r-- | scripts/lib/coverage-report.awk | 84 | ||||
| -rw-r--r-- | scripts/lib/render-coverage-index.awk | 48 | ||||
| -rw-r--r-- | scripts/lib/render-hotspots.awk | 56 | ||||
| -rw-r--r-- | scripts/lib/render-phases.awk | 41 | ||||
| -rw-r--r-- | scripts/lib/render-timing.awk | 41 | ||||
| -rw-r--r-- | scripts/lib/timing-extract.awk | 32 | ||||
| -rw-r--r-- | scripts/lib/trace-extract.awk | 71 | ||||
| -rw-r--r-- | scripts/lib/trace-init.sh | 42 | ||||
| -rw-r--r-- | scripts/lib/trace-phase-extract.awk | 75 |
10 files changed, 595 insertions, 0 deletions
diff --git a/scripts/lib/common.sh b/scripts/lib/common.sh new file mode 100644 index 0000000..0fdf98a --- /dev/null +++ b/scripts/lib/common.sh @@ -0,0 +1,105 @@ +# scripts/lib/common.sh +# Shared helpers for the trbldoc report scripts. +# +# POSIX sh compatible. Verified to work under mingw bash, WSL bash and +# Alpine/busybox ash. Sourced by the entrypoint scripts; do not execute +# directly. + +# Resolve the repository root from the location of the scripts/ directory. +# Sets: REPO_ROOT, SCRIPTS_DIR, LIB_DIR +tb_resolve_paths() { + # $1: path to the calling script ($0) + script_path="$1" + # Directory containing the calling script (scripts/). + SCRIPTS_DIR="$(CDPATH= cd -- "$(dirname -- "$script_path")" && pwd)" + LIB_DIR="$SCRIPTS_DIR/lib" + REPO_ROOT="$(CDPATH= cd -- "$SCRIPTS_DIR/.." && pwd)" + export REPO_ROOT SCRIPTS_DIR LIB_DIR +} + +# Print an ISO-8601 UTC timestamp suitable for directory names. +# Falls back to epoch seconds when the platform date(1) cannot format UTC. +tb_timestamp() { + if date -u +%Y%m%dT%H%M%SZ 2>/dev/null; then + return 0 + fi + printf '%s\n' "$(date +%s)" +} + +# Emit an informational message to stderr. +tb_info() { + printf '[trbldoc-report] %s\n' "$*" >&2 +} + +# Emit an error message to stderr and return non-zero. +tb_die() { + printf '[trbldoc-report] ERROR: %s\n' "$*" >&2 + exit 1 +} + +# Verify a command exists on PATH, otherwise abort. +tb_require() { + command -v "$1" >/dev/null 2>&1 || tb_die "required tool not found on PATH: $1" +} + +# Resolve the bats suite directories for a target keyword. +# $1: all|unit|integration ; echoes space-separated dirs relative to REPO_ROOT. +tb_suite_dirs() { + case "$1" in + unit) printf '%s\n' "$REPO_ROOT/tests/unit" ;; + integration) printf '%s\n' "$REPO_ROOT/tests/integration" ;; + all) printf '%s\n' "$REPO_ROOT/tests/unit $REPO_ROOT/tests/integration" ;; + *) tb_die "unknown suite '$1' (expected: all|unit|integration)" ;; + esac +} + +# Resolve bats suite directories relative to repository root. +# $1: all|unit|integration ; echoes a space-separated relative path list. +tb_suite_dirs_rel() { + case "$1" in + unit) printf '%s\n' "tests/unit" ;; + integration) printf '%s\n' "tests/integration" ;; + all) printf '%s\n' "tests/unit tests/integration" ;; + *) tb_die "unknown suite '$1' (expected: all|unit|integration)" ;; + esac +} + +# Convert a POSIX-style MSYS path (/d/foo/bar) to a WSL Linux path +# (/mnt/d/foo/bar). Other paths are returned unchanged. +tb_to_wsl_path() { + p="$1" + case "$p" in + [A-Za-z]:\\*|[A-Za-z]:/*) + d="$(printf '%s' "$p" | cut -c1 | tr 'A-Z' 'a-z')" + rest="$(printf '%s' "$p" | cut -c3- | sed 's#\\\\#/#g')" + rest="$(printf '%s' "$rest" | sed 's#^/*##')" + printf '/mnt/%s/%s\n' "$d" "$rest" + ;; + /[A-Za-z]/*) + d="$(printf '%s' "$p" | cut -c2 | tr 'A-Z' 'a-z')" + rest="$(printf '%s' "$p" | cut -c4-)" + printf '/mnt/%s/%s\n' "$d" "$rest" + ;; + *) + printf '%s\n' "$p" + ;; + esac +} + +# Returns success if the local `bats` command appears usable for this repo. +# Some mixed Windows/MSYS setups expose a broken bats wrapper that exits 0 +# without running tests or producing output; this guard catches that case. +tb_bats_usable() { + suite="${1:-integration}" + dirs="$(tb_suite_dirs "$suite")" + # shellcheck disable=SC2086 + count="$(bats --count $dirs 2>/dev/null | tr -d '[:space:]')" + case "$count" in + ''|*[!0-9]*) + return 1 + ;; + *) + return 0 + ;; + esac +} diff --git a/scripts/lib/coverage-report.awk b/scripts/lib/coverage-report.awk new file mode 100644 index 0000000..982eb45 --- /dev/null +++ b/scripts/lib/coverage-report.awk @@ -0,0 +1,84 @@ +# scripts/lib/coverage-report.awk +# Produce line-coverage reports for shell sources from an executed-line set. +# +# Inputs: +# -v COVERED=<file> TSV of "<base>\t<line>" executed lines (sorted -u). +# -v HTMLDIR=<dir> directory to write per-file <base>.html reports into. +# ARGV the source files to report on (absolute paths). +# +# Output (stdout): one CSV row per source file: +# file,total_executable,covered,percent +# +# "Executable" lines are determined heuristically: non-blank, non-comment +# lines that are not pure structural tokens (then/do/done/else/fi/esac/{/}). +# Heredoc/multiline string bodies are approximated and may read as missed; +# see docs/coverage.md for the documented limitations. + +function trim(s) { sub(/^[ \t]+/, "", s); sub(/[ \t]+$/, "", s); return s } + +function is_exec(line, t) { + t = trim(line) + if (t == "") return 0 + if (t ~ /^#/) return 0 + if (t ~ /^(then|do|done|else|fi|esac)([ \t].*)?$/) return 0 + if (t ~ /^[{}]([ \t;].*)?$/) return 0 + if (t ~ /^[()]([ \t].*)?$/) return 0 + if (t ~ /^;;/) return 0 + return 1 +} + +function esc(s) { + gsub(/&/, "\\&", s) + gsub(/</, "\\<", s) + gsub(/>/, "\\>", s) + return s +} + +function basename(p) { sub(/.*\//, "", p); sub(/.*\\/, "", p); return p } + +BEGIN { + FS = "\t" + # Load the executed-line set. + while ((getline ln < COVERED) > 0) { + nf = split(ln, a, "\t") + if (nf >= 2) covered[a[1] SUBSEP a[2]] = 1 + } + close(COVERED) +} + +# New source file: open its HTML report and reset counters. +FNR == 1 { + if (cur != "") finish_file() + cur = FILENAME + curbase = basename(FILENAME) + total = 0; hit = 0 + html = HTMLDIR "/" curbase ".html" + print "<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"utf-8\">" > html + print "<title>coverage: " esc(curbase) "</title>" > html + print "<style>body{font:13px/1.5 monospace;margin:0;padding:1rem;background:#fff;color:#111}" > html + print "h1{font:600 16px sans-serif}" > html + print "table{border-collapse:collapse;width:100%}" > html + print "td{padding:0 .5rem;white-space:pre;vertical-align:top}" > html + print "td.n{color:#999;text-align:right;user-select:none;border-right:1px solid #ddd}" > html + print ".hit{background:#e6ffed}.miss{background:#ffeef0}.na{color:#999}" > html + print "</style></head><body>" > html + print "<h1>coverage: " esc(curbase) "</h1><table>" > html +} + +{ + cls = "na" + if (is_exec($0)) { + total++ + if ((curbase SUBSEP FNR) in covered) { hit++; cls = "hit" } else { cls = "miss" } + } + print "<tr class=\"" cls "\"><td class=\"n\">" FNR "</td><td>" esc($0) "</td></tr>" > html +} + +function finish_file( pct) { + print "</table></body></html>" > html + close(html) + pct = (total > 0 ? (hit * 100.0 / total) : 0) + printf "%s,%d,%d,%.1f\n", curbase, total, hit, pct +} + +END { if (cur != "") finish_file() } diff --git a/scripts/lib/render-coverage-index.awk b/scripts/lib/render-coverage-index.awk new file mode 100644 index 0000000..cc5af1b --- /dev/null +++ b/scripts/lib/render-coverage-index.awk @@ -0,0 +1,48 @@ +# scripts/lib/render-coverage-index.awk +# Render the coverage index page from summary rows. +# Input CSV rows (no header): "<file>,<executable>,<covered>,<percent>". +# Per-file detail pages are expected at html/<file>.html. +# Variables: -v TITLE="..." + +function esc(s) { + gsub(/&/, "\\&", s); gsub(/</, "\\<", s); gsub(/>/, "\\>", s) + return s +} + +function color(p) { + if (p + 0 >= 80) return "#2da44e" + if (p + 0 >= 50) return "#bf8700" + return "#cf222e" +} + +BEGIN { FS = ","; n = 0; tot = 0; cov = 0 } + +{ + n++ + f[n] = $1; ex[n] = $2 + 0; cv[n] = $3 + 0; pc[n] = $4 + 0 + tot += $2 + 0; cov += $3 + 0 +} + +END { + overall = (tot > 0 ? cov * 100.0 / tot : 0) + print "<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"utf-8\">" + print "<title>" esc(TITLE) "</title>" + print "<style>body{font:14px/1.5 sans-serif;margin:0;padding:1.5rem;color:#111}" + print "h1{font-size:18px}.overall{font-size:15px;margin:.5rem 0 1rem}" + print "table{border-collapse:collapse;width:100%;max-width:820px}" + print "th,td{padding:.35rem .5rem;text-align:left;border-bottom:1px solid #eee}" + print "td.num{text-align:right;font-variant-numeric:tabular-nums}" + print ".track{background:#eee;border-radius:3px;height:.7em;width:160px}" + print ".fill{height:.7em;border-radius:3px}" + print "a{color:#0969da;text-decoration:none}a:hover{text-decoration:underline}" + print "</style></head><body>" + print "<h1>" esc(TITLE) "</h1>" + printf "<div class=\"overall\">Overall: <strong style=\"color:%s\">%.1f%%</strong> (%d/%d executable lines)</div>\n", color(overall), overall, cov, tot + print "<table><thead><tr><th>file</th><th>coverage</th><th>%</th><th>covered</th><th>executable</th></tr></thead><tbody>" + for (i = 1; i <= n; i++) { + printf "<tr><td><a href=\"html/%s.html\">%s</a></td>", esc(f[i]), esc(f[i]) + printf "<td><div class=\"track\"><div class=\"fill\" style=\"width:%.0f%%;background:%s\"></div></div></td>", pc[i], color(pc[i]) + printf "<td class=\"num\">%.1f%%</td><td class=\"num\">%d</td><td class=\"num\">%d</td></tr>\n", pc[i], cv[i], ex[i] + } + print "</tbody></table></body></html>" +} diff --git a/scripts/lib/render-hotspots.awk b/scripts/lib/render-hotspots.awk new file mode 100644 index 0000000..0eb2b6d --- /dev/null +++ b/scripts/lib/render-hotspots.awk @@ -0,0 +1,56 @@ +# scripts/lib/render-hotspots.awk +# Render a per-line hotspot HTML report from sorted TSV rows. +# Input TSV (sorted slowest-first): "<total_ms>\t<hits>\t<file>\t<line>". +# Variables: +# -v TITLE="..." +# -v SRC=<path> source file for the primary target, used to show line text. + +function esc(s) { + gsub(/&/, "\\&", s); gsub(/</, "\\<", s); gsub(/>/, "\\>", s) + return s +} + +BEGIN { + FS = "\t"; n = 0 + # Load source lines for text display (best effort). + if (SRC != "") { + i = 0 + while ((getline ln < SRC) > 0) { i++; srctext[i] = ln } + close(SRC) + srcbase = SRC; sub(/.*\//, "", srcbase); sub(/.*\\/, "", srcbase) + } +} + +{ + n++ + ms[n] = $1 + 0 + hits[n] = $2 + 0 + file[n] = $3 + line[n] = $4 + 0 + if (n == 1) maxv = $1 + 0 +} + +END { + print "<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"utf-8\">" + print "<title>" esc(TITLE) "</title>" + print "<style>body{font:14px/1.5 sans-serif;margin:0;padding:1.5rem;color:#111}" + print "h1{font-size:18px}.meta{color:#555;margin-bottom:1rem}" + print "table{border-collapse:collapse;width:100%}" + print "th,td{padding:.3rem .5rem;text-align:left;border-bottom:1px solid #eee}" + print "td.num{text-align:right;font-variant-numeric:tabular-nums}" + print "code{font:12px/1.4 monospace;white-space:pre}" + print ".bar{height:.7em;background:#e8703a;border-radius:2px}" + print "</style></head><body>" + print "<h1>" esc(TITLE) "</h1>" + print "<div class=\"meta\">cumulative self-time per source line, slowest first</div>" + print "<table><thead><tr><th>#</th><th>ms</th><th>hits</th><th>file:line</th><th>source</th><th></th></tr></thead><tbody>" + for (i = 1; i <= n; i++) { + w = (maxv > 0 ? (ms[i] * 100.0 / maxv) : 0) + txt = "" + if (file[i] == srcbase && (line[i] in srctext)) txt = srctext[line[i]] + printf "<tr><td class=\"num\">%d</td><td class=\"num\">%.1f</td><td class=\"num\">%d</td>", i, ms[i], hits[i] + printf "<td>%s:%d</td><td><code>%s</code></td>", esc(file[i]), line[i], esc(txt) + printf "<td><div class=\"bar\" style=\"width:%.1f%%\"></div></td></tr>\n", w + } + print "</tbody></table></body></html>" +} diff --git a/scripts/lib/render-phases.awk b/scripts/lib/render-phases.awk new file mode 100644 index 0000000..fcb11ec --- /dev/null +++ b/scripts/lib/render-phases.awk @@ -0,0 +1,41 @@ +# scripts/lib/render-phases.awk +# Render per-phase hotspot HTML from sorted TSV rows. +# Input TSV: "<total_ms>\t<hits>\t<phase>". +# Variables: +# -v TITLE="..." + +function esc(s) { + gsub(/&/, "\\&", s); gsub(/</, "\\<", s); gsub(/>/, "\\>", s) + return s +} + +BEGIN { FS = "\t"; n = 0 } + +{ + n++ + ms[n] = $1 + 0 + hits[n] = $2 + 0 + phase[n] = $3 + if (n == 1) maxv = ms[n] +} + +END { + print "<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"utf-8\">" + print "<title>" esc(TITLE) "</title>" + print "<style>body{font:14px/1.5 sans-serif;margin:0;padding:1.5rem;color:#111}" + print "h1{font-size:18px}.meta{color:#555;margin-bottom:1rem}" + print "table{border-collapse:collapse;width:100%}" + print "th,td{padding:.3rem .5rem;text-align:left;border-bottom:1px solid #eee}" + print "td.num{text-align:right;font-variant-numeric:tabular-nums}" + print ".bar{height:.7em;background:#4cbf8f;border-radius:2px}" + print "</style></head><body>" + print "<h1>" esc(TITLE) "</h1>" + print "<div class=\"meta\">cumulative self-time by pipeline phase, slowest first</div>" + print "<table><thead><tr><th>#</th><th>ms</th><th>hits</th><th>phase</th><th></th></tr></thead><tbody>" + for (i = 1; i <= n; i++) { + w = (maxv > 0 ? (ms[i] * 100.0 / maxv) : 0) + printf "<tr><td class=\"num\">%d</td><td class=\"num\">%.1f</td><td class=\"num\">%d</td><td>%s</td>", i, ms[i], hits[i], esc(phase[i]) + printf "<td><div class=\"bar\" style=\"width:%.1f%%\"></div></td></tr>\n", w + } + print "</tbody></table></body></html>" +} diff --git a/scripts/lib/render-timing.awk b/scripts/lib/render-timing.awk new file mode 100644 index 0000000..ffd807f --- /dev/null +++ b/scripts/lib/render-timing.awk @@ -0,0 +1,41 @@ +# scripts/lib/render-timing.awk +# Render a per-test timing HTML report from sorted TSV rows. +# Input TSV (already sorted slowest-first): "<seconds>\t<suite>\t<test>". +# Variables: -v TITLE="..." + +function esc(s) { + gsub(/&/, "\\&", s); gsub(/</, "\\<", s); gsub(/>/, "\\>", s) + return s +} + +BEGIN { FS = "\t"; n = 0; total = 0 } + +{ + n++ + sec[n] = $1 + 0 + suite[n] = $2 + name[n] = $3 + total += $1 + 0 + if (n == 1) maxv = $1 + 0 +} + +END { + print "<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"utf-8\">" + print "<title>" esc(TITLE) "</title>" + print "<style>body{font:14px/1.5 sans-serif;margin:0;padding:1.5rem;color:#111}" + print "h1{font-size:18px}.meta{color:#555;margin-bottom:1rem}" + print "table{border-collapse:collapse;width:100%}" + print "th,td{padding:.3rem .5rem;text-align:left;border-bottom:1px solid #eee}" + print "td.num{text-align:right;font-variant-numeric:tabular-nums}" + print ".bar{height:.7em;background:#4c8bf5;border-radius:2px}" + print "</style></head><body>" + print "<h1>" esc(TITLE) "</h1>" + printf "<div class=\"meta\">%d tests · %.3f s total</div>\n", n, total + print "<table><thead><tr><th>#</th><th>seconds</th><th>test</th><th></th></tr></thead><tbody>" + for (i = 1; i <= n; i++) { + w = (maxv > 0 ? (sec[i] * 100.0 / maxv) : 0) + printf "<tr><td class=\"num\">%d</td><td class=\"num\">%.3f</td><td>%s</td>", i, sec[i], esc(name[i]) + printf "<td><div class=\"bar\" style=\"width:%.1f%%\"></div></td></tr>\n", w + } + print "</tbody></table></body></html>" +} diff --git a/scripts/lib/timing-extract.awk b/scripts/lib/timing-extract.awk new file mode 100644 index 0000000..9cb15d7 --- /dev/null +++ b/scripts/lib/timing-extract.awk @@ -0,0 +1,32 @@ +# scripts/lib/timing-extract.awk +# Extract per-test durations from bats JUnit report XML. +# +# Emits TSV: "<seconds>\t<suite>\t<test name>" for each <testcase>. +# The caller sorts and renders. Handles testcase elements that span the line +# regardless of whether they are self-closing or contain <failure> children. + +function attr(line, key, re, s) { + re = key "=\"" + if (index(line, re) == 0) return "" + s = line + sub(".*" re, "", s) # drop up to opening quote + sub(/".*/, "", s) # drop from closing quote + return s +} + +function unesc(s) { + gsub(/"/, "\"", s) + gsub(/'/, "'", s) + gsub(/</, "<", s) + gsub(/>/, ">", s) + gsub(/&/, "\\&", s) + return s +} + +/<testcase[ \t]/ { + name = unesc(attr($0, "name")) + cls = unesc(attr($0, "classname")) + t = attr($0, "time") + if (t == "") t = "0" + printf "%s\t%s\t%s\n", t, cls, name +} diff --git a/scripts/lib/trace-extract.awk b/scripts/lib/trace-extract.awk new file mode 100644 index 0000000..7b8682a --- /dev/null +++ b/scripts/lib/trace-extract.awk @@ -0,0 +1,71 @@ +# scripts/lib/trace-extract.awk +# Extract instrumented source locations from xtrace logs produced via +# scripts/lib/trace-init.sh. +# +# Marker lines look like (bash repeats the leading '@' once per nesting level): +# @@TBL@@ /path/to/trbldoc.sh:1234 1712345678.123456 +# +# Variables: +# MODE=cov (default) -> prints "<base>\t<line>" per marker line +# MODE=time -> prints "<total_ms>\t<hits>\t<base>\t<line>" at END, +# computing per-line durations from consecutive markers +# WITHIN each trace file (one file == one process, so +# parallel xargs workers are not cross-attributed). + +function basename(p) { + sub(/.*\//, "", p) # strip up to last forward slash + sub(/.*\\/, "", p) # strip up to last backslash (defensive) + return p +} + +# Parse a marker line into globals g_base and g_line (and g_epoch for time). +# Returns 1 on success, 0 if the line is not a usable marker. +function parse(line, s, n, f, loc, rest) { + if (line !~ /@+TBL@@ /) return 0 + s = line + sub(/^.*TBL@@ /, "", s) # drop marker and everything before it + n = split(s, f, " ") + if (n < 1) return 0 + loc = f[1] # <source>:<lineno> + if (!match(loc, /:[0-9]+$/)) return 0 + g_line = substr(loc, RSTART + 1) + g_base = basename(substr(loc, 1, RSTART - 1)) + if (MODE == "time") { + g_epoch = (n >= 2 ? f[2] : "") + } + return 1 +} + +BEGIN { if (MODE == "") MODE = "cov" } + +# Reset per-process state at the start of each trace file. +FNR == 1 { have_prev = 0 } + +{ + if (!parse($0)) next + if (MODE == "cov") { + print g_base "\t" g_line + next + } + # MODE == time: attribute elapsed time to the PREVIOUS marker. + if (have_prev && g_epoch != "" && prev_epoch != "") { + d = g_epoch - prev_epoch + if (d < 0) d = 0 + key = prev_base SUBSEP prev_line + ms[key] += d * 1000.0 + hits[key] += 1 + base_of[key] = prev_base + line_of[key] = prev_line + } + prev_base = g_base + prev_line = g_line + prev_epoch = g_epoch + have_prev = 1 +} + +END { + if (MODE != "time") exit + for (k in ms) { + printf "%.3f\t%d\t%s\t%s\n", ms[k], hits[k], base_of[k], line_of[k] + } +} diff --git a/scripts/lib/trace-init.sh b/scripts/lib/trace-init.sh new file mode 100644 index 0000000..80f1ab0 --- /dev/null +++ b/scripts/lib/trace-init.sh @@ -0,0 +1,42 @@ +# scripts/lib/trace-init.sh +# Instrumentation hook sourced automatically by every non-interactive bash +# invocation via the BASH_ENV environment variable. +# +# The bats suite runs the tool with `bash "$TRBLDOC"`, so exporting +# BASH_ENV=<this file> causes each trbldoc.sh process to enable xtrace at +# startup WITHOUT modifying trbldoc.sh or the test files. The emitted trace is +# consumed by the coverage and hotspot aggregators. +# +# Activated only when TRBLDOC_TRACE_DIR is set, and only for scripts whose +# basename appears in TRBLDOC_TRACE_TARGETS (default: trbldoc.sh). This keeps +# unrelated bash helpers (md2html stub, bats internals) out of the trace. +# +# TRBLDOC_TRACE_MODE selects the PS4 format: +# cov -> "<marker> <source>:<lineno>" (line coverage) +# time -> "<marker> <source>:<lineno> <EPOCHREALTIME>" (line timing; bash>=5) +# +# Every traced line is prefixed with the marker token @@TBL@@. bash repeats the +# FIRST PS4 character once per call-nesting level, so the aggregators match the +# marker with a tolerant pattern rather than a fixed prefix. + +if [ -n "${TRBLDOC_TRACE_DIR:-}" ]; then + __tb_self="$0" + __tb_base="${__tb_self##*/}" + case " ${TRBLDOC_TRACE_TARGETS:-trbldoc.sh} " in + *" ${__tb_base} "*) + # Dedicated append fd per process; PID keeps parallel xargs workers apart. + __tb_trace_file="${TRBLDOC_TRACE_DIR}/trace.$$.log" + # shellcheck disable=SC3023 + exec 7>>"$__tb_trace_file" + export BASH_XTRACEFD=7 + if [ "${TRBLDOC_TRACE_MODE:-cov}" = "time" ]; then + # EPOCHREALTIME is bash>=5; used only for the hotspot profiler. + PS4='@@TBL@@ ${BASH_SOURCE}:${LINENO} ${EPOCHREALTIME} ' + else + PS4='@@TBL@@ ${BASH_SOURCE}:${LINENO} ' + fi + export PS4 + set -x + ;; + esac +fi diff --git a/scripts/lib/trace-phase-extract.awk b/scripts/lib/trace-phase-extract.awk new file mode 100644 index 0000000..429f8cf --- /dev/null +++ b/scripts/lib/trace-phase-extract.awk @@ -0,0 +1,75 @@ +# scripts/lib/trace-phase-extract.awk +# Aggregate xtrace timing markers into coarse trbldoc pipeline phases. +# +# Input: trace.*.log files emitted by scripts/lib/trace-init.sh in time mode. +# Output rows: "<total_ms>\t<hits>\t<phase>" at END. +# +# Required -v vars: +# SRC_BASE +# L_DISCOVER_END +# L_EXTRACT_END +# L_ORDER_END +# L_RENDER_END +# L_NAV_END +# L_REFS_END +# L_TOC_END +# L_ASSEMBLE_END + +function basename(p) { + sub(/.*\//, "", p) + sub(/.*\\/, "", p) + return p +} + +function parse(line, s, n, f, loc) { + if (line !~ /@+TBL@@ /) return 0 + s = line + sub(/^.*TBL@@ /, "", s) + n = split(s, f, " ") + if (n < 2) return 0 + loc = f[1] + if (!match(loc, /:[0-9]+$/)) return 0 + g_line = substr(loc, RSTART + 1) + 0 + g_base = basename(substr(loc, 1, RSTART - 1)) + g_epoch = f[2] + return 1 +} + +function phase_for(line) { + if (line <= L_DISCOVER_END) return "discover_sources" + if (line <= L_EXTRACT_END) return "extract_chunks" + if (line <= L_ORDER_END) return "build_render_order" + if (line <= L_RENDER_END) return "render_chunks" + if (line <= L_NAV_END) return "build_nav" + if (line <= L_REFS_END) return "resolve_refs" + if (line <= L_TOC_END) return "apply_toc" + if (line <= L_ASSEMBLE_END) return "assemble_site" + return "other" +} + +BEGIN { + if (SRC_BASE == "") SRC_BASE = "trbldoc.sh" +} + +FNR == 1 { have_prev = 0 } + +{ + if (!parse($0)) next + if (g_base != SRC_BASE) next + if (have_prev && g_epoch != "" && prev_epoch != "") { + d = g_epoch - prev_epoch + if (d < 0) d = 0 + ph = phase_for(prev_line) + ms[ph] += d * 1000.0 + hits[ph] += 1 + } + prev_line = g_line + prev_epoch = g_epoch + have_prev = 1 +} + +END { + for (ph in ms) { + printf "%.3f\t%d\t%s\n", ms[ph], hits[ph], ph + } +} |
