feat(core): mid-stream clock re-sync — live offset survives wall-clock steps and drift
Networking-audit deferred plan §2. The host↔client offset was measured once at connect; an NTP step or slow drift silently corrupted the clock-based jump-to-live signal, the ABR one-way-delay signal, and every latency stat — 4a3b1ae2's disarm backstop stopped the IDR storm but lost the detector for the session. Now the client re-estimates mid-stream and recovers it. - quic: ClockResync — the connect-time 8-round probe/echo estimate as a select!-driven state machine (rounds matched by echoed t1, stale batches ignored), plus accept_resync (batch min-RTT ≤ max(2 ms, 1.5× connect RTT) so a congested window can never bias the offset). No wire change: the host has always answered ClockProbe at any time on the control stream. - client: the offset lives in an Arc<AtomicI64> seeded at connect; the control task re-probes every 60 s and immediately after the pump's FIRST no-op clock flush (the "clock stepped under me" signal, sent on the next report tick). On apply: store, reset stale_frames/noop_clock_flushes, re-arm the clock detector if a step had disarmed it. The disarm heuristic stays as the final backstop. Public NativeClient::clock_offset_ns keeps the connect-time value (ABI untouched); new clock_offset_now_ns() / clock_offset_shared() expose the live value. - consumers migrated to the live offset: pf-client-core session stats, the pf-presenter e2e stamp, Windows session/render, Android feeder/drain/ DisplayTracker (the tracker holds the shared handle, not the client, so the leaked render-callback refcount can't pin the session). - probe: --clock-resync runs a second full handshake mid-connection and asserts a sane, consistent estimate. Live against the local canary host: offsets 8646/2139 ns, disagreement 6 µs, 8/8 rounds — OK. Unit tests cover the round collection, stale-echo rejection, batch restart, min-RTT selection, and the acceptance guard. cargo ndk check green. Remaining manual validation: `sudo date -s "+2 sec"` on a live streaming client → expect one no-op flush, a re-sync, re-armed detector, no IDR pulse. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,7 +12,7 @@
|
||||
use crate::present::Presenter;
|
||||
use crate::session::{FrameRx, FrameTimes};
|
||||
use crossbeam_channel::RecvTimeoutError;
|
||||
use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
|
||||
use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU32, AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
@@ -122,12 +122,13 @@ unsafe impl Send for SendPresenter {}
|
||||
|
||||
/// Spawn the render thread. `frames` carries `(frame, FrameTimes)`; `clock_offset_ns` maps our
|
||||
/// wall clock onto the host's so the end-to-end (capture→on-glass) number is cross-machine valid
|
||||
/// (same math as the pump's host+network stage).
|
||||
/// (same math as the pump's host+network stage). A live handle (loaded per present) so
|
||||
/// mid-stream clock re-syncs keep the number honest after an NTP step / drift.
|
||||
pub fn spawn(
|
||||
presenter: Presenter,
|
||||
frames: FrameRx,
|
||||
shared: Arc<RenderShared>,
|
||||
clock_offset_ns: i64,
|
||||
clock_offset_ns: Arc<AtomicI64>,
|
||||
) -> RenderThread {
|
||||
let boxed = SendPresenter(presenter);
|
||||
let shared_w = shared.clone();
|
||||
@@ -162,7 +163,12 @@ fn poll_window_dpi() -> Option<u32> {
|
||||
}
|
||||
}
|
||||
|
||||
fn run(presenter: SendPresenter, frames: FrameRx, shared: Arc<RenderShared>, clock_offset_ns: i64) {
|
||||
fn run(
|
||||
presenter: SendPresenter,
|
||||
frames: FrameRx,
|
||||
shared: Arc<RenderShared>,
|
||||
clock_offset_ns: Arc<AtomicI64>,
|
||||
) {
|
||||
let mut p = presenter.0;
|
||||
let mut applied = (0u32, 0u32, 0u32); // last (w, h, dpi) handed to the presenter
|
||||
let mut presented = 0u32;
|
||||
@@ -232,8 +238,9 @@ fn run(presenter: SendPresenter, frames: FrameRx, shared: Arc<RenderShared>, clo
|
||||
let displayed_ns = now_ns();
|
||||
// End-to-end = capture → displayed, host-clock corrected, measured directly
|
||||
// (never the sum of stage percentiles). Clamped (0, 10 s).
|
||||
let e2e =
|
||||
(displayed_ns as i128 + clock_offset_ns as i128 - t.pts_ns as i128).max(0) as u64;
|
||||
let e2e = (displayed_ns as i128 + clock_offset_ns.load(Ordering::Relaxed) as i128
|
||||
- t.pts_ns as i128)
|
||||
.max(0) as u64;
|
||||
if e2e > 0 && e2e < 10_000_000_000 {
|
||||
e2e_us.push(e2e / 1000);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user