feat(client/present): the cadence statistic — measure judder, not just latency
WP1 of design/presenter-cadence-rework-implementation-plan.md. Shared core
type plus the Android binding; desktop and Apple follow.
Every stat we publish is a latency — a difference between two points on one
frame. No latency can see judder, because judder is a property of the
SEQUENCE. A stream that shows each frame one refresh early and the next one
late has excellent percentiles and looks broken; a stream whose every
interval is exactly two refreshes has worse latency than one alternating 1
and 3, and looks perfect. That blind spot is why a smoothness complaint
could not be confirmed or refuted from our own telemetry.
PresentIntervals quantises the spacing between consecutive on-glass instants
onto the learned panel grid and reports the modal spacing plus the fraction
of intervals that miss it — the judder number, in permille to match the
phase coherence already next to it. Scale-free: the mode absorbs the cadence
ratio, so 60-on-120 and 120-on-120 are both "one tall bucket" and directly
comparable. That is what makes it usable as one ruler across clients,
refresh rates and stream rates, and for a feature-on/off A/B.
Deliberate choices, each with a test:
- fed the MEASURED on-glass instant, never the requested present time,
which would measure our own intent and always look perfect
- fed SurfaceFlinger's raw CLOCK_MONOTONIC render stamp, not the
realtime-rebased one the latency stats use: cadence is about spacing,
and a realtime clock step would forge a hitch that never happened
- stalls (>8 refreshes) and out-of-order callbacks counted apart from the
ratio, so a window that looks smooth because the stream was PAUSED
cannot be mistaken for a good one
- sub-refresh jitter is not judder: the display quantises it away, so the
metric must too
- the predecessor survives a window drain, else one interval per window
would go unscored forever
Always-on via the 1 Hz pf.present line, so the HUD-off wireless A/B the
baseline measurement needs is readable from logcat. HUD surfacing waits on
the stats-unification spec amendment (plan S3) and on the in-flight HUD work.
Gates: punktfunk-core 189 tests green (10 new); cargo ndk check + clippy
arm64 clean — the 5 remaining warnings are pre-existing and in other files.
This commit is contained in:
@@ -185,7 +185,7 @@ unsafe extern "C" fn on_frame_rendered(
|
||||
let display_us = paired.and_then(|(d, _)| clamp(displayed_ns - d));
|
||||
let latch_us = paired.and_then(|(_, r)| clamp(displayed_ns - r));
|
||||
// Always-on half: the presenter's pf-present line reads these with the HUD off.
|
||||
t.meter.note_latch(latch_us);
|
||||
t.meter.note_latch(latch_us, system_nano);
|
||||
if !t.stats.enabled() {
|
||||
return; // HUD hidden — skip the skew math + the stats lock
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
use ndk::media::media_codec::MediaCodec;
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::atomic::{AtomicBool, AtomicI32, Ordering};
|
||||
use std::sync::atomic::{AtomicBool, AtomicI32, AtomicI64, Ordering};
|
||||
use std::sync::Mutex;
|
||||
use std::time::Instant;
|
||||
|
||||
@@ -152,6 +152,10 @@ pub(super) struct PresentMeter {
|
||||
/// This device delivers render callbacks at all (API ≥ 33 and the platform accepted the
|
||||
/// registration). Until one arrives, `undisplayed` is meaningless and the rail stays down.
|
||||
confirms: AtomicBool,
|
||||
/// The learned panel period the cadence statistic quantises against, republished by
|
||||
/// [`Presenter::pump`] (the callback thread has no access to the vsync clock). 0 until the
|
||||
/// grid is known, which simply means cadence is not scored yet.
|
||||
panel_period_ns: AtomicI64,
|
||||
}
|
||||
|
||||
struct PresentMeterInner {
|
||||
@@ -166,6 +170,9 @@ struct PresentMeterInner {
|
||||
/// Capture→decoded end-to-end µs (skew-corrected, clamped) — always on for the same reason:
|
||||
/// the wireless A/B's headline without having to reach the on-screen HUD.
|
||||
e2e_us: Vec<u64>,
|
||||
/// The cadence (judder) statistic — the only stat here that is not a latency, and the only
|
||||
/// one that can see a pacing defect. See [`punktfunk_core::phase::PresentIntervals`].
|
||||
intervals: punktfunk_core::phase::PresentIntervals,
|
||||
}
|
||||
|
||||
impl PresentMeter {
|
||||
@@ -177,19 +184,33 @@ impl PresentMeter {
|
||||
feed_us: Vec::with_capacity(256),
|
||||
codec_us: Vec::with_capacity(256),
|
||||
e2e_us: Vec::with_capacity(256),
|
||||
intervals: punktfunk_core::phase::PresentIntervals::new(),
|
||||
}),
|
||||
undisplayed: AtomicI32::new(0),
|
||||
confirms: AtomicBool::new(false),
|
||||
panel_period_ns: AtomicI64::new(0),
|
||||
}
|
||||
}
|
||||
|
||||
/// Republish the learned panel period for the cadence statistic (presenter thread).
|
||||
pub(super) fn set_panel_period(&self, period_ns: i64) {
|
||||
self.panel_period_ns.store(period_ns, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// One displayed frame's release→displayed latch, µs. Callback thread; poison-proof.
|
||||
///
|
||||
/// Also the glass budget's CONFIRM: this frame left the BufferQueue, so one outstanding
|
||||
/// release is settled. Clamped at zero — the legacy `arrival` path renders without going
|
||||
/// through [`Presenter::pump`], so confirms can outnumber counted releases.
|
||||
pub(super) fn note_latch(&self, latch_us: Option<u64>) {
|
||||
///
|
||||
/// `present_mono_ns` is SurfaceFlinger's own render timestamp, raw on `CLOCK_MONOTONIC` —
|
||||
/// deliberately not the realtime-rebased instant the latency stats use. Cadence is a
|
||||
/// statistic about *spacing*, and a realtime clock step (NTP) would forge a hitch that never
|
||||
/// happened. Garbage stamps need no special handling here: an implausible one lands in the
|
||||
/// stall or disordered counters rather than the judder ratio.
|
||||
pub(super) fn note_latch(&self, latch_us: Option<u64>, present_mono_ns: i64) {
|
||||
self.confirms.store(true, Ordering::Relaxed);
|
||||
let period_ns = self.panel_period_ns.load(Ordering::Relaxed);
|
||||
let _ = self
|
||||
.undisplayed
|
||||
.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |v| {
|
||||
@@ -200,6 +221,7 @@ impl PresentMeter {
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
g.displays += 1;
|
||||
g.intervals.record(present_mono_ns, period_ns);
|
||||
if let Some(l) = latch_us {
|
||||
if g.latch_us.len() < 4096 {
|
||||
g.latch_us.push(l);
|
||||
@@ -258,7 +280,16 @@ impl PresentMeter {
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)] // one caller unpacks it in place; a struct would be noise
|
||||
fn drain(&self) -> (Vec<u64>, u64, Vec<u64>, Vec<u64>, Vec<u64>) {
|
||||
fn drain(
|
||||
&self,
|
||||
) -> (
|
||||
Vec<u64>,
|
||||
u64,
|
||||
Vec<u64>,
|
||||
Vec<u64>,
|
||||
Vec<u64>,
|
||||
Option<punktfunk_core::phase::PresentCadence>,
|
||||
) {
|
||||
let mut g = self
|
||||
.inner
|
||||
.lock()
|
||||
@@ -271,6 +302,7 @@ impl PresentMeter {
|
||||
std::mem::take(&mut g.feed_us),
|
||||
std::mem::take(&mut g.codec_us),
|
||||
std::mem::take(&mut g.e2e_us),
|
||||
g.intervals.take(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -410,6 +442,11 @@ impl Presenter {
|
||||
stats: &crate::stats::VideoStats,
|
||||
now_mono_ns: i64,
|
||||
) -> bool {
|
||||
// The callback thread scores cadence but cannot see the vsync clock — republish the grid
|
||||
// it quantises against. Relaxed: a period change is rare and one stale sample is noise.
|
||||
if let Some(c) = clock {
|
||||
meter.set_panel_period(c.panel_period_ns().max(c.period_ns()));
|
||||
}
|
||||
// Budget bookkeeping first: reopen on the predicted latch, force-open on the backstop.
|
||||
if let Some(f) = &self.inflight {
|
||||
if now_mono_ns >= f.reopen_at_ns {
|
||||
@@ -547,7 +584,11 @@ impl Presenter {
|
||||
/// `pace` (decoded→release) / `latch` (release→displayed) /
|
||||
/// `feed`+`codec` (the decode stage split: received→queued hand-off/slot wait + the
|
||||
/// codec-pure queued→decoded time) / `e2e` (capture→decoded, skew-corrected — the wireless
|
||||
/// A/B headline) / `vsync` (the measured panel period).
|
||||
/// A/B headline) / `vsync` (the measured panel period) /
|
||||
/// `judder` (‰ of present intervals off the modal spacing — the cadence statistic, and the
|
||||
/// only number here that can see a pacing defect) / `mode` (the modal spacing in refreshes:
|
||||
/// 1 at panel rate, 2 for 60-on-120) / `stalls` + `disorder` (excluded from the ratio; see
|
||||
/// [`punktfunk_core::phase::PresentIntervals`]).
|
||||
///
|
||||
/// Returns this window's CIRCULAR latch statistics `(vector-mean latch ns mod panel period,
|
||||
/// coherence ‰)` when a window actually flushed — the phase-lock reporter's v2 error signal
|
||||
@@ -561,7 +602,7 @@ impl Presenter {
|
||||
return None;
|
||||
}
|
||||
self.last_flush = Instant::now();
|
||||
let (latch, displays, feed, codec, e2e) = meter.drain();
|
||||
let (latch, displays, feed, codec, e2e, cadence) = meter.drain();
|
||||
if self.released == 0 && displays == 0 {
|
||||
return None; // idle stream — nothing worth a line
|
||||
}
|
||||
@@ -584,7 +625,8 @@ impl Presenter {
|
||||
paceMs p50={:.2} max={:.2} latchMs p50={:.2} max={:.2} \
|
||||
feedMs p50={:.2} max={:.2} codecMs p50={:.2} max={:.2} \
|
||||
e2eMs p50={:.2} max={:.2} circ={:.2}ms coh={} \
|
||||
vsyncMs={:.2} panelMs={:.2}",
|
||||
vsyncMs={:.2} panelMs={:.2} \
|
||||
judder={}permille mode={}vsync stalls={} disorder={}",
|
||||
self.released,
|
||||
displays,
|
||||
self.paced_drops,
|
||||
@@ -607,6 +649,10 @@ impl Presenter {
|
||||
circ.map(|(_, c)| c).unwrap_or(0),
|
||||
period_ms,
|
||||
panel_ns as f64 / 1e6,
|
||||
cadence.map(|c| c.judder_permille).unwrap_or(0),
|
||||
cadence.map(|c| c.mode_units).unwrap_or(0),
|
||||
cadence.map(|c| c.stalls).unwrap_or(0),
|
||||
cadence.map(|c| c.disordered).unwrap_or(0),
|
||||
);
|
||||
self.released = 0;
|
||||
// Margin adaptation, off the MEASURED latch. A release targets the first grid point past
|
||||
|
||||
@@ -129,6 +129,140 @@ pub fn circular_latch(samples_us: &[u64], period_ns: i64) -> Option<(u64, u16)>
|
||||
Some((mean_ns, (r * 1000.0) as u16))
|
||||
}
|
||||
|
||||
/// Largest present spacing still treated as cadence. Anything wider is a stall (a stream pause,
|
||||
/// an occluded window, a codec rebuild) and is counted separately: folding a 5-second gap in as
|
||||
/// "one irregular interval" would be true but useless, and folding it in as several would make a
|
||||
/// single hitch dominate the window.
|
||||
const CADENCE_MAX_UNITS: usize = 8;
|
||||
|
||||
/// Minimum intervals before a cadence summary means anything — same evidence bar as
|
||||
/// [`circular_latch`]. At any sane frame rate a 1 s window clears this many times over; it is
|
||||
/// there so a window truncated by a reanchor does not publish a judder figure off three samples.
|
||||
const CADENCE_MIN_SAMPLES: u32 = 8;
|
||||
|
||||
/// One window's present-cadence summary (see [`PresentIntervals`]).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct PresentCadence {
|
||||
/// The most common spacing, in whole panel refreshes. This is the stream's cadence ratio:
|
||||
/// 1 when stream rate matches the panel, 2 for 60-on-120, 4 for 30-on-120.
|
||||
pub mode_units: u8,
|
||||
/// Fraction of intervals that were NOT the mode, in ‰ (same unit as the phase coherence).
|
||||
/// **This is the judder number.** 0 = a perfectly regular cadence at any ratio.
|
||||
pub judder_permille: u16,
|
||||
/// Intervals folded into the histogram (excludes stalls and disordered samples).
|
||||
pub samples: u32,
|
||||
/// Spacings wider than [`CADENCE_MAX_UNITS`] — stalls, not judder. Reported so a window that
|
||||
/// looks smooth *because the stream was paused* cannot be mistaken for a good one.
|
||||
pub stalls: u32,
|
||||
/// Present instants that did not advance (duplicate or out-of-order callbacks). A platform
|
||||
/// bookkeeping signal, not a display defect — kept out of the judder ratio deliberately.
|
||||
pub disordered: u32,
|
||||
}
|
||||
|
||||
/// Present-interval distribution in whole panel refreshes — the cadence (judder) statistic.
|
||||
///
|
||||
/// Every other stat we publish is a latency: a difference between two points on one frame. No
|
||||
/// latency can see judder, because judder is a property of the *sequence*. A stream that shows
|
||||
/// each frame one refresh early and the next one late has excellent percentiles and looks
|
||||
/// broken; a stream whose every interval is exactly two refreshes has worse latency than one
|
||||
/// that alternates 1 and 3, and looks perfect. Quantising the spacing between consecutive
|
||||
/// on-glass instants onto the panel grid measures the thing the eye actually reacts to.
|
||||
///
|
||||
/// Scale-free by construction: it needs no reference clock, and the *mode* absorbs the cadence
|
||||
/// ratio, so 60-on-120 and 120-on-120 are both "smooth = one tall bucket" and comparable to each
|
||||
/// other. That is what makes it usable as one ruler across clients, refresh rates and stream
|
||||
/// rates — including for a feature-on/feature-off A/B on the same device.
|
||||
///
|
||||
/// Feed it the **measured on-glass instant**, never the instant a present was *requested*:
|
||||
/// requested times would measure our own intent and report a perfect cadence no matter what the
|
||||
/// display did with it. Every client has the real one (Android's `OnFrameRendered` system time,
|
||||
/// the desktop's `VK_KHR_present_wait` stamp, Apple's drawable `presentedTime`).
|
||||
///
|
||||
/// Pure state and arithmetic — no clock, no allocation. The caller owns the window: drain with
|
||||
/// [`take`](Self::take) on its own 1 s tumbling boundary, per `design/stats-unification.md`.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct PresentIntervals {
|
||||
last_present_ns: i64,
|
||||
/// Counts indexed by whole refreshes, `0..=CADENCE_MAX_UNITS`.
|
||||
hist: [u32; CADENCE_MAX_UNITS + 1],
|
||||
samples: u32,
|
||||
stalls: u32,
|
||||
disordered: u32,
|
||||
}
|
||||
|
||||
impl PresentIntervals {
|
||||
pub fn new() -> PresentIntervals {
|
||||
PresentIntervals::default()
|
||||
}
|
||||
|
||||
/// Forget the previous instant without discarding the window's counts. Call on any
|
||||
/// discontinuity where the next present is not a continuation of this cadence (reanchor,
|
||||
/// codec rebuild, surface recreate) so the gap across it is not scored as a stall.
|
||||
pub fn split(&mut self) {
|
||||
self.last_present_ns = 0;
|
||||
}
|
||||
|
||||
/// Fold one on-glass instant. `period_ns` is the learned panel period
|
||||
/// ([`PanelGrid::period_ns`]); a non-positive one means the grid is not known yet and the
|
||||
/// sample is held as the new predecessor without being scored.
|
||||
pub fn record(&mut self, present_ns: i64, period_ns: i64) {
|
||||
let prev = std::mem::replace(&mut self.last_present_ns, present_ns);
|
||||
if prev <= 0 || period_ns <= 0 {
|
||||
return; // first sample of a run, or no grid to quantise against
|
||||
}
|
||||
let spacing = present_ns - prev;
|
||||
if spacing <= 0 {
|
||||
// A repeated or out-of-order callback. Keep the LATER instant as the predecessor so
|
||||
// one disordered delivery cannot corrupt every following spacing.
|
||||
self.disordered += 1;
|
||||
self.last_present_ns = prev.max(present_ns);
|
||||
return;
|
||||
}
|
||||
// Round to the nearest whole refresh: a present is "on the grid" if it is closer to this
|
||||
// vblank than the next, which is exactly what the display did with it.
|
||||
let units = (spacing * 2 + period_ns) / (period_ns * 2);
|
||||
if units as usize > CADENCE_MAX_UNITS {
|
||||
self.stalls += 1;
|
||||
return;
|
||||
}
|
||||
self.hist[units as usize] += 1;
|
||||
self.samples += 1;
|
||||
}
|
||||
|
||||
/// This window's summary, or `None` under [`CADENCE_MIN_SAMPLES`].
|
||||
pub fn summary(&self) -> Option<PresentCadence> {
|
||||
if self.samples < CADENCE_MIN_SAMPLES {
|
||||
return None;
|
||||
}
|
||||
let (mode_units, mode_count) = self
|
||||
.hist
|
||||
.iter()
|
||||
.enumerate()
|
||||
.max_by_key(|&(_, c)| *c)
|
||||
.map(|(i, &c)| (i as u8, c))?;
|
||||
Some(PresentCadence {
|
||||
mode_units,
|
||||
judder_permille: (u64::from(self.samples - mode_count) * 1000 / u64::from(self.samples))
|
||||
as u16,
|
||||
samples: self.samples,
|
||||
stalls: self.stalls,
|
||||
disordered: self.disordered,
|
||||
})
|
||||
}
|
||||
|
||||
/// Drain the window: the summary (if it clears the evidence bar) and a reset of the counts.
|
||||
/// The previous instant SURVIVES the drain — the cadence continues across a window boundary,
|
||||
/// and dropping it would manufacture one unscored interval per window.
|
||||
pub fn take(&mut self) -> Option<PresentCadence> {
|
||||
let out = self.summary();
|
||||
self.hist = [0; CADENCE_MAX_UNITS + 1];
|
||||
self.samples = 0;
|
||||
self.stalls = 0;
|
||||
self.disordered = 0;
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -294,3 +428,154 @@ mod panel_grid_tests {
|
||||
assert_eq!(g.period_ns(), P120, "and the real grid wins it back");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod cadence_tests {
|
||||
use super::*;
|
||||
|
||||
const P: i64 = 8_333_333; // 120 Hz in ns
|
||||
|
||||
/// Fold `n` presents spaced by `spacings` in rotation, starting at an arbitrary instant.
|
||||
fn cadence(spacings: &[i64], n: usize) -> PresentIntervals {
|
||||
let mut pi = PresentIntervals::new();
|
||||
let mut t = 1_000_000_000i64;
|
||||
pi.record(t, P);
|
||||
for i in 0..n {
|
||||
t += spacings[i % spacings.len()];
|
||||
pi.record(t, P);
|
||||
}
|
||||
pi
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_regular_cadence_has_no_judder() {
|
||||
let s = cadence(&[P], 60).summary().unwrap();
|
||||
assert_eq!((s.mode_units, s.judder_permille), (1, 0));
|
||||
assert_eq!(s.samples, 60);
|
||||
}
|
||||
|
||||
/// The property that makes this one ruler across rates: a stream at half the panel rate is
|
||||
/// SMOOTH, not judder — the mode absorbs the cadence ratio.
|
||||
fn ratio_is_absorbed_not_penalised(mult: i64, expect_units: u8) {
|
||||
let s = cadence(&[P * mult], 40).summary().unwrap();
|
||||
assert_eq!((s.mode_units, s.judder_permille), (expect_units, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sixty_on_onetwenty_reads_smooth() {
|
||||
ratio_is_absorbed_not_penalised(2, 2); // 60 fps on a 120 Hz panel
|
||||
ratio_is_absorbed_not_penalised(4, 4); // 30 fps on a 120 Hz panel
|
||||
}
|
||||
|
||||
/// D3's signature: the same mean spacing as `sixty_on_onetwenty_reads_smooth`, delivered as
|
||||
/// alternating 1 and 3 refreshes. Identical average frame rate, identical latency
|
||||
/// percentiles — and this is the one that looks broken.
|
||||
#[test]
|
||||
fn the_sawtooth_that_latency_stats_cannot_see() {
|
||||
let s = cadence(&[P, P * 3], 40).summary().unwrap();
|
||||
assert_eq!(s.judder_permille, 500);
|
||||
assert!(matches!(s.mode_units, 1 | 3));
|
||||
}
|
||||
|
||||
/// Sub-refresh jitter is not judder: the display quantises it away, so the metric must too.
|
||||
/// Only a spacing that crosses the half-refresh boundary changes which vblank was used.
|
||||
#[test]
|
||||
fn jitter_inside_a_refresh_is_not_judder() {
|
||||
let s = cadence(&[P + P * 2 / 5, P - P * 2 / 5], 40)
|
||||
.summary()
|
||||
.unwrap();
|
||||
assert_eq!((s.mode_units, s.judder_permille), (1, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_stall_is_counted_apart_from_judder() {
|
||||
let mut pi = PresentIntervals::new();
|
||||
let mut t = 1_000_000_000i64;
|
||||
pi.record(t, P);
|
||||
for _ in 0..20 {
|
||||
t += P;
|
||||
pi.record(t, P);
|
||||
}
|
||||
t += P * 400; // a pause, not a pacing defect
|
||||
pi.record(t, P);
|
||||
let s = pi.summary().unwrap();
|
||||
assert_eq!((s.judder_permille, s.stalls, s.samples), (0, 1, 20));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn out_of_order_callbacks_do_not_corrupt_the_run() {
|
||||
let mut pi = PresentIntervals::new();
|
||||
let mut t = 1_000_000_000i64;
|
||||
pi.record(t, P);
|
||||
for _ in 0..10 {
|
||||
t += P;
|
||||
pi.record(t, P);
|
||||
}
|
||||
pi.record(t - P * 3, P); // a late/duplicate delivery
|
||||
for _ in 0..10 {
|
||||
t += P;
|
||||
pi.record(t, P);
|
||||
}
|
||||
let s = pi.summary().unwrap();
|
||||
assert_eq!(s.disordered, 1);
|
||||
assert_eq!(
|
||||
s.judder_permille, 0,
|
||||
"keeping the later instant means the following spacings stay on the grid"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_unknown_grid_scores_nothing() {
|
||||
let s = cadence(&[P], 60);
|
||||
let mut pi = PresentIntervals::new();
|
||||
let mut t = 1_000_000_000i64;
|
||||
for _ in 0..60 {
|
||||
t += P;
|
||||
pi.record(t, 0); // PanelGrid has not learned a period yet
|
||||
}
|
||||
assert!(pi.summary().is_none());
|
||||
assert!(s.summary().is_some(), "control");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_short_window_publishes_nothing() {
|
||||
assert!(cadence(&[P], 5).summary().is_none());
|
||||
}
|
||||
|
||||
/// The cadence continues across a window boundary — dropping the predecessor on drain would
|
||||
/// silently discard one interval per window, every window.
|
||||
#[test]
|
||||
fn take_resets_the_counts_but_not_the_cadence() {
|
||||
let mut pi = cadence(&[P], 20);
|
||||
assert!(pi.take().is_some());
|
||||
assert!(pi.summary().is_none(), "counts cleared");
|
||||
let mut t = 1_000_000_000 + P * 20;
|
||||
for _ in 0..10 {
|
||||
t += P;
|
||||
pi.record(t, P);
|
||||
}
|
||||
let s = pi.summary().unwrap();
|
||||
assert_eq!(
|
||||
s.samples, 10,
|
||||
"the first post-drain present scored against the pre-drain one"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_forgets_the_predecessor() {
|
||||
let mut pi = cadence(&[P], 20);
|
||||
pi.take();
|
||||
pi.split();
|
||||
let mut t = 5_000_000_000i64; // a reanchor: the gap across it is meaningless
|
||||
for _ in 0..10 {
|
||||
t += P;
|
||||
pi.record(t, P);
|
||||
}
|
||||
let s = pi.summary().unwrap();
|
||||
assert_eq!(
|
||||
(s.samples, s.stalls),
|
||||
(9, 0),
|
||||
"the gap was not scored at all"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user