Skip to content

Instantly share code, notes, and snippets.

@ElijahLynn
Created June 27, 2026 05:09
Show Gist options
  • Select an option

  • Save ElijahLynn/dc18971e77101b32a823215b8fa67a98 to your computer and use it in GitHub Desktop.

Select an option

Save ElijahLynn/dc18971e77101b32a823215b8fa67a98 to your computer and use it in GitHub Desktop.
Warp GPU A/B test harness (i915 PMU + ydotool, native Wayland)
#!/usr/bin/env python3
"""
Whole-GPU utilization sampler via the i915 PMU -- the same source btop and
intel_gpu_top use. Reads per-engine `*-busy` perf counters (nanoseconds the
engine was busy) and normalizes by PERF_FORMAT_TOTAL_TIME_ENABLED:
engine_util% = delta(busy_ns) / delta(time_enabled_ns) * 100
This measures the GPU system-wide (pid=-1), so it includes warp's rendering
AND the GNOME compositor compositing warp's frames -- matching btop.
Requires CAP_PERFMON or root on systems with perf_event_paranoid >= 2
(that's why btop ships with cap_perfmon). Run with sudo if it errors EACCES.
Usage: gpu_pmu_sampler.py <duration_s> <interval_ms> <out_csv>
"""
import ctypes, ctypes.util, os, struct, sys, time, glob
DURATION = float(sys.argv[1]) if len(sys.argv) > 1 else 20.0
INTERVAL = (float(sys.argv[2]) if len(sys.argv) > 2 else 200.0) / 1000.0
OUT = sys.argv[3] if len(sys.argv) > 3 else "/tmp/gpu_pmu.csv"
PMU = "/sys/bus/event_source/devices/i915"
PERF_FORMAT_TOTAL_TIME_ENABLED = 1 << 0
__NR_perf_event_open = 298 # x86_64
libc = ctypes.CDLL(ctypes.util.find_library("c") or "libc.so.6", use_errno=True)
class perf_event_attr(ctypes.Structure):
_fields_ = [
("type", ctypes.c_uint32),
("size", ctypes.c_uint32),
("config", ctypes.c_uint64),
("sample_period", ctypes.c_uint64),
("sample_type", ctypes.c_uint64),
("read_format", ctypes.c_uint64),
("flags", ctypes.c_uint64),
("wakeup_events", ctypes.c_uint32),
("bp_type", ctypes.c_uint32),
("config1", ctypes.c_uint64),
("config2", ctypes.c_uint64),
("branch_sample_type", ctypes.c_uint64),
("sample_regs_user", ctypes.c_uint64),
("sample_stack_user", ctypes.c_uint32),
("clockid", ctypes.c_int32),
("sample_regs_intr", ctypes.c_uint64),
("aux_watermark", ctypes.c_uint32),
("sample_max_stack", ctypes.c_uint16),
("__reserved_2", ctypes.c_uint16),
("aux_sample_size", ctypes.c_uint32),
("__reserved_3", ctypes.c_uint32),
]
def perf_type():
return int(open(f"{PMU}/type").read().strip())
def event_config(name):
# files look like "config=0x1000"
txt = open(f"{PMU}/events/{name}").read().strip()
for part in txt.split(","):
if part.startswith("config="):
return int(part.split("=")[1], 0)
return int(txt, 0)
def perf_open(ptype, config):
attr = perf_event_attr()
attr.size = ctypes.sizeof(perf_event_attr)
attr.type = ptype
attr.config = config
attr.read_format = PERF_FORMAT_TOTAL_TIME_ENABLED
nr_cpus = os.cpu_count() or 1
for cpu in range(nr_cpus):
fd = libc.syscall(__NR_perf_event_open, ctypes.byref(attr),
-1, cpu, -1, 0)
if fd >= 0:
return fd
err = ctypes.get_errno()
if err != 22: # EINVAL -> try next cpu, like btop
raise OSError(err, f"perf_event_open failed: {os.strerror(err)}")
raise OSError(err, f"perf_event_open failed on all cpus: {os.strerror(err)}")
def read_counter(fd):
buf = os.read(fd, 16)
value, time_enabled = struct.unpack("QQ", buf)
return value, time_enabled
def main():
ptype = perf_type()
engines = [] # (label, fd)
for name in ["rcs0-busy", "bcs0-busy", "vcs0-busy", "vecs0-busy"]:
path = f"{PMU}/events/{name}"
if not os.path.exists(path):
continue
try:
fd = perf_open(ptype, event_config(name))
except OSError as e:
print(f"error opening {name}: {e}\n"
f" -> i915 PMU needs CAP_PERFMON/root here. Re-run with sudo.",
file=sys.stderr)
sys.exit(13)
engines.append((name.replace("-busy", ""), fd))
if not engines:
print("no i915 busy engines found", file=sys.stderr)
sys.exit(1)
labels = [e[0] for e in engines]
prev = {lab: read_counter(fd) for lab, fd in engines}
samples = {lab: [] for lab in labels}
with open(OUT, "w") as f:
f.write("t_ms," + ",".join(f"{l}_pct" for l in labels) + "\n")
end = time.monotonic() + DURATION
while time.monotonic() < end:
time.sleep(INTERVAL)
row = [str(int(time.monotonic() * 1000))]
for lab, fd in engines:
v, t = read_counter(fd)
pv, pt = prev[lab]
dt = t - pt or 1
util = max(0.0, min(100.0, (v - pv) * 100.0 / dt))
samples[lab].append(util)
row.append(f"{util:.1f}")
prev[lab] = (v, t)
f.write(",".join(row) + "\n")
print(" whole-GPU (i915 PMU, like btop):")
for lab in labels:
s = samples[lab]
if not s:
continue
print(f" {lab:6s} avg={sum(s)/len(s):5.1f}% peak={max(s):5.1f}%")
rcs = samples.get("rcs0", [])
if rcs:
print(f" -> render(rcs0) avg={sum(rcs)/len(rcs):.1f}% peak={max(rcs):.1f}%")
if __name__ == "__main__":
main()
#!/usr/bin/env bash
#
# Non-interactive single-mode runner for the Warp GPU A/B test (native Wayland).
#
# Sequence:
# 1. write dev-build settings ([system] force_x11=false, prefer_low_power_gpu=<mode>)
# 2. relaunch warp-oss with WARP_ENABLE_WAYLAND=1
# 3. LEAD-IN grace period (you click the Warp window and leave it focused)
# 4. sample whole-GPU i915 PMU (like btop) while driving a fixed ydotool workload
#
# Usage: run_mode_auto.sh <label> <low_power true|false> [lead_s] [sample_s] [workload_s]
set -euo pipefail
REPO="/home/elijah/projects/warp"
BIN="$REPO/target/debug/warp-oss"
CFG="$HOME/.config/warp-oss/settings.toml"
HERE="$(cd "$(dirname "$0")" && pwd)"
export YDOTOOL_SOCKET="${YDOTOOL_SOCKET:-$XDG_RUNTIME_DIR/.ydotool_socket}"
LABEL="${1:?label}"; LOW_POWER="${2:?true|false}"
LEAD="${3:-12}"; SAMPLE_S="${4:-20}"; WORKLOAD_S="${5:-18}"
CSV="/tmp/gpu_${LABEL}.csv"
[ -x "$BIN" ] || { echo "missing $BIN"; exit 1; }
# ydotoold as the normal user (uinput ACL grants access; no sudo).
if ! pgrep -x ydotoold >/dev/null; then
ydotoold --socket-path "$YDOTOOL_SOCKET" --socket-own "$(id -u):$(id -g)" >/tmp/ydotoold.log 2>&1 &
sleep 1.5
fi
# Configure mode: native Wayland + low-power preference.
python3 - "$CFG" "$LOW_POWER" <<'PY'
import sys, re, pathlib
cfg, low = pathlib.Path(sys.argv[1]), sys.argv[2]
text = cfg.read_text() if cfg.exists() else ""
out, skip = [], False
for ln in text.splitlines():
if re.match(r'^\s*\[system\]\s*$', ln): skip = True; continue
if skip and re.match(r'^\s*\[', ln): skip = False
if not skip: out.append(ln)
body = "\n".join(out).rstrip()
block = f"[system]\nforce_x11 = false\nprefer_low_power_gpu = {low}\n"
cfg.parent.mkdir(parents=True, exist_ok=True)
cfg.write_text((body + "\n\n" if body else "") + block)
print(f" config: force_x11=false prefer_low_power_gpu={low}")
PY
# Relaunch in native Wayland.
pkill -x warp-oss >/dev/null 2>&1 || true
sleep 1.5
WARP_ENABLE_WAYLAND=1 "$BIN" >/tmp/warp-oss.run.log 2>&1 &
echo " launching warp-oss (WARP_ENABLE_WAYLAND=1)..."
sleep 7
# Confirm windowing backend from the log if it reports it.
if grep -qiE "wayland" /tmp/warp-oss.run.log; then echo " log mentions wayland"; fi
echo " >>> CLICK THE WARP WINDOW NOW and keep it focused (~${LEAD}s lead-in)."
for ((i=LEAD; i>0; i--)); do printf " workload starts in %2ds...\r" "$i"; sleep 1; done
echo " WORKLOAD RUNNING -- do not touch keyboard/mouse "
"$HERE/gpu_pmu_sampler.py" "$SAMPLE_S" 200 "$CSV" & sp=$!
"$HERE/workload.sh" "$WORKLOAD_S" || true
wait "$sp"
echo " [$LABEL] csv=$CSV"
#!/usr/bin/env bash
#
# Deterministic, repeatable input workload for Warp, driven via ydotool
# (kernel uinput -> works under native GNOME Wayland).
#
# It alternates between:
# 1. "typey stuff": filling the prompt with text, then clearing (Ctrl+U).
# -> exercises input-box re-rendering on every keystroke.
# 2. output + autoscroll: running `seq 1 N` to flood the terminal.
# -> exercises terminal grid re-rendering + autoscroll.
# 3. scrollback navigation via PageUp/PageDown.
#
# IMPORTANT: input goes to whatever window is FOCUSED. Make sure the Warp
# window is focused before this runs. Keys are restricted to letters/digits/
# space/Enter/PageUp/PageDown so they are safe across keyboard layouts.
#
# Usage: workload.sh <duration_s>
set -euo pipefail
export YDOTOOL_SOCKET="${YDOTOOL_SOCKET:-$XDG_RUNTIME_DIR/.ydotool_socket}"
DUR="${1:-18}"
# Linux input event keycodes (see /usr/include/linux/input-event-codes.h)
KEY_ENTER=28
KEY_LEFTCTRL=29
KEY_U=22
KEY_PAGEUP=104
KEY_PAGEDOWN=109
line="the quick brown fox jumps over the lazy dog 0123456789 "
ctrl_u() { ydotool key ${KEY_LEFTCTRL}:1 ${KEY_U}:1 ${KEY_U}:0 ${KEY_LEFTCTRL}:0; }
enter() { ydotool key ${KEY_ENTER}:1 ${KEY_ENTER}:0; }
end=$(( $(date +%s) + DUR ))
while [ "$(date +%s)" -lt "$end" ]; do
# 1. typey stuff (no execution) then clear
for _ in 1 2 3 4 5 6; do ydotool type --key-delay 6 "$line"; done
ctrl_u
# 2. output flood + autoscroll
ydotool type --key-delay 6 "seq 1 4000"
enter
sleep 0.6
# 3. scrollback churn
for _ in $(seq 1 12); do ydotool key ${KEY_PAGEUP}:1 ${KEY_PAGEUP}:0; done
for _ in $(seq 1 12); do ydotool key ${KEY_PAGEDOWN}:1 ${KEY_PAGEDOWN}:0; done
done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment