From d46becfd60601b6ed6968f3e8bc4dadab3a9dbe1 Mon Sep 17 00:00:00 2001 From: Samuel Onoja Date: Fri, 25 Sep 2026 04:22:29 +0100 Subject: [PATCH] test: add native client soak harness --- README.md | 3 + docs/native-soak-testing.md | 45 ++++++++++ scripts/soak-native.sh | 162 ++++++++++++++++++++++++++++++++++++ 3 files changed, 210 insertions(+) create mode 100644 docs/native-soak-testing.md create mode 100755 scripts/soak-native.sh diff --git a/README.md b/README.md index 36c1357..11a33e7 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,9 @@ Set `FERESE_TRACE_PERFORMANCE=1` to emit five-second per-output summaries under the `ferese::render` tracing target. Summaries include rendered frames, damaged pixels, average and longest render time, and DRM missed-deadline totals. +The native-client soak runner and its nested/DRM procedure are documented in +[docs/native-soak-testing.md](docs/native-soak-testing.md). + Ferese reads `$XDG_CONFIG_HOME/ferese/config.toml`, or `~/.config/ferese/config.toml` when `XDG_CONFIG_HOME` is unset. New scrolling columns use half the viewport by default. To make every newly opened column use diff --git a/docs/native-soak-testing.md b/docs/native-soak-testing.md new file mode 100644 index 0000000..8bb9aae --- /dev/null +++ b/docs/native-soak-testing.md @@ -0,0 +1,45 @@ +# Native-client soak testing + +The M6 soak runner repeatedly creates and destroys native Wayland clients while +exercising focus, resize, column width, fullscreen, floating, and workspace +paths. Every tenth iteration also terminates a client abruptly. The runner +checks that the compositor and authenticated IPC socket remain responsive and +records compositor RSS growth. + +Build Ferese and `feresectl`, start the compositor, and run: + +```bash +FERESE_TRACE_PERFORMANCE=1 cargo run -p ferese -- foot +``` + +From a terminal connected to Ferese, or from the host with the Ferese socket +selected, run: + +```bash +WAYLAND_DISPLAY=wayland-1 \ +FERESE_PID="$(pgrep -n -x ferese)" \ +./scripts/soak-native.sh +``` + +The default duration is 24 hours. A shorter validation run is: + +```bash +WAYLAND_DISPLAY=wayland-1 \ +FERESE_PID="$(pgrep -n -x ferese)" \ +FERESE_SOAK_SECONDS=300 \ +./scripts/soak-native.sh +``` + +Environment controls: + +- `FERESE_SOAK_SECONDS`: run duration, default `86400`; +- `FERESE_SOAK_DELAY`: delay between operations, default `0.15` seconds; +- `FERESE_SOAK_CLIENT`: native client executable, default `foot`; +- `FERESECTL`: path to `feresectl`, default `target/debug/feresectl`; +- `FERESE_PID`: compositor PID; automatic lookup is only a fallback; +- `FERESE_SOAK_LOG`: result log, default `/tmp/ferese-soak-.log`. + +Run the same workload once with the nested backend and once from a direct DRM +session. A passing run requires the compositor to remain alive, IPC queries to +succeed, and RSS samples to stabilize rather than grow without bound. Preserve +the soak log and the `ferese::render` summaries with the release artifacts. diff --git a/scripts/soak-native.sh b/scripts/soak-native.sh new file mode 100755 index 0000000..7547822 --- /dev/null +++ b/scripts/soak-native.sh @@ -0,0 +1,162 @@ +#!/usr/bin/env bash + +set -euo pipefail + +soak_seconds="${FERESE_SOAK_SECONDS:-86400}" +iteration_delay="${FERESE_SOAK_DELAY:-0.15}" +client_program="${FERESE_SOAK_CLIENT:-foot}" +feresectl_program="${FERESECTL:-target/debug/feresectl}" +compositor_pid="${FERESE_PID:-}" +log_path="${FERESE_SOAK_LOG:-/tmp/ferese-soak-$$.log}" + +if [[ -z "${WAYLAND_DISPLAY:-}" ]]; then + echo "WAYLAND_DISPLAY must name the Ferese socket" >&2 + exit 2 +fi + +if [[ ! "$soak_seconds" =~ ^[1-9][0-9]*$ ]]; then + echo "FERESE_SOAK_SECONDS must be a positive integer" >&2 + exit 2 +fi + +if [[ ! -x "$feresectl_program" ]]; then + cargo build -p feresectl +fi + +if ! command -v "$client_program" >/dev/null 2>&1; then + echo "soak client not found: $client_program" >&2 + exit 2 +fi + +if [[ -z "$compositor_pid" ]]; then + compositor_pid="$(pgrep -n -x ferese || true)" +fi + +if [[ -z "$compositor_pid" || ! -r "/proc/$compositor_pid/status" ]]; then + echo "set FERESE_PID to the running compositor process" >&2 + exit 2 +fi + +client_pids=() + +cleanup() { + local pid + + for pid in "${client_pids[@]}"; do + if kill -0 "$pid" 2>/dev/null; then + kill -TERM "$pid" 2>/dev/null || true + fi + done +} + +trap cleanup EXIT INT TERM + +rss_kib() { + awk '/^VmRSS:/ { print $2; exit }' "/proc/$compositor_pid/status" +} + +control() { + "$feresectl_program" "$@" >/dev/null +} + +launch_client() { + "$client_program" >/dev/null 2>&1 & + client_pids+=("$!") +} + +reap_clients() { + local pid + + for pid in "${client_pids[@]}"; do + if kill -0 "$pid" 2>/dev/null; then + kill -TERM "$pid" 2>/dev/null || true + fi + wait "$pid" 2>/dev/null || true + done + client_pids=() +} + +wait_for_focus() { + local attempt + local focused + + for ((attempt = 0; attempt < 40; attempt += 1)); do + focused="$("$feresectl_program" get-focused-window 2>/dev/null || true)" + if [[ "$focused" != "null" && -n "$focused" ]]; then + return 0 + fi + sleep 0.05 + done + + echo "a launched client did not receive focus" >&2 + return 1 +} + +start_time="$SECONDS" +deadline=$((start_time + soak_seconds)) +iteration=0 +initial_rss="$(rss_kib)" +maximum_rss="$initial_rss" + +{ + echo "ferese native soak" + echo "pid=$compositor_pid display=$WAYLAND_DISPLAY client=$client_program" + echo "duration_seconds=$soak_seconds initial_rss_kib=$initial_rss" +} | tee "$log_path" + +while ((SECONDS < deadline)); do + if ! kill -0 "$compositor_pid" 2>/dev/null; then + echo "FAIL compositor exited at iteration $iteration" | tee -a "$log_path" + exit 1 + fi + + launch_client + launch_client + wait_for_focus + + control focus left + control focus right + control resize left + control resize right + control cycle-column-width + control cycle-column-width + control toggle-fullscreen + control toggle-fullscreen + control toggle-floating + control toggle-floating + control workspace 2 + control workspace 1 + control close + sleep "$iteration_delay" + control close + sleep "$iteration_delay" + + if ((iteration % 10 == 0)); then + launch_client + wait_for_focus + kill -TERM "${client_pids[-1]}" 2>/dev/null || true + fi + reap_clients + + current_rss="$(rss_kib)" + if ((current_rss > maximum_rss)); then + maximum_rss="$current_rss" + fi + + if ((iteration % 100 == 0)); then + printf 'iteration=%d elapsed=%d rss_kib=%d max_rss_kib=%d\n' \ + "$iteration" "$((SECONDS - start_time))" "$current_rss" "$maximum_rss" \ + | tee -a "$log_path" + fi + + iteration=$((iteration + 1)) + sleep "$iteration_delay" +done + +control get-outputs +control get-workspaces +final_rss="$(rss_kib)" + +printf 'PASS iterations=%d final_rss_kib=%d max_rss_kib=%d growth_kib=%d\n' \ + "$iteration" "$final_rss" "$maximum_rss" "$((final_rss - initial_rss))" \ + | tee -a "$log_path"