Skip to content

UXEventRecorder

UXEventRecorder tees a copy of each event into a list, and replays them to a sink.

#use <UXKit> // or #import "UXEventRecorder.xc"
UXEventRecorder* r = new UXEventRecorder();
r.start(nowMs);
// … the application runs; the event tap calls r.record(e, nowMs) …
r.stop();
r.replay(&self.dispatch); // feed them back, in order

Use it for deterministic bug repros, demos, and “do that again”.

The neutral run loop puts every event through one place. A recorder is a tee off that funnel and adds nothing to the dispatch path.

The toolkit therefore needs no special recording mode. Anything that reaches the application reaches the recorder, with no second code path that could behave differently.

The recorder does not own two things:

  • the clock: the caller passes nowMs in, from the driver
  • the sink: the caller provides where replayed events go, which is the application’s own dispatch

The model is backend-neutral and testable with synthetic time. A test records at times it chose, replays into a sink it wrote, and asserts on the output, with no window, no driver and no waiting.

UXTimerScheduler uses the same design, for the same reason.

static UXEvent* copyEvent(UXEvent* e)

Drivers usually reuse one UXEvent object for every event they announce, filling in the fields and dispatching, because an allocation per mouse-move is too expensive.

A recorder storing pointers would end up with a thousand references to one object holding whatever arrived last. Copying keeps each recorded event intact, and lets a tape captured under AppKit replay with no AppKit present.

void replay(callback sink void(UXEvent* e))
void replayRange(callback sink void(UXEvent* e), i32 fromMs, i32 toMs)

Events are fed to the sink in order, as fast as the loop runs. Timing is recorded, not enforced.

The caller decides the replay speed: instant for a test, real-time for a demo, one at a time for a debugging session. The recorder does not choose, because the three cases need different things.

replayRange makes a long recording useful. A bug 40 seconds into a session is reproduced by replaying 38 to 42, without sitting through the rest.

r.start(nowMs); // clears the tape and begins
r.stop(); // stops; the tape is kept
r.isRecording();

start clears the tape, so it begins a new recording and does not resume. stop keeps the events, so the tape is available to replay or inspect afterwards. clear discards it.

A record call while not recording is ignored. The tap can stay installed permanently and costs one boolean test when nothing is being recorded.

start · stop · isRecording · record · count · eventAt · timeAt · durationMs · clear · replay · replayRange

void start(i32 nowMs)

Clears the tape, begins recording, and takes nowMs as time zero. Every recorded timestamp is relative to it.

void stop(void)
bool isRecording(void)
void record(UXEvent* e, i32 nowMs)

Called by the tap. Copies the event and stamps it. A no-op when not recording.

i32 count(void)
UXEvent* eventAt(i32 i)

One recorded event, unwrapped from its UXRecordedEvent, for asserting on what arrived without replaying it.

i32 timeAt(i32 i)

That event’s offset in milliseconds from the start of recording. Together with eventAt, it lets you inspect a recording without the boxing type.

i32 durationMs(void)

The last event’s timestamp, which is how long the recording spans. 0 for an empty tape.

Use it to choose a replayRange window, or as the length a demo paces against.

void clear(void)
void replay(callback sink void(UXEvent* e))

Replays every event, in order.

void replayRange(callback sink void(UXEvent* e), i32 fromMs, i32 toMs)

Replays only the events whose timestamps fall in the window.