diff --git a/docs/PERF_BASELINE_PROTOCOL.md b/docs/PERF_BASELINE_PROTOCOL.md index b526b50..b90e534 100644 --- a/docs/PERF_BASELINE_PROTOCOL.md +++ b/docs/PERF_BASELINE_PROTOCOL.md @@ -63,10 +63,12 @@ with the installed build's interpreter. version-probe result and timing, then a session-establishment attempt (fast failure versus hang matters more than the version answer), plus the resolved first rung and a no-emission `wtype ""` spawn cost as a - lower bound. End-to-end key-to-screen latency is deliberately not - taken here: emitting keystrokes into a live desktop from a background - run would disturb the session. It belongs to the hardware checklist - with a dedicated target window. + lower bound. On niri the portal probe is skipped, as the injector skips + it, and the report says so with `portal_skip_reason: "niri"` and + `portal_available: false`. End-to-end key-to-screen latency is + deliberately not taken here: emitting keystrokes into a live desktop + from a background run would disturb the session. It belongs to the + hardware checklist with a dedicated target window. - **Thresholds for the model ladder:** derive free-memory cutoffs from the maximum observed per-rung footprint (transcribe peak, not just load) plus explicit headroom for the compositor and other GPU diff --git a/scripts/measure_perf_baseline.py b/scripts/measure_perf_baseline.py index 2fb326a..ed1ad47 100644 --- a/scripts/measure_perf_baseline.py +++ b/scripts/measure_perf_baseline.py @@ -433,8 +433,11 @@ def cmd_inject_probe(args: argparse.Namespace) -> int: Never emits keystrokes: end-to-end key-to-screen latency would type into the live desktop, so it stays a hardware-checklist item (#32). + + On niri the portal is skipped with the injector's own niri guard: the + injector never offers the portal there, so the report must not claim it. """ - from vocalinux.text_injection.text_injector import TextInjector + from vocalinux.text_injection.text_injector import TextInjector, _is_niri_session probe_text = "the quick brown fox jumps over the lazy dog 0123456789" started = time.monotonic() @@ -462,16 +465,20 @@ def cmd_inject_probe(args: argparse.Namespace) -> int: env=host_env(), ) wtype_roundtrips.append(time.monotonic() - begin) - try: - from vocalinux.text_injection.remote_desktop_portal import RemoteDesktopPortal + portal_available = False + portal_seconds = None + portal_skip_reason = "niri" if _is_niri_session() else None + if portal_skip_reason is None: + try: + from vocalinux.text_injection.remote_desktop_portal import RemoteDesktopPortal - portal_started = time.monotonic() - portal_available = RemoteDesktopPortal().probe() - portal_seconds = time.monotonic() - portal_started - except Exception as exc: # noqa: BLE001 - probe must never fail the pass - portal_available = False - portal_seconds = None - print(f"portal probe raised: {exc}", file=sys.stderr) + portal_started = time.monotonic() + portal_available = RemoteDesktopPortal().probe() + portal_seconds = time.monotonic() - portal_started + except Exception as exc: # noqa: BLE001 - probe must never fail the pass + portal_available = False + portal_seconds = None + print(f"portal probe raised: {exc}", file=sys.stderr) print( json.dumps( { @@ -481,6 +488,7 @@ def cmd_inject_probe(args: argparse.Namespace) -> int: "wtype_empty_roundtrip_stats": describe(wtype_roundtrips), "portal_available": portal_available, "portal_probe_seconds": portal_seconds, + "portal_skip_reason": portal_skip_reason, }, indent=2, sort_keys=True, diff --git a/tests/test_perf_baseline_protocol.py b/tests/test_perf_baseline_protocol.py index c09bfa2..eab4bce 100644 --- a/tests/test_perf_baseline_protocol.py +++ b/tests/test_perf_baseline_protocol.py @@ -7,7 +7,10 @@ helpers tested here. Tests run offline with fixture text only and never touch audio hardware, the GPU, or the network. """ +import argparse import importlib.util +import json +import shutil from pathlib import Path import pytest @@ -134,3 +137,90 @@ DBUS_TOGGLE_LOG = """\ def test_parse_wake_latencies_pairs_dbus_toggle_starts_only() -> None: """Only IDLE toggles open capture; a stop toggle never borrows the next run.""" assert PROTOCOL.parse_wake_latencies(DBUS_TOGGLE_LOG) == pytest.approx([0.146, 0.649]) + + +#: Session variables the injector's niri guard reads, plus Wayland; cleared so +#: the host's own session (this machine runs niri) cannot leak into a test. +_SESSION_ENV = ( + "NIRI_SOCKET", + "XDG_CURRENT_DESKTOP", + "XDG_SESSION_DESKTOP", + "DESKTOP_SESSION", + "WAYLAND_DISPLAY", +) + + +class _FakeInjector: + """Stands in for TextInjector: a Wayland session that selected wtype.""" + + wayland_mode = True + wayland_tool = "wtype" + + +def _run_inject_probe( + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], + session_env: dict[str, str], + portal_answers: bool, +) -> tuple[dict, list[bool]]: + """Run inject-probe under a fake session; return its report and portal probe calls.""" + from vocalinux.text_injection import remote_desktop_portal, text_injector + + probes: list[bool] = [] + + class _SpyPortal: + def probe(self) -> bool: + probes.append(True) + return portal_answers + + for name in _SESSION_ENV: + monkeypatch.delenv(name, raising=False) + for name, value in session_env.items(): + monkeypatch.setenv(name, value) + monkeypatch.setattr(text_injector, "TextInjector", _FakeInjector) + monkeypatch.setattr(remote_desktop_portal, "RemoteDesktopPortal", _SpyPortal) + monkeypatch.setattr(shutil, "which", lambda _name: None) + assert PROTOCOL.cmd_inject_probe(argparse.Namespace()) == 0 + return json.loads(capsys.readouterr().out), probes + + +@pytest.mark.parametrize( + "session_env", + [ + {"XDG_CURRENT_DESKTOP": "niri"}, + {"NIRI_SOCKET": "/run/user/1000/niri.wayland-1.1.sock"}, + ], +) +def test_inject_probe_never_probes_portal_on_niri( + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], + session_env: dict[str, str], +) -> None: + """The injector never offers the portal on niri, so the probe must not claim it. + + Both niri signals are covered: the probe reuses the injector's guard, so a + session the injector treats as niri is reported the same way here. + """ + report, probes = _run_inject_probe( + monkeypatch, capsys, session_env=session_env, portal_answers=True + ) + assert probes == [] + assert report["portal_available"] is False + assert report["portal_probe_seconds"] is None + assert report["portal_skip_reason"] == "niri" + + +def test_inject_probe_reports_portal_answer_off_niri( + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], +) -> None: + """Off niri the portal is probed and its answer is reported unchanged.""" + report, probes = _run_inject_probe( + monkeypatch, + capsys, + session_env={"XDG_CURRENT_DESKTOP": "GNOME"}, + portal_answers=True, + ) + assert probes == [True] + assert report["portal_available"] is True + assert report["portal_skip_reason"] is None