feat(apple): capture->client latency HUD (skew-corrected) via the connect offset
ci / rust (push) Has been cancelled
ci / rust (push) Has been cancelled
The Apple client now consumes the connector's clock offset. PunktfunkConnection
reads punktfunk_connection_clock_offset_ns into clockOffsetNs at connect; a new
LatencyMeter (PunktfunkKit, NSLock + percentiles, mirrors FrameMeter) records each
AU's capture->client-receipt latency = now(CLOCK_REALTIME) + offset - pts_ns, and
SessionModel drains p50/p95 into the macOS HUD ("capture->client N/N ms p50/p95",
"(same-host)" when the host didn't answer the skew handshake). Wired at the
existing onFrame hook in ContentView — additive, no change to the decode/present
path. Unit test for the meter (percentiles, skew flag, absurd-value guard).
This is the first cross-machine latency the real Apple client reports. SCOPE:
stage-1 AVSampleBufferDisplayLayer decodes+presents compressed samples internally
with no per-frame callback, so this excludes decode+present; true decode->present
needs the stage-2 presenter (VTDecompressionSession + CAMetalLayer). Rebuild
PunktfunkCore.xcframework (for the new C getter) before swift build/test on a Mac.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
// Unit tests for LatencyMeter: percentiles, the skew-corrected flag, reset-on-drain, and the
|
||||
// absurd-value guard. Latencies are constructed by stamping a pts a known interval in the past, so
|
||||
// the result is that interval plus the (tiny) clock advance between reads — asserted with tolerance.
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
@testable import PunktfunkKit
|
||||
|
||||
final class LatencyMeterTests: XCTestCase {
|
||||
private func nowRealtimeNs() -> UInt64 {
|
||||
var ts = timespec()
|
||||
clock_gettime(CLOCK_REALTIME, &ts)
|
||||
return UInt64(ts.tv_sec) * 1_000_000_000 + UInt64(ts.tv_nsec)
|
||||
}
|
||||
|
||||
func testEmptyDrainIsNil() {
|
||||
XCTAssertNil(LatencyMeter().drain())
|
||||
}
|
||||
|
||||
func testRecordsPercentilesAndResets() {
|
||||
let m = LatencyMeter()
|
||||
let now = nowRealtimeNs()
|
||||
// Each frame "captured" 5 ms ago, no skew offset → latency ≈ 5 ms.
|
||||
for _ in 0..<50 { m.record(ptsNs: now - 5_000_000, offsetNs: 0) }
|
||||
guard let s = m.drain() else { return XCTFail("expected samples") }
|
||||
XCTAssertEqual(s.count, 50)
|
||||
XCTAssertFalse(s.skewCorrected, "offset 0 ⇒ not skew-corrected")
|
||||
XCTAssertEqual(s.p50Ms, 5.0, accuracy: 2.0)
|
||||
XCTAssertGreaterThanOrEqual(s.p99Ms, s.p50Ms)
|
||||
XCTAssertNil(m.drain(), "drain resets the window")
|
||||
}
|
||||
|
||||
func testSkewCorrectedFlagSetByNonZeroOffset() {
|
||||
let m = LatencyMeter()
|
||||
let now = nowRealtimeNs()
|
||||
m.record(ptsNs: now - 1_000_000, offsetNs: 250_000) // 1 ms ago, +0.25 ms offset
|
||||
XCTAssertEqual(m.drain()?.skewCorrected, true)
|
||||
}
|
||||
|
||||
func testDropsAbsurdValues() {
|
||||
let m = LatencyMeter()
|
||||
let now = nowRealtimeNs()
|
||||
// pts 1 s in the future → negative latency → dropped.
|
||||
m.record(ptsNs: now + 1_000_000_000, offsetNs: 0)
|
||||
// pts absurdly far in the past → > 10 s latency → dropped.
|
||||
m.record(ptsNs: now - 20_000_000_000, offsetNs: 0)
|
||||
XCTAssertNil(m.drain())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user