From 7f769ca2f0aa97a47ba64f165dc186861565e234 Mon Sep 17 00:00:00 2001 From: saphid <4596216+saphid@users.noreply.github.com> Date: Mon, 28 Sep 2026 21:07:59 +1000 Subject: [PATCH] Mac in the headset: end the Frame's viewer browser after Stop; relay fixes - Chromium on the Frame outlived its last viewer window (verified: 11 processes left after a run). Once nothing is shown, Stop ends it, unless Show was pressed again meanwhile; its profile is Frame Control's own. - Relay --delay: if the agent side fails, close the viewer side too (review round 9). - Docs: the final scroll run captured 57 fps; don't blame ScreenCaptureKit. Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/mac-in-headset.md | 2 +- scripts/macview-bench.py | 19 +++++++++++++------ tests/test_macview.py | 17 +++++++++++++++++ ui/frame_macview.py | 19 ++++++++++++++++++- 4 files changed, 49 insertions(+), 8 deletions(-) diff --git a/docs/mac-in-headset.md b/docs/mac-in-headset.md index 27b3494..09c1742 100644 --- a/docs/mac-in-headset.md +++ b/docs/mac-in-headset.md @@ -204,7 +204,7 @@ about 5 ms to scroll compared with the baseline's way of measuring: | Scenario | Content | Input to drawn | fps drawn | Grades | |---|---|---|---|---| | test | 10.5 / 16.7 | 29.6 / 36.3 | 60 | all within target | -| scroll | 19.8 / 27.4 | – | 55.9 | fps, late frames (4.8%) and worst gap (222 ms) only "acceptable": Wi-Fi stalls and ScreenCaptureKit's 46–53 fps from virtual displays | +| scroll | 19.8 / 27.4 | – | 55.9 | fps, late frames (4.8%) and worst gap (222 ms) only "acceptable": Wi-Fi stalls (the Mac captured 57 fps in this run; earlier runs got 46–53 from virtual displays) | | type | 15.0 / 21.4 | 43.0 / 60.5 | – | all within target | Of the targets, click to photon is met without the Frame's compositor (the diff --git a/scripts/macview-bench.py b/scripts/macview-bench.py index 36045da..4d9976b 100755 --- a/scripts/macview-bench.py +++ b/scripts/macview-bench.py @@ -172,11 +172,18 @@ class Relay: await writer.drain() writer.close() - sender = asyncio.ensure_future(send()) - while data := await reader.read(65536): - await queue.put((self.loop.time() + self.delay, data)) - await queue.put((0, None)) - await sender + async def pump(): + while data := await reader.read(65536): + await queue.put((self.loop.time() + self.delay, data)) + await queue.put((0, None)) + + sender, pumper = asyncio.ensure_future(send()), asyncio.ensure_future(pump()) + done, _ = await asyncio.wait({sender, pumper}, return_when=asyncio.FIRST_COMPLETED) + if sender in done: # the agent's side went away: stop reading, so the connection closes + pumper.cancel() + writer.close() + return + await sender # the viewer's side ended: deliver what's queued first async def _shaped(self, reader, writer): # agent -> Frame queue = asyncio.Queue() @@ -461,7 +468,7 @@ def run_scenario(args, scenario, agent_port, token, frame_ssh): relay.loop.call_soon_threadsafe(relay.loop.stop) if chrome: chrome.terminate() - time.sleep(1.5) + time.sleep(3) # Stop ends the Frame's viewer browser 2 s later def composited(f): diff --git a/tests/test_macview.py b/tests/test_macview.py index 097c44f..950be75 100644 --- a/tests/test_macview.py +++ b/tests/test_macview.py @@ -19,6 +19,7 @@ import sys import tempfile import time import unittest +from unittest import mock from pathlib import Path ROOT = Path(__file__).resolve().parent.parent @@ -82,6 +83,22 @@ class Helpers(unittest.TestCase): mv.show("display:1") self.assertIn("Screen Recording", str(cm.exception)) + def test_stop_all_ends_the_viewer_browser_unless_shown_again(self): + calls = [] + mv = frame_macview.MacView(["ssh"], lambda remote, **kw: calls.append(remote) or "", "frame") + mv.agent = mock.Mock(poll=lambda: None) + mv.call = lambda path, **kw: {"closed": 1} + mv.shown = {"window:5"} + with mock.patch.object(frame_macview.time, "sleep"): + gen = mv.shows + mv.shown.clear() + mv._end_viewer_browser(gen) + self.assertEqual(len(calls), 1) + self.assertIn("pkill -f '[f]rame-control/mac-view", calls[0]) + mv.shows += 1 # Show pressed during the wait: leave the new viewer alone + mv._end_viewer_browser(gen) + self.assertEqual(len(calls), 1) + class WS: """A minimal WebSocket client (masked frames out, plain frames in).""" diff --git a/ui/frame_macview.py b/ui/frame_macview.py index c4fa69b..98f634b 100644 --- a/ui/frame_macview.py +++ b/ui/frame_macview.py @@ -128,6 +128,7 @@ class MacView: self.remote_port = None self.supervisor = None self.closing = False + self.shows = 0 # counts Show presses, so a late cleanup can't close a new viewer self.shown = set() # sources with a viewer out there, connected or retrying self.browser_flags = list(BROWSER_FLAGS) @@ -281,6 +282,7 @@ class MacView: def show(self, src, quality="balanced", width=None, height=None): if src != "test" and not src.startswith(("window:", "display:", "separate:")): raise MacViewError("Pick a window or display to show.") + self.shows += 1 q = QUALITY.get(quality) or QUALITY["balanced"] state = self.call("/status") if src != "test" and not state.get("screen"): @@ -325,7 +327,22 @@ class MacView: self.shown.clear() if not (self.agent and self.agent.poll() is None): return {"closed": 0} - return self.call("/close", method="POST", **({"src": src} if src else {})) + out = self.call("/close", method="POST", **({"src": src} if src else {})) + if not self.shown: + threading.Thread(target=self._end_viewer_browser, args=(self.shows,), daemon=True).start() + return out + + def _end_viewer_browser(self, shows): + """Chromium on the Frame outlives its last viewer window (verified + 2026-09-28), so once nothing is shown, end it. It runs with a profile + of its own, so nothing else is touched.""" + time.sleep(2) # the viewers close their windows first + if self.shown or self.shows != shows: + return + try: + self.run("pkill -f '[f]rame-control/mac-view|[d]ata/frame-mac-view' || true", timeout=10) + except Exception: + pass def state(self): reason = self.unavailable()