Created
June 27, 2026 05:09
-
-
Save ElijahLynn/dc18971e77101b32a823215b8fa67a98 to your computer and use it in GitHub Desktop.
Warp GPU A/B test harness (i915 PMU + ydotool, native Wayland)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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