Samples /proc/PID/stat directly (no pidstat dependency) for the qbz binary and any WebKit child processes. Outputs mean/p50/p95/max per process plus appends a CSV row to /tmp/qbz-cpu-measurements.csv for cross-run comparison. Usage: ./scripts/measure-cpu-window.sh <label> [duration_seconds] Built during the 2026-05-11 perf debug session to replace seat-of-the-pants "does it feel faster?" with comparable numbers across changes. cc
216 lines
7.7 KiB
Bash
Executable File
216 lines
7.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# measure-cpu-window.sh — sample CPU% of QBZ + WebKit children over a window
|
|
#
|
|
# Usage:
|
|
# ./scripts/measure-cpu-window.sh [label] [duration_seconds]
|
|
#
|
|
# Examples:
|
|
# ./scripts/measure-cpu-window.sh blur-on # 30s sample, labelled "blur-on"
|
|
# ./scripts/measure-cpu-window.sh blur-off 60 # 60s sample, labelled "blur-off"
|
|
# ./scripts/measure-cpu-window.sh # auto-label with timestamp
|
|
#
|
|
# Reads /proc/PID/stat directly (no pidstat dependency). Samples per second.
|
|
# CPU% is normalized per-core (100% = one fully busy core), matching the
|
|
# convention used in `top` and in the bug report at issue #414/#415.
|
|
#
|
|
# Output:
|
|
# - Live one-line update per second (qbz + webkit + total)
|
|
# - At end: mean, p50, p95, max for the whole window
|
|
# - CSV row appended to /tmp/qbz-cpu-measurements.csv for cross-run comparison
|
|
#
|
|
# To compare runs:
|
|
# 1. Pick a stable window state (e.g. 4K full-screen, home view, track playing)
|
|
# 2. Run script, do NOT touch the window during the sample
|
|
# 3. Change the variable (e.g. resize, toggle backdrop-filter), repeat
|
|
# 4. cat /tmp/qbz-cpu-measurements.csv
|
|
|
|
set -euo pipefail
|
|
|
|
LABEL="${1:-run-$(date +%H%M%S)}"
|
|
DURATION="${2:-30}"
|
|
LOG_CSV="/tmp/qbz-cpu-measurements.csv"
|
|
CLK_TCK="$(getconf CLK_TCK)"
|
|
|
|
if [[ ! "$DURATION" =~ ^[0-9]+$ ]] || (( DURATION < 5 )); then
|
|
echo "error: duration must be a positive integer >= 5" >&2
|
|
exit 2
|
|
fi
|
|
|
|
# Find QBZ main process — match both legacy binary name (qbz-nix, dev builds)
|
|
# and the production binary (qbz). Prefer the largest-RSS match to dodge
|
|
# wrapper shells.
|
|
find_qbz_pid() {
|
|
local candidates pid best_rss=0 best_pid=""
|
|
candidates="$(pgrep -f 'target/(debug|release)/qbz(-nix)?$|/usr/(local/)?bin/qbz$|^qbz$' 2>/dev/null || true)"
|
|
if [[ -z "$candidates" ]]; then
|
|
candidates="$(pgrep -x qbz 2>/dev/null || true)"
|
|
fi
|
|
if [[ -z "$candidates" ]]; then
|
|
candidates="$(pgrep -f 'qbz-nix' 2>/dev/null || true)"
|
|
fi
|
|
for pid in $candidates; do
|
|
[[ -r "/proc/$pid/status" ]] || continue
|
|
local rss
|
|
rss="$(awk '/^VmRSS:/ {print $2}' "/proc/$pid/status" 2>/dev/null || echo 0)"
|
|
if (( rss > best_rss )); then
|
|
best_rss=$rss
|
|
best_pid=$pid
|
|
fi
|
|
done
|
|
[[ -n "$best_pid" ]] && echo "$best_pid"
|
|
}
|
|
|
|
QBZ_PID="$(find_qbz_pid || true)"
|
|
if [[ -z "${QBZ_PID:-}" ]]; then
|
|
echo "error: no qbz process found. Is the app running?" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# All WebKit child processes. WebKitWebProcess is the one that does CSS
|
|
# layout/paint, so its CPU% is the most relevant for backdrop-filter cost.
|
|
# WebKitNetworkProcess does I/O — usually low CPU.
|
|
find_webkit_pids() {
|
|
pgrep -f 'WebKit(Web|Network|GPU)Process' 2>/dev/null || true
|
|
}
|
|
|
|
WEBKIT_PIDS=()
|
|
mapfile -t WEBKIT_PIDS < <(find_webkit_pids)
|
|
|
|
read_jiffies() {
|
|
# Field 14 (utime) + field 15 (stime) of /proc/PID/stat. Pre-paren tokens
|
|
# in field 2 (comm) can contain spaces, so split off comm carefully.
|
|
local pid="$1" stat content rest
|
|
[[ -r "/proc/$pid/stat" ]] || { echo 0; return; }
|
|
content="$(cat "/proc/$pid/stat" 2>/dev/null || echo "")"
|
|
[[ -z "$content" ]] && { echo 0; return; }
|
|
rest="${content#* (}"
|
|
rest="${rest#*) }"
|
|
read -r _state _ppid _pgrp _sid _tty _tpgid _flags _minflt _cminflt _majflt _cmajflt utime stime _rest <<< "$rest"
|
|
echo $(( utime + stime ))
|
|
}
|
|
|
|
read_window_size() {
|
|
# Best-effort: read the persisted window size from the qbz log. Not used in
|
|
# math, just printed as context in the CSV.
|
|
local logf="$HOME/.local/share/qbz/qbz.log"
|
|
if [[ -r "$logf" ]]; then
|
|
grep -oE 'size=[0-9]+x[0-9]+' "$logf" 2>/dev/null | tail -1 | cut -d= -f2 || echo "unknown"
|
|
else
|
|
echo "unknown"
|
|
fi
|
|
}
|
|
|
|
printf '%s\n' "=== qbz CPU sampler ==="
|
|
printf 'label: %s\n' "$LABEL"
|
|
printf 'qbz pid: %s\n' "$QBZ_PID"
|
|
if (( ${#WEBKIT_PIDS[@]} > 0 )); then
|
|
printf 'webkit: %s\n' "${WEBKIT_PIDS[*]}"
|
|
else
|
|
printf 'webkit: (none found — is the GUI window open?)\n'
|
|
fi
|
|
printf 'duration: %ss\n' "$DURATION"
|
|
printf 'window: %s (persisted)\n' "$(read_window_size)"
|
|
printf 'log: %s\n' "$LOG_CSV"
|
|
printf '\nSampling — leave the window UNTOUCHED for the full window.\n\n'
|
|
|
|
# Take initial snapshot
|
|
prev_qbz="$(read_jiffies "$QBZ_PID")"
|
|
declare -A prev_wk
|
|
for pid in "${WEBKIT_PIDS[@]:-}"; do
|
|
[[ -z "$pid" ]] && continue
|
|
prev_wk[$pid]="$(read_jiffies "$pid")"
|
|
done
|
|
prev_ts="$(date +%s.%N)"
|
|
|
|
samples_qbz=()
|
|
samples_wk_total=()
|
|
samples_total=()
|
|
|
|
printf '%4s %8s %8s %8s\n' "t/s" "qbz%" "webkit%" "total%"
|
|
printf '%4s %8s %8s %8s\n' "---" "----" "-------" "------"
|
|
|
|
for (( t=1; t<=DURATION; t++ )); do
|
|
sleep 1
|
|
now_ts="$(date +%s.%N)"
|
|
dt="$(awk -v a="$now_ts" -v b="$prev_ts" 'BEGIN{printf "%.4f", a-b}')"
|
|
prev_ts="$now_ts"
|
|
|
|
cur_qbz="$(read_jiffies "$QBZ_PID")"
|
|
d_qbz=$(( cur_qbz - prev_qbz ))
|
|
prev_qbz="$cur_qbz"
|
|
pct_qbz="$(awk -v j="$d_qbz" -v dt="$dt" -v hz="$CLK_TCK" 'BEGIN{ if (dt<=0) print 0; else printf "%.1f", (j/hz)/dt*100 }')"
|
|
|
|
wk_total=0
|
|
for pid in "${WEBKIT_PIDS[@]:-}"; do
|
|
[[ -z "$pid" ]] && continue
|
|
if [[ ! -r "/proc/$pid/stat" ]]; then
|
|
continue
|
|
fi
|
|
cur="$(read_jiffies "$pid")"
|
|
d=$(( cur - ${prev_wk[$pid]:-$cur} ))
|
|
prev_wk[$pid]="$cur"
|
|
(( d < 0 )) && d=0
|
|
wk_total=$(( wk_total + d ))
|
|
done
|
|
pct_wk="$(awk -v j="$wk_total" -v dt="$dt" -v hz="$CLK_TCK" 'BEGIN{ if (dt<=0) print 0; else printf "%.1f", (j/hz)/dt*100 }')"
|
|
|
|
pct_total="$(awk -v a="$pct_qbz" -v b="$pct_wk" 'BEGIN{ printf "%.1f", a+b }')"
|
|
|
|
samples_qbz+=("$pct_qbz")
|
|
samples_wk_total+=("$pct_wk")
|
|
samples_total+=("$pct_total")
|
|
|
|
printf '%4d %8s %8s %8s\n' "$t" "$pct_qbz" "$pct_wk" "$pct_total"
|
|
done
|
|
|
|
# Aggregate
|
|
summarize() {
|
|
local name="$1"
|
|
shift
|
|
local n="$#"
|
|
local sorted
|
|
sorted="$(printf '%s\n' "$@" | sort -n)"
|
|
local mean p50 p95 max
|
|
mean="$(printf '%s\n' "$@" | awk '{s+=$1} END{ if (NR>0) printf "%.1f", s/NR; else print 0 }')"
|
|
p50="$(printf '%s\n' "$sorted" | awk -v n="$n" 'NR==int(n*0.50)+1 || (NR==1 && n==1) {print; exit}')"
|
|
p95="$(printf '%s\n' "$sorted" | awk -v n="$n" 'NR==int(n*0.95)+1 || (NR==n) {print; exit}')"
|
|
max="$(printf '%s\n' "$sorted" | tail -1)"
|
|
printf ' %-10s mean=%5s%% p50=%5s%% p95=%5s%% max=%5s%%\n' "$name" "$mean" "$p50" "$p95" "$max"
|
|
echo "$mean,$p50,$p95,$max"
|
|
}
|
|
|
|
echo ""
|
|
echo "=== Summary ==="
|
|
summary_qbz="$(summarize "qbz" "${samples_qbz[@]}" | tail -1)"
|
|
summarize "qbz" "${samples_qbz[@]}" >/dev/null
|
|
|
|
read -r mean_qbz p50_qbz p95_qbz max_qbz <<< "$(echo "$summary_qbz" | tr ',' ' ')"
|
|
|
|
mean_wk=0 p50_wk=0 p95_wk=0 max_wk=0
|
|
if (( ${#samples_wk_total[@]} > 0 )); then
|
|
summary_wk="$(summarize "webkit" "${samples_wk_total[@]}" | tail -1)"
|
|
read -r mean_wk p50_wk p95_wk max_wk <<< "$(echo "$summary_wk" | tr ',' ' ')"
|
|
fi
|
|
|
|
summary_total="$(summarize "total" "${samples_total[@]}" | tail -1)"
|
|
read -r mean_total p50_total p95_total max_total <<< "$(echo "$summary_total" | tr ',' ' ')"
|
|
|
|
# Re-print summaries to stdout (the summarize calls above piped them out)
|
|
{
|
|
printf ' %-10s mean=%5s%% p50=%5s%% p95=%5s%% max=%5s%%\n' "qbz" "$mean_qbz" "$p50_qbz" "$p95_qbz" "$max_qbz"
|
|
printf ' %-10s mean=%5s%% p50=%5s%% p95=%5s%% max=%5s%%\n' "webkit" "$mean_wk" "$p50_wk" "$p95_wk" "$max_wk"
|
|
printf ' %-10s mean=%5s%% p50=%5s%% p95=%5s%% max=%5s%%\n' "total" "$mean_total" "$p50_total" "$p95_total" "$max_total"
|
|
}
|
|
|
|
# CSV header if first run
|
|
if [[ ! -f "$LOG_CSV" ]]; then
|
|
echo "timestamp,label,duration_s,window_size,qbz_mean,qbz_p50,qbz_p95,qbz_max,webkit_mean,webkit_p50,webkit_p95,webkit_max,total_mean,total_p50,total_p95,total_max" > "$LOG_CSV"
|
|
fi
|
|
|
|
WINDOW="$(read_window_size)"
|
|
echo "$(date -Iseconds),$LABEL,$DURATION,$WINDOW,$mean_qbz,$p50_qbz,$p95_qbz,$max_qbz,$mean_wk,$p50_wk,$p95_wk,$max_wk,$mean_total,$p50_total,$p95_total,$max_total" >> "$LOG_CSV"
|
|
|
|
echo ""
|
|
echo "Appended to $LOG_CSV"
|
|
echo "Compare runs: cat $LOG_CSV | column -t -s,"
|