Skip to main content

datui_lib/analysis/
sample_draw.rs

1//! The view's sample: the draw that fills it as the table shows it, and taking it
2//! away. The sample is a view step between source and query
3//! ([`crate::table::Sampled`]); a [`Job::SampleDraw`] draws it off the UI thread a
4//! chunk at a time ([`crate::analysis::table_sample`]), each chunk laid under the view's
5//! frames as it lands, like a followed pipe's rows.
6
7use crate::analysis::table_sample::{Limit, MemoryCheck, MemoryProbe};
8use crate::app::jobs::{Answer, Job, SampleDraw};
9use crate::table::DataTableState;
10use crate::{App, AppEvent, analysis::analysis_modal, analysis::data_quality, analysis::sampling};
11use std::sync::Arc;
12
13/// The sample form, and what the draws learned of memory and of the paths they took.
14pub struct SampleState {
15    /// The Sample form over the table (`S`).
16    pub form: Option<crate::analysis::sample_modal::SampleForm>,
17    /// Where available memory is read from for the sample's check: the system's,
18    /// unless a test sets it.
19    pub(crate) memory_probe: crate::analysis::table_sample::MemoryProbe,
20    /// How each random sample of a stream was drawn on this dataset, by source: redrawn,
21    /// the same seed keeps the same rows whether or not the count has come in.
22    pub(crate) paths: Vec<(String, crate::analysis::table_sample::DrawPath)>,
23}
24
25impl SampleState {
26    /// A new dataset is on screen: the paths draws took were the last one's.
27    pub(crate) fn reset_for_dataset(&mut self) {
28        self.paths.clear();
29    }
30}
31
32/// What the status line says while a sample is drawn.
33const DRAWING: &str = "Sampling...";
34
35/// Why a sample is not drawn under a pivot.
36const PIVOT_OVER_A_SAMPLE: &str = "A pivot cannot be laid on a sample as it is drawn: sample \
37     the pivoted view (Rows from: All rows), or take the pivot away with R";
38
39/// Why the sample under a pivot stays.
40const PIVOT_OFF_A_SAMPLE: &str = "The pivot on the sample cannot move to the source: R takes \
41     away both";
42
43impl App {
44    /// The memory check a sample is drawn under: `analysis.sample_memory_limit`
45    /// against what is held, or the memory available now.
46    pub(crate) fn memory_check(&self) -> MemoryCheck {
47        MemoryCheck {
48            limit: Limit::of_setting(self.app_config.analysis.sample_memory_limit),
49            probe: Arc::clone(&self.sample.memory_probe),
50        }
51    }
52
53    /// Read available memory from `probe` instead of the system (for tests).
54    pub fn set_memory_probe(&mut self, probe: MemoryProbe) {
55        self.sample.memory_probe = probe;
56    }
57
58    /// Draw `sample` as the view's sample, replacing any. Rows show as they land, with
59    /// `replay` (an applied view's query, filters, sort and columns) laid over them;
60    /// without it the view's own steps go back on unless the sample was drawn through
61    /// them. `anyway` skips the running memory check; `then_analyze` runs Analysis's
62    /// tool once drawn.
63    pub(crate) fn apply_table_sample(
64        &mut self,
65        sample: sampling::Sample,
66        replay: Option<crate::view::ViewSettings>,
67        anyway: bool,
68        then_analyze: bool,
69    ) {
70        self.draw_table_sample(sample, None, replay, anyway, then_analyze);
71    }
72
73    /// [`Self::apply_table_sample`], drawing a stream's random sample the way `path`
74    /// says (as a saved view recorded), giving the same rows.
75    pub(crate) fn draw_table_sample(
76        &mut self,
77        sample: sampling::Sample,
78        path: Option<crate::analysis::table_sample::DrawPath>,
79        replay: Option<crate::view::ViewSettings>,
80        anyway: bool,
81        then_analyze: bool,
82    ) {
83        use crate::analysis::data_quality::QualityScope;
84        let Some(state) = self.data_table_state.as_ref() else {
85            return;
86        };
87        let source = state.unsampled();
88        // A view scope reads the view as shown, so its steps are part of the sample and not
89        // laid on again; its order decides which rows a row range reads.
90        let reads_view = !sample.scope.uses_source();
91        let ranged = matches!(
92            sample.scope,
93            QualityScope::FirstRows(_) | QualityScope::ViewRows { .. }
94        );
95        let sorted = !source.get_sort_columns().is_empty() || !source.get_sort_ascending();
96        let through = reads_view
97            && (source.changes_rows() || !source.column_changes().is_empty() || ranged && sorted);
98        // Steps for the new sample: an applied view's, those on the replaced sample, or the
99        // view's own unless the sample stands for them.
100        let replay = replay.or_else(|| match state.sampled() {
101            Some(_) => Some(crate::view_settings_of(state)),
102            None => (!through).then(|| crate::view_settings_of(source)),
103        });
104        // A pivot needs all its rows, which a sample being drawn lacks: say so rather than
105        // drop it.
106        if replay
107            .as_ref()
108            .is_some_and(|settings| settings.pivot.is_some())
109        {
110            self.error_modal.show(PIVOT_OVER_A_SAMPLE.to_string());
111            return;
112        }
113        self.put_down_sample_draw();
114        let Some(state) = self.data_table_state.as_ref() else {
115            return;
116        };
117        let source = state.unsampled();
118        let (cut, known_total) = Self::table_sample_source(source, &sample.scope);
119        // How a stream's random sample is drawn is decided once per source: the view's
120        // word, the earlier draw's, or whether the count is in. The seed keeps the rows.
121        let path_key = Self::sample_path_key(source, &sample.scope);
122        let path = (sample.method == sampling::SampleMethod::Spread).then(|| {
123            path.or_else(|| {
124                self.sample
125                    .paths
126                    .iter()
127                    .find(|(key, _)| *key == path_key)
128                    .map(|(_, path)| *path)
129            })
130            .unwrap_or(match known_total {
131                Some(of) => crate::analysis::table_sample::DrawPath::Bernoulli { of },
132                None => crate::analysis::table_sample::DrawPath::Reservoir,
133            })
134        });
135        let bytes_per_row = Some(source.sample_row_bytes(sample.scope.uses_source()));
136        let streaming = self.app_config.performance.streaming;
137        let rows = Arc::new(crate::analysis::table_sample::SampleRows::default());
138        let (memory, watch) = if anyway {
139            (MemoryCheck::off(), sampling::ReadWatch::default())
140        } else {
141            let memory = self.memory_check();
142            // A sampler that keeps its rows to the end is checked as it holds them.
143            let held = memory.clone();
144            let watch = sampling::ReadWatch::judging_held(Arc::new(move |bytes, rows| {
145                held.holds_too_much(bytes, rows)
146            }));
147            (memory, watch)
148        };
149        let job = Job::SampleDraw(Box::new(SampleDraw {
150            sample: sample.clone(),
151            rows: Arc::clone(&rows),
152            watch: watch.clone(),
153            through,
154            replay,
155            then_analyze,
156            path,
157            path_key,
158            schema: None,
159        }));
160        self.spawn_job(job, Some(DRAWING), move |worker| {
161            let report = worker.reporter();
162            let failed = |error: color_eyre::eyre::Report| {
163                crate::error_display::user_message_from_report(&error, None)
164            };
165            let lf = cut.cut(&sample.scope).map_err(failed)?;
166            let schema = lf.clone().collect_schema().map_err(|e| failed(e.into()))?;
167            report(crate::Progress::SampleBegun(schema));
168            let live = crate::analysis::table_sample::Live {
169                rows,
170                notify: Arc::new(move || report(crate::Progress::SampleGrew)),
171                memory,
172                watch,
173                bytes_per_row,
174            };
175            let drawn = crate::analysis::table_sample::draw(
176                &lf,
177                &sample,
178                known_total,
179                path,
180                streaming,
181                &live,
182            )
183            .map_err(failed)?;
184            Ok(Answer::SampleDrawn(drawn))
185        });
186    }
187
188    /// Where a view's sample is drawn from: the loaded source for a source scope, else
189    /// the view as shown (in order, for a row range) with every column whole.
190    fn table_sample_source(
191        state: &DataTableState,
192        scope: &crate::analysis::data_quality::QualityScope,
193    ) -> (sampling::SampleSource, Option<usize>) {
194        use crate::analysis::data_quality::QualityScope;
195        if scope.uses_source() {
196            let (lf, source) = state.data_quality_source_scan();
197            return (sampling::SampleSource::loaded(lf, source), None);
198        }
199        let lf = match scope {
200            QualityScope::FirstRows(_) | QualityScope::ViewRows { .. } => state.lf().clone(),
201            _ => state.analysis_lf(),
202        };
203        let columns: Vec<polars::prelude::Expr> = state
204            .schema()
205            .iter_names()
206            .map(|name| polars::prelude::col(name.clone()))
207            .collect();
208        (
209            sampling::SampleSource::view(lf.select(columns)),
210            sampling::view_scope_rows(state.num_rows_if_valid(), scope),
211        )
212    }
213
214    /// Remove the view's sample: the source view returns with the sample's query,
215    /// filters and sort laid on it, unless the sample was drawn through the view's own.
216    pub(crate) fn clear_table_sample(&mut self) {
217        let Some(sampled) = self.data_table_state.as_ref().and_then(|s| s.sampled()) else {
218            return;
219        };
220        let through = sampled.through();
221        let settings = self.data_table_state.as_ref().map(crate::view_settings_of);
222        // Moving a pivot off the sample would read the whole source: the sample stays, and
223        // the way out is said.
224        if !through && settings.as_ref().is_some_and(|s| s.pivot.is_some()) {
225            self.error_modal.show(PIVOT_OFF_A_SAMPLE.to_string());
226            return;
227        }
228        self.put_down_sample_draw();
229        let Some(state) = self.data_table_state.take() else {
230            return;
231        };
232        let mut source = state.into_unsampled();
233        if !through && let Some(settings) = settings {
234            let laid = source.deferred(|s| {
235                s.reset_view_for_replay();
236                Self::replay_view(s, &settings, None).map(|_| ())
237            });
238            if let Err(error) = laid {
239                self.error_modal.show(format!(
240                    "The view's steps did not go back on the source: {error}"
241                ));
242            }
243        }
244        self.data_table_state = Some(source);
245        self.sample_changed();
246        self.flash_note("Sample cleared".to_string());
247        self.spawn_async_collect(Self::LOADING_BUFFER);
248    }
249
250    /// The view's rows changed under every result read from them: Analysis's, the
251    /// chart's, and in-flight pages.
252    pub(crate) fn sample_changed(&mut self) {
253        self.forget_the_rows_read();
254        self.chart.cache.clear();
255        let modal = &mut self.analysis_modal;
256        modal.describe_results = None;
257        modal.distribution_results = None;
258        modal.correlation_results = None;
259        modal.quality.results = None;
260        modal.quality.last_plan = None;
261        let sampled = self
262            .data_table_state
263            .as_ref()
264            .is_some_and(|state| state.sampled().is_some());
265        self.analysis_modal.follow_view_sample(sampled);
266        self.sync_quality_plan();
267    }
268
269    /// The draw in flight, if one is: its record.
270    pub(crate) fn sample_draw(&self) -> Option<&SampleDraw> {
271        match self.jobs.current(|job| matches!(job, Job::SampleDraw(_))) {
272            Some((_, Job::SampleDraw(draw))) => Some(draw),
273            _ => None,
274        }
275    }
276
277    /// Whether the view's sample is being drawn.
278    pub fn sample_drawing(&self) -> bool {
279        self.sample_draw().is_some()
280    }
281
282    /// Esc while the sample is drawn: it stops, and the rows so far stay.
283    pub(crate) fn stop_sample_draw(&mut self) {
284        if let Some(draw) = self.sample_draw() {
285            draw.watch.stop();
286        }
287    }
288
289    /// Stop the draw in flight and drop what it still sends: replaced by another
290    /// sample, or its view is gone.
291    pub(crate) fn put_down_sample_draw(&mut self) {
292        if let Some(draw) = self.sample_draw() {
293            draw.watch.stop();
294        }
295        self.jobs.supersede(|job| matches!(job, Job::SampleDraw(_)));
296    }
297
298    /// Whether the view on screen is the one the draw in flight fills.
299    fn draw_fills_view(&self, draw: &SampleDraw) -> bool {
300        self.data_table_state
301            .as_ref()
302            .and_then(|state| state.sampled())
303            .is_some_and(|sampled| sampled.holds(&draw.rows))
304    }
305
306    /// The draw cut its rows to scope: keep their columns for the view taking its first
307    /// rows.
308    pub(crate) fn sample_begun(&mut self, schema: &polars::prelude::SchemaRef) {
309        if let Some(Job::SampleDraw(draw)) = self
310            .jobs
311            .current_mut(|job| matches!(job, Job::SampleDraw(_)))
312        {
313            draw.schema = Some(schema.clone());
314        }
315    }
316
317    /// The view becomes the sample's, steps laid on, replacing its source view or the
318    /// previous sample. Only once rows have come: a draw failing first leaves the view.
319    /// Returns whether the view is the draw's.
320    fn take_on_sample(&mut self, draw: &SampleDraw) -> bool {
321        if self.draw_fills_view(draw) {
322            return true;
323        }
324        let Some(schema) = draw.schema.as_ref() else {
325            return false;
326        };
327        let Some(state) = self.data_table_state.take() else {
328            return false;
329        };
330        let source = state.into_unsampled();
331        let mut view = match DataTableState::sampled_from(
332            source,
333            draw.sample.clone(),
334            schema,
335            Arc::clone(&draw.rows),
336            draw.through,
337            draw.path,
338        ) {
339            Ok(view) => view,
340            Err(error) => {
341                self.put_down_sample_draw();
342                self.error_modal
343                    .show(format!("Cannot show the sample: {error}"));
344                return false;
345            }
346        };
347        if let Some(settings) = &draw.replay {
348            let laid = view.deferred(|s| Self::replay_view(s, settings, None));
349            match laid {
350                Ok(crate::Replayed::Planned) => {}
351                // Refused before the draw started; never left off silently.
352                Ok(crate::Replayed::Pivot(_)) => {
353                    self.error_modal.show(PIVOT_OVER_A_SAMPLE.to_string());
354                }
355                Err(error) => self.flash_note(format!(
356                    "The view's steps did not apply to the sample: {error}"
357                )),
358            }
359        }
360        self.data_table_state = Some(view);
361        self.sample_changed();
362        true
363    }
364
365    /// The draw kept another chunk: the view reads it, staying where it is.
366    pub(crate) fn sample_grew(&mut self) {
367        let Some(draw) = self.sample_draw().cloned() else {
368            return;
369        };
370        if !self.take_on_sample(&draw) {
371            return;
372        }
373        let grew = self
374            .data_table_state
375            .as_mut()
376            .and_then(DataTableState::sample_grew);
377        // Rows read before it grew are fewer and in another order: none may land now.
378        if grew == Some(false) {
379            self.forget_the_rows_read();
380        }
381        // Re-read the page when nothing else is reading it; else the next chunk or the end
382        // does.
383        if grew.is_some() && self.rows_in_flight().is_none() && self.in_normal_table_view() {
384            self.spawn_collect(None);
385        }
386    }
387
388    /// The draw ended: rows go into source order, and a waiting Analysis run starts. A
389    /// draw stopped by memory says why.
390    pub(crate) fn sample_drawn(
391        &mut self,
392        draw: SampleDraw,
393        current: bool,
394        drawn: crate::analysis::table_sample::Drawn,
395    ) -> Option<AppEvent> {
396        if !current || !self.take_on_sample(&draw) {
397            return None;
398        }
399        if let Some(path) = drawn.path {
400            self.sample.paths.retain(|(key, _)| *key != draw.path_key);
401            self.sample.paths.push((draw.path_key.clone(), path));
402        }
403        if let Some(state) = self.data_table_state.as_mut() {
404            state.sample_drawn(drawn);
405        }
406        self.forget_the_rows_read();
407        if let Some(reason) = draw.rows.stopped() {
408            self.flash_note(reason);
409        }
410        self.spawn_collect(None);
411        if draw.then_analyze && self.overlay == crate::Overlay::Analysis {
412            self.analysis_modal.computing = None;
413            return self.start_analysis_run();
414        }
415        None
416    }
417
418    /// The draw failed or stopped before keeping a row: the view stays as it was. Rows
419    /// that came stay, as a sample cut short.
420    pub(crate) fn sample_draw_failed(&mut self, draw: &SampleDraw, current: bool, message: &str) {
421        if !current {
422            return;
423        }
424        if self.draw_fills_view(draw)
425            && let Some(state) = self.data_table_state.as_mut()
426        {
427            state.sample_drawn(crate::analysis::table_sample::Drawn {
428                cut: true,
429                path: draw.path,
430                ..Default::default()
431            });
432            self.forget_the_rows_read();
433            self.spawn_collect(None);
434        }
435        if draw.then_analyze {
436            self.analysis_modal.computing = None;
437        }
438        if message == sampling::CANCELLED {
439            self.flash_note("Sample stopped".to_string());
440        } else {
441            self.error_modal.show(message.to_string());
442        }
443    }
444
445    /// Apply `view`, whose rows are a sample: back to its source, its drawn-through
446    /// view goes on, and the sample is redrawn from its seed; the view's own steps go on
447    /// the sample as it arrives.
448    pub(crate) fn apply_sampled_view(
449        &mut self,
450        view: &crate::view::SavedView,
451        saved: &crate::view::SavedSample,
452        why: Option<crate::view::MatchReason>,
453    ) -> color_eyre::Result<()> {
454        let sample = saved.sample()?;
455        let through = saved.through.as_deref();
456        if through.is_some_and(|through| through.pivot.is_some()) || view.settings.pivot.is_some() {
457            return Err(color_eyre::eyre::eyre!("{PIVOT_OVER_A_SAMPLE}"));
458        }
459        self.put_down_sample_draw();
460        let Some(state) = self.data_table_state.take() else {
461            return Ok(());
462        };
463        let mut source = state.into_unsampled();
464        let replayed = source.try_transition(|s| {
465            s.reset_view_for_replay();
466            match through {
467                Some(through) => Self::replay_view(s, through, None).map(|_| ()),
468                None => Ok(()),
469            }
470        });
471        self.data_table_state = Some(source);
472        replayed?;
473        self.sample_changed();
474        if let Some(path) = &self.path {
475            use crate::logging::LogFailure;
476            self.views
477                .manager
478                .record_use(&view.id, path)
479                .or_log("record a view's use");
480        }
481        self.views.active_id = Some(view.id.clone());
482        self.restore_view_chart(view.settings.chart.as_ref());
483        let mut settings = view.settings.clone();
484        settings.sample = None;
485        self.draw_table_sample(sample, saved.path, Some(settings), false, false);
486        if let Some(why) = why {
487            self.flash_view_applied(&view.name, why);
488        }
489        self.first_rows_settled();
490        Ok(())
491    }
492
493    /// Where the value tools (Describe, Distribution, Correlation) read the shared
494    /// sample from, and its known size. Row ranges count in the table's order; other
495    /// view scopes skip the sort, which no statistic needs and which would make a
496    /// sampled read read everything.
497    pub(crate) fn sample_source(
498        &self,
499        state: &DataTableState,
500    ) -> (sampling::SampleSource, Option<usize>) {
501        Self::sample_source_for(state, &self.analysis_modal.sample.scope)
502    }
503
504    pub(crate) fn sample_source_for(
505        state: &DataTableState,
506        scope: &data_quality::QualityScope,
507    ) -> (sampling::SampleSource, Option<usize>) {
508        if scope.uses_source() {
509            let (lf, source) = state.data_quality_source_scan();
510            return (sampling::SampleSource::loaded(lf, source), None);
511        }
512        let lf = match scope {
513            data_quality::QualityScope::FirstRows(_)
514            | data_quality::QualityScope::ViewRows { .. } => state.lf().clone(),
515            _ => state.analysis_lf(),
516        };
517        (
518            sampling::SampleSource::view(lf.select(state.binary_stub_exprs())),
519            sampling::view_scope_rows(state.num_rows_if_valid(), scope),
520        )
521    }
522
523    /// Read the shared sample, as the tool on screen does, to show as a table: Data
524    /// Quality's kept sample is cut when it matches, otherwise redrawn from its seed.
525    pub(crate) fn read_sample_view(&mut self) -> Option<AppEvent> {
526        let sample = self.analysis_modal.sample.clone();
527        self.read_sample_rows(sample, None)
528    }
529
530    /// Show the sample's rows in place of the table, as Data Quality's drill-in does;
531    /// Esc brings back the table and Analysis.
532    pub(crate) fn show_sample_view(&mut self, df: polars::prelude::DataFrame, label: String) {
533        let Some(state) = self.data_table_state.as_ref() else {
534            return;
535        };
536        let view = match state.sample_view(df) {
537            Ok(view) => view,
538            Err(error) => {
539                self.error_modal
540                    .show(format!("Cannot show the sample: {error}"));
541                return;
542            }
543        };
544        if let Some(original) = self.data_table_state.replace(view) {
545            self.quality.evidence_return = Some(Box::new(original));
546            self.quality.evidence_label = Some(label);
547            self.step_back();
548            self.forget_the_rows_read();
549            self.spawn_async_collect("Loading the sample...");
550        }
551    }
552
553    /// Adopt a new shared sample: every tool's old results go and the tool on screen
554    /// reruns. Data Quality only takes it into its plan until Run.
555    pub(crate) fn apply_sample(&mut self, sample: sampling::Sample) -> Option<AppEvent> {
556        // A run it would start waits for a cancelled one, with every result kept.
557        if self.analysis_modal.selected_tool != Some(analysis_modal::AnalysisTool::DataQuality)
558            && self.read_waits_for_cancelled()
559        {
560            return None;
561        }
562        // A first run on the sample as it stands takes nothing from the other tools.
563        if sample != self.analysis_modal.sample {
564            self.analysis_modal.describe_results = None;
565            self.analysis_modal.distribution_results = None;
566            self.analysis_modal.correlation_results = None;
567            self.analysis_modal.quality.results = None;
568            self.analysis_modal.quality.last_plan = None;
569            self.analysis_modal.quality.from_cache = false;
570        }
571        self.analysis_modal.sample = sample;
572        self.analysis_modal.sample_dataset = Some(self.dataset_generation);
573        self.analysis_modal.sample_run_for = Some(self.dataset_generation);
574        self.sync_quality_plan();
575        if self.analysis_modal.selected_tool == Some(analysis_modal::AnalysisTool::DataQuality) {
576            return None;
577        }
578        self.start_analysis_run()
579    }
580}