Skip to main content

datui_lib/
measurements.rs

1//! What opening a dataset cost, measured rather than guessed.
2//!
3//! Every number here is one datui produced itself: it timed its own listing, counted
4//! its own requests, and added up the bytes it received. Where Polars does the reading
5//! datui cannot count the requests or the bytes, and this module records nothing rather
6//! than record a figure it cannot stand behind. That is why so much of what it holds is
7//! optional: a stretch reports a file count, a request count and a byte count only
8//! where it has one, and `docs/user-guide/dataset-info.md` says which routes have
9//! which.
10//!
11//! The tallies are written by the threads doing the reading, which is why they are
12//! atomic, and read by the render, which is why nothing here ever blocks.
13
14use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
15use std::time::Duration;
16
17/// One stretch of work, and what it cost.
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub struct Cost {
20    /// How long it took, from the first request to the last answer.
21    pub took: Duration,
22    /// The data files found, or the footers read — `None` where the stretch did the
23    /// work without ever learning a count.
24    ///
25    /// Every route that reaches a user today knows its count. The one that does not is
26    /// the walk to a prefix's two ends, which never lists what lies between them; that
27    /// route cannot currently be reached (see `schema_from_one_cloud_hive`), and this
28    /// is what keeps it from reporting the two ends as the size of the dataset if it
29    /// ever is.
30    pub files: Option<usize>,
31    /// Requests datui made itself and can count, with the bytes they returned.
32    ///
33    /// `None` on every listing, and not only the local ones: a directory is read rather
34    /// than requested, and a remote prefix is one call whose round trips happen inside
35    /// the object store, which does not say how many there were. A figure of zero would
36    /// read as "no data moved" rather than "not measured here".
37    pub over_the_wire: Option<OverTheWire>,
38}
39
40/// Two stretches' wire figures as one.
41///
42/// The requests add. The bytes add only where both stretches weighed theirs: one that
43/// did not leaves the pair unable to say what its requests brought back, and carrying
44/// the other's figure forward would present it as the bytes behind all of them.
45fn combine_wire(a: OverTheWire, b: OverTheWire) -> OverTheWire {
46    OverTheWire {
47        requests: a.requests + b.requests,
48        bytes: a.bytes.zip(b.bytes).map(|(x, y)| x + y),
49    }
50}
51
52/// Every stretch added up: how long the open has taken, and what it asked for.
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub struct Total {
55    /// The stretches' times added together.
56    pub took: Duration,
57    /// The requests and bytes of the stretches that counted any, or `None` if none did.
58    pub over_the_wire: Option<OverTheWire>,
59}
60
61/// What datui asked for over a network, exactly.
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub struct OverTheWire {
64    /// Requests datui issued.
65    pub requests: usize,
66    /// Bytes those requests returned, where datui counted them.
67    ///
68    /// Nothing reports requests without weighing them today — only the footer reads
69    /// report requests at all, and they weigh what comes back. It is optional so that a
70    /// stretch which one day counts round trips it does not read cannot be made to
71    /// claim a figure of zero, which would say they arrived empty.
72    pub bytes: Option<u64>,
73}
74
75/// A running tally of one kind of work.
76///
77/// `ran` is not redundant with the other fields: a listing that returned nothing still
78/// took time and is still a measurement, so zero cannot stand in for "this never
79/// happened".
80#[derive(Debug, Default)]
81struct Tally {
82    ran: AtomicBool,
83    nanos: AtomicU64,
84    files: AtomicUsize,
85    /// Counted as the requests are made, and still climbing while a pass runs.
86    live_requests: AtomicUsize,
87    live_bytes: AtomicU64,
88    /// Whether anything recorded here counted bytes at all.
89    counted_bytes: AtomicBool,
90    /// What those counters stood at when a pass last finished, which is what is shown.
91    ///
92    /// Separate from the live pair because the time and the file count only move when a
93    /// pass ends: showing the live figures beside them would put a pass's requests next
94    /// to the previous pass's time, and read as forty-one requests over two files.
95    counted: AtomicBool,
96    requests: AtomicUsize,
97    bytes: AtomicU64,
98    /// Whether any stretch recorded here knew a count at all.
99    counted_files: AtomicBool,
100}
101
102impl Tally {
103    /// Record a finished stretch of work, adding it to whatever this tally already
104    /// holds.
105    ///
106    /// Adding rather than replacing because one kind of work can happen in more than
107    /// one stretch. A cloud dataset past a wave of concurrent reads opens on two
108    /// footers and then reads every footer behind the open; a dataset whose open could
109    /// not settle its row count reads every footer again to take it. What the footers
110    /// cost is all of those passes, re-reads included, which is why the count this
111    /// keeps is footers read rather than files. A dataset being opened starts from a
112    /// tally that holds nothing, so nothing is carried over from the last one.
113    fn record(&self, took: Duration, files: Option<usize>, over_the_wire: bool) {
114        self.nanos.fetch_add(
115            took.as_nanos().min(u64::MAX as u128) as u64,
116            Ordering::Relaxed,
117        );
118        if let Some(files) = files {
119            self.files.fetch_add(files, Ordering::Relaxed);
120            self.counted_files.store(true, Ordering::Relaxed);
121        }
122        if over_the_wire {
123            // Read from the live counters here rather than taken from the caller: the
124            // caller would have had to read them a moment earlier, and a request landing
125            // in between would be written back out again.
126            // `fetch_max`, not `store`: were two metered passes ever to finish at
127            // once, whichever read the live counter first would write its lower figure
128            // back over the higher one. Today they cannot — a count is refused while
129            // the pass behind an open is still reading, and every other pair is
130            // serialized — so this guards an interleaving the rest of the design
131            // currently forbids. It costs nothing, and within a dataset these counters
132            // only climb, so taking the larger is correct whether or not that holds.
133            self.requests.fetch_max(
134                self.live_requests.load(Ordering::Relaxed),
135                Ordering::Relaxed,
136            );
137            self.bytes
138                .fetch_max(self.live_bytes.load(Ordering::Relaxed), Ordering::Relaxed);
139            self.counted.store(true, Ordering::Relaxed);
140        }
141        // Last, and released, so a render that sees `ran` sees every field behind it.
142        self.ran.store(true, Ordering::Release);
143    }
144
145    /// Record a stretch in place of whatever this tally held, rather than adding to it.
146    fn replace(&self, took: Duration, files: Option<usize>) {
147        self.nanos.store(
148            took.as_nanos().min(u64::MAX as u128) as u64,
149            Ordering::Relaxed,
150        );
151        match files {
152            Some(files) => {
153                self.files.store(files, Ordering::Relaxed);
154                self.counted_files.store(true, Ordering::Relaxed);
155            }
156            None => self.counted_files.store(false, Ordering::Relaxed),
157        }
158        self.ran.store(true, Ordering::Release);
159    }
160
161    /// One more request, and the bytes it returned. Called from the threads doing the
162    /// reading, once per request, before the stretch is recorded.
163    fn request(&self, bytes: u64) {
164        self.live_requests.fetch_add(1, Ordering::Relaxed);
165        self.live_bytes.fetch_add(bytes, Ordering::Relaxed);
166        self.counted_bytes.store(true, Ordering::Relaxed);
167    }
168
169    /// A whole pass's worth of requests at once, for work counted against a meter of
170    /// its own and then folded in here.
171    fn add_requests(&self, wire: OverTheWire) {
172        self.live_requests
173            .fetch_add(wire.requests, Ordering::Relaxed);
174        if let Some(bytes) = wire.bytes {
175            self.live_bytes.fetch_add(bytes, Ordering::Relaxed);
176            self.counted_bytes.store(true, Ordering::Relaxed);
177        }
178    }
179
180    fn cost(&self) -> Option<Cost> {
181        if !self.ran.load(Ordering::Acquire) {
182            return None;
183        }
184        Some(Cost {
185            took: Duration::from_nanos(self.nanos.load(Ordering::Relaxed)),
186            files: self
187                .counted_files
188                .load(Ordering::Relaxed)
189                .then(|| self.files.load(Ordering::Relaxed)),
190            over_the_wire: self.counted.load(Ordering::Relaxed).then(|| OverTheWire {
191                requests: self.requests.load(Ordering::Relaxed),
192                bytes: self
193                    .counted_bytes
194                    .load(Ordering::Relaxed)
195                    .then(|| self.bytes.load(Ordering::Relaxed)),
196            }),
197        })
198    }
199}
200
201/// What an open reports as it goes: how far its footer pass has got, and what the work
202/// has cost so far.
203///
204/// The two travel together down every route an open can take, so they are handed down
205/// together. Both are shared with the threads doing the reading, and both belong to the
206/// open that created them — see [`Meter`] for why a new open builds new ones rather
207/// than clearing these.
208#[derive(Debug, Clone, Default)]
209pub struct OpenReport {
210    /// How far the footer pass has got, for the loading screen.
211    pub progress: std::sync::Arc<crate::schema_union::FooterProgress>,
212    /// What the work has cost, for the Info panel.
213    pub meter: std::sync::Arc<Meter>,
214    /// Where to look for what a previous open of this dataset learned, and where to
215    /// leave what this one learns. `None` for a route with nowhere to keep it, and for
216    /// the tests that do not care.
217    ///
218    /// It travels with the other two because it belongs to the same moment — an open
219    /// reports what it is doing, records what it cost, and remembers what it found, and
220    /// all three are handed down the same routes.
221    pub remembered: Option<crate::cache::CacheManager>,
222    /// Where what is remembered for the home screen is written, off the open's path:
223    /// the app's, which the home listing settles before it reads.
224    pub(crate) writes: crate::background::CacheWrites,
225}
226
227/// What the open on screen cost, as it is measured.
228///
229/// Shared with the threads that do the listing and the footer reads, which is why it is
230/// held behind an `Arc` and written through `&self`.
231///
232/// There is no way to clear one. Opening a dataset builds a new meter instead, for the
233/// reason the footer counter does: abandoning a load cancels nothing, so the reads of
234/// the directory that was walked away from are still running, and a meter they still held
235/// would go on adding their figures to the next dataset's.
236#[derive(Debug, Default)]
237pub struct Meter {
238    listing: Tally,
239    footers: Tally,
240    /// The most recent page of rows, replacing rather than adding: this one says what
241    /// the page on screen cost, not what every page since the open came to.
242    last_page: Tally,
243    /// Whether a pass to settle the row count has already been counted.
244    counted_rows: AtomicBool,
245}
246
247impl Meter {
248    /// Finding the dataset's files took `took` and returned `files` of them.
249    ///
250    /// `over_the_wire` is always `false` here today, and the parameter is kept so the
251    /// two stretches record the same way. No listing route can count its requests: a
252    /// local walk makes none, and every remote one — the flat `list` and the
253    /// level-by-level walk a glob uses alike — hands the paging to the object store,
254    /// which does not say how many round trips it took.
255    pub fn listed(&self, took: Duration, files: Option<usize>, over_the_wire: bool) {
256        self.listing.record(took, files, over_the_wire);
257    }
258
259    /// A pass over `files` footers took `took`.
260    ///
261    /// `over_the_wire` publishes the requests counted by [`Self::footer_request`] since
262    /// the meter was made. A pass that read from a disk passes `false`: there were no
263    /// requests, and a zero would read as none having been needed.
264    pub fn read_footers(&self, took: Duration, files: Option<usize>, over_the_wire: bool) {
265        self.footers.record(took, files, over_the_wire);
266    }
267
268    /// A pass over `files` footers, run to settle the dataset's row count, took `took`.
269    ///
270    /// Recorded once and then never again, and the return says which happened. Counting
271    /// runs whenever the row count is invalidated — clearing a filter does it, so a few
272    /// minutes of exploring runs it several times — and those later passes are re-work
273    /// on a dataset that is already open. Adding them would make a section headed by
274    /// what opening the dataset cost climb for as long as the session lasted.
275    pub fn counted_rows(
276        &self,
277        took: Duration,
278        files: Option<usize>,
279        wire: Option<OverTheWire>,
280    ) -> bool {
281        // Only for an open this meter measured, and checked before the one shot rather
282        // than after it. A dataset opened by a route that reports nothing — a directory
283        // handed straight to Polars because `single_spine_schema` is off — still has
284        // its rows counted afterwards, and that count writing here would raise a
285        // section out of nothing whose every figure is work done after the dataset was
286        // already on screen.
287        if self.listing.cost().is_none() {
288            return false;
289        }
290        if self
291            .counted_rows
292            .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
293            .is_err()
294        {
295            return false;
296        }
297        // Folded in only once the one-shot has been won. A pass that is declined must
298        // leave the counters alone, or its requests would be published by whichever
299        // pass records next.
300        if let Some(w) = wire {
301            self.footers.add_requests(w);
302        }
303        self.footers.record(took, files, wire.is_some());
304        true
305    }
306
307    /// Reading the page now on screen took `took` and read `files` of the dataset's
308    /// files.
309    ///
310    /// Replaces rather than adds: this is the cost of the page a user is looking at,
311    /// and adding every page they have scrolled through would answer a question nobody
312    /// asked. `files` is `None` where Polars was handed the whole scan and decided for
313    /// itself what to read.
314    ///
315    /// No byte figure. Polars does this read and does not report what it fetched, and
316    /// the row-group sizes the footers hold would give the size of whole row groups for
317    /// every column — not the columns on screen, and not what crossed the wire. A
318    /// number that wrong is worse than none.
319    pub fn read_page(&self, took: Duration, files: Option<usize>) {
320        self.last_page.replace(took, files);
321    }
322
323    /// What the page now on screen cost, or `None` before one has been read.
324    pub fn last_page(&self) -> Option<Cost> {
325        self.last_page.cost()
326    }
327
328    /// One footer request, and the bytes it returned. Counted as the reads happen; what
329    /// they come to is published when the pass ends.
330    pub fn footer_request(&self, bytes: u64) {
331        self.footers.request(bytes);
332    }
333
334    /// What finding the files cost, or `None` if no listing has been measured.
335    pub fn listing(&self) -> Option<Cost> {
336        self.listing.cost()
337    }
338
339    /// What reading the footers cost, or `None` if no footer pass has been measured.
340    pub fn footers(&self) -> Option<Cost> {
341        self.footers.cost()
342    }
343
344    /// Everything measured so far, added up. `None` until something has been measured.
345    ///
346    /// No file count. The stretches count different things — the listing counts the
347    /// dataset's files, the footer pass counts footers read, which on a staged open is
348    /// more than there are files — so adding them gives a number that is not the size
349    /// of anything, sitting under the same word the listing row uses for the size of
350    /// the dataset.
351    ///
352    /// The wire figures are the sum of the stretches that counted them, and are `None`
353    /// when no stretch did: a local dataset's total is a time, not a time and a
354    /// pretence of nothing having been transferred.
355    pub fn total(&self) -> Option<Total> {
356        // Listing and footers only. The page is not part of opening the dataset — it is
357        // what looking at one costs, and it changes every time the view moves.
358        let parts: Vec<Cost> = [self.listing(), self.footers()]
359            .into_iter()
360            .flatten()
361            .collect();
362        // Two stretches or none. One stretch's total is that stretch, printed twice
363        // under two labels — which a single remote object would do, having a footer to
364        // read and nothing to list.
365        if parts.len() < 2 {
366            return None;
367        }
368        Some(Total {
369            took: parts.iter().map(|p| p.took).sum(),
370            over_the_wire: parts
371                .iter()
372                .filter_map(|p| p.over_the_wire)
373                .reduce(combine_wire),
374        })
375    }
376}
377
378#[cfg(test)]
379mod tests {
380    use super::*;
381
382    /// A meter that has measured nothing says so, rather than saying nothing happened.
383    ///
384    /// The two are different claims and the difference is the whole point of the
385    /// section: a dataset opened by a route that does not measure must show no figures
386    /// at all, not a listing that took no time over no files.
387    #[test]
388    fn nothing_measured_is_not_the_same_as_nothing_having_happened() {
389        let meter = Meter::default();
390        assert_eq!(meter.listing(), None);
391        assert_eq!(meter.footers(), None);
392        assert_eq!(meter.total(), None);
393
394        // A listing that genuinely returned nothing is still a measurement.
395        meter.listed(Duration::ZERO, Some(0), false);
396        assert_eq!(
397            meter.listing(),
398            Some(Cost {
399                took: Duration::ZERO,
400                files: Some(0),
401                over_the_wire: None
402            }),
403            "an empty prefix was still listed, and the time it took is a real figure"
404        );
405    }
406
407    /// A staged open reads two footers to open and the rest behind it, and what the
408    /// footers cost is both passes.
409    ///
410    /// The second pass adding to the first is the behaviour under test: replacing would
411    /// report the tail of the work as though it were all of it, which on a large prefix
412    /// is the difference between "four seconds of footers" and "one".
413    #[test]
414    fn a_second_footer_pass_adds_to_the_first_rather_than_replacing_it() {
415        let meter = Meter::default();
416        meter.footer_request(1_000);
417        meter.footer_request(1_000);
418        meter.read_footers(Duration::from_millis(200), Some(2), true);
419
420        // The pass behind the open, reading the rest against the same meter.
421        for _ in 0..98 {
422            meter.footer_request(1_000);
423        }
424        meter.read_footers(Duration::from_millis(3_000), Some(98), true);
425
426        let footers = meter.footers().expect("both passes were measured");
427        assert_eq!(footers.took, Duration::from_millis(3_200), "both times");
428        assert_eq!(footers.files, Some(100), "both counts");
429        assert_eq!(
430            footers.over_the_wire,
431            Some(OverTheWire {
432                requests: 100,
433                bytes: Some(100_000)
434            }),
435            "and every request either pass made"
436        );
437    }
438
439    /// The total adds the stretches, and claims wire figures only from the stretches
440    /// that had any.
441    #[test]
442    fn the_total_adds_what_there_is_and_claims_no_more() {
443        let meter = Meter::default();
444        meter.listed(Duration::from_millis(100), Some(3), false);
445        meter.footer_request(4_096);
446        meter.read_footers(Duration::from_millis(900), Some(3), true);
447
448        assert_eq!(
449            meter.total(),
450            Some(Total {
451                took: Duration::from_secs(1),
452                over_the_wire: Some(OverTheWire {
453                    requests: 1,
454                    bytes: Some(4_096)
455                }),
456            }),
457            "the times of both, and the requests of the one that made them — and no \
458             file count, which would be two different denominators added together"
459        );
460
461        // A total over stretches that made no requests claims none, rather than zero.
462        let local = Meter::default();
463        local.listed(Duration::from_millis(1), Some(2), false);
464        local.read_footers(Duration::from_millis(2), Some(2), false);
465        assert_eq!(
466            local.total().and_then(|t| t.over_the_wire),
467            None,
468            "nothing crossed a wire, so there is no figure to give"
469        );
470    }
471
472    /// A total carries only the figures its stretches actually had.
473    ///
474    /// A listing never reports requests — no route can count a listing's round trips,
475    /// since the object store does its own paging — so a total's requests and bytes are
476    /// the footer reads', and a listing beside them adds only time.
477    #[test]
478    fn a_total_gives_no_figure_it_cannot_account_for() {
479        let meter = Meter::default();
480        meter.listed(Duration::from_millis(1), None, false);
481        for _ in 0..4 {
482            meter.footer_request(250);
483        }
484        meter.read_footers(Duration::from_millis(3), Some(2), true);
485
486        let listing = meter.listing().expect("the walk was measured");
487        assert_eq!(
488            listing.over_the_wire, None,
489            "a listing claims nothing over the wire, having no way to count it"
490        );
491
492        let total = meter.total().expect("and a total over the two stretches");
493        assert_eq!(
494            total.took,
495            Duration::from_millis(4),
496            "the two times added up"
497        );
498        assert_eq!(
499            total.over_the_wire,
500            Some(OverTheWire {
501                requests: 4,
502                bytes: Some(1_000)
503            }),
504            "and the footer reads' requests and bytes, which account for each other"
505        );
506    }
507
508    /// A stretch that counted no bytes leaves the total unable to name any.
509    ///
510    /// Nothing produces this shape today — every stretch that reports requests also
511    /// counts their bytes — so this holds the arithmetic rather than a route: were a
512    /// stretch ever to report requests it could not weigh, carrying the other's bytes
513    /// forward would present them as the bytes behind all of them.
514    #[test]
515    fn a_total_will_not_weigh_requests_nothing_weighed() {
516        let a = Cost {
517            took: Duration::from_millis(1),
518            files: None,
519            over_the_wire: Some(OverTheWire {
520                requests: 4,
521                bytes: None,
522            }),
523        };
524        let b = Cost {
525            took: Duration::from_millis(3),
526            files: Some(2),
527            over_the_wire: Some(OverTheWire {
528                requests: 4,
529                bytes: Some(1_000),
530            }),
531        };
532        assert_eq!(
533            Some(combine_wire(
534                a.over_the_wire.unwrap(),
535                b.over_the_wire.unwrap()
536            )),
537            Some(OverTheWire {
538                requests: 8,
539                bytes: None
540            }),
541            "eight requests, and no byte figure: a thousand bytes is what four of them \
542             returned, not eight"
543        );
544    }
545
546    /// The page replaces, and is no part of what opening the dataset cost.
547    ///
548    /// Every other stretch adds, because reading a dataset's footers twice really did
549    /// cost twice. A page is different: there is one on screen, the figure describes
550    /// that one, and scrolling through a hundred of them must not report the hundred
551    /// added together as though the last one had taken a minute.
552    #[test]
553    fn the_page_on_screen_replaces_the_one_before_it_and_is_not_part_of_the_open() {
554        let meter = Meter::default();
555        meter.listed(Duration::from_millis(10), Some(3), false);
556        meter.read_footers(Duration::from_millis(20), Some(3), false);
557
558        meter.read_page(Duration::from_millis(500), Some(2));
559        meter.read_page(Duration::from_millis(300), Some(1));
560        assert_eq!(
561            meter.last_page(),
562            Some(Cost {
563                took: Duration::from_millis(300),
564                files: Some(1),
565                over_the_wire: None
566            }),
567            "the page on screen is the one before last replaced, not added to it"
568        );
569
570        let total = meter.total().expect("the open was measured");
571        assert_eq!(
572            total.took,
573            Duration::from_millis(30),
574            "and the total is the listing and the footers — looking at a page is not \
575             part of opening the dataset"
576        );
577
578        // A page Polars chose the files for reports a time and no count.
579        meter.read_page(Duration::from_millis(40), None);
580        assert_eq!(
581            meter.last_page().and_then(|c| c.files),
582            None,
583            "a whole-scan read says how long it took and not how many files it touched"
584        );
585    }
586
587    /// Publishing never moves a figure backwards.
588    ///
589    /// Two metered passes can finish at once — a dataset filtered while the pass behind
590    /// its open is still reading — and the one that read the live counter first would,
591    /// storing, write its lower figure over the other's higher one. The state below is
592    /// exactly that interleaving caught mid-way: a figure already published, and a live
593    /// counter that a slower pass read before it climbed.
594    #[test]
595    fn publishing_cannot_lower_a_figure_another_pass_has_already_published() {
596        let tally = Tally::default();
597        tally.requests.store(100, Ordering::Relaxed);
598        tally.bytes.store(100_000, Ordering::Relaxed);
599        tally.live_requests.store(50, Ordering::Relaxed);
600        tally.live_bytes.store(50_000, Ordering::Relaxed);
601        // As a pass that really read bytes leaves it.
602        tally.counted_bytes.store(true, Ordering::Relaxed);
603
604        tally.record(Duration::from_millis(1), Some(1), true);
605
606        let cost = tally.cost().expect("the pass was recorded");
607        assert_eq!(
608            cost.over_the_wire,
609            Some(OverTheWire {
610                requests: 100,
611                bytes: Some(100_000)
612            }),
613            "the higher figure stands; storing the live read would have halved both"
614        );
615    }
616
617    /// A count pass the one shot declines leaves the figures completely alone.
618    ///
619    /// Not just the time and the count: its requests must not reach the live counters
620    /// either, or the next pass to record would publish them as its own.
621    #[test]
622    fn a_declined_count_adds_nothing_at_all() {
623        let meter = Meter::default();
624        // As an open that measured itself leaves it: counting belongs to one of those.
625        meter.listed(Duration::from_millis(1), Some(3), false);
626        let wire = OverTheWire {
627            requests: 2,
628            bytes: Some(8_192),
629        };
630        assert!(
631            meter.counted_rows(Duration::from_millis(100), Some(3), Some(wire)),
632            "the first count is the one that is measured"
633        );
634        let after_first = meter.footers().expect("and it was measured");
635
636        for _ in 0..3 {
637            assert!(
638                !meter.counted_rows(Duration::from_millis(100), Some(3), Some(wire)),
639                "later counts are re-work and are declined"
640            );
641        }
642        assert_eq!(
643            meter.footers(),
644            Some(after_first),
645            "so the figures stand where the first count left them"
646        );
647
648        // And the declined passes left nothing behind for the next pass to publish.
649        meter.read_footers(Duration::from_millis(50), Some(1), true);
650        assert_eq!(
651            meter.footers().and_then(|c| c.over_the_wire),
652            Some(wire),
653            "a pass recording afterwards publishes the two requests that were really \
654             made, not the eight the declined passes would have added"
655        );
656    }
657}