Eye tracker: ft-eyes sleeps until the next frame is due

ft-eyes looked for new frames about 1,000 times a second: each pass of its loop asked the
control socket with a non-blocking recvfrom (a BlockingIOError nearly every time), read
both cameras' counters, and slept 1 ms. The frames come every 11.1 ms per camera, and only
as counters in ft-eyegrab's shared memory, so there's no fd to wait on.

Now each pass ends in select() on the control socket, with a timeout until 2 ms before the
next frame of either camera is due (from when its last one was seen), then every 1 ms until
it comes. A command wakes it at once. A camera with no frame for 0.1 s (headset off,
grabber idle) isn't waited for, and with both stopped it looks every 20 ms. Waiting for the
frame grabber's file uses the same select, 0.2 s at a time, instead of sleeping through
commands.

A new frame is still seen within about 1 ms of when it lands. On a synthetic share at 90 Hz
per camera, on a heavily loaded headset (load average 23, so ft-eyes rarely sat idle), its
waits went from 178 to 81 a second; unloaded, the old loop's 1 ms sleeps add up to about
1,000.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DeeJanuzandClaude Opus 5.5 committed 2026-10-03 09:15:27 -06:00
1 parent a11cef49ba
commit 7851ce90cc
1 file changed
+19 -3
+19 -3
View File
@@ -50,6 +50,7 @@ import json
import math
import mmap
import os
import select
import socket
import struct
import sys
@@ -87,6 +88,15 @@ GAP = 3.0 # s without frames: the headset was off, and may sit diffe
CLICK_BEFORE = 0.3 # a click's frames: the 300 ms before it (like the probe's fixation)
CALIB_MIN = 15 # frames an eye needs in a calibration dot's window
CALIB_SPREAD = 4.0 # px: more than this and the eye moved during the dot
# Waiting for frames. They come only as a counter in shared memory, so there's nothing to block
# on: sleep until a camera's next frame is due, then look every POLL. ft-eyegrab passes each one
# on within a ms or two of when its camera starts the one after, so they arrive 11.1 ms apart,
# give or take that.
PERIOD = 1 / 90 # s between a camera's frames
EARLY = 0.002 # start looking this long before a frame is due
POLL = 0.001 # then this often until it comes
STALLED = 0.1 # s without a frame: that camera stopped (headset off), and isn't waited for
IDLE_POLL = 0.02 # how often to look while both are stopped
class Cams:
@@ -357,7 +367,7 @@ def wait_for_cams(sock, tracker, want):
return Cams()
except (OSError, ValueError, RuntimeError):
serve(sock, tracker)
time.sleep(0.2)
select.select([sock], [], [], 0.2)
def serve(sock, tracker):
@@ -420,6 +430,7 @@ def main():
print("ft-eyes: frames found, tracking", file=sys.stderr, flush=True)
seen = [cams.count(0), cams.count(1)]
report = time.monotonic()
arrived = [0.0, 0.0] # when each camera's newest frame was seen (monotonic)
while True:
serve(sock, tracker)
want()
@@ -429,6 +440,7 @@ def main():
if n == seen[c]:
continue
seen[c] = n
arrived[c] = time.monotonic()
got = cams.frame(c, n - 1) # only the newest: never fall behind
if got:
eyes[c].feed(*got)
@@ -447,8 +459,6 @@ def main():
shifts += [float(v) for v in e.shift.value]
pupils += [e.gaze[3], e.gaze[4]] if fresh else [math.nan, math.nan]
out.write(latest, (yaw, pitch), flags, per, shifts, pupils)
else:
time.sleep(0.001)
now = time.monotonic()
if now - report >= 5:
if verbose:
@@ -466,6 +476,12 @@ def main():
print("ft-eyes: frames went away; waiting", file=sys.stderr, flush=True)
cams = wait_for_cams(sock, tracker, want)
seen = [cams.count(0), cams.count(1)]
# Until the next frame is due (a command on the socket wakes us sooner).
wait = IDLE_POLL
for c in (0, 1):
if now - arrived[c] < STALLED:
wait = min(wait, max(arrived[c] + PERIOD - EARLY - now, POLL))
select.select([sock], [], [], wait)
if __name__ == "__main__":