#!/usr/bin/env bash
# Cold-cache file manager benchmark against the installed field, see AGENTS.md "Testing".
# RUN IT FROM A COPY OUTSIDE THIS TREE, because it launches the tree it measures.
# Reads the sudo password as the first line of stdin, so it never appears in argv.
set -uo pipefail
# This one runs from a copy outside the tree, so the guard is sourced by the tree's own path rather
# than by $0's. It owns FIXTURE_ROOT and checks every path this script writes or deletes.
# Exported, and overridable: tools/flea-bench-manifest is a child process that defaults to this
# same path, so an unexported value left it reporting a different tree than the run measured.
export FLEA_REPO="${FLEA_REPO:-$HOME/Work/claude/flea}"
. "$FLEA_REPO/tools/flea-sandbox-guard"

# The CPU column needs a leaf cgroup, which only a delegated user scope may make; see AGENTS.md "Testing".
# Sample input: /proc/self/cgroup "0::/user.slice/user-1000.slice/user@1000.service/app.slice/flea-field-bench-123.scope"
case "$(cut -d: -f3 /proc/self/cgroup)" in
  */flea-field-bench-*.scope) ;;
  *)
    if [ -n "${FLEA_BENCH_REENTERED:-}" ]; then
      printf 'FAIL: systemd-run did not place this bench in a scope of its own\n' >&2
      exit 1
    fi
    export FLEA_BENCH_REENTERED=1
    # A scope execs in this same process and never reads stdin, measured, so the password below still arrives.
    exec systemd-run --user --scope --quiet -p Delegate=yes "--unit=flea-field-bench-$$" -- bash "$0" "$@"
    ;;
esac

# First statement after the re-entry: nothing else may touch stdin before the password is taken.
read -r -s SUDO_PW || true
[ -n "${SUDO_PW:-}" ] || { printf 'FAIL: no sudo password arrived on stdin\n' >&2; exit 1; }

export PATH="$HOME/.local/bin:$PATH"
eval "$(omarchy-drive env)"
# omarchy-drive env omits the Qt platform theme, so without this the field benches a Flea that resolves no icon.
export QT_QPA_PLATFORMTHEME="$(systemctl --user show-environment | grep '^QT_QPA_PLATFORMTHEME=' | cut -d= -f2-)"
# Omarchy sets this with hl.env in envs.lua, so it reaches a Hyprland-launched client and is not in
# the systemd user environment at all; a live client is the only authoritative reader. Same fallback
# ~/tmp/flea-launch.sh already uses.
if [ -z "$QT_QPA_PLATFORMTHEME" ]; then
  barpid=$(pgrep -x quickshell | head -1)
  [ -n "$barpid" ] && export QT_QPA_PLATFORMTHEME=$(tr "\0" "\n" < "/proc/$barpid/environ" | grep "^QT_QPA_PLATFORMTHEME=" | cut -d= -f2-)
fi
[ -n "$QT_QPA_PLATFORMTHEME" ] || { printf 'FAIL: no live client published QT_QPA_PLATFORMTHEME either, so every Qt entrant would be benched with no icon theme\n' >&2; exit 1; }

FIXTURE=${FIXTURE:-$FIXTURE_ROOT/flea-bench-btrfs}
# The PAYLOAD count, which is what a fixture is for and does not move when a marker is added. The
# total was a literal here and broke the moment .flea-bench-fixture landed; a count that has to be
# re-derived by hand after an unrelated change is the same defect with a different constant.
EXPECT_FILES=${EXPECT_FILES:-100000}
DROP_THUMBS=${DROP_THUMBS:-no}
export FLEA_UI="$FLEA_REPO/ui"
export FLEA_BIN="$FLEA_REPO/target/release/flea"
# Strata is built from source rather than packaged, so its path is a variable and not a PATH lookup.
STRATA_BIN=${STRATA_BIN:-$HOME/Work/strata/target/release/strata}
# The one entrant here that persists no thumbnail. strata renders each one in a sandboxed helper
# that writes result.png into /tmp/strata-preview-<pid>-<n> and deletes the directory on drop, so it
# fills the screen and leaves the cache this harness counts empty. Same two names, and the same live
# watch, as tools/flea-bench-capability.
TRANSIENT_ID=${TRANSIENT_ID:-strata}
TRANSIENT_PREFIX=${TRANSIENT_PREFIX:-/tmp/strata-preview-}
# The TUI bracket's own XDG_CONFIG_HOME: a copy of the committed configuration in a mktemp -d of
# this run's own, so an entrant reads the bench's settings and never the operator's, this repo never
# collects the state files entrants write, and each run starts from the same clean tree. Provenance
# per entrant is in tools/flea-bench-tui/README.md.
TUI_CONF_SRC="$FLEA_REPO/tools/flea-bench-tui"
TUI_CONF=""
# Cleared before the trap, so the cleanup can never remove a directory the caller named in its environment.
FLEA_STATE=""
# Set at the first launch, so the cleanup never kills an operator's window the start-up refusal left alone.
ENTRANT_LAUNCHED=""
# The bracket's terminal, named once. foot is sixel only and every entrant here that previews at all
# prefers the Kitty protocol, so the old bracket's zero across all eight was this choice and not a
# result about the entrants.
TUI_TERM=kitty
# A class of its own, so the kill list can find this run's terminals without touching the operator's.
TUI_CLASS=flea-bench-kitty
RUNS=${RUNS:-3}
SETTLE_GAP_MS=500
# In hundredths, because /proc/loadavg is always two decimals and this shell has no floats. An idle
# box is a run condition: a cargo suite or a browser inside the window moves every settle number.
# 150 was too loose, measured: a scale run started at 1.10 and its mapped column came out 205 ms
# above the previous baseline with nothing in the diff that could account for it. A field that
# competes on 200 ms differences cannot start under a load of one.
BENCH_MAX_LOAD_HUNDREDTHS=${BENCH_MAX_LOAD_HUNDREDTHS:-50}
# How long to WAIT for the box to go idle before refusing. A bare refusal at 50 would make the
# refusal the normal case, because the one-minute average on a 12-core box takes minutes to fall
# after a run, and an operator who keeps fighting refusals raises the threshold, which is how 150
# happened. Waiting is the answer; refusing after a bounded wait is the backstop.
BENCH_IDLE_WAIT_S=${BENCH_IDLE_WAIT_S:-900}
# How long the run actually waited, recorded rather than remembered.
BENCH_IDLE_WAITED=0
# How long the preview pass waits before calling it no preview. Three entrants have no image preview
# feature at all, so this is the time each of them costs the run.
PREVIEW_TIMEOUT_MS=${PREVIEW_TIMEOUT_MS:-15000}
# Unique colours in the band, above which it holds an image rather than text. Text is a handful of
# palette entries however much of it there is; a photograph is tens of thousands. Measured on this
# box with a named fixture image as the cursor file, on the real 2000 file fixture:
#   no preview  broot 6, mc 472, ranger 536, xplr 538
#   preview     yazi 121598, spf 420753, nnn 431030, lf 447285
# So this clears the busiest text UI by 9x and the smallest real preview by 24x. PNG compression
# density was the first shape of this and it was WRONG: xplr's file table on 2000 entries reads 163
# against yazi's 307, under 2x, and a threshold derived from a three file directory did not survive
# the fixture the bench actually runs on.
PREVIEW_MIN_COLOURS=${PREVIEW_MIN_COLOURS:-5000}
LAUNCH_TIMEOUT_MS=${LAUNCH_TIMEOUT_MS:-120000}
POLL_SLEEP=0.02
OUT="${1:-$HOME/bench/results.csv}"
# The key sets an entrant produced, kept beside the CSV. A total cannot tell a format an entrant
# skipped from work it had not done yet, and these can be classified after the run; a count cannot.
KEYS_DIR="${OUT%.csv}.keys"
KEY_MAP="$KEYS_DIR/fixture.map"
THUMBS="$HOME/.cache/thumbnails"
# The cache is the operator's own and never becomes a sandbox, so the guard asserts only that this
# is a real path under it: an unset HOME would otherwise make the drop below rm -rf /.cache.
sandbox_cache_require "$THUMBS"
# Named once because the orphan check globs on it and the park path is this prefix with one pid appended.
THUMBS_PARKED_PREFIX="$HOME/.cache/thumbnails-parked-"
THUMBS_PARKED="$THUMBS_PARKED_PREFIX$$"
sandbox_cache_require "$THUMBS_PARKED"
THUMBS_PARKED_OK=no

fail() { printf 'FAIL: %s\n' "$*" >&2; exit 1; }

# The scope the re-entry above made; every entrant run gets a leaf of its own under it.
BENCH_CG="/sys/fs/cgroup$(cut -d: -f3 /proc/self/cgroup)"
[ -w "$BENCH_CG" ] || fail "the bench scope $BENCH_CG is not writable, so no run could be given a control group of its own"
RUN_CG=""
ENTRANT_SCOPE=""
HELPER_TICKS=0
HELPER_SIDE=""

# EPOCHREALTIME avoids a fork per sample, which a 20 ms poll loop cannot afford.
NOW_MS=0
now_ms() {
  local t=$EPOCHREALTIME s us
  s=${t%.*}
  us=${t#*.}
  NOW_MS=$(( s * 1000 + 10#${us:0:3} ))
}

drop_caches() {
  printf '%s\n' "$SUDO_PW" | sudo -S -k -p "" sh -c "sync; echo 3 > /proc/sys/vm/drop_caches" \
    || fail "could not drop caches"
}

# The thumbnail cache belongs to the user, so it is moved exactly once and moved back once.
park_thumbs() {
  local orphan
  # A glob that matches nothing comes back as the literal pattern, so each candidate is tested before it is believed.
  for orphan in "$THUMBS_PARKED_PREFIX"*; do
    [ -e "$orphan" ] || continue
    fail "$orphan was parked by an invocation that was killed before its restore ran, the operator's own cache may be inside it, so this run will not park on top of it"
  done
  # Below the orphan check, so a DROP_THUMBS=no invocation still reports what a killed one left behind.
  [ "$DROP_THUMBS" = yes ] || return 0
  if [ -d "$THUMBS" ]; then
    mv "$THUMBS" "$THUMBS_PARKED" || fail "could not park $THUMBS"
  fi
  THUMBS_PARKED_OK=yes
}

# Only ever removes the empty tree this script made, because park_thumbs ran first.
drop_thumbs() {
  [ "$DROP_THUMBS" = yes ] || return 0
  [ "$THUMBS_PARKED_OK" = yes ] || fail "drop_thumbs ran before park_thumbs"
  rm -rf "$THUMBS"
  mkdir -p "$THUMBS/large" "$THUMBS/fail"
  # A crashed run can leave a populated cache behind, and the run then reports cache hits as a result.
  local d
  for d in "$THUMBS"/*/; do
    [ "$(ls -A "$d" | wc -l)" = 0 ] || fail "the scratch thumbnail cache is not empty at the start of a cold run: $d"
  done
}

# Sample input: ~/.cache/thumbnails holds large/, normal/, fail/ and any other size an entrant asks for.
# Counting large/ alone reads a structural zero for an entrant that asks for a smaller size; see AGENTS.md "Testing".
count_thumbs() {
  local d name n
  THUMBS_N=0
  THUMBS_BY_DIR=""
  for d in "$THUMBS"/*/; do
    [ -d "$d" ] || continue
    name=${d%/}; name=${name##*/}
    n=$(find "$d" -type f | wc -l)
    # fail/ holds one marker per file a thumbnailer refused, so it is reported and never added to the total.
    [ "$name" = fail ] || THUMBS_N=$(( THUMBS_N + n ))
    THUMBS_BY_DIR="${THUMBS_BY_DIR:+$THUMBS_BY_DIR;}$name=$n"
  done
  [ -n "$THUMBS_BY_DIR" ] || THUMBS_BY_DIR=none
}

WATCH_PIDS=()
WATCH_RENDERS=""
WATCH_OPENS=""
WATCH_RENDERED=""
WATCH_N=0
# Why the last watch_stop could not count, empty when it could; it holds no comma because it lands in a CSV column.
WATCH_REFUSED=""
# How long to wait for inotifywait to report its watches up. /tmp is walked recursively, so this is
# a real wait, and a render that lands before the watch is armed is invisible to the count.
WATCH_ARM_TIMEOUT_MS=${WATCH_ARM_TIMEOUT_MS:-30000}

# Sample input, the line inotifywait writes to stderr once it is ready: "Watches established."
watch_armed() {
  local err=$1 deadline
  now_ms; deadline=$(( NOW_MS + WATCH_ARM_TIMEOUT_MS ))
  while ! grep -q 'Watches established' "$err" 2>/dev/null; do
    now_ms
    [ "$NOW_MS" -gt "$deadline" ] && return 1
    sleep 0.05
  done
}

# Two watches, because neither answers both questions: /tmp records a render finishing and the
# fixture records which file earned it. bwrap binds the input into the helper's namespace without
# moving the dentry, so the helper's open still reports against the fixture's own directory watch.
watch_start() {
  local id=$1
  WATCH_RENDERS="$KEYS_DIR/$id-run$RUN_INDEX.renders"
  WATCH_OPENS="$KEYS_DIR/$id-run$RUN_INDEX.opens"
  WATCH_RENDERED="$KEYS_DIR/$id-run$RUN_INDEX.rendered"
  : > "$WATCH_RENDERS"
  : > "$WATCH_OPENS"
  : > "$WATCH_RENDERS.err"
  : > "$WATCH_OPENS.err"
  inotifywait -m -r -e close_write --format '%w%f' /tmp > "$WATCH_RENDERS" 2> "$WATCH_RENDERS.err" &
  WATCH_PIDS=("$!")
  inotifywait -m -e open --format '%f' "$FIXTURE" > "$WATCH_OPENS" 2> "$WATCH_OPENS.err" &
  WATCH_PIDS+=("$!")
  watch_armed "$WATCH_RENDERS.err" || fail "the render watch never armed on /tmp"
  watch_armed "$WATCH_OPENS.err" || fail "the fixture watch never armed on $FIXTURE"
}

# Kills the watches without reading them, for a run that never reached its count.
watch_kill() {
  local pid
  for pid in ${WATCH_PIDS[*]:-}; do kill "$pid" 2>/dev/null; done
  WATCH_PIDS=()
}

# Sample input, one render line: /tmp/strata-preview-1145070-38/result.png
# Sample input, one open line: clip_1006.mp4
watch_stop() {
  local id=$1 pid pids opens_n
  for pid in ${WATCH_PIDS[*]:-}; do
    kill -0 "$pid" 2>/dev/null || fail "a $id watch died during the run, so nothing it wrote is a count"
  done
  watch_kill
  WATCH_N=$(grep -c "^$TRANSIENT_PREFIX[0-9]*-[0-9]*/result\.png\$" "$WATCH_RENDERS")
  # Two entrant processes in one window would let one run's renders be counted for another.
  pids=$(grep -o "^$TRANSIENT_PREFIX[0-9]*" "$WATCH_RENDERS" | LC_ALL=C sort -u | grep -c .)
  [ "$pids" -le 1 ] || fail "$pids $id processes wrote renders in this run, so no count from it is attributable"
  opens_n=$(grep -c . "$WATCH_OPENS")
  WATCH_REFUSED=""
  # corner: the pairing holds for a strata that renders one file at a time into a per-render dir, and v0.19.0's reused workers write none.
  if [ "$opens_n" -lt "$WATCH_N" ] || [ $(( opens_n - WATCH_N )) -gt 1 ]; then
    WATCH_REFUSED="its watches saw $WATCH_N renders for $opens_n opened fixture files so neither is a count"
    return 0
  fi
  grep . "$WATCH_OPENS" | head -n "$WATCH_N" > "$WATCH_RENDERED"
}

# The extension of every name the entrant rendered, in the fixture map's own extension order, so a
# format it never reached still gets a zero row exactly as flea-bench-keys classify gives one.
watch_classify() {
  local name ext n out="" total=0
  local -A count=()
  while IFS= read -r name; do
    case $name in *.*) ext=${name##*.} ;; *) ext=none ;; esac
    count[$ext]=$(( ${count[$ext]:-0} + 1 ))
  done < "$WATCH_RENDERED"
  for ext in $(cut -d' ' -f2 "$KEY_MAP" | LC_ALL=C sort -u); do
    n=${count[$ext]:-0}
    out="${out:+$out;}$ext=$n"
    total=$(( total + n ))
  done
  # A name the fixture map does not explain is work on something else, named and never folded in.
  printf '%s;unknown=%s\n' "$out" "$(( WATCH_N - total ))"
}

# Unique colours in the preview band. An empty answer is a failed capture and counts as no preview,
# because a preview nobody could see was not measured.
preview_colours() {
  grim -g "$PREVIEW_REGION" "$PREVIEW_SHOT" 2>/dev/null || return 1
  magick "$PREVIEW_SHOT" -format %k info: 2>/dev/null
}

# The TUI bracket's number: time from launch to the cursor file's preview being on screen. It runs
# in its own launch, so the settle pass never pays for a screen capture, and it points the entrant
# at a named fixture image, because the file that sorts first here is a video and almost nothing
# previews one. Two consecutive polls, so a torn frame mid-redraw cannot pass for a preview.
measure_preview() {
  local id=$1 cmd=$2 t0 deadline hits=0 colours first_hit=0 launch
  # A fixture with no image in it cannot answer this question at all, and that is not the same
  # answer as an entrant that had one and drew nothing. The scale fixture holds no image.
  PREVIEW_MS=-
  [ -n "$PREVIEW_TARGET" ] || return 0
  PREVIEW_MS=-1
  # A bare file path makes ranger OPEN the file and exit, so it draws nothing and the window is gone
  # before the first poll. --selectfile is ranger's own flag for landing the cursor on a file.
  case $id in
    ranger) launch=${cmd//$FIXTURE/--selectfile=$PREVIEW_TARGET} ;;
    *) launch=${cmd//$FIXTURE/$PREVIEW_TARGET} ;;
  esac
  clean_bench
  now_ms; t0=$NOW_MS
  deadline=$(( t0 + PREVIEW_TIMEOUT_MS ))
  eval "$launch" >/dev/null 2>&1 &
  wait_for_window "$TUI_CLASS" "$deadline" "$TUI_TERM" "--class $TUI_CLASS" || { clean_bench; return 0; }
  while :; do
    now_ms
    [ "$NOW_MS" -gt "$deadline" ] && break
    colours=$(preview_colours)
    if [ -n "$colours" ] && [ "$colours" -ge "$PREVIEW_MIN_COLOURS" ]; then
      hits=$(( hits + 1 ))
      [ "$hits" = 1 ] && first_hit=$NOW_MS
      [ "$hits" -ge 2 ] && { PREVIEW_MS=$(( first_hit - t0 )); break; }
    else
      hits=0
    fi
    sleep 0.2
  done
  clean_bench
}

# The keys an entrant produced, unique and sorted for join. fail/ is excluded the same way
# count_thumbs excludes it: a refusal marker is a refusal, never a thumbnail, and it is captured to
# its own file because "refused" and "never attempted" are the two things a bare zero conflates.
capture_keys() {
  local id=$1
  KEYS_FILE="$KEYS_DIR/$id-run$RUN_INDEX.keys"
  find "$THUMBS" -type f -name '*.png' -not -path "$THUMBS/fail/*" -printf '%f\n' 2>/dev/null \
    | sed 's/\.png$//' | LC_ALL=C sort -u > "$KEYS_FILE"
  find "$THUMBS/fail" -type f -name '*.png' -printf '%f\n' 2>/dev/null \
    | sed 's/\.png$//' | LC_ALL=C sort -u > "$KEYS_DIR/$id-run$RUN_INDEX.fail.keys"
}

# A plain mv moves the parked cache INTO a recreated $THUMBS, so -T closes that race and the refusal makes it loud.
restore_thumbs() {
  [ "$THUMBS_PARKED_OK" = yes ] || return 0
  rm -rf "$THUMBS"
  [ -e "$THUMBS" ] && fail "$THUMBS was recreated under the restore, refusing to bury $THUMBS_PARKED"
  if [ -d "$THUMBS_PARKED" ]; then
    mv -T "$THUMBS_PARKED" "$THUMBS" || fail "could not restore $THUMBS_PARKED to $THUMBS, so the operator's cache is still parked there"
  fi
  THUMBS_PARKED_OK=no
}

# Sample input: /proc/123/stat "... (qs) S 1 ... 14 3 0 0 20 ..." fields 14 to 17 are utime, stime, cutime and cstime.
# CPU_TICKS is the process alone; CPU_TREE_TICKS adds the reaped subtree, which is where a decode two levels down lands.
cpu_ticks() {
  local pid=$1 line
  read -r line < "/proc/$pid/stat" 2>/dev/null || { CPU_TICKS=-1; CPU_TREE_TICKS=-1; return 1; }
  # The comm field can contain spaces, so cut everything up to the closing paren first.
  local rest=${line##*") "}
  local -a f
  read -r -a f <<< "$rest"
  CPU_TICKS=$(( ${f[11]} + ${f[12]} ))
  CPU_TREE_TICKS=$(( CPU_TICKS + ${f[13]} + ${f[14]} ))
}

# The helper is the second process an entrant does its work in, declared per entrant and resolved after launch.
HELPER_COMM=""
HELPER_TOKEN=""
HELPER_PID=""
HELPER_POLLS_LEFT=0
# find_helper forks pgrep, so the search runs on one poll in 25 instead of every one; its period is 25 poll bodies and is not a wall clock guarantee.
HELPER_SEARCH_POLLS=25
find_helper() {
  [ -n "$HELPER_COMM" ] || return 0
  # A cached pid that stops being readable is a helper that exited, so forget it and look again.
  [ -n "$HELPER_PID" ] && [ ! -r "/proc/$HELPER_PID/stat" ] && HELPER_PID=""
  [ -n "$HELPER_PID" ] && return 0
  HELPER_POLLS_LEFT=$(( HELPER_POLLS_LEFT - 1 ))
  [ "$HELPER_POLLS_LEFT" -gt 0 ] && return 0
  HELPER_POLLS_LEFT=$HELPER_SEARCH_POLLS
  pids_for "$HELPER_COMM" "$HELPER_TOKEN"
  [ ${#PIDS[@]} -eq 0 ] && return 0
  HELPER_PID=${PIDS[0]}
  return 0
}

# ENTRANT_TICKS is the entrant alone for cpu_s and TREE_TICKS is the watched set, which is also the settle clock.
# WATCHED is that same set keyed by pid, published so the descendant walk reads one list and cannot drift from the CPU signal.
declare -A WATCHED=()
settle_ticks() {
  local pid=$1
  cpu_ticks "$pid" || return 1
  ENTRANT_TICKS=$CPU_TICKS
  TREE_TICKS=$CPU_TREE_TICKS
  WATCHED=( [$pid]=1 )
  find_helper
  if [ -n "$HELPER_PID" ] && cpu_ticks "$HELPER_PID"; then
    TREE_TICKS=$(( TREE_TICKS + CPU_TREE_TICKS ))
    HELPER_TICKS=$CPU_TREE_TICKS
    WATCHED[$HELPER_PID]=1
    # Flea's backend is forked inside the run's leaf and tumblerd is activated outside it, so only the second is added to the leaf's total.
    # Sample input: /proc/456/cgroup "0::/user.slice/user-1000.slice/user@1000.service/app.slice/flea-field-bench-123.scope/run-flea-1"
    if [ -z "$HELPER_SIDE" ]; then
      HELPER_SIDE=outside
      # An if and not an &&: this is the function's last command, and an && list's false for tumblerd read as thunar gone.
      if grep -qx "0::${RUN_CG#/sys/fs/cgroup}" "/proc/$HELPER_PID/cgroup" 2>/dev/null; then HELPER_SIDE=inside; fi
    fi
  fi
}

# Sample input: <leaf>/cpu.stat "usage_usec 1021252", the first of its key and value lines.
# usage_usec counts every process that ever ran in the leaf, reaped or reparented or still alive.
cgroup_usage_usec() {
  local key value
  CG_USEC=-1
  while read -r key value; do
    [ "$key" = usage_usec ] && { CG_USEC=$value; return 0; }
  done < "$1/cpu.stat"
  return 1
}

# Sample input: /proc/456/cgroup "0::/user.slice/user-1000.slice/user@1000.service/app.slice/kitty-455-1.scope"
# kitty moves each child into a scope named for kitty's pid (/usr/lib/kitty/kitty/child.py), so a TUI entrant counts there if that kitty is in the leaf.
entrant_cg() {
  local pid=$1 kind=$2 line name kitty_pid
  # The 2>/dev/null goes first: a redirect that fails is reported before any redirect after it applies.
  read -r line 2>/dev/null < "/proc/$pid/cgroup" || return 1
  ENTRANT_CG=/sys/fs/cgroup${line#0::}
  [ "$ENTRANT_CG" = "$RUN_CG" ] && return 0
  [ "$kind" = tui ] || return 1
  name=${ENTRANT_CG##*/}
  case "$name" in kitty-*-*.scope) ;; *) return 1 ;; esac
  kitty_pid=${name#kitty-}
  kitty_pid=${kitty_pid%%-*}
  grep -qx "0::${RUN_CG#/sys/fs/cgroup}" "/proc/$kitty_pid/cgroup" 2>/dev/null || return 1
  ENTRANT_SCOPE=$ENTRANT_CG
}

# cgroupfs reports every file as size 0, so emptiness is a read and never test -s.
cg_empty() {
  local pid
  read -r pid 2>/dev/null < "$1/cgroup.procs" && return 1
  return 0
}

# Anything still in control group $1 after its grace outlived the kill list, so it is named and ended by the group rather than left resident through the next entrant.
sweep_cg() {
  local cg=$1 pid n=0 polls=0
  # In tenths of a second: a process still exiting when clean_bench returns is not a leftover, and the group kill gets ten seconds.
  local grace_polls=10 kill_polls=100
  while ! cg_empty "$cg" && [ "$polls" -lt "$grace_polls" ]; do sleep 0.1; polls=$(( polls + 1 )); done
  [ -r "$cg/cgroup.procs" ] || return 0
  while read -r pid; do
    printf 'LEFTOVER: pid %s (%s) outlived the kill list in %s\n' "$pid" "$(cat "/proc/$pid/comm" 2>/dev/null)" "$cg" >&2
    n=$(( n + 1 ))
  done < "$cg/cgroup.procs"
  [ "$n" -gt 0 ] && echo 1 > "$cg/cgroup.kill"
  polls=0
  while ! cg_empty "$cg" && [ "$polls" -lt "$kill_polls" ]; do sleep 0.1; polls=$(( polls + 1 )); done
}

# The leaf, and a TUI's kitty scope beside it, are swept; systemd removes the scope once it is empty, and the leaf is ours to remove.
release_run_cg() {
  [ -n "$RUN_CG" ] || return 0
  case "$RUN_CG" in
    "$BENCH_CG"/run-*) ;;
    *) printf 'WARNING: not removing control group %s, it is not a run leaf of this bench\n' "$RUN_CG" >&2; return 1 ;;
  esac
  sweep_cg "$RUN_CG"
  [ -n "$ENTRANT_SCOPE" ] && sweep_cg "$ENTRANT_SCOPE"
  ENTRANT_SCOPE=""
  rmdir "$RUN_CG" 2>/dev/null || printf 'WARNING: control group %s could not be removed\n' "$RUN_CG" >&2
  RUN_CG=""
}

# Sample input: /proc/123/task/456/children holds "789 1011 ", space separated, with NO trailing newline.
# cutime and cstime hold only the children a watched pid has REAPED, so a decode still running is invisible to them.
descendant_ticks() {
  local pid kid t line rest
  local -a queue=("${!WATCHED[@]}") kids=() f=()
  DESC_TICKS=0
  # Breadth first over the whole tree, because flea's decode is two levels down: the backend forks prlimit which execs bwrap, and the thumbnailer runs under bwrap's namespace init.
  while [ ${#queue[@]} -gt 0 ]; do
    pid=${queue[0]}; queue=("${queue[@]:1}")
    for t in "/proc/$pid/task"/*; do
      [ -r "$t/children" ] || continue
      # A thread can exit between the test and the open, which leaves mapfile unrun, so the array is emptied first; and redirections apply left to right, so the discard goes before the path.
      kids=(); mapfile -t kids 2>/dev/null < "$t/children"
      for kid in ${kids[*]:-}; do
        # A watched pid is not a descendant: its ticks are already in TREE_TICKS, and flea's backend is a child of qs.
        [ -n "${WATCHED[$kid]:-}" ] && continue
        # Sample input: /proc/789/stat "789 (bwrap) S 456 ... 14 3 0 0 20 ...", the same format cpu_ticks consumes, fields 14 to 17 being utime, stime, cutime and cstime.
        read -r line 2>/dev/null < "/proc/$kid/stat" || continue
        # The comm field can hold spaces and parens, so everything up to the closing paren goes first, exactly as cpu_ticks does.
        rest=${line##*") "}
        # An array assignment splits the fields the way drm_kb does, and it is 15x cheaper than the here-string form because that one goes through a temporary.
        f=( $rest )
        # A zombie is counted too, because its ticks move into its parent's cutime at the reap and this total has to stay continuous, and a missing field contributes 0 rather than aborting the run under set -u.
        DESC_TICKS=$(( DESC_TICKS + ${f[11]:-0} + ${f[12]:-0} + ${f[13]:-0} + ${f[14]:-0} ))
        queue+=("$kid")
      done
    done
  done
}

# Sample input: /proc/123/smaps_rollup "Pss:              342220 kB", one key and one value a line.
# mapfile takes the whole file at one position, because a line by line read of a seq_file tears; see AGENTS.md "Testing".
pss_kb() {
  local pid=$1 l k v seen=0 pclean=0 pdirty=0
  local -a lines=()
  PSS_KB=0; PSS_ANON_KB=0; USS_KB=0
  mapfile -t lines < "/proc/$pid/smaps_rollup" 2>/dev/null
  for l in "${lines[@]}"; do
    k=${l%%:*}
    v=${l#*:}; v=${v// /}; v=${v%kB}
    case "$k" in
      Pss) PSS_KB=$v; seen=1 ;;
      Pss_Anon) PSS_ANON_KB=$v ;;
      Private_Clean) pclean=$v ;;
      Private_Dirty) pdirty=$v ;;
    esac
  done
  USS_KB=$(( pclean + pdirty ))
  [ "$seen" = 1 ] || return 1
}

# Sample input: /proc/123/fdinfo/35 holds "drm-client-id:\t14" then "drm-resident-system0:\t55124 KiB".
# Several file descriptors share one drm client, so the client id is what de-duplicates them.
drm_kb() {
  local pid=$1 file line id="" kib_per_mib=1024
  local -a lines=() word=()
  local -A seen=()
  DRM_KB=0
  for file in /proc/$pid/fdinfo/*; do
    [ -r "$file" ] || continue
    # mapfile for the same reason pss_kb uses it: this is a seq_file and a line by line read tears.
    mapfile -t lines < "$file" 2>/dev/null
    for line in "${lines[@]}"; do
      word=( $line )
      case "${word[0]:-}" in
        "drm-client-id:") id=${word[1]} ;;
        "drm-resident-system0:")
          [ -n "${seen[$id]:-}" ] && continue
          seen[$id]=1
          [ "${word[2]:-}" = MiB ] && word[1]=$(( word[1] * kib_per_mib ))
          DRM_KB=$(( DRM_KB + word[1] ))
          ;;
      esac
    done
  done
}

# Resolve every live pid whose comm matches exactly, optionally requiring a cmdline token.
pids_for() {
  local comm=$1 token=${2:-} pid c
  PIDS=()
  for pid in $(pgrep -x "$comm" 2>/dev/null); do
    [ -r "/proc/$pid/cmdline" ] || continue
    if [ -n "$token" ]; then
      c=$(tr '\0' ' ' < "/proc/$pid/cmdline")
      case "$c" in *"$token"*) ;; *) continue ;; esac
    fi
    PIDS+=("$pid")
  done
}

kill_entrant() {
  local comm=$1 token=${2:-} pid
  for _round in $(seq 1 200); do
    pids_for "$comm" "$token"
    [ ${#PIDS[@]} -eq 0 ] && return 0
    for pid in "${PIDS[@]}"; do kill "$pid" 2>/dev/null; done
    sleep 0.05
  done
  for pid in "${PIDS[@]}"; do kill -9 "$pid" 2>/dev/null; done
  sleep 0.5
  pids_for "$comm" "$token"
  [ ${#PIDS[@]} -eq 0 ] || fail "$comm would not die"
}

# Fields: comm|token, in kill order, so clean_bench and the start-up refusal read one list and cannot drift apart.
declare -a KILL_TARGETS=(
  "qs|$FLEA_UI"
  "flea|--backend"
  # The bracket's terminal, matched by the class this run gives it: a bare kitty target would name
  # every terminal the operator has open.
  "$TUI_TERM|--class $TUI_CLASS"
  # The previewers the as-intended configuration brings with it, none of which is a descendant the
  # settle walk would have reaped.
  "ueberzugpp|"
  "kitten|"
  "preview-tui|"
  "nautilus|"
  # The nautilus previewer, whose comm the kernel truncates to 15 characters.
  "org.gnome.Nauti|"
  "thunar|"
  "pcmanfm|"
  "nemo|"
  # Single instance: a second launch hands off to the running one and exits, so it must die between runs.
  "dolphin|"
  # Single instance too: a GApplication with an id hands a second launch to the running one and exits.
  "strata|"
  "yazi|"
  "mc|"
  "broot|"
  "nnn|"
  "lf|"
  "ranger|"
  "xplr|"
  "spf|"
  # Hyprland's Application Not Responding dialog, which pcmanfm leaves behind on the media fixture every run.
  "hyprland-dialog|"
  # D-Bus activated by an entrant, outlives it, and writes to the shared cache, so a survivor lands in the next run's cold arm.
  "tumblerd|"
  # nautilus starts its indexer over D-Bus and it outlives it: on 2026-09-25 it crawled $HOME for two hours, 14.5 GB, under every later entrant's cold launch.
  "localsearch-3|"
  # The indexer's extractor, whose comm the kernel truncates to 15 characters.
  "localsearch-ext|"
)

# The processes in KILL_TARGETS one entrant owns. Two ids are not named after their own process.
kill_comms_for() {
  case $1 in
    flea) printf 'qs\nflea\n' ;;
    # tumblerd outlives thunar by 300 s, and a full-field start refuses to begin beside it.
    thunar) printf 'thunar\ntumblerd\n' ;;
    # The indexer nautilus starts outlives it the same way, so ONLY=nautilus ends it too.
    nautilus) printf 'nautilus\nlocalsearch-3\nlocalsearch-ext\n' ;;
    superfile) printf 'spf\n' ;;
    *) printf '%s\n' "$1" ;;
  esac
}

# One entrant, re-measured on a box in use. The full list closes every file manager on it by name,
# and the operator's own windows are not this run's to close, so ONLY scopes both the kill and the
# refusal to the entrant named. A row that names nothing is a typo and takes the run down with it.
if [ -n "${ONLY:-}" ]; then
  declare -a scoped_targets=()
  while IFS= read -r only_comm; do
    for kill_row in "${KILL_TARGETS[@]}"; do
      [ "${kill_row%%|*}" = "$only_comm" ] && scoped_targets+=("$kill_row")
    done
  done < <(kill_comms_for "$ONLY")
  [ ${#scoped_targets[@]} -gt 0 ] \
    || fail "ONLY=$ONLY names no process in the kill list, so this run could not clean up after itself"
  KILL_TARGETS=("${scoped_targets[@]}")
fi

clean_bench() {
  local row comm token
  for row in "${KILL_TARGETS[@]}"; do
    IFS='|' read -r comm token <<< "$row"
    kill_entrant "$comm" "$token"
  done
  close_entrant_dialogs
}

# A killed entrant can leave Hyprland's not-responding dialog over every entrant after it, and the text ends with the window's class.
# Sample input: hyprland-dialog's cmdline "--title Application Not Responding --text An application flea-media-btrfs - pcmanfm is not responding. ..."
close_entrant_dialogs() {
  local pid text row id kind class rest
  for pid in $(pgrep -x hyprland-dialog 2>/dev/null); do
    text=$(tr '\0' ' ' < "/proc/$pid/cmdline" 2>/dev/null)
    for row in "${ENTRANTS[@]}"; do
      IFS='|' read -r id kind class rest <<< "$row"
      case "$text" in *" - $class is not responding"*) ;; *) continue ;; esac
      printf 'closing the not-responding dialog %s that %s left behind\n' "$pid" "$id" >&2
      kill "$pid" 2>/dev/null
      break
    done
  done
}

# This run kills everything in KILL_TARGETS and, with DROP_THUMBS=yes, removes the shared cache out from under it.
require_bench_idle() {
  local row comm token pid n=0 load load_hundredths waited=0
  # Sample input: /proc/loadavg "0.44 0.19 0.30 1/595 2585811", and the first field is the minute.
  while :; do
    load=$(cut -d' ' -f1 /proc/loadavg)
    load_hundredths=$(( 10#${load/./} ))
    [ "$load_hundredths" -le "$BENCH_MAX_LOAD_HUNDREDTHS" ] && break
    [ "$waited" -ge "$BENCH_IDLE_WAIT_S" ] \
      && fail "the one-minute load average is still $load after ${waited}s, over the $BENCH_MAX_LOAD_HUNDREDTHS hundredths this run allows, so something else is using the box"
    [ "$waited" = 0 ] && printf 'waiting for the box to go idle: load %s, this run needs %s hundredths\n' "$load" "$BENCH_MAX_LOAD_HUNDREDTHS" >&2
    sleep 10
    waited=$(( waited + 10 ))
  done
  BENCH_IDLE_WAITED=$waited
  # Any dialog on screen floats over the entrants and takes their focus, and one this run did not cause is not its to close.
  pgrep -x hyprland-dialog >/dev/null && fail "a hyprland-dialog is on screen, and it would float over and take focus from every entrant"
  [ "$waited" -gt 0 ] && printf 'the box settled after %ss at load %s\n' "$waited" "$load" >&2
  for row in "${KILL_TARGETS[@]}"; do
    IFS='|' read -r comm token <<< "$row"
    pids_for "$comm" "$token"
    for pid in "${PIDS[@]}"; do
      printf 'FAIL: pid %s is already alive and this run would kill it: %s\n' "$pid" "$(tr '\0' ' ' < "/proc/$pid/cmdline")" >&2
      n=$(( n + 1 ))
    done
  done
  [ "$n" -eq 0 ] || fail "the $n process(es) named above would be killed by this run, so it refuses to start"
}

wait_for_pid() {
  local comm=$1 token=${2:-} deadline=$3
  while :; do
    pids_for "$comm" "$token"
    [ ${#PIDS[@]} -ge 1 ] && { ENTRANT_PID=${PIDS[0]}; return 0; }
    now_ms
    [ "$NOW_MS" -gt "$deadline" ] && return 1
    sleep 0.01
  done
}

# True when a client pid is this run's own: its comm, and its command line carrying the token when one is given.
window_owned() {
  local pid=$1 comm=$2 token=$3 c
  [ "$(cat "/proc/$pid/comm" 2>/dev/null)" = "$comm" ] || return 1
  [ -n "$token" ] || return 0
  c=$(tr '\0' ' ' < "/proc/$pid/cmdline" 2>/dev/null)
  case "$c" in *"$token"*) return 0 ;; esac
  return 1
}

# A window of this class that this run does not own is named once, so a wrong owner rule never fails silently.
declare -A FOREIGN_SEEN=()
note_foreign_window() {
  local pid=$1 class=$2 owner_comm=$3 owner_token=$4
  [ -n "${FOREIGN_SEEN[$pid]:-}" ] && return 0
  FOREIGN_SEEN[$pid]=1
  printf 'FOREIGN WINDOW: pid %s (%s) holds class %s, and this run waits for %s %s\n' \
    "$pid" "$(cat "/proc/$pid/comm" 2>/dev/null)" "$class" "$owner_comm" "$owner_token" >&2
}

# Mapped when a client of this class is owned by this run's entrant, polled directly because omarchy-drive waits at 50 ms.
wait_for_window() {
  local class=$1 deadline=$2 owner_comm=$3 owner_token=$4 clients pid seen
  while :; do
    clients=$(hyprctl -j clients 2>/dev/null)
    # Stamped before the ownership check, so its forks never reach mapped_ms and every entrant pays the same.
    now_ms
    seen=$NOW_MS
    # The cheap match first, so a poll with no such window costs what it always did and jq runs only once one exists.
    case $clients in
      *"\"class\": \"$class\""*)
        # Sample input, one client of hyprctl -j clients: {"class": "com.thisisgm.flea", "pid": 12345, ...}
        for pid in $(printf '%s' "$clients" | jq -r --arg c "$class" '.[] | select(.class == $c) | .pid'); do
          if window_owned "$pid" "$owner_comm" "$owner_token"; then
            NOW_MS=$seen
            return 0
          fi
          note_foreign_window "$pid" "$class" "$owner_comm" "$owner_token"
        done
        ;;
    esac
    [ "$seen" -gt "$deadline" ] && return 1
    sleep 0.01
  done
}

# One run of one entrant: cold caches, launch, then poll until neither the watched set nor its live descendants earn a tick for a whole gap.
run_one() {
  local id=$1 kind=$2 class=$3 comm=$4 token=$5 shift_cmd=$8
  local t0 deadline mapped=-1 settled=-1 last_settle=-1 last_change=0 best_pss=0 pid
  # A GUI entrant is judged on its thumbnails and never enters the preview pass, so its column reads
  # not-applicable rather than -1, which in a TUI row means the preview never arrived.
  local preview_ms=-
  local best_anon=0 best_uss=0 last_entrant=0 last_tree=0 last_desc=0 last_helper=0

  HELPER_COMM=$6
  HELPER_TOKEN=$7
  HELPER_PID=""
  HELPER_POLLS_LEFT=0
  HELPER_TICKS=0
  HELPER_SIDE=""

  clean_bench
  # Above drop_caches, not below it: arming the watch walks /tmp and opens the fixture directory, so
  # the cache that costs has to be dropped with everything else rather than handed to this entrant.
  [ "$DROP_THUMBS" = yes ] && [ "$id" = "$TRANSIENT_ID" ] && watch_start "$id"
  drop_caches
  drop_thumbs

  now_ms; t0=$NOW_MS
  deadline=$(( t0 + LAUNCH_TIMEOUT_MS ))

  # A leaf of this run's own, entered by the launching shell before it forks the entrant, so the entrant and everything it ever starts are in it.
  local leaf="$BENCH_CG/run-$id-$RUN_INDEX"
  mkdir "$leaf" || fail "could not make the control group $leaf"
  RUN_CG=$leaf
  ENTRANT_LAUNCHED=yes
  { printf '%s\n' "$BASHPID" > "$RUN_CG/cgroup.procs" \
      || { printf 'FAIL: could not join %s, so %s was not launched\n' "$RUN_CG" "$id" >&2; exit 1; }
    eval "$shift_cmd" >/dev/null 2>&1; } &

  # A TUI row's window belongs to its terminal, which the class this run gives it already names.
  local owner_comm=$comm owner_token=$token
  if [ "$kind" = tui ]; then
    owner_comm=$TUI_TERM
    owner_token="--class $TUI_CLASS"
  fi
  if [ "$class" != "-" ]; then
    wait_for_window "$class" "$deadline" "$owner_comm" "$owner_token" && mapped=$(( NOW_MS - t0 ))
  fi

  wait_for_pid "$comm" "$token" "$deadline" || { watch_kill; printf '%s,%s,ERROR,no-process\n' "$id" "$RUN_INDEX"; clean_bench; release_run_cg; return 1; }
  pid=$ENTRANT_PID
  entrant_cg "$pid" "$kind" \
    || fail "$id pid $pid is not in its control group $RUN_CG, so its CPU column would count nothing"

  while :; do
    if ! settle_ticks "$pid"; then break; fi
    now_ms
    last_entrant=$ENTRANT_TICKS
    last_tree=$TREE_TICKS
    last_helper=$HELPER_TICKS
    if [ "$TREE_TICKS" -ne "$last_settle" ]; then
      last_settle=$TREE_TICKS
      last_change=$NOW_MS
    else
      # The walk costs a read per thread and per descendant, so it runs only on a poll the watched set earned no tick on.
      descendant_ticks
      # last_desc starts at 0 and not at -1, or an entrant with no descendants is stamped once for free and every settle in the field carries a spare poll.
      if [ "$DESC_TICKS" -ne "$last_desc" ]; then
        last_desc=$DESC_TICKS
        last_change=$NOW_MS
      fi
    fi
    if pss_kb "$pid" && [ "$PSS_KB" -gt "$best_pss" ]; then
      best_pss=$PSS_KB; best_anon=$PSS_ANON_KB; best_uss=$USS_KB
    fi
    if [ "$last_change" -gt 0 ] && [ $(( NOW_MS - last_change )) -ge "$SETTLE_GAP_MS" ]; then
      settled=$(( last_change - t0 ))
      break
    fi
    [ "$NOW_MS" -gt "$deadline" ] && { settled=-1; break; }
    sleep "$POLL_SLEEP"
  done

  # n/a and not 0: a run that never counted must stay tellable from one that counted and found none.
  local cpu_s cpu_tree_s cpu_reaped_s helper_add=0 child_pss=0 thumbs_n=n/a thumbs_dirs=- thumbs_fmt=- ranked=yes
  # drm-resident-system0 is RAM that PSS cannot see, read once after settle because fdinfo is too costly to poll.
  drm_kb "$pid"
  # cpu.stat counts microseconds and /proc/<pid>/stat counts USER_HZ ticks, 100 a second on this kernel.
  local usec_per_second=1000000 ticks_per_second=100
  cpu_s=$(awk -v t="$last_entrant" -v hz="$ticks_per_second" 'BEGIN { printf "%.2f", t / hz }')
  # The old reading, kept so a table from before the leaf can still be compared: a sandboxed decode never reaches it, see AGENTS.md "Testing".
  cpu_reaped_s=$(awk -v t="$last_tree" -v hz="$ticks_per_second" 'BEGIN { printf "%.2f", t / hz }')
  cgroup_usage_usec "$RUN_CG" || fail "no usage_usec in $RUN_CG/cpu.stat, so this row has no CPU column"
  local run_usec=$CG_USEC
  # Read at settle and not at launch: kitty's move can land after the launch check, and its scope leaves with the child.
  if [ "$kind" = tui ]; then
    entrant_cg "$pid" "$kind" || fail "$id pid $pid is in no control group of this run at settle, so its CPU column would lose its own share"
    if [ "$ENTRANT_CG" != "$RUN_CG" ]; then
      cgroup_usage_usec "$ENTRANT_CG" || fail "no usage_usec in $ENTRANT_CG/cpu.stat, so this row has no CPU column"
      run_usec=$(( run_usec + CG_USEC ))
    fi
  fi
  [ "$HELPER_SIDE" = outside ] && helper_add=$last_helper
  cpu_tree_s=$(awk -v u="$run_usec" -v us="$usec_per_second" -v t="$helper_add" -v hz="$ticks_per_second" 'BEGIN { printf "%.2f", u / us + t / hz }')
  # Evidence that the media run actually exercised thumbnail generation, not just a warm cache.
  if [ "$DROP_THUMBS" = yes ]; then
    count_thumbs
    thumbs_n=$THUMBS_N
    thumbs_dirs=$THUMBS_BY_DIR
    if [ "$id" = "$TRANSIENT_ID" ]; then
      # The cache reading above stays in thumbs_by_dir, where it is a true statement about the cache
      # and a false one about this entrant; the count and the formats come off the live watch.
      watch_stop "$id"
      # A count the watches cannot vouch for leaves this row unranked rather than ending every row after it.
      if [ -n "$WATCH_REFUSED" ]; then
        thumbs_n=unmeasurable
        thumbs_fmt=unmeasurable
        ranked="unranked: $WATCH_REFUSED"
        printf 'UNRANKED: %s %s, so its work was not measured and its time is not a result\n' "$id" "$WATCH_REFUSED" >&2
      else
        thumbs_n=$WATCH_N
        thumbs_fmt=$(watch_classify)
      fi
    else
      capture_keys "$id"
      thumbs_fmt=$("$FLEA_REPO/tools/flea-bench-keys" classify "$KEY_MAP" "$KEYS_FILE")
    fi
    if [ "$kind" = tui ]; then
      # A TUI previews the cursor file, it never fills a grid, and the terminal decides whether it
      # can draw an image at all. A 0 here reads as a slow thumbnailer and it is neither slow nor a
      # thumbnailer, so the column says so and names the terminal that would have had to draw it.
      thumbs_n=n/a
      thumbs_fmt=n/a
      ranked="n/a: tui in $TUI_TERM"
    elif [ "$thumbs_n" = 0 ] && [ "$id" != "$TRANSIENT_ID" ]; then
      # A failed measurement, not a fast one, and this instrument cannot tell which failure it is.
      # strata renders every thumbnail into a scratch dir it deletes on drop, so it fills the screen
      # and leaves the cache empty: a 0 here is the cache's silence, never the entrant's ability.
      thumbs_n=unmeasurable
      thumbs_fmt=unmeasurable
      ranked="unranked: nothing reached the thumbnail cache so its work was not measured"
      printf 'UNRANKED: %s left nothing in the thumbnail cache, so its work was not measured and its time is not a result\n' "$id" >&2
    fi
  fi

  # A second launch, after every number above is taken, so the settle pass is never measured under
  # a screen capture. A GUI entrant is judged on its thumbnails and never comes here.
  if [ "$kind" = tui ]; then
    measure_preview "$id" "$shift_cmd"
    preview_ms=$PREVIEW_MS
  fi

  if [ -n "$HELPER_COMM" ]; then
    pids_for "$HELPER_COMM" "$HELPER_TOKEN"
    if [ ${#PIDS[@]} -ge 1 ] && pss_kb "${PIDS[0]}"; then child_pss=$PSS_KB; fi
  fi

  printf '%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s\n' "$id" "$RUN_INDEX" "$mapped" "$settled" "$best_pss" "$cpu_s" "$child_pss" "$thumbs_n" "$best_anon" "$best_uss" "$DRM_KB" "$thumbs_dirs" "$cpu_tree_s" "$thumbs_fmt" "$ranked" "$preview_ms" "$cpu_reaped_s"
  clean_bench
  release_run_cg
}

# Two checks, because one count cannot do both jobs. The payload is what the fixture is for, and a
# bare ls would hide a stray dotfile from a check whose whole job is to refuse an altered fixture,
# so the dotfiles are judged by NAME rather than folded into a total nobody can re-derive.
fixture_payload=$(ls -U "$FIXTURE" | wc -l)
[ "$fixture_payload" = "$EXPECT_FILES" ] \
  || fail "fixture holds $fixture_payload visible entries, not $EXPECT_FILES"
# A fixture marker or an encoder seed is expected; anything else is a stray and is named.
fixture_marked=no
while IFS= read -r dotfile; do
  case $dotfile in
    # The marker names, not the .flea-* prefix: .flea-work-<tag>-<pid>-<seq> is what archivework.rs
    # leaves behind on a SIGKILL, and a leaked one would otherwise satisfy this assertion AND escape
    # the stray arm below, so the thing known to accumulate is the thing that would fool the check.
    .flea-bench-fixture|.flea-media-fixture|.flea-matched-fixture|.flea-test-sandbox)
      fixture_marked=yes ;;
    .seed.*) ;;
    *) fail "fixture carries an unexpected dotfile: $dotfile" ;;
  esac
done < <(ls -A "$FIXTURE" | grep '^\.')
# A missing marker means this is not the fixture the harness built, which a name check alone cannot
# see: it only judges the dotfiles that are there.
[ "$fixture_marked" = yes ] || fail "fixture carries none of the marker names above, so it is not one this harness built"
[ "$(findmnt -no FSTYPE -T "$FIXTURE")" = "btrfs" ] || fail "fixture is not on btrfs"
require_bench_idle

# Everything this run made, and nothing else: the parked cache goes back, and the only directory
# removed is the one mktemp -d handed out above, checked absolute and two components deep first.
bench_cleanup() {
  # A run that fails mid-entrant ends that entrant here, or it stays resident through every run after it.
  watch_kill
  # A subshell, because clean_bench can fail and an exit inside this trap would skip the restore below.
  [ -n "$ENTRANT_LAUNCHED" ] && ( clean_bench )
  release_run_cg
  restore_thumbs
  case ${TUI_CONF:-} in
    /*/*) rm -rf "$TUI_CONF" ;;
    *) printf 'WARNING: not removing TUI config path %s, it is not an absolute path two deep\n' "${TUI_CONF:-}" >&2 ;;
  esac
  case ${FLEA_STATE:-} in
    /tmp/flea-bench-state.*) rm -rf "$FLEA_STATE" ;;
    "") ;;
    *) printf 'WARNING: not removing Flea state path %s, it is not the mktemp directory this run made\n' "$FLEA_STATE" >&2 ;;
  esac
}
trap bench_cleanup EXIT
# A signal exits and lets the EXIT trap clean up once: a TERM trap that returned let a stopped bench keep benching.
trap 'exit 130' INT
trap 'exit 143' TERM

# After the trap, never before it: a temp directory made above the handler that removes it leaks on
# every early exit between the two.
TUI_CONF=$(mktemp -d) || fail "no temp directory for the TUI configuration"
cp -r "$TUI_CONF_SRC/." "$TUI_CONF/" || fail "could not copy $TUI_CONF_SRC"
# lf reads previewer as a path and does not expand a variable in it, measured: the config drew no
# image until the path was absolute. The committed file carries the name so it stays readable, and
# the run materialises it. The manifest records the materialised copy, which is what actually ran.
find "$TUI_CONF" -type f -exec sed -i "s|\$XDG_CONFIG_HOME|$TUI_CONF|g" {} +

# Flea runs on its shipped defaults, because an operator's saved view, sort or rows decide what the first screen holds.
FLEA_STATE=$(mktemp -d /tmp/flea-bench-state.XXXXXX) || fail "no temp directory for Flea's state"
mkdir -p "$FLEA_STATE/flea" "$FLEA_STATE/no-config" || fail "could not make $FLEA_STATE/flea"
# An empty config directory for this one write, so an old view.json cannot migrate into the defaults.
XDG_STATE_HOME="$FLEA_STATE" XDG_CONFIG_HOME="$FLEA_STATE/no-config" "$FLEA_BIN" --ui-state '{"sort":{"key":"name","reverse":false}}' >/dev/null \
  || fail "flea --ui-state refused to write the shipped defaults into $FLEA_STATE"
printf 'flea-state: %s, the shipped defaults, whose text size follows the system font as every entrant does\n' "$FLEA_STATE"

# The band the TUI bracket watches for its preview, and the file it captures into.
# corner: one monitor and one tiled window, which require_bench_idle already guarantees. The band is
# the right 43 percent of the screen between 12 and 55 percent of its height, measured off
# screenshots of yazi, ranger, lf, nnn and superfile as inside every previewing pane and clear of
# every status line, so a clock or a cursor cannot pass for a preview.
# Sample input, line 2 of hyprctl monitors: "	2560x1440@119.99800 at 0x0"
mon=$(hyprctl monitors | sed -n '2p' | cut -d@ -f1 | tr -d '[:space:]')
mon_w=${mon%x*}
mon_h=${mon#*x}
[ -n "$mon_w" ] && [ -n "$mon_h" ] || fail "could not read the monitor size out of hyprctl monitors"
preview_w=$(( mon_w * 43 / 100 ))
preview_h=$(( mon_h * 43 / 100 ))
PREVIEW_REGION="$(( mon_w * 55 / 100 )),$(( mon_h * 12 / 100 )) ${preview_w}x${preview_h}"
PREVIEW_SHOT="$TUI_CONF/.preview-probe.png"
# The named image the TUI bracket is timed against. The fixture's first name is a video and almost
# nothing previews one, so the target is named here and recorded in the manifest.
PREVIEW_TARGET_NAME=$(ls -U "$FIXTURE" | grep -m1 '\.jpg$')
PREVIEW_TARGET=""
[ -n "$PREVIEW_TARGET_NAME" ] && PREVIEW_TARGET="$FIXTURE/$PREVIEW_TARGET_NAME"
park_thumbs

mkdir -p "$(dirname "$OUT")"
: > "$OUT"
echo "id,run,mapped_ms,settled_ms,pss_kb,cpu_s,child_pss_kb,thumbs_n,pss_anon_kb,uss_kb,drm_kb,thumbs_by_dir,cpu_tree_s,thumbs_by_format,ranked,preview_ms,cpu_reaped_s" >> "$OUT"

# Fields: id|kind|class|comm|token|helper_comm|helper_token|command, and the command holds any later pipe.
declare -a ENTRANTS=(
  # Launch the real binary, not qs: every rival is launched by its own launcher and flea's sets prctl before exec.
  # An entrant whose work runs in a second process names it as its helper: Flea's backend, and thunar's D-Bus activated tumblerd.
  # FLEA_UI is passed as well as FLEA_BIN, and it is not decoration. paths.rs resolves the shell as
  # FLEA_UI, then /usr/share/flea/ui, then a walk up from the binary. With nothing installed the
  # walk-up lands on this checkout, so the bench has been measuring the right UI by fallback rather
  # than by instruction; the moment a package installs /usr/share/flea/ui that branch is checked
  # second and wins, and the run would launch this tree's binary against the installed shell.
  "flea|gui|com.thisisgm.flea|qs|$FLEA_UI|flea|--backend|FLEA_BIN=$FLEA_BIN FLEA_UI=$FLEA_UI XDG_STATE_HOME=$FLEA_STATE setsid nohup $FLEA_BIN --gui $FIXTURE"
  "nautilus|gui|org.gnome.Nautilus|nautilus||||setsid nohup nautilus --new-window $FIXTURE"
  "thunar|gui|thunar|thunar||tumblerd||setsid nohup thunar $FIXTURE"
  "pcmanfm|gui|pcmanfm|pcmanfm||||setsid nohup pcmanfm $FIXTURE"
  "nemo|gui|nemo|nemo||||setsid nohup nemo $FIXTURE"
  "dolphin|gui|org.kde.dolphin|dolphin||||setsid nohup dolphin $FIXTURE"
  # Built from source at ~/Work/strata, not packaged: the row is skipped when that binary is absent.
  "strata|gui|io.github.lgse.Strata|strata||||setsid nohup $STRATA_BIN $FIXTURE"
  # The bracket runs in kitty, not foot: foot is sixel only and every entrant here that previews at
  # all prefers the Kitty protocol, so the old bracket's 0 across all eight was the harness's
  # terminal choice rather than a result about the entrants. XDG_CONFIG_HOME is the bench's own.
  "yazi|tui|$TUI_CLASS|yazi||||XDG_CONFIG_HOME=$TUI_CONF setsid nohup $TUI_TERM --class $TUI_CLASS yazi $FIXTURE"
  "mc|tui|$TUI_CLASS|mc||||XDG_CONFIG_HOME=$TUI_CONF setsid nohup $TUI_TERM --class $TUI_CLASS mc $FIXTURE"
  "broot|tui|$TUI_CLASS|broot||||XDG_CONFIG_HOME=$TUI_CONF setsid nohup $TUI_TERM --class $TUI_CLASS broot $FIXTURE"
  # -a auto-creates the NNN_FIFO its preview plugin needs and -P p loads that plugin at start, both
  # of which are how /usr/share/nnn/plugins/preview-tui's own header says to wire it.
  "nnn|tui|$TUI_CLASS|nnn||||XDG_CONFIG_HOME=$TUI_CONF NNN_PLUG=p:preview-tui setsid nohup $TUI_TERM --class $TUI_CLASS nnn -a -P p $FIXTURE"
  "lf|tui|$TUI_CLASS|lf||||XDG_CONFIG_HOME=$TUI_CONF setsid nohup $TUI_TERM --class $TUI_CLASS lf $FIXTURE"
  "ranger|tui|$TUI_CLASS|ranger||||XDG_CONFIG_HOME=$TUI_CONF setsid nohup $TUI_TERM --class $TUI_CLASS ranger $FIXTURE"
  "xplr|tui|$TUI_CLASS|xplr||||XDG_CONFIG_HOME=$TUI_CONF setsid nohup $TUI_TERM --class $TUI_CLASS xplr $FIXTURE"
  "superfile|tui|$TUI_CLASS|spf||||XDG_CONFIG_HOME=$TUI_CONF setsid nohup $TUI_TERM --class $TUI_CLASS spf $FIXTURE"
)

# nnn loads plugins from its config directory, and the plugin this uses is the one nnn itself ships.
# Linked at run time rather than committed, so this repo carries no copy of it and no dangling link.
if [ -x /usr/share/nnn/plugins/preview-tui ]; then
  mkdir -p "$TUI_CONF/nnn/plugins"
  cp /usr/share/nnn/plugins/preview-tui "$TUI_CONF/nnn/plugins/preview-tui"
else
  printf 'WARNING: nnn ships no preview-tui on this box, so its row previews nothing\n' >&2
fi

# A release binary older than the sources it was built from benches code nobody wrote, and the
# manifest would record the current sha beside it. -print -quit stops at the first one.
# ui/ is deliberately NOT checked: QML is read at run time and is never compiled into the binary, so
# a newer .qml is the thing being benched rather than a stale build, and checking it refused a run
# that was measuring exactly the change it had just been handed.
[ -x "$FLEA_BIN" ] || fail "$FLEA_BIN is missing, so run cargo build --release before benching"
stale_src=$(find "$FLEA_REPO/src" "$FLEA_REPO/Cargo.toml" "$FLEA_REPO/Cargo.lock" -newer "$FLEA_BIN" -print -quit)
[ -z "$stale_src" ] || fail "$stale_src is newer than $FLEA_BIN, so this run would bench a stale build"

# What was benched, written beside the numbers before the first one is taken: a timing figure with
# no version behind it is not citable, and a run that produced numbers without this is wasted.
MANIFEST="${OUT%.csv}.manifest.md"
# Derived from the entrant row rather than retyped, so it cannot drift from what runs.
# -f8- and not -f8: the table's own comment says the command field holds any later pipe, and the
# read at the run site gives its eighth name the whole remainder. cut -f8 stops at the next pipe, so
# the RUN and the RECORD would diverge on the one case the format declares legal, and the line above
# claims they cannot. No flea command has a pipe today, so this is latent rather than wrong.
flea_cmd=$(printf '%s\n' "${ENTRANTS[@]}" | grep '^flea|' | cut -d'|' -f8-)
printf '%s\n' "${ENTRANTS[@]}" | cut -d'|' -f1,2,4 \
  | FLEA_LAUNCH_LINE="$flea_cmd" TUI_TERMINAL="$TUI_TERM" EXPECT_FILES="$EXPECT_FILES" \
    TUI_CONFIG_DIR="$TUI_CONF" PREVIEW_TARGET="$PREVIEW_TARGET" \
    BENCH_IDLE_WAITED="$BENCH_IDLE_WAITED" \
    "$FLEA_REPO/tools/flea-bench-manifest" "$MANIFEST" "$FIXTURE" \
  || fail "the manifest did not write, so this run would produce numbers nobody can cite"
echo "manifest: $MANIFEST"

# Only a run that drops the cache produces a comparable key set, and on the 100k fixture this map
# would be 100k md5 calls for a question that run does not ask.
mkdir -p "$KEYS_DIR" || fail "could not make the key directory at $KEYS_DIR"
if [ "$DROP_THUMBS" = yes ]; then
  "$FLEA_REPO/tools/flea-bench-keys" map "$FIXTURE" "$KEY_MAP" \
    || fail "the fixture key map did not build, so no thumbnail count in this run could be classified"
  echo "key map: $KEY_MAP, $(wc -l < "$KEY_MAP") keys"
fi

for RUN_INDEX in $(seq 1 "$RUNS"); do
  for row in "${ENTRANTS[@]}"; do
    IFS='|' read -r id kind class comm token helper_comm helper_token cmd <<< "$row"
    [ -n "${ONLY:-}" ] && [ "$ONLY" != "$id" ] && continue
    # An absent entrant is named and skipped, never silently dropped from the table, see hard rule 7.
    if [ "$id" = strata ]; then
      [ -x "$STRATA_BIN" ] || { printf 'SKIP: strata is not built at %s\n' "$STRATA_BIN" >&2; continue; }
    else
      command -v "$comm" >/dev/null 2>&1 || { printf 'SKIP: %s is not installed\n' "$id" >&2; continue; }
    fi
    run_one "$id" "$kind" "$class" "$comm" "$token" "$helper_comm" "$helper_token" "$cmd" >> "$OUT"
    tail -1 "$OUT"
  done
done

clean_bench
restore_thumbs
# What the run turned out to be: the capability each entrant demonstrated, and every row this
# harness refuses to rank, written where the versions are rather than left for a reader to infer.
"$FLEA_REPO/tools/flea-bench-manifest" "$MANIFEST" close "$OUT" \
  || printf 'WARNING: the manifest did not close, so the capability and refusal blocks are missing\n' >&2
echo "done, results in $OUT"
