fix(client/net): split the receipt stamp from the pull + bleed standing latency
ci / docs-site (push) Successful in 56s
apple / swift (push) Successful in 1m14s
ci / web (push) Successful in 2m30s
decky / build-publish (push) Successful in 19s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 9s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 9s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 7s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 9s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 9s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 9s
ci / bench (push) Successful in 5m57s
release / apple (push) Successful in 9m8s
deb / build-publish (push) Successful in 9m33s
arch / build-publish (push) Successful in 13m14s
docker / deploy-docs (push) Successful in 23s
flatpak / build-publish (push) Failing after 8m1s
deb / build-publish-host (push) Successful in 10m24s
android / android (push) Successful in 14m56s
apple / screenshots (push) Successful in 6m43s
ci / rust (push) Successful in 19m29s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 16m35s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 15m29s
windows-host / package (push) Successful in 15m13s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 5m48s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 6m38s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 7m45s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 8m51s
ci / docs-site (push) Successful in 56s
apple / swift (push) Successful in 1m14s
ci / web (push) Successful in 2m30s
decky / build-publish (push) Successful in 19s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 9s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 9s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 7s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 9s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 9s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 9s
ci / bench (push) Successful in 5m57s
release / apple (push) Successful in 9m8s
deb / build-publish (push) Successful in 9m33s
arch / build-publish (push) Successful in 13m14s
docker / deploy-docs (push) Successful in 23s
flatpak / build-publish (push) Failing after 8m1s
deb / build-publish-host (push) Successful in 10m24s
android / android (push) Successful in 14m56s
apple / screenshots (push) Successful in 6m43s
ci / rust (push) Successful in 19m29s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 16m35s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 15m29s
windows-host / package (push) Successful in 15m13s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 5m48s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 6m38s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 7m45s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 8m51s
The two-pair investigation (wired Mac clients stuck at a rock-steady ~18-19 ms "network" that survived the load ending and cleared only on reconnect) exposed two structural gaps, one of measurement and one of recovery: - Receipt was stamped at the hand-off PULL (Swift nextAU, pf-client-core, Android decode loops), not at reassembly completion — so any client-side standing state between the reassembler and the pull read as NETWORK latency, undiagnosable from the HUD. ABI v9: `PunktfunkFrame`/`Frame` grow `received_ns`, stamped by `Session::poll_frame` as the AU crosses the session boundary. Every embedder now uses the core stamp; the Apple client keeps the pull instant as `AccessUnit.pulledNs` and shows the receipt→pull wait as its own "client queue" term (detailed HUD tier from 2 ms + a `queue_p50` stats-log field). Decode stages keep their pull anchor on all platforms, so no historical stage shifts meaning. - The jump-to-live detectors deliberately ignore anything under 6 queued frames / 400 ms behind — so a small, constant, loss-free elevation (a sub-frame standing backlog, or a stale clock offset after a wall-clock step/slew) is carried for the rest of the session. New third detector (`StandingLatency`, unit-tested ladder): window-MIN one-way delay ≥ 10 ms above the session floor with zero loss for ~4.5 s escalates gently — a free clock re-sync first (an applied re-sync re-bases the floor), then at most 3 flush+keyframe bleeds sharing the jump-to-live cooldown, then a loud disarm naming what it means. Loss windows reset the run: congestion belongs to FEC/ABR, not this detector. Also: mid-stream re-sync apply/discard logs debug→info — they are the forensic trail for the stale-offset case and were invisible in the field. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -102,6 +102,12 @@ final class SessionModel: ObservableObject {
|
||||
@Published var decodeValid = false
|
||||
@Published var displayP50Ms = 0.0
|
||||
@Published var displayValid = false
|
||||
/// Client-queue wait: core reassembly receipt → the pump's pull (`AccessUnit.pulledNs −
|
||||
/// receivedNs`, ABI v9 receipt split — the 2026-07 two-pair investigation). ~0 on a healthy
|
||||
/// stream; a persistent value is a client-side standing backlog that used to hide inside
|
||||
/// "network". Shown in the detailed tier only when it says something (≥ ~2 ms).
|
||||
@Published var clientQueueP50Ms = 0.0
|
||||
@Published var clientQueueValid = false
|
||||
/// The measured OS present floor (design/apple-presentation-rebuild.md): the deadline
|
||||
/// engine's vend→glass pipeline depth — an OS property no client can pace under (~2 refresh
|
||||
/// intervals composited; would read ~1 under direct-to-display). The HUD subtracts it from
|
||||
@@ -147,6 +153,9 @@ final class SessionModel: ObservableObject {
|
||||
let endToEnd = LatencyMeter()
|
||||
let decodeStage = LatencyMeter()
|
||||
let displayStage = LatencyMeter()
|
||||
/// Client-queue sampler (see `clientQueueP50Ms`) — fed per AU by the stream view's onFrame,
|
||||
/// drained by the same 1 s tick as the stage meters.
|
||||
let clientQueue = LatencyMeter()
|
||||
/// The OS present floor sampler (see `osFloorP50Ms`) — fed one sample per display-link
|
||||
/// update by the deadline engine, drained by the same 1 s tick as the stage meters.
|
||||
let presentFloor = LatencyMeter()
|
||||
@@ -489,6 +498,7 @@ final class SessionModel: ObservableObject {
|
||||
endToEndValid = false
|
||||
decodeValid = false
|
||||
displayValid = false
|
||||
clientQueueValid = false
|
||||
osFloorValid = false
|
||||
lostFrames = 0
|
||||
lostPct = 0
|
||||
@@ -679,6 +689,12 @@ final class SessionModel: ObservableObject {
|
||||
} else {
|
||||
self.osFloorValid = false
|
||||
}
|
||||
if let q = self.clientQueue.drain() {
|
||||
self.clientQueueP50Ms = q.p50Ms
|
||||
self.clientQueueValid = true
|
||||
} else {
|
||||
self.clientQueueValid = false
|
||||
}
|
||||
// Mirror the window to the unified log (see statsLog) — one line per second,
|
||||
// stages in ms, only while frames actually flowed. `fps` counts RECEIVED AUs;
|
||||
// `presents` counts frames that reached glass (the display meter's sample count)
|
||||
@@ -691,7 +707,7 @@ final class SessionModel: ObservableObject {
|
||||
let line = String(
|
||||
format: "fps=%d presents=%d e2e_p50=%.1f e2e_p95=%.1f hostnet_p50=%.1f "
|
||||
+ "decode_p50=%.1f display_p50=%.1f lost=%d "
|
||||
+ "floor_p50=%.1f display_adj=%.1f e2e_adj=%.1f",
|
||||
+ "floor_p50=%.1f display_adj=%.1f e2e_adj=%.1f queue_p50=%.1f",
|
||||
frames,
|
||||
displayWindow?.count ?? 0,
|
||||
self.endToEndValid ? self.endToEndP50Ms : -1,
|
||||
@@ -702,7 +718,8 @@ final class SessionModel: ObservableObject {
|
||||
lost,
|
||||
self.osFloorValid ? self.osFloorP50Ms : -1,
|
||||
self.displayValid ? self.displayAdjP50Ms : -1,
|
||||
self.endToEndValid ? self.endToEndAdjP50Ms : -1)
|
||||
self.endToEndValid ? self.endToEndAdjP50Ms : -1,
|
||||
self.clientQueueValid ? self.clientQueueP50Ms : -1)
|
||||
statsLog.info("\(line, privacy: .public)")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user