timing_core 0.2.0 copy "timing_core: ^0.2.0" to clipboard
timing_core: ^0.2.0 copied to clipboard

Record format and analysis for transport timing runs: one-way latency, jitter, loss, clock offset and drift per device pair. Pure Dart, web-safe.

timing_core #

Pub Version

timing_core defines the record format and the analysis used to characterise the timing of a transport between devices: one-way latency, jitter, loss, clock offset and drift for each pair of devices. It is the basis of the transport_timing application and its analysis application, and can be used on its own to log and analyse timing in another program. The package is pure Dart and runs on all platforms, including the web.

API documentation · Guide: validating timing in a lab

Installation #

dart pub add timing_core

Recording #

Each device writes one log per run. The header describes the run, and logs with the same runId are analysed together.

final log = RunLogWriter(
  sink, // a BytesSink, to keep the log in memory until the run is over
  RunHeader(
    runId: runId,
    startedAt: DateTime.now(),
    deviceId: 'ipad1',
    deviceName: 'iPad 1',
    sourceId: mySourceId, // how other devices identify what this one sends
    test: 'latency',
    backend: 'lsl-coordinator',
    sampleRate: 1000,
    receiveMode: 'polled',
    pollInterval: 0.001,
  ),
);

log.sent(seq, sendClock);
log.received(
  sourceId,
  seq,
  receivedClock: timing.receivedClock,
  sourceClock: timing.sourceClock,
  clockOffset: timing.clockOffset,
  uncertainty: timing.uncertainty,
);
log.clockSync(sourceId, receivedClock: s.receivedClock, offset: s.offset,
    remoteTime: s.remoteTime, uncertainty: s.uncertainty);

await log.close();

The received and clockSync fields are those of peer_coordinator's MessageTiming and ClockSyncSample.

The file #

A log is an XDF file, so it opens in any XDF tool. The run header is in the file header's transport_timing element, as JSON.

Stream Channels Time stamp
tt.sent seq this device's clock at the send
tt.received, one per sender (its source_id) seq, source_clock, received_clock, clock_offset, uncertainty the sender's clock at the send
tt.clock, one per sender offset, remote_time, uncertainty, reset this device's clock at the estimate
tt.markers text: kind id [source]; kinds touch (the application heard of a touch), touch_os (the system's own time for it), shown (the frame showing a sample was drawn) this device's clock
tt.events text: name [json] this device's clock

Clocks are in seconds; NaN is a value that was not known. Every clock-offset estimate is also a standard XDF clock offset of its tt.received stream, so a reader that synchronises clocks maps that stream's time stamps onto the receiving device's clock, and latency is one subtraction:

streams, _ = pyxdf.load_xdf('tt_run_ipad1.xdf')
rx = next(s for s in streams if s['info']['name'][0] == 'tt.received')
latency = rx['time_series'][:, 2] - rx['time_stamps']   # received_clock − sent

Analysing #

final report = analyse([for (final bytes in files) RunLog.parse(bytes)]);
print(formatReport(report));

The same analysis is available from the command line: dart run timing_core:analyse [--json] <log>...

For each sender and receiver, a PairReport contains:

Field Meaning
Latency received − (sent + clock offset), computed with the offset estimate that was current when the sample arrived, and again with the offset read from a line fitted through all estimates of the run. No values are trimmed or clamped: slightly negative values reflect the error of the offset, and the spread is part of the measurement
Uncertainty The round-trip time of the probes behind the offsets. The offset, and therefore the latency, is known to within half of it
Loss, duplicates, reordering Derived from sequence numbers
Send and receive intervals The regularity of sending and receiving
Clock The offset at the start and end of the run, and the drift in parts per million
pollInterval If the receiver polls, a sample waits between zero and one interval before it is seen, which adds half the interval to the mean latency. Runs are compared by receive mode to separate this from the network

The log of a receiver is sufficient for all of these except losses at the very start or end of a run and the device name of the sender, which require the log of the sender.

Reporting results #

Alignment of time stamps #

Data from different devices are aligned with the time stamp of a sample and the clock offset: time on this clock = time stamp + offset.

The time stamp is the sender's clock when it sent (or, for LSL, pushed) the sample. It does not include delays inside the sending application, or the latency of an input device such as a mouse or keyboard that triggered the event.

The offset is estimated from timed round trips, of which the fastest of each burst is kept. Whatever the two directions of that round trip each took, the offset is wrong by no more than half the round trip. The whole round trip is reported as uncertainty, and half of it as the "±" of the report.

Between estimates the clocks drift apart by tens of parts per million, which is tens of microseconds per second. residualSd indicates how well a linear fit describes the drift; a drift that is not linear is visible there.

Latency #

latency = received − (time stamp + offset). Latency is resolved no more finely than the offset is known, so a latency below the "±" cannot be distinguished from zero, and negative values occur for the same reason.

The meaning of received depends on how samples are consumed:

Consumption When the clock is read What the latency contains
Event-driven (LSLInlet.sampleStream(), WebSocket, WebRTC) As the sample becomes available to the process The send path of the sender, the network, and the network stack of the receiver
Polled When the next poll finds the sample The same, and a wait of between zero and one polling interval

A sample that was already waiting when receiving began (before the first listener, or after a pause) has the time at which it was collected, not the time at which it arrived. The same applies to samples held back by maxBacklog. This is useful for diagnosing whether a consumer keeps up with a stream. A stalled consumer does add latency, but for locating slow points in a topology the wait in the backlog can be excluded.

A sender that uses chunking may wait until the number of samples given by chunkSize is reached. The first sample of a chunk then waits longer than the last.

End-to-end latency that includes hardware, operating system and application is measured with the interactive test of transport_timing. It requires a separate device with a high-precision clock (for example a Bela board), a photodiode and a force-sensitive resistor, and characterises the complete path from a touch to the rendering of the display.

0
likes
150
points
125
downloads

Documentation

API reference

Publisher

verified publisherzeyus.com

Weekly Downloads

Record format and analysis for transport timing runs: one-way latency, jitter, loss, clock offset and drift per device pair. Pure Dart, web-safe.

Homepage
Repository (GitHub)
View/report issues
Contributing

License

MIT (license)

Dependencies

xdf, xml

More

Packages that depend on timing_core