Skip to main content

datui_lib/loading/
counting.rs

1//! Row counts, footer passes and line indexing behind a dataset's first rows, and
2//! what waits on them: an End, a `:N`, the status line.
3
4use crate::app::background::{LenCount, OwedCount};
5use crate::app::jobs::{Answer, Job};
6use crate::table::DataTableState;
7use crate::{App, AppEvent, logging};
8use std::sync::Arc;
9
10/// The dataset's row count, footer pass and line indexing, and what waits on them.
11#[derive(Default)]
12pub struct Counting {
13    /// The footer counter of the dataset on screen, reported to by its pass behind the
14    /// open; handed over by the installing load. See [`Self::footer_progress`].
15    pub(crate) footer_progress: Arc<crate::formats::schema_union::FooterProgress>,
16    /// The count when this frame began, or `None` with no pass running. Read once per
17    /// frame so the loading body and footer, painted moments apart while the pass
18    /// runs, print the same number.
19    pub(crate) footers_this_frame: Option<(usize, usize)>,
20    /// Where the last load-ahead was asked from. See [`App::load_ahead`].
21    pub(crate) loaded_ahead_from: Option<(u64, usize, usize, usize)>,
22    // `len_generation` of the background row count in flight, so a (possibly minutes-long)
23    // count is not respawned on every scroll.
24    pub(crate) len_count_inflight: Option<u64>,
25    /// The `len_generation` of a promised count not yet started. A local full count
26    /// competes with the first page for disk and Polars workers, and the page may make
27    /// it unnecessary, so it starts after a paint. See [`App::frame_painted`].
28    pub(crate) count_after_paint: Option<u64>,
29    /// Counts started, so a test can say none began before the page was painted.
30    #[cfg(test)]
31    pub(crate) counts_spawned: std::cell::Cell<usize>,
32    /// Times an installed dataset's own first rows were asked for, so a test can say a
33    /// view applied on open read them instead.
34    #[cfg(test)]
35    pub(crate) first_rows_asked: usize,
36    // `len_generation` whose background count failed: while current, the count shows
37    // "?" rather than a provisional total.
38    pub(crate) len_count_failed: Option<u64>,
39    /// End pressed on a remote dataset before its count: jump when this generation's
40    /// count arrives.
41    pub(crate) end_after_count: Option<u64>,
42    /// End pressed while a dataset still read its footers (which bring its end): jump
43    /// when they land, for that `dataset_generation` only, so a directory left behind
44    /// cannot move the next one's view.
45    pub(crate) end_when_the_footers_land: Option<u64>,
46    /// End pressed while a text file's lines were indexed: jump when done, for that
47    /// dataset alone.
48    pub(crate) end_when_indexed: Option<u64>,
49    /// Stops the indexing thread of the dataset on screen's lines.
50    pub(crate) indexing_stop: Arc<std::sync::atomic::AtomicBool>,
51    /// The lines being indexed, until they all are.
52    pub(crate) indexing_lines: Option<Arc<crate::formats::lines::Lines>>,
53    /// The indexing waits while home is up.
54    pub(crate) indexing_paused: bool,
55    /// `:N` past the lines indexed so far, for that dataset: gone to once they all are.
56    pub(crate) goto_when_indexed: Option<(u64, usize)>,
57    /// The last count started: what it has read of the footers, and its stop (Esc).
58    pub(crate) count_progress: Arc<crate::formats::schema_union::FooterProgress>,
59    /// The `dataset_generation` an exact count was asked for (`c` in Info) despite
60    /// having more files than are counted unasked.
61    pub(crate) exact_count_asked: Option<u64>,
62    /// `c` pressed while a stopped count wound down: count again when its answer for
63    /// this `len_generation` arrives.
64    pub(crate) count_after_stop: Option<u64>,
65    /// Footers found while the user viewed a query, pivot or drill-down. Held, not
66    /// applied: widening the scan under a query takes its columns away. Offered again
67    /// once the view is back on the data.
68    pub(crate) footers_held: Option<(u64, crate::table::FootersFound)>,
69    /// Fields a followed NDJSON pipe brought after the open, held like footers.
70    pub(crate) followed_fields_held: Option<(u64, Vec<polars::prelude::Field>)>,
71    /// A re-read owed after a footer pass came back empty, held because its collect
72    /// would bump the generation under running work. The dataset still needs the
73    /// ordinary count; retried after every event until that work is done.
74    pub(crate) reread_owed: Option<u64>,
75    /// The objects a listing had found when this frame began, read once like
76    /// `footers_this_frame`.
77    pub(crate) listed_this_frame: Option<usize>,
78}
79
80impl Counting {
81    /// A new dataset is on screen, its footers counted on `footers`: the last one's
82    /// pass stops, and Ends or `:N`s waiting on its footers, count or lines are
83    /// forgotten: a `len_generation` does not say which dataset, so a leftover would
84    /// act on the next.
85    pub(crate) fn reset_for_dataset(
86        &mut self,
87        footers: Arc<crate::formats::schema_union::FooterProgress>,
88    ) {
89        self.stop_footer_pass();
90        self.footer_progress = footers;
91        self.end_when_the_footers_land = None;
92        self.end_after_count = None;
93        self.end_when_indexed = None;
94        self.goto_when_indexed = None;
95    }
96
97    /// Stop the footer pass of the dataset on screen: unread beats read and dropped.
98    pub(crate) fn stop_footer_pass(&self) {
99        self.footer_progress.cancel();
100    }
101
102    /// Stop indexing the lines for good, so nothing holds the file.
103    pub(crate) fn stop_indexing(&mut self) {
104        self.indexing_stop
105            .store(true, std::sync::atomic::Ordering::Relaxed);
106        if let Some(lines) = self.indexing_lines.take() {
107            lines.stop_indexing();
108        }
109    }
110
111    /// Home is up: indexing and the reads waiting on it pause until the table is back
112    /// ([`App::begin_frame`]).
113    pub(crate) fn pause_indexing(&mut self) {
114        if self.indexing_lines.is_some() {
115            self.indexing_stop
116                .store(true, std::sync::atomic::Ordering::Relaxed);
117            self.indexing_paused = true;
118        }
119    }
120
121    /// The markers a running query keeps for the view it may roll back to.
122    pub(crate) fn markers(&self) -> CountMarkers {
123        CountMarkers {
124            len_count_inflight: self.len_count_inflight,
125            count_after_paint: self.count_after_paint,
126            len_count_failed: self.len_count_failed,
127        }
128    }
129
130    /// Put back the markers of a view a failed query rolled back to.
131    pub(crate) fn restore(&mut self, markers: CountMarkers) {
132        self.len_count_inflight = markers.len_count_inflight;
133        self.count_after_paint = markers.count_after_paint;
134        self.len_count_failed = markers.len_count_failed;
135    }
136}
137
138/// A frame's count markers: in flight, held for a paint, and failed.
139#[derive(Clone, Copy)]
140pub(crate) struct CountMarkers {
141    pub(crate) len_count_inflight: Option<u64>,
142    pub(crate) count_after_paint: Option<u64>,
143    pub(crate) len_count_failed: Option<u64>,
144}
145
146/// Bytes of a text file indexed per step; between steps the indexer checks it is
147/// still wanted.
148const INDEX_STEP: usize = 16 << 20;
149
150impl App {
151    /// Whether the footer's row count is on its way, so a spinner stands in. Asked by
152    /// the bar and by the run loop that turns the spinner, so they agree.
153    pub fn row_count_pending(&self) -> bool {
154        // A load in flight: the number held is the outgoing dataset's. A dataset reading
155        // its own footers: it declines the ordinary count, and the number held only
156        // reaches as far as the buffer (`Rows: 70` for six thousand files).
157        self.counting.len_count_inflight.is_some()
158            || self.loading.awaiting_dataset()
159            // A re-read owed after failed footers starts a count too; without this the bar
160            // would print the buffer's partial number meanwhile.
161            || self.counting.reread_owed.is_some()
162            || self
163                .data_table_state
164                .as_ref()
165                .is_some_and(|state| state.counts_itself_later())
166            // The lines are all indexed but their answer, which brings the count, is not
167            // yet handled: the indexer marks them done before it answers.
168            || self.lines_answer_owed()
169    }
170
171    /// Whether the lines of the dataset on screen have an indexing answer still to be
172    /// handled. Not for a frame shown in their place (the quality evidence view).
173    fn lines_answer_owed(&self) -> bool {
174        let dataset = self.dataset_generation;
175        self.data_table_state
176            .as_ref()
177            .is_some_and(|state| state.lines_to_index().is_some())
178            && self
179                .jobs
180                .current(
181                    |job| matches!(job, Job::IndexLines { dataset: asked } if *asked == dataset),
182                )
183                .is_some()
184    }
185
186    pub(crate) fn dataset_is_still_reading_its_footers(&self) -> bool {
187        self.data_table_state
188            .as_ref()
189            .is_some_and(|state| state.footers_pending().is_some())
190    }
191
192    /// The first rows of an open are on screen, or will not be read: its wait is over.
193    pub(crate) fn first_rows_settled(&mut self) {
194        self.loading.first_rows_settled();
195    }
196
197    /// Read the rows on screen again now that the frame they were read through was
198    /// replaced by the join. The dataset's errand, not its open's: the join already
199    /// dropped the buffer, so this must not be dropped.
200    pub(crate) fn reread_after_the_footers_joined(&mut self) {
201        // Any re-read satisfies an owed one.
202        self.counting.reread_owed = None;
203        // An End waiting on these footers. Taken either way: a flag from a gone dataset is
204        // not this one's.
205        if self.counting.end_when_the_footers_land.take() == Some(self.dataset_generation) {
206            self.status_message = None;
207            if let Some(next) = self.jump_key(crate::Scroll::End) {
208                // The jump reads the page it lands on, so reading this one first would be wasted.
209                let _ = self.events.send(next);
210                return;
211            }
212            // Unless it asked for no read (already at the end, or waiting on the ordinary
213            // count): the join dropped the buffer, so fall through and read.
214        }
215        self.spawn_async_collect(Self::LOADING_BUFFER);
216    }
217
218    /// Run a buffer collect asked for while other work held the generation; like
219    /// `reread_when_the_work_allows`, retried after every event.
220    pub(crate) fn collect_when_the_work_allows(&mut self) {
221        let Some(&Job::OwedRows { dataset, .. }) = self.jobs.owed(Self::owed_rows) else {
222            return;
223        };
224        if dataset != self.dataset_generation {
225            // The dataset it was owed to is gone: put down the errand and its keys; the status
226            // line belongs to the replacement.
227            self.jobs.take_owed(Self::owed_rows);
228            return;
229        }
230        if self.work_a_bump_would_strand() {
231            return;
232        }
233        let Some(Job::OwedRows { status, .. }) = self.jobs.take_owed(Self::owed_rows) else {
234            return;
235        };
236        if !self.spawn_async_collect(&status) {
237            self.busy = false;
238            self.status_message = None;
239            // The owed collect may have been an open's first rows; else the bar would read
240            // "Loading buffer... 70%" forever.
241            self.first_rows_settled();
242        }
243    }
244
245    /// Run the re-read a failed footer pass owes the dataset once it strands nothing
246    /// (`work_the_join_would_cancel`), as held columns wait.
247    pub(crate) fn reread_when_the_work_allows(&mut self) {
248        let Some(generation) = self.counting.reread_owed else {
249            return;
250        };
251        if generation != self.dataset_generation {
252            // The dataset it was owed to is gone; so is the errand.
253            self.counting.reread_owed = None;
254            return;
255        }
256        if self.work_the_join_would_cancel() {
257            return;
258        }
259        self.reread_after_the_footers_joined();
260    }
261
262    /// Retire an End waiting on a count that can no longer answer it: only the flag and
263    /// its message; the jump is not re-issued (see `BackgroundLenReady`).
264    fn retire_the_end_that_was_waiting(&mut self) {
265        self.counting.end_after_count = None;
266        self.take_down_the_counting_status();
267    }
268
269    /// Take down "Counting rows to find the end...", and only that: the line may now
270    /// belong to a load or an export.
271    fn take_down_the_counting_status(&mut self) {
272        if matches!(
273            self.status_message.as_deref(),
274            Some(Self::COUNTING_FOR_END | Self::INDEXING_FOR_ROW)
275        ) {
276            self.status_message = None;
277        }
278    }
279
280    /// Whether the bar is still keeping quiet about a fetch for the view.
281    pub(crate) fn fetch_too_young_to_mention(&self) -> bool {
282        self.status_message.as_deref() == Some(Self::LOADING_BUFFER)
283            && self
284                .rows_in_flight()
285                .is_some_and(|inflight| inflight.began.elapsed() < Self::A_FETCH_WORTH_SAYING)
286    }
287
288    /// Work the re-read after a join would cancel: anything a bump would strand
289    /// ([`crate::app::jobs::Jobs::would_strand`]), plus a chart being prepared, since the join changes the
290    /// frame (a fresh `len_generation`) under it.
291    pub(crate) fn work_the_join_would_cancel(&self) -> bool {
292        self.work_a_bump_would_strand() || self.chart_preparing()
293    }
294
295    /// Give the dataset what its footers found, if it can take it now. Not while a
296    /// query, pivot, melt or drill-down is the root: widening the scan under it takes
297    /// its columns away, so they are held and retried after every event. Returns
298    /// whether the dataset took them, so the caller re-reads the rows on screen.
299    pub(crate) fn join_held_footers(&mut self) -> bool {
300        let Some((generation, _)) = self.counting.footers_held.as_ref() else {
301            return false;
302        };
303        if *generation != self.dataset_generation {
304            // The dataset they belong to is gone; so are they.
305            self.counting.footers_held = None;
306            return false;
307        }
308        if self.data_table_state.is_none() || self.work_the_join_would_cancel() {
309            return false;
310        }
311        let Some((generation, found)) = self.counting.footers_held.take() else {
312            return false;
313        };
314        let state = self
315            .data_table_state
316            .as_mut()
317            .expect("checked just above, and nothing since takes it");
318        // The frame on screen knows whether it still grows from the scan.
319        match state.join_dataset_schema(found) {
320            Ok(()) => true,
321            Err(found) => {
322                self.counting.footers_held = Some((generation, *found));
323                false
324            }
325        }
326    }
327
328    /// Start the pass reading the rest of a staged open's footers. Not waited on: the
329    /// dataset works meanwhile. Judged by `dataset_generation`, since collects bump the
330    /// task generation many times during it.
331    pub(crate) fn start_pending_footers(&mut self) {
332        let Some(join) = self
333            .data_table_state
334            .as_ref()
335            .and_then(|state| state.footers_pending())
336        else {
337            return;
338        };
339        let dataset = self.dataset_generation;
340        let progress = self.counting.footer_progress.clone();
341        // Answered either way, even on failure or panic: a waiting dataset will not count
342        // itself, since the pass was bringing its count.
343        self.spawn_job(Job::FootersJoin { dataset }, None, move |_| {
344            Ok(Answer::FootersJoined(join(&progress).map(Box::new)))
345        });
346    }
347
348    /// Run the indexing of the screen's dataset if lines remain (new, or paused for
349    /// home). A dataset no longer on screen stops for good; reads waiting on it give up.
350    pub(crate) fn index_lines(&mut self) {
351        use std::sync::atomic::Ordering;
352        self.counting.indexing_stop.store(true, Ordering::Relaxed);
353        self.counting.indexing_paused = false;
354        let lines = self
355            .data_table_state
356            .as_ref()
357            .and_then(|state| state.lines_to_index().cloned());
358        if let Some(old) = self.counting.indexing_lines.take()
359            && lines.as_ref().is_none_or(|lines| !Arc::ptr_eq(lines, &old))
360        {
361            old.stop_indexing();
362        }
363        let Some(lines) = lines.filter(|lines| lines.resume_indexing()) else {
364            return;
365        };
366        self.counting.indexing_lines = Some(lines.clone());
367        let stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
368        self.counting.indexing_stop = stop.clone();
369        let dataset = self.dataset_generation;
370        // Not waited on; a read of every line waits on its own worker.
371        self.spawn_job(Job::IndexLines { dataset }, None, move |_| {
372            loop {
373                // Paused or replaced: whoever stopped it decides the waiting reads' fate.
374                if stop.load(Ordering::Relaxed) {
375                    return Err("stopped".to_string());
376                }
377                // A panic stops it where it is: the rows so far are all there is.
378                let done = logging::catch_panic(|| lines.index_more(INDEX_STEP)).unwrap_or(true);
379                if done {
380                    lines.stop_indexing();
381                    return Ok(Answer::LinesIndexed(lines.rows()));
382                }
383            }
384        });
385    }
386
387    /// More lines are indexed: the frames take them; once all are, the count and any
388    /// waiting End follow.
389    pub(crate) fn lines_indexed(&mut self, generation: u64, rows: usize) {
390        if generation != self.dataset_generation {
391            return;
392        }
393        let Some(state) = self.data_table_state.as_mut() else {
394            return;
395        };
396        self.counting.indexing_lines = None;
397        if !state.lines_indexed(rows) {
398            // Set aside during the quality evidence view: they land on the dataset that comes
399            // back.
400            if let Some(held) = self.quality.evidence_return.as_mut() {
401                held.lines_indexed(rows);
402            }
403            return;
404        }
405        if let Some((goto, row)) = self.counting.goto_when_indexed.take()
406            && goto == generation
407        {
408            self.take_down_the_counting_status();
409            let _ = self
410                .events
411                .send(AppEvent::Applied(crate::Applied::GoToLine(row)));
412        }
413        if self.counting.end_when_indexed.take() == Some(generation) {
414            self.take_down_the_counting_status();
415            if let Some(next) = self.jump_key(crate::Scroll::End) {
416                let _ = self.events.send(next);
417                return;
418            }
419        }
420        // The count indexing held back starts now, and rows past the first are read.
421        if self.in_normal_table_view() && !self.loading.awaiting_dataset() {
422            self.spawn_collect(None);
423        }
424    }
425
426    /// Whether the dataset's count waits to be asked for: it has more files than
427    /// `[read] exact_count_files` and an estimate to show meanwhile.
428    pub(crate) fn count_held_at_estimate(&self, state: &DataTableState) -> bool {
429        let limit = self.app_config.read.exact_count_files;
430        limit > 0
431            && state.files_to_count().is_some_and(|files| files > limit)
432            && self.counting.exact_count_asked != Some(self.dataset_generation)
433            && state.row_estimate(None).is_some()
434    }
435
436    /// The row count estimated from a sample of footers while uncounted: the dataset's
437    /// own, or what its footer pass has said so far.
438    pub(crate) fn row_estimate(&self) -> Option<crate::formats::schema_union::RowEstimate> {
439        self.data_table_state
440            .as_ref()?
441            .row_estimate(self.counting.footer_progress.estimate())
442    }
443
444    /// `(read, of)` when the running count reads footers it can report, so it can be
445    /// stopped.
446    pub(crate) fn footers_counted(&self) -> Option<(usize, usize)> {
447        self.counting.len_count_inflight?;
448        self.counting
449            .count_progress
450            .reading()
451            .filter(|_| !self.counting.count_progress.is_cancelled())
452    }
453
454    /// `c` in Info: count every row exactly despite the file count.
455    pub(crate) fn count_exactly(&mut self) {
456        let Some(state) = self.data_table_state.as_ref() else {
457            return;
458        };
459        if state.is_num_rows_valid() {
460            return;
461        }
462        let generation = state.len_generation();
463        self.counting.exact_count_asked = Some(self.dataset_generation);
464        // The footer pass still brings the count; the request holds until it lands.
465        if state.counts_itself_later() {
466            return;
467        }
468        // A count stopped before is asked again.
469        if self.counting.len_count_failed == Some(generation) {
470            self.counting.len_count_failed = None;
471        }
472        // One stopped and not yet wound down: again once it has.
473        if self.counting.len_count_inflight == Some(generation)
474            && self.counting.count_progress.is_cancelled()
475        {
476            self.counting.count_after_stop = Some(generation);
477            return;
478        }
479        if self.counting.len_count_inflight != Some(generation) {
480            self.counting.len_count_inflight = Some(generation);
481            let job = LenCount::for_state(state);
482            self.spawn_count(job);
483        }
484    }
485
486    /// Esc while a count reads footers: stop it. What it read is kept for the next.
487    pub(crate) fn stop_count(&mut self) {
488        self.counting.count_progress.cancel();
489    }
490
491    /// What the footer pass found, joined if its dataset is still the one on screen
492    /// (going home and back keeps it).
493    pub(crate) fn footers_joined(
494        &mut self,
495        dataset: u64,
496        found: Option<crate::table::FootersFound>,
497    ) -> Option<AppEvent> {
498        if dataset == self.dataset_generation {
499            let Some(found) = found else {
500                // The pass failed: the dataset stays as opened and stops waiting, so it counts
501                // itself the ordinary way via the collect below.
502                if let Some(state) = self.data_table_state.as_mut() {
503                    state.give_up_on_pending_footers();
504                }
505                // The jump now waits on the ordinary count. Owed, not run: the collect bumps the
506                // generation, which an export or analysis may be waiting on.
507                self.counting.reread_owed = Some(dataset);
508                self.reread_when_the_work_allows();
509                return None;
510            };
511            self.counting.footers_held = Some((dataset, found));
512            if self.join_held_footers() {
513                self.reread_after_the_footers_joined();
514            }
515        }
516        None
517    }
518
519    /// Count the rows off the UI thread; the answer comes back as `BackgroundLenReady`
520    /// or `BackgroundLenFailed`.
521    pub(crate) fn spawn_count(&mut self, job: LenCount) {
522        #[cfg(test)]
523        self.counting
524            .counts_spawned
525            .set(self.counting.counts_spawned.get() + 1);
526        self.counting.count_progress = job.progress.clone();
527        let count = OwedCount::new(job, self.events.clone());
528        self.runtime
529            .spawn_blocking(move || count.answer(LenCount::run));
530    }
531
532    /// Whether waited-on rows of the frame on screen (an open's, a query's, a scroll's
533    /// page) are still being read. A load-ahead is nobody's wait; counts do not queue
534    /// behind it.
535    fn waited_on_rows_pending(&self, generation: u64) -> bool {
536        self.loading.awaiting_dataset()
537            || self.jobs.owed(Self::owed_rows).is_some()
538            || (self.rows_waited_on()
539                && self
540                    .rows_in_flight()
541                    .is_some_and(|inflight| inflight.dataset == generation))
542    }
543
544    /// Whether a paint now would start or retire the count waiting on one. A test
545    /// harness, which paints nothing, asks this to know when to report a paint.
546    pub fn count_waits_for_a_frame(&self) -> bool {
547        self.counting
548            .count_after_paint
549            .is_some_and(|generation| !self.waited_on_rows_pending(generation))
550    }
551
552    /// A frame was painted: read the rows it needed and start the count waiting on it.
553    pub fn frame_painted(&mut self) {
554        self.pointer.painted();
555        self.count_what_was_painted();
556        // A resize sets the rows on screen while drawing, after the event pass; rematched
557        // finds draw on the frame the wake brings.
558        if self.refresh_stale_live_matches() {
559            let _ = self.events.send(AppEvent::Wake);
560        }
561        if let Some(state) = &mut self.data_table_state
562            && state.needs_recollect
563        {
564            state.needs_recollect = false;
565            self.spawn_async_collect(App::LOADING_BUFFER);
566        }
567    }
568
569    /// Start the count waiting on a paint unless its rows are still being read; retire
570    /// it if its frame is gone or the rows already gave the count.
571    fn count_what_was_painted(&mut self) {
572        let Some(generation) = self.counting.count_after_paint else {
573            return;
574        };
575        if self.waited_on_rows_pending(generation) {
576            return;
577        }
578        self.counting.count_after_paint = None;
579        let wanted = self
580            .data_table_state
581            .as_ref()
582            .filter(|state| state.len_generation() == generation && !state.is_num_rows_valid());
583        match wanted {
584            Some(state) => {
585                self.counting.len_count_inflight = Some(generation);
586                self.spawn_count(LenCount::for_state(state));
587            }
588            None => {
589                if self.counting.len_count_inflight == Some(generation) {
590                    self.counting.len_count_inflight = None;
591                }
592            }
593        }
594    }
595
596    /// The installed page may have given the row count, or belong to another frame:
597    /// either way the waiting count is no longer owed.
598    pub(crate) fn retire_a_count_the_rows_answered(&mut self) {
599        let Some(generation) = self.counting.count_after_paint else {
600            return;
601        };
602        let answered = self
603            .data_table_state
604            .as_ref()
605            .is_none_or(|state| state.len_generation() != generation || state.is_num_rows_valid());
606        if answered {
607            self.counting.count_after_paint = None;
608            if self.counting.len_count_inflight == Some(generation) {
609                self.counting.len_count_inflight = None;
610            }
611        }
612    }
613
614    /// Answers from row counts, footer passes and line indexing.
615    pub(crate) fn counting_event(&mut self, event: AppEvent) -> Option<AppEvent> {
616        match event {
617            AppEvent::BackgroundLenReady {
618                len_generation,
619                num_rows,
620                file_row_groups,
621            } => {
622                if self.counting.len_count_inflight == Some(len_generation) {
623                    self.counting.len_count_inflight = None;
624                }
625                if self.counting.len_count_failed == Some(len_generation) {
626                    self.counting.len_count_failed = None;
627                }
628                // A count of the view a running query replaced goes back with it.
629                if let Some(run) = self.prompt.query_running.as_mut() {
630                    run.rollback
631                        .count_landed(len_generation, num_rows, file_row_groups.as_deref());
632                    if run.counts.len_count_inflight == Some(len_generation) {
633                        run.counts.len_count_inflight = None;
634                    }
635                }
636                // Apply the exact total only if the data is unchanged since the count spawned; the
637                // buffer has usually painted, so this just corrects the scrollbar and total.
638                if let Some(state) = self.data_table_state.as_mut()
639                    && state.count_landed(len_generation, num_rows, file_row_groups.as_deref())
640                {
641                    // End was pressed before there was an end to go to.
642                    if self.counting.end_after_count == Some(len_generation) {
643                        self.counting.end_after_count = None;
644                        self.status_message = None;
645                        return self.jump_key(crate::Scroll::End);
646                    }
647                } else if self.counting.end_after_count == Some(len_generation) {
648                    // The count End waited on answers a frame a join has replaced. Retire the flag
649                    // and its status; do not re-ask: the current frame may be a different dataset's
650                    // (`end_after_count` names only a `len_generation`), which would then jump to its
651                    // end unasked.
652                    self.retire_the_end_that_was_waiting();
653                }
654                self.remember_a_downloads_shape();
655                None
656            }
657            AppEvent::FramePainted => {
658                self.frame_painted();
659                None
660            }
661            AppEvent::BackgroundLenFailed { len_generation } => {
662                if self.counting.len_count_inflight == Some(len_generation) {
663                    self.counting.len_count_inflight = None;
664                }
665                if self.counting.count_after_stop.take() == Some(len_generation) {
666                    self.count_exactly();
667                    return None;
668                }
669                if let Some(run) = self.prompt.query_running.as_mut()
670                    && run.counts.len_count_inflight == Some(len_generation)
671                {
672                    run.counts.len_count_inflight = None;
673                    run.counts.len_count_failed = Some(len_generation);
674                }
675                // Mark the count failed so the row count shows "?", only for the frame on screen:
676                // counts for two frames can run at once (a join, query, filter or sort takes a
677                // fresh `len_generation`), and an orphan's failure must not overwrite a live
678                // frame's.
679                if self
680                    .data_table_state
681                    .as_ref()
682                    .is_some_and(|state| state.len_generation() == len_generation)
683                {
684                    self.counting.len_count_failed = Some(len_generation);
685                }
686                // Only for the count End was waiting on, not another frame's failure.
687                if self.counting.end_after_count == Some(len_generation) {
688                    self.counting.end_after_count = None;
689                    if self
690                        .data_table_state
691                        .as_ref()
692                        .is_some_and(|state| state.len_generation() == len_generation)
693                    {
694                        self.status_message =
695                            Some("Could not count the rows to find the end".to_string());
696                    } else {
697                        // Its frame is gone, so its failure says nothing of the one on screen; retired
698                        // quietly, as above.
699                        self.take_down_the_counting_status();
700                    }
701                }
702                None
703            }
704            _ => unreachable!("not an event for counting_event"),
705        }
706    }
707
708    /// Count, behind the Info panel, the values the read's column types made null, for
709    /// the Notes: one pass the first time the panel opens on a typed dataset.
710    pub(crate) fn count_unfit(&mut self) {
711        let dataset = self.dataset_generation;
712        let Some(state) = self.data_table_state.as_ref() else {
713            return;
714        };
715        let read = state
716            .unfit_to_count()
717            .map(|(source, typed)| (source, typed, None));
718        let view = state
719            .changes_unfit_to_count()
720            .map(|(source, typed, version)| (source, typed, Some(version)));
721        let streaming = self.app_config.performance.streaming;
722        for (source, typed, version) in [read, view].into_iter().flatten() {
723            let running = self
724                .jobs
725                .current(|job| {
726                    matches!(job, Job::UnfitCount { dataset: d, version: v }
727                        if *d == dataset && *v == version)
728                })
729                .is_some();
730            if running {
731                continue;
732            }
733            self.spawn_job(Job::UnfitCount { dataset, version }, None, move |_| {
734                let counted = crate::analysis::statistics::collect_lazy(
735                    crate::formats::column_types::unfit_frame(source, &typed),
736                    streaming,
737                )
738                .map_err(|e| crate::error_display::user_message_from_polars(&e))?;
739                Ok(Answer::UnfitCounted(
740                    crate::formats::column_types::unfit_counts(&counted, &typed),
741                ))
742            });
743        }
744    }
745
746    /// Whether the values the read's column types made null are being counted.
747    pub fn unfit_count_pending(&self) -> bool {
748        self.jobs
749            .current(|job| matches!(job, Job::UnfitCount { .. }))
750            .is_some()
751    }
752}