Serve frames from the pump instead of reading QMP per request

/stream and /frame.png now read the pump's shared buffer. A slow client falls
behind in frames rather than in QMP reads, and reconnects no longer multiply the
load on the emulator.

/frame.png returns 503 when there is no frame rather than raising, because that is
a real state: the emulator can be powered off and the page still has to load. The
same reason create_app now tolerates client=None.

Measured on the live server with 4 concurrent streams, which is what a reconnecting
browser produces: keypress latency went from 6 ms avg to 5 ms, so -1 ms, i.e. noise.
All four clients received the same ~27 frames. /stream first byte in 4 ms.

Note this rules out my earlier guess: I had assumed concurrent streams were
starving keypresses, and the numbers said otherwise both before and after. The
pump is worth having for constant QMP load, not because contention was the
slowness.
This commit is contained in:
mckero committed 2026-08-28 05:55:03 +01:00
1 parent b75ef5fdcd
commit b500697224
2 files changed
+77 -15

No files matched your search

+48
View File
@@ -220,5 +220,53 @@ class TestFrontEndHoldMs(unittest.TestCase):
self.assertIn("MIN_HOLD_MS", self.body)
class TestStreamUsesPump(unittest.TestCase):
"""Serving frames must not cost QMP reads: the pump owns the grabbing."""
def _app(self):
client = StubClient()
app = webui.create_app(client, frame_addr=0x1000, status_addr=0x2000)
app.config.update(TESTING=True)
return client, app, app.test_client()
def _wait(self, app, timeout=3.0):
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
if app.config["PUMP"].latest() is not None:
return True
time.sleep(0.02)
return False
def test_exposes_the_pump(self):
_, app, _ = self._app()
self.assertIn("PUMP", app.config)
def test_serving_frames_costs_no_qmp_reads(self):
client, app, http = self._app()
self.assertTrue(self._wait(app), "pump produced no frame")
before = len([n for n, _ in client.sent if n == "memsave"])
http.get("/stream?limit=1")
http.get("/frame.png")
after = len([n for n, _ in client.sent if n == "memsave"])
# The pump keeps grabbing in the background, so allow a little drift;
# what must not happen is a read per request.
self.assertLess(after - before, 8,
"serving a frame triggered fresh QMP reads")
def test_frame_png_still_returns_a_png(self):
_, app, http = self._app()
self.assertTrue(self._wait(app))
resp = http.get("/frame.png")
self.assertEqual(resp.status_code, 200)
self.assertTrue(resp.get_data().startswith(b"\x89PNG\r\n\x1a\n"))
def test_frame_png_returns_503_when_there_is_no_frame(self):
"""No frame is a state, not a crash: the emulator may be powered off."""
app = webui.create_app(None, frame_addr=0x1000, status_addr=0x2000)
app.config.update(TESTING=True)
self.assertEqual(app.test_client().get("/frame.png").status_code, 503)
if __name__ == "__main__":
unittest.main()