Skip to main content

gpui_kit_grid/engine/
mod.rs

1//! The projection engine: filter and sort a snapshot into the display order.
2//! No GPUI types.
3//!
4//! Each part of the engine has its own file: the shown lines and their
5//! readers (`display`), grouping (`group`), aggregates (`aggregate`),
6//! expansion (`expand`) and the tree (`tree`). This file keeps the view,
7//! the projection and the flat path.
8
9mod aggregate;
10mod display;
11mod expand;
12mod group;
13mod tree;
14
15use std::{
16    cmp::Ordering,
17    panic,
18    sync::{
19        Arc, OnceLock,
20        atomic::{AtomicBool, AtomicUsize, Ordering as AtomicOrdering},
21    },
22    thread,
23};
24
25use rustc_hash::{FxHashMap, FxHashSet};
26use serde::{Deserialize, Serialize};
27
28pub use self::{
29    aggregate::{Aggregate, AggregateFn},
30    display::{
31        AggregateRow, DisplayRow, ExpandDepth, Expansion, GroupId, GroupInfo, GroupPath, GroupTable,
32    },
33    group::{
34        AggregateOrder, EveryGroupFooter, GroupBy, GroupFooter, GroupKey, GroupOrder,
35        GroupOrderContext, KeyOrder, NoGroupFooter,
36    },
37    tree::{
38        Branch, TreeFilter, TreeFilterContext, TreeFilterRule, TreeSpec, TreeTotals,
39        TreeTotalsContext, TreeTotalsRule,
40    },
41};
42use self::{
43    display::{AggregateValues, Lines, SummaryStamps},
44    group::GroupSpec,
45};
46use crate::{
47    model::{CellValue, ColumnSet, GridColumns, GridRow},
48    quick_filter::{QuickFilter, QuickFilterPass},
49    store::{RowId, StoreSnapshot},
50};
51
52/// Sort direction of a column. Serialized as `"ascending"` or
53/// `"descending"`.
54#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
55#[serde(rename_all = "lowercase")]
56pub enum SortDirection {
57    /// Smallest first.
58    Ascending,
59    /// Largest first.
60    Descending,
61}
62
63type Filter<R> = Arc<dyn Fn(&R) -> bool + Send + Sync>;
64
65/// What the view shows: a filter, a quick filter and a multi-column sort,
66/// a grouping or a tree, aggregates and the expansion.
67///
68/// Cloning is cheap: every part is shared or small.
69pub struct ViewSpec<R: GridRow> {
70    sort: Vec<(R::Column, SortDirection)>,
71    filter: Option<(ColumnSet, Filter<R>)>,
72    quick_filter: Option<QuickFilter>,
73    /// Flat, grouped or a tree; the last of `with_group_by` and
74    /// `with_tree` wins.
75    structure: Structure<R>,
76    /// The order of sibling groups; `None` is [`KeyOrder`].
77    group_order: Option<Arc<dyn GroupOrder<R>>>,
78    /// Which groups have a footer; `None` is [`NoGroupFooter`].
79    group_footer: Option<Arc<dyn GroupFooter>>,
80    /// Aggregates by column; `None` has none. They outlive a change of the
81    /// structure, as the group order and footer do.
82    aggregates: Option<Arc<[(R::Column, Aggregate<R>)]>>,
83    /// Set by the grid, which owns it like the quick filter.
84    expansion: Expansion<R::Key>,
85}
86
87/// How a view arranges its rows.
88pub(crate) enum Structure<R: GridRow> {
89    /// A flat list.
90    Flat,
91    /// Groups by these levels.
92    Groups(GroupSpec<R>),
93    /// A tree by `GridRow::parent_key`.
94    Tree(TreeSpec<R>),
95}
96
97impl<R: GridRow> Clone for Structure<R> {
98    fn clone(&self) -> Self {
99        match self {
100            Self::Flat => Self::Flat,
101            Self::Groups(groups) => Self::Groups(groups.clone()),
102            Self::Tree(tree) => Self::Tree(tree.clone()),
103        }
104    }
105}
106
107impl<R: GridRow> Clone for ViewSpec<R> {
108    fn clone(&self) -> Self {
109        Self {
110            sort: self.sort.clone(),
111            filter: self.filter.clone(),
112            quick_filter: self.quick_filter.clone(),
113            structure: self.structure.clone(),
114            group_order: self.group_order.clone(),
115            group_footer: self.group_footer.clone(),
116            aggregates: self.aggregates.clone(),
117            expansion: self.expansion.clone(),
118        }
119    }
120}
121
122impl<R: GridRow> Default for ViewSpec<R> {
123    fn default() -> Self {
124        Self {
125            sort: Vec::new(),
126            filter: None,
127            quick_filter: None,
128            structure: Structure::Flat,
129            group_order: None,
130            group_footer: None,
131            aggregates: None,
132            expansion: Expansion::default(),
133        }
134    }
135}
136
137impl<R: GridRow> ViewSpec<R> {
138    /// No filter, insertion order.
139    pub fn new() -> Self {
140        Self::default()
141    }
142
143    /// Sorts by these columns, first one most significant. Ties keep
144    /// insertion order, so the order is stable.
145    pub fn with_sort(mut self, sort: impl IntoIterator<Item = (R::Column, SortDirection)>) -> Self {
146        self.sort = sort.into_iter().collect();
147        self
148    }
149
150    /// Shows only rows for which `filter` returns true. `depends_on` lists
151    /// the columns the filter reads; pass every column when unsure. A
152    /// rebuild of a large snapshot calls `filter` from several threads at
153    /// once.
154    pub fn with_filter(
155        mut self,
156        depends_on: ColumnSet,
157        filter: impl Fn(&R) -> bool + Send + Sync + 'static,
158    ) -> Self {
159        self.filter = Some((depends_on, Arc::new(filter)));
160        self
161    }
162
163    /// Shows only rows that also pass the quick text search; `None` turns
164    /// it off. A row is shown when it passes both the filter and the
165    /// quick filter.
166    pub fn with_quick_filter(mut self, filter: Option<QuickFilter>) -> Self {
167        self.quick_filter = filter;
168        self
169    }
170
171    /// The sort columns.
172    pub fn sort(&self) -> &[(R::Column, SortDirection)] {
173        &self.sort
174    }
175
176    /// The quick filter, if it is on.
177    pub fn quick_filter(&self) -> Option<&QuickFilter> {
178        self.quick_filter.as_ref()
179    }
180
181    /// Columns whose changes can move rows: the sort columns, the columns
182    /// both filters read and, in a grouped view, the grouping columns, the
183    /// columns `GroupKey`s read and those the group order reads (for an
184    /// order by an aggregate, what that aggregate reads). Inserts and
185    /// removals always do.
186    ///
187    /// A change of the other aggregated columns
188    /// ([`Self::aggregate_columns`]) changes values alone: the aggregate job
189    /// ([`ProjectionEngine::aggregate`]) refreshes them in the same order.
190    pub fn dependencies(&self) -> ColumnSet {
191        let mut columns = ColumnSet::EMPTY;
192        for (column, _) in &self.sort {
193            columns.insert(column.ix());
194        }
195        if let Some((depends_on, _)) = &self.filter {
196            columns.union_with(*depends_on);
197        }
198        if let Some(quick_filter) = &self.quick_filter {
199            columns.union_with(quick_filter.columns());
200        }
201        if let Structure::Groups(groups) = &self.structure {
202            for level in groups.levels() {
203                match (level.as_column(), level.as_key()) {
204                    (Some(column), _) => columns.insert(column.ix()),
205                    (None, Some(key)) => columns.union_with(key.depends_on()),
206                    (None, None) => {}
207                }
208            }
209            // An order by an aggregate reads what that aggregate reads.
210            let order_columns = self.group_order().depends_on();
211            columns.union_with(order_columns);
212            for (column, aggregate) in self.aggregates() {
213                if order_columns.contains(column.ix()) {
214                    columns.union_with(aggregate.depends_on(*column));
215                }
216            }
217        }
218        columns
219    }
220
221    /// Flat, grouped or a tree.
222    pub(crate) fn structure(&self) -> &Structure<R> {
223        &self.structure
224    }
225}
226
227/// The display order of one view at one store version.
228///
229/// A projection has two orders. [`Self::rows`] lists every data row that
230/// passes the view, as if everything were expanded. The lines shown
231/// ([`Self::line`], [`Self::len`]) are what the body paints: in a flat view
232/// the same rows; with grouping also group captions and footers, without
233/// the rows of collapsed groups; in a tree, the rows whose ancestors are
234/// all expanded. Display positions index the lines.
235#[derive(Debug, Default)]
236pub struct Projection {
237    store_version: u64,
238    /// Every data row that passes the view, in order as if all were
239    /// expanded; the rows of a group follow each other.
240    rows: Vec<RowId>,
241    /// The place of each row in `rows`, by slot; `u32::MAX` for none. In a
242    /// flat projection it is the display position too: a flat projection
243    /// keeps no second list.
244    row_to_order: Vec<u32>,
245    /// The places of the rows whose slot an earlier row of `rows` holds in
246    /// `row_to_order`: a fading row shares its slot with the new row that
247    /// reused it. Empty otherwise.
248    shared_order: FxHashMap<RowId, u32>,
249    /// Live rows of `rows` when built: no fading rows.
250    row_count: usize,
251    /// The shown lines of a grouped or tree projection.
252    lines: Lines,
253    /// The aggregates of the whole view.
254    totals: AggregateValues,
255    /// Change stamps of the aggregates and captions while any is hot;
256    /// `None` for none.
257    summary_stamps: Option<Box<SummaryStamps>>,
258}
259
260impl Projection {
261    /// A flat projection of `rows`, in display order, all of them live
262    /// until [`Self::with_row_count`] says otherwise.
263    fn new<R: GridRow>(snapshot: &StoreSnapshot<R>, rows: Vec<RowId>) -> Self {
264        let (row_to_order, shared_order) =
265            places(rows.iter().copied().enumerate(), snapshot.capacity());
266        Self {
267            store_version: snapshot.version(),
268            row_count: rows.len(),
269            rows,
270            row_to_order,
271            shared_order,
272            lines: Lines::Flat,
273            totals: AggregateValues::default(),
274            summary_stamps: None,
275        }
276    }
277
278    /// A copy for a job that derives a new projection from this one; there
279    /// is no public `Clone`.
280    pub(crate) fn duplicate(&self) -> Self {
281        Self {
282            store_version: self.store_version,
283            rows: self.rows.clone(),
284            row_to_order: self.row_to_order.clone(),
285            shared_order: self.shared_order.clone(),
286            row_count: self.row_count,
287            lines: self.lines.clone(),
288            totals: self.totals.clone(),
289            summary_stamps: self.summary_stamps.clone(),
290        }
291    }
292
293    /// The store version the projection was built from.
294    pub fn store_version(&self) -> u64 {
295        self.store_version
296    }
297
298    /// Every data row that passes the view, in order as if every group and
299    /// tree row were expanded: the rows of a group follow each other. In a
300    /// flat view these are the displayed rows.
301    pub fn rows(&self) -> &[RowId] {
302        &self.rows
303    }
304
305    /// Returns true if the row passes the view, displayed or not: a row
306    /// of a collapsed group passes too, so commands may act on it when it
307    /// is selected.
308    pub fn contains(&self, id: RowId) -> bool {
309        self.order_ix(id).is_some()
310    }
311}
312
313/// Builds projections. The default is [`Rebuild`]; incremental engines
314/// implement the same trait.
315pub trait ProjectionEngine<R: GridRow>: Send + Sync + 'static {
316    /// Builds the display order of `snapshot` for `spec`.
317    fn rebuild(&self, snapshot: &StoreSnapshot<R>, spec: &ViewSpec<R>) -> Projection;
318
319    /// Drops rows removed since `previous` was built. Removing rows never
320    /// reorders the rest and never makes a filtered-out row visible, so this
321    /// is an O(n) filter, with no sorting. With groups it also recounts
322    /// them, drops the emptied ones and lays the lines out again with each
323    /// group's expansion; the aggregate job ([`Self::aggregate`]) refreshes
324    /// the aggregates.
325    fn compact(&self, previous: &Projection, snapshot: &StoreSnapshot<R>) -> Projection {
326        if previous.is_tree() {
327            tree::compact(previous, snapshot)
328        } else {
329            display::compact(previous, snapshot)
330        }
331    }
332
333    /// The order of `previous` with fresh aggregates and row counts from
334    /// `snapshot`. Neither reorders nor drops rows: [`Self::compact`] drops
335    /// removed ones. Groups combine their subgroups in key order, as a
336    /// rebuild does, so over the same order the result is the rebuild's.
337    fn aggregate(
338        &self,
339        previous: &Projection,
340        snapshot: &StoreSnapshot<R>,
341        spec: &ViewSpec<R>,
342    ) -> Projection {
343        if previous.is_tree() {
344            tree::refresh(previous, snapshot, spec)
345        } else {
346            aggregate::refresh(previous, snapshot, spec)
347        }
348    }
349
350    /// The order of `previous` laid out for `spec.expansion()`. The
351    /// snapshot resolves the keys of tree rows: a tree is expanded by key.
352    fn expand(
353        &self,
354        previous: &Projection,
355        snapshot: &StoreSnapshot<R>,
356        spec: &ViewSpec<R>,
357    ) -> Projection {
358        if previous.is_tree() {
359            tree::relayout(previous, snapshot, spec)
360        } else {
361            expand::relayout(previous, snapshot, spec)
362        }
363    }
364}
365
366/// Full filter, quick filter and sort of the whole snapshot.
367///
368/// A large snapshot is split across threads: each thread takes rows in
369/// chunks, keeps the visible ones and sorts them, and the sorted runs are
370/// merged. Every row has its own insertion sequence, the last sort key, so
371/// the order is the same on any number of threads. By default a rebuild
372/// leaves two cores to the UI thread and ingest and uses at most eight
373/// ([`Self::with_max_threads`] changes that). On wasm and for small
374/// snapshots it runs on the calling thread alone.
375#[derive(Clone, Copy, Debug, Default)]
376pub struct Rebuild {
377    /// Set by [`Self::with_max_threads`]; `None`: [`default_thread_budget`].
378    max_threads: Option<usize>,
379}
380
381impl Rebuild {
382    /// Splits a rebuild across at most `threads` threads, the calling one
383    /// included: `1` rebuilds on the calling thread alone, and `0` counts as
384    /// `1`. Each thread still takes at least 8192 rows, so a small snapshot
385    /// stays on one thread whatever the count. The count is used as given:
386    /// neither the cores of the machine nor the default's limit of eight cap
387    /// it. On wasm a rebuild always runs on the calling thread.
388    ///
389    /// The default is the available cores less two (for the UI thread and
390    /// ingest), at least one and at most eight.
391    pub fn with_max_threads(mut self, threads: usize) -> Self {
392        self.max_threads = Some(threads.max(1));
393        self
394    }
395
396    /// The most threads one rebuild uses, the calling one included: the
397    /// count given to [`Self::with_max_threads`], or the default budget.
398    /// Always `1` on wasm.
399    pub fn max_threads(&self) -> usize {
400        if cfg!(target_family = "wasm") {
401            // Threads may not exist on wasm.
402            1
403        } else {
404            self.max_threads.unwrap_or_else(default_thread_budget)
405        }
406    }
407
408    /// The flat path: filter and sort into one flat list, fading rows in
409    /// their places. A view with aggregates totals the live rows in one
410    /// more pass, in the order of the rows.
411    fn rebuild_flat<R: GridRow>(
412        &self,
413        snapshot: &StoreSnapshot<R>,
414        spec: &ViewSpec<R>,
415    ) -> Projection {
416        let parallelism = Parallelism::for_view(snapshot.len(), spec, self.max_threads());
417        let (rows, fading) = project(snapshot, spec, parallelism);
418        let live = rows.len() - fading;
419        let projection = Projection::new(snapshot, rows).with_row_count(live);
420        let aggregates = aggregate::ViewAggregates::new(spec);
421        if aggregates.is_empty() {
422            return projection;
423        }
424        let (totals, _) = aggregate::flat_totals(
425            &aggregates,
426            projection
427                .rows
428                .iter()
429                .filter_map(|id| snapshot.row(*id).map(|row| &**row)),
430        );
431        projection.with_totals(AggregateValues::new(aggregates.columns(), totals))
432    }
433}
434
435fn directed(ordering: Ordering, direction: SortDirection) -> Ordering {
436    match direction {
437        SortDirection::Ascending => ordering,
438        SortDirection::Descending => ordering.reverse(),
439    }
440}
441
442impl<R: GridRow> ProjectionEngine<R> for Rebuild {
443    fn rebuild(&self, snapshot: &StoreSnapshot<R>, spec: &ViewSpec<R>) -> Projection {
444        match spec.structure() {
445            Structure::Flat => self.rebuild_flat(snapshot, spec),
446            Structure::Groups(groups) => group::rebuild(self, snapshot, spec, groups),
447            Structure::Tree(tree) => tree::rebuild(self, snapshot, spec, tree),
448        }
449    }
450}
451
452/// Rows below which another thread does not pay for itself.
453const MIN_ROWS_PER_THREAD: usize = 8 * 1024;
454/// Rows a thread takes at a time: small enough to even out slower cores,
455/// large enough that the shared counter is not contended.
456const CHUNK_ROWS: usize = 1024;
457/// Cores the default budget leaves to the UI thread and to ingest.
458const RESERVED_CORES: usize = 2;
459/// The default budget's limit: more threads gain little, as the merge grows
460/// with their count. An explicit [`Rebuild::with_max_threads`] may exceed it.
461const MAX_THREADS: usize = 8;
462
463/// How one rebuild splits its work.
464#[derive(Clone, Copy, Debug)]
465struct Parallelism {
466    /// Threads, the calling one included.
467    threads: usize,
468    /// Rows a thread takes from the snapshot at a time.
469    chunk_rows: usize,
470}
471
472impl Parallelism {
473    /// A thread per [`MIN_ROWS_PER_THREAD`] rows, at most `budget`. In
474    /// insertion order with no filter there is nothing worth splitting: the
475    /// rows are only listed.
476    fn for_view<R: GridRow>(rows: usize, spec: &ViewSpec<R>, budget: usize) -> Self {
477        let has_work =
478            !spec.sort.is_empty() || spec.filter.is_some() || spec.quick_filter.is_some();
479        if has_work {
480            Self::for_rows(rows, budget)
481        } else {
482            Self {
483                threads: 1,
484                chunk_rows: CHUNK_ROWS,
485            }
486        }
487    }
488
489    /// A thread per [`MIN_ROWS_PER_THREAD`] rows, at most `budget`, for a
490    /// sort that always has keys, such as a grouped one.
491    fn for_rows(rows: usize, budget: usize) -> Self {
492        Self {
493            threads: budget.min(rows / MIN_ROWS_PER_THREAD).max(1),
494            chunk_rows: CHUNK_ROWS,
495        }
496    }
497}
498
499/// The threads one rebuild uses by default: the available parallelism less
500/// [`RESERVED_CORES`], at least one and at most [`MAX_THREADS`].
501fn default_thread_budget() -> usize {
502    static BUDGET: OnceLock<usize> = OnceLock::new();
503    *BUDGET.get_or_init(|| {
504        thread::available_parallelism().map_or(1, |cores| {
505            cores
506                .get()
507                .saturating_sub(RESERVED_CORES)
508                .clamp(1, MAX_THREADS)
509        })
510    })
511}
512
513/// The display order of `snapshot` for `spec`, with the number of fading
514/// rows in it.
515fn project<R: GridRow>(
516    snapshot: &StoreSnapshot<R>,
517    spec: &ViewSpec<R>,
518    parallelism: Parallelism,
519) -> (Vec<RowId>, usize) {
520    match spec.sort.split_first() {
521        // Storage order is not insertion order once slots are reused.
522        None => sorted_rows(
523            snapshot,
524            spec,
525            parallelism,
526            |id, _, seq| (seq, id),
527            |a, b| a.0.cmp(&b.0),
528            |&(_, id)| id,
529        ),
530        // Extract the most significant key once per row; later keys are
531        // read only to break ties.
532        Some((&(first, first_direction), rest)) => sorted_rows(
533            snapshot,
534            spec,
535            parallelism,
536            |id, row, seq| (row.value(first), seq, id, row),
537            move |a, b| compare_sorted(a, b, first_direction, rest),
538            |&(_, _, id, _)| id,
539        ),
540    }
541}
542
543/// A sorted row: its most significant key, insertion sequence, id and row.
544type SortEntry<'a, R> = (CellValue<'a>, u64, RowId, &'a Arc<R>);
545
546/// Compares by the sort keys, then by insertion. Called for nearly every
547/// pair of rows by the sort and the merges; without `inline(always)` it is
548/// not inlined there, which costs a tenth of a text sort.
549#[inline(always)]
550fn compare_sorted<R: GridRow>(
551    a: &SortEntry<'_, R>,
552    b: &SortEntry<'_, R>,
553    first_direction: SortDirection,
554    rest: &[(R::Column, SortDirection)],
555) -> Ordering {
556    directed(a.0.cmp_total(&b.0), first_direction)
557        .then_with(|| {
558            rest.iter()
559                .map(|&(column, direction)| {
560                    directed(a.3.value(column).cmp_total(&b.3.value(column)), direction)
561                })
562                .find(|ordering| ordering.is_ne())
563                .unwrap_or(Ordering::Equal)
564        })
565        .then_with(|| a.1.cmp(&b.1))
566}
567
568/// The visible rows of `snapshot` and its visible fading rows, ordered by
569/// their entries and mapped by `row_id`, with the number of fading rows
570/// among them. `entry` takes a row's insertion sequence: a fading row keeps
571/// its place by its last values and its sequence. `compare` is a total
572/// order under which no two rows are equal (the insertion sequence breaks
573/// ties), so the result does not depend on how the work is split.
574fn sorted_rows<'a, R: GridRow, E: Copy + Send + Sync, T>(
575    snapshot: &'a StoreSnapshot<R>,
576    spec: &ViewSpec<R>,
577    parallelism: Parallelism,
578    entry: impl Fn(RowId, &'a Arc<R>, u64) -> E + Sync,
579    compare: impl Fn(&E, &E) -> Ordering + Sync,
580    row_id: impl Fn(&E) -> T,
581) -> (Vec<T>, usize) {
582    let mut fading = fading_entries(snapshot, spec, &entry);
583    let fading_count = fading.len();
584    if parallelism.threads <= 1 {
585        let mut visible = VisibleRows::new(spec);
586        let mut entries: Vec<E> = snapshot
587            .iter()
588            .filter(|(_, row)| visible.contains(row))
589            .map(|(id, row)| entry(id, row, snapshot.seq(id)))
590            .collect();
591        entries.append(&mut fading);
592        entries.sort_unstable_by(|a, b| compare(a, b));
593        let rows = entries.into_iter().map(|entry| row_id(&entry)).collect();
594        return (rows, fading_count);
595    }
596
597    let rows: Vec<(RowId, &Arc<R>)> = snapshot.iter().collect();
598    let next_chunk = AtomicUsize::new(0);
599    // The first thread done with the chunks sorts the fading rows into its
600    // run: one more run would cost one more merge of every row.
601    let fading_taken = AtomicBool::new(fading.is_empty());
602    // One thread's share: chunks taken until none is left, so a slower
603    // thread takes fewer. One set of quick filter buffers per thread.
604    let sorted_run = || {
605        let mut visible = VisibleRows::new(spec);
606        let mut entries = Vec::new();
607        loop {
608            let start = next_chunk.fetch_add(parallelism.chunk_rows, AtomicOrdering::Relaxed);
609            if start >= rows.len() {
610                break;
611            }
612            let chunk = &rows[start..rows.len().min(start + parallelism.chunk_rows)];
613            entries.extend(
614                chunk
615                    .iter()
616                    .filter(|(_, row)| visible.contains(row))
617                    .map(|&(id, row)| entry(id, row, snapshot.seq(id))),
618            );
619        }
620        if !fading_taken.swap(true, AtomicOrdering::Relaxed) {
621            entries.extend_from_slice(&fading);
622        }
623        entries.sort_unstable_by(|a, b| compare(a, b));
624        entries
625    };
626    let mut runs = run_on_threads(&vec![&sorted_run; parallelism.threads]);
627    runs.retain(|run| !run.is_empty());
628
629    // Neighbouring runs merge in pairs, a thread per pair, until two are
630    // left for the calling thread.
631    while runs.len() > 2 {
632        let merges: Vec<_> = runs
633            .chunks(2)
634            .map(|pair| {
635                let compare = &compare;
636                move || match pair {
637                    [first, second] => {
638                        let mut merged = Vec::with_capacity(first.len() + second.len());
639                        merge(first, second, compare, |entry| *entry, &mut merged);
640                        merged
641                    }
642                    _ => pair.concat(),
643                }
644            })
645            .collect();
646        runs = run_on_threads(&merges);
647    }
648    let mut rows = Vec::with_capacity(runs.iter().map(Vec::len).sum());
649    match runs.as_slice() {
650        [first, second] => merge(first, second, &compare, row_id, &mut rows),
651        runs => rows.extend(runs.iter().flatten().map(row_id)),
652    }
653    (rows, fading_count)
654}
655
656/// The entries of the fading rows of `snapshot` that pass the view's
657/// filters, by their last versions and insertion sequences. At most the
658/// removals of one fade: listed on the calling thread.
659fn fading_entries<'a, R: GridRow, E>(
660    snapshot: &'a StoreSnapshot<R>,
661    spec: &ViewSpec<R>,
662    entry: &impl Fn(RowId, &'a Arc<R>, u64) -> E,
663) -> Vec<E> {
664    let mut fading = snapshot.fading_rows().peekable();
665    if fading.peek().is_none() {
666        return Vec::new();
667    }
668    let mut visible = VisibleRows::new(spec);
669    fading
670        .filter(|(_, row, ..)| visible.contains(row))
671        .map(|(id, row, _, seq)| entry(id, row, seq))
672        .collect()
673}
674
675/// The place of each row by slot (`u32::MAX` for none) from `(place, id)`
676/// pairs, and the places of the rows whose slot an earlier pair took, by id:
677/// a fading row shares its slot with the new row that reused it.
678fn places(
679    rows: impl Iterator<Item = (usize, RowId)>,
680    capacity: usize,
681) -> (Vec<u32>, FxHashMap<RowId, u32>) {
682    let mut by_slot = vec![u32::MAX; capacity];
683    let mut shared = FxHashMap::default();
684    for (place, id) in rows {
685        match by_slot.get_mut(id.ix()) {
686            Some(slot) if *slot == u32::MAX => *slot = place as u32,
687            _ => {
688                shared.insert(id, place as u32);
689            }
690        }
691    }
692    (by_slot, shared)
693}
694
695/// The place of a row whose slot another row holds: see [`places`].
696#[inline]
697fn shared_place(shared: &FxHashMap<RowId, u32>, id: RowId) -> Option<usize> {
698    if shared.is_empty() {
699        None
700    } else {
701        shared.get(&id).map(|&place| place as usize)
702    }
703}
704
705/// The fading rows of a snapshot, as a rebuild tells them from live rows: a
706/// bit per slot first, then the id, as a slot may hold both a fading row
707/// and the new row that reused it. Without fading rows it holds nothing and
708/// costs a bounds check.
709#[derive(Default)]
710struct FadingIds {
711    slots: Vec<u64>,
712    ids: FxHashSet<RowId>,
713}
714
715impl FadingIds {
716    fn new<R: GridRow>(snapshot: &StoreSnapshot<R>) -> Self {
717        let mut fading = Self::default();
718        for (id, ..) in snapshot.fading_rows() {
719            if fading.slots.is_empty() {
720                fading.slots = vec![0; snapshot.capacity().div_ceil(64)];
721            }
722            if let Some(word) = fading.slots.get_mut(id.ix() / 64) {
723                *word |= 1 << (id.ix() % 64);
724            }
725            fading.ids.insert(id);
726        }
727        fading
728    }
729
730    /// Returns true if the row is a fading one.
731    #[inline]
732    fn contains(&self, id: RowId) -> bool {
733        self.slots
734            .get(id.ix() / 64)
735            .is_some_and(|word| word & (1 << (id.ix() % 64)) != 0)
736            && self.ids.contains(&id)
737    }
738
739    /// Returns true if the snapshot has no fading rows.
740    fn is_empty(&self) -> bool {
741        self.ids.is_empty()
742    }
743}
744
745/// Appends the entries of two sorted runs to `out` in order, mapped by
746/// `map`.
747fn merge<E, T>(
748    first: &[E],
749    second: &[E],
750    compare: impl Fn(&E, &E) -> Ordering,
751    map: impl Fn(&E) -> T,
752    out: &mut Vec<T>,
753) {
754    let (mut first_ix, mut second_ix) = (0, 0);
755    while let (Some(a), Some(b)) = (first.get(first_ix), second.get(second_ix)) {
756        if compare(b, a).is_lt() {
757            out.push(map(b));
758            second_ix += 1;
759        } else {
760            out.push(map(a));
761            first_ix += 1;
762        }
763    }
764    out.extend(first[first_ix..].iter().map(&map));
765    out.extend(second[second_ix..].iter().map(&map));
766}
767
768/// Runs every job and returns the results in order: the first on the
769/// calling thread, each other on a scoped thread of its own, or on the
770/// calling thread when that thread fails to start. A panic in a job
771/// resumes on the calling thread.
772fn run_on_threads<T: Send>(jobs: &[impl FnOnce() -> T + Copy + Send]) -> Vec<T> {
773    let Some((&first, rest)) = jobs.split_first() else {
774        return Vec::new();
775    };
776    thread::scope(|scope| {
777        let started: Vec<_> = rest
778            .iter()
779            .map(|&job| {
780                thread::Builder::new()
781                    .name("live-grid-rebuild".into())
782                    .spawn_scoped(scope, job)
783                    .map_err(|_| job)
784            })
785            .collect();
786        let mut results = Vec::with_capacity(jobs.len());
787        results.push(first());
788        for job in started {
789            results.push(match job {
790                Ok(thread) => thread
791                    .join()
792                    .unwrap_or_else(|panic| panic::resume_unwind(panic)),
793                Err(job) => job(),
794            });
795        }
796        results
797    })
798}
799
800/// The view's filter, then its quick filter, row by row. Holds the quick
801/// filter's buffers for one thread.
802struct VisibleRows<'a, R: GridRow> {
803    filter: Option<&'a Filter<R>>,
804    quick_filter: Option<QuickFilterPass<'a, R>>,
805}
806
807impl<'a, R: GridRow> VisibleRows<'a, R> {
808    fn new(spec: &'a ViewSpec<R>) -> Self {
809        Self {
810            filter: spec.filter.as_ref().map(|(_, filter)| filter),
811            // Off, it costs a branch per row.
812            quick_filter: spec.quick_filter.as_ref().map(QuickFilterPass::new),
813        }
814    }
815
816    fn contains(&mut self, row: &'a R) -> bool {
817        self.filter.is_none_or(|filter| filter(row))
818            && self
819                .quick_filter
820                .as_mut()
821                .is_none_or(|quick_filter| quick_filter.matches(row))
822    }
823}
824
825#[cfg(test)]
826mod tests {
827    use std::time::{Duration, Instant};
828
829    use super::*;
830    use crate::{
831        highlight::HighlightPolicy,
832        model::{ColumnId, ColumnIx, OwnedValue},
833        quick_filter::ContainsAllTerms,
834        store::{Ingest, Op},
835    };
836
837    #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
838    enum Column {
839        Id,
840        Name,
841        Price,
842    }
843
844    impl GridColumns for Column {
845        fn all() -> &'static [Self] {
846            &[Self::Id, Self::Name, Self::Price]
847        }
848
849        fn id(self) -> &'static str {
850            match self {
851                Self::Id => "id",
852                Self::Name => "name",
853                Self::Price => "price",
854            }
855        }
856
857        fn ix(self) -> ColumnIx {
858            ColumnIx::new(self as u16)
859        }
860    }
861
862    struct Row;
863
864    impl GridRow for Row {
865        type Key = u32;
866        type Column = Column;
867
868        fn key(&self) -> &u32 {
869            &0
870        }
871
872        fn value(&self, _column: Column) -> CellValue<'_> {
873            CellValue::Empty
874        }
875    }
876
877    #[test]
878    fn test_quick_filter_dependencies() {
879        let mut name = ColumnSet::EMPTY;
880        name.insert(Column::Name.ix());
881        let quick_filter = QuickFilter::new("sber", name, &ContainsAllTerms);
882        let spec = ViewSpec::<Row>::new()
883            .with_sort([(Column::Price, SortDirection::Ascending)])
884            .with_quick_filter(quick_filter);
885        assert_eq!(spec.quick_filter().map(QuickFilter::text), Some("sber"));
886        let dependencies = spec.dependencies();
887        assert!(dependencies.contains(Column::Name.ix()));
888        assert!(dependencies.contains(Column::Price.ix()));
889        assert!(!dependencies.contains(Column::Id.ix()));
890
891        // Turned off, the quick filter columns no longer rebuild the view.
892        let off = spec.with_quick_filter(None);
893        assert!(off.quick_filter().is_none());
894        assert!(!off.dependencies().contains(Column::Name.ix()));
895        assert!(off.dependencies().contains(Column::Price.ix()));
896    }
897
898    #[test]
899    fn test_sort_direction_serde() {
900        // Saved layouts depend on these names.
901        for (direction, json) in [
902            (SortDirection::Ascending, r#""ascending""#),
903            (SortDirection::Descending, r#""descending""#),
904        ] {
905            assert_eq!(serde_json::to_string(&direction).unwrap(), json);
906            assert_eq!(
907                serde_json::from_str::<SortDirection>(json).unwrap(),
908                direction
909            );
910        }
911        assert!(serde_json::from_str::<SortDirection>(r#""Ascending""#).is_err());
912    }
913
914    #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
915    enum TradeColumn {
916        Id,
917        Group,
918        Price,
919        Name,
920        Flag,
921        Mixed,
922    }
923
924    impl GridColumns for TradeColumn {
925        fn all() -> &'static [Self] {
926            &[
927                Self::Id,
928                Self::Group,
929                Self::Price,
930                Self::Name,
931                Self::Flag,
932                Self::Mixed,
933            ]
934        }
935
936        fn id(self) -> &'static str {
937            match self {
938                Self::Id => "id",
939                Self::Group => "group",
940                Self::Price => "price",
941                Self::Name => "name",
942                Self::Flag => "flag",
943                Self::Mixed => "mixed",
944            }
945        }
946
947        fn ix(self) -> ColumnIx {
948            ColumnIx::new(self as u16)
949        }
950    }
951
952    /// Few distinct values per column, so sort keys tie often and only the
953    /// insertion sequence tells rows apart.
954    struct Trade {
955        id: u32,
956        group: i64,
957        price: f64,
958        name: String,
959        flag: Option<bool>,
960        /// A column of every type, for the order across types.
961        mixed: u8,
962    }
963
964    impl Trade {
965        fn random(rng: &mut fastrand::Rng, id: u32) -> Self {
966            const NAME_CHARS: &[char] = &['a', 'B', 'я', 'Ж', '1'];
967            Self {
968                id,
969                group: rng.i64(0..4),
970                price: [0.5, 1.0, 1.5, f64::NAN, -0.0][rng.usize(..5)],
971                name: (0..rng.usize(0..=2))
972                    .map(|_| NAME_CHARS[rng.usize(..NAME_CHARS.len())])
973                    .collect(),
974                flag: [None, Some(false), Some(true)][rng.usize(..3)],
975                mixed: rng.u8(0..8),
976            }
977        }
978    }
979
980    impl GridRow for Trade {
981        type Key = u32;
982        type Column = TradeColumn;
983
984        fn key(&self) -> &u32 {
985            &self.id
986        }
987
988        fn value(&self, column: TradeColumn) -> CellValue<'_> {
989            match column {
990                TradeColumn::Id => CellValue::Int(self.id.into()),
991                TradeColumn::Group => CellValue::Int(self.group),
992                TradeColumn::Price => CellValue::Float(self.price),
993                TradeColumn::Name => CellValue::Text(&self.name),
994                TradeColumn::Flag => self.flag.map_or(CellValue::Empty, CellValue::Bool),
995                TradeColumn::Mixed => match self.mixed {
996                    0 => CellValue::Empty,
997                    1 => CellValue::Bool(true),
998                    2 | 3 => CellValue::Int(i64::from(self.mixed)),
999                    4 | 5 => CellValue::Float(f64::from(self.mixed) / 2.0),
1000                    _ => CellValue::Text(if self.mixed == 6 { "x" } else { "Y" }),
1001                },
1002            }
1003        }
1004    }
1005
1006    fn random_spec(rng: &mut fastrand::Rng) -> ViewSpec<Trade> {
1007        let columns = TradeColumn::all();
1008        let direction = |rng: &mut fastrand::Rng| {
1009            [SortDirection::Ascending, SortDirection::Descending][rng.usize(..2)]
1010        };
1011        let sort: Vec<_> = (0..rng.usize(0..=3))
1012            .map(|_| (columns[rng.usize(..columns.len())], direction(rng)))
1013            .collect();
1014        let mut quick_columns = ColumnSet::EMPTY;
1015        for column in columns {
1016            if rng.bool() {
1017                quick_columns.insert(column.ix());
1018            }
1019        }
1020        let text = ["", "a", "я 1", "b ж", ".5", "1 a"][rng.usize(..6)];
1021        let spec = ViewSpec::new()
1022            .with_sort(sort)
1023            .with_quick_filter(QuickFilter::new(text, quick_columns, &ContainsAllTerms));
1024        if rng.bool() {
1025            spec.with_filter(ColumnSet::first(columns.len()), |trade: &Trade| {
1026                trade.group != 1
1027            })
1028        } else {
1029            spec
1030        }
1031    }
1032
1033    /// The store's highlight policy: removed rows fade with `fade`, longer
1034    /// than any test.
1035    fn policy(fade: bool) -> HighlightPolicy {
1036        if fade {
1037            HighlightPolicy::default().with_removal_fade(Duration::from_secs(3_600))
1038        } else {
1039            HighlightPolicy::default()
1040        }
1041    }
1042
1043    #[test]
1044    fn test_parallel_rebuild_matches_sequential() {
1045        // Small chunks and odd thread counts put chunk and run boundaries
1046        // everywhere: the order must not depend on them. Removed rows fade
1047        // in odd seeds: one more sorted run.
1048        let epoch = Instant::now();
1049        let mut fading = 0;
1050        for seed in 0..48 {
1051            let mut rng = fastrand::Rng::with_seed(seed);
1052            let mut ingest = Ingest::<Trade>::new(policy(seed % 2 == 1), epoch);
1053            let count = [0, 1, 2, 7, 100, 1_000, 3_000][rng.usize(..7)];
1054            ingest.apply(
1055                (0..count).map(|id| Op::UpsertSilent(Arc::new(Trade::random(&mut rng, id)))),
1056                epoch,
1057            );
1058            // Removed slots are reused, so storage order differs from
1059            // insertion order.
1060            ingest.apply(
1061                (0..count / 3).map(|_| Op::Remove(rng.u32(..count.max(1)))),
1062                epoch,
1063            );
1064            ingest.apply(
1065                (count..count + count / 4)
1066                    .map(|id| Op::UpsertSilent(Arc::new(Trade::random(&mut rng, id)))),
1067                epoch,
1068            );
1069            let snapshot = ingest.snapshot();
1070            for _ in 0..8 {
1071                let spec = random_spec(&mut rng);
1072                let sequential = project(
1073                    &snapshot,
1074                    &spec,
1075                    Parallelism {
1076                        threads: 1,
1077                        chunk_rows: CHUNK_ROWS,
1078                    },
1079                );
1080                fading += sequential.1;
1081                let parallelism = Parallelism {
1082                    threads: rng.usize(2..=7),
1083                    chunk_rows: rng.usize(1..=97),
1084                };
1085                assert_eq!(
1086                    project(&snapshot, &spec, parallelism),
1087                    sequential,
1088                    "seed {seed}, {count} rows, {parallelism:?}, sort {:?}, quick filter {:?}",
1089                    spec.sort(),
1090                    spec.quick_filter().map(QuickFilter::text),
1091                );
1092            }
1093        }
1094        assert!(fading > 1_000, "{fading} fading rows");
1095    }
1096
1097    #[test]
1098    fn test_parallelism_for_view() {
1099        let sorted =
1100            ViewSpec::<Trade>::new().with_sort([(TradeColumn::Name, SortDirection::Ascending)]);
1101        let budget = Rebuild::default().max_threads();
1102        // Small snapshots and plain listing stay on the calling thread.
1103        assert_eq!(
1104            Parallelism::for_view(MIN_ROWS_PER_THREAD - 1, &sorted, budget).threads,
1105            1
1106        );
1107        assert_eq!(
1108            Parallelism::for_view(1 << 20, &ViewSpec::<Trade>::new(), budget).threads,
1109            1
1110        );
1111        // Large ones use the default budget, which leaves cores to the UI
1112        // and ingest.
1113        let threads = Parallelism::for_view(1 << 20, &sorted, budget).threads;
1114        assert_eq!(threads, default_thread_budget());
1115        assert!((1..=MAX_THREADS).contains(&threads));
1116        if let Ok(cores) = thread::available_parallelism() {
1117            assert!(threads == 1 || threads + RESERVED_CORES <= cores.get());
1118        }
1119        // Rows still limit an explicit count.
1120        for (rows, threads) in [
1121            (MIN_ROWS_PER_THREAD * 3 - 1, 2),
1122            (MIN_ROWS_PER_THREAD * 3, 3),
1123            (MIN_ROWS_PER_THREAD * 40, 12),
1124        ] {
1125            assert_eq!(
1126                Parallelism::for_view(rows, &sorted, 12).threads,
1127                threads,
1128                "{rows} rows"
1129            );
1130        }
1131    }
1132
1133    #[test]
1134    fn test_rebuild_max_threads() {
1135        assert_eq!(Rebuild::default().max_threads(), default_thread_budget());
1136        assert_eq!(Rebuild::default().with_max_threads(1).max_threads(), 1);
1137        assert_eq!(
1138            Rebuild::default().with_max_threads(0).max_threads(),
1139            1,
1140            "at least one"
1141        );
1142        // The default's limit does not cap an explicit count.
1143        assert_eq!(
1144            Rebuild::default()
1145                .with_max_threads(MAX_THREADS * 2)
1146                .max_threads(),
1147            MAX_THREADS * 2
1148        );
1149    }
1150
1151    #[test]
1152    fn test_parallel_rebuild_resumes_panic() {
1153        // A panicking application filter fails the rebuild on the calling
1154        // thread with its own message, as it would on one thread.
1155        let epoch = Instant::now();
1156        let mut rng = fastrand::Rng::with_seed(1);
1157        let mut ingest = Ingest::<Trade>::new(HighlightPolicy::default(), epoch);
1158        ingest.apply(
1159            (0..2_000).map(|id| Op::UpsertSilent(Arc::new(Trade::random(&mut rng, id)))),
1160            epoch,
1161        );
1162        let snapshot = ingest.snapshot();
1163        let spec = ViewSpec::new().with_filter(ColumnSet::first(1), |trade: &Trade| {
1164            assert_ne!(trade.id, 1_500, "filter failed");
1165            true
1166        });
1167        let parallelism = Parallelism {
1168            threads: 4,
1169            chunk_rows: 16,
1170        };
1171        let panic = panic::catch_unwind(panic::AssertUnwindSafe(|| {
1172            project(&snapshot, &spec, parallelism)
1173        }))
1174        .expect_err("the filter panics");
1175        let message = panic
1176            .downcast_ref::<String>()
1177            .map(String::as_str)
1178            .unwrap_or_default();
1179        assert!(message.contains("filter failed"), "{message}");
1180    }
1181
1182    /// A snapshot of `count` random trades with some removed, so slots are
1183    /// reused and storage order differs from insertion order.
1184    fn random_snapshot(rng: &mut fastrand::Rng, count: u32) -> Arc<StoreSnapshot<Trade>> {
1185        random_snapshot_with(rng, count, false)
1186    }
1187
1188    /// [`random_snapshot`], the removed rows fading when `fade`: new rows
1189    /// take their slots.
1190    fn random_snapshot_with(
1191        rng: &mut fastrand::Rng,
1192        count: u32,
1193        fade: bool,
1194    ) -> Arc<StoreSnapshot<Trade>> {
1195        let epoch = Instant::now();
1196        let mut ingest = Ingest::<Trade>::new(policy(fade), epoch);
1197        ingest.apply(
1198            (0..count).map(|id| Op::UpsertSilent(Arc::new(Trade::random(rng, id)))),
1199            epoch,
1200        );
1201        ingest.apply(
1202            (0..count / 3).map(|_| Op::Remove(rng.u32(..count.max(1)))),
1203            epoch,
1204        );
1205        ingest.apply(
1206            (count..count + count / 4).map(|id| Op::UpsertSilent(Arc::new(Trade::random(rng, id)))),
1207            epoch,
1208        );
1209        ingest.snapshot()
1210    }
1211
1212    #[test]
1213    fn test_flat_projection_readers() {
1214        let engine = Rebuild::default().with_max_threads(1);
1215        for seed in 0..24 {
1216            let mut rng = fastrand::Rng::with_seed(seed);
1217            let count = [0, 1, 7, 100, 300][rng.usize(..5)];
1218            let snapshot = random_snapshot(&mut rng, count);
1219            let spec = random_spec(&mut rng);
1220            let projection = engine.rebuild(&snapshot, &spec);
1221            let context = format!("seed {seed}, {count} rows");
1222
1223            // The lines are the rows at depth 0; nothing else is listed.
1224            assert_eq!(projection.len(), projection.rows().len(), "{context}");
1225            assert_eq!(projection.row_count(), projection.len(), "{context}");
1226            assert_eq!(projection.is_empty(), projection.rows().is_empty());
1227            for (ix, &id) in projection.rows().iter().enumerate() {
1228                assert_eq!(projection.row(ix), Some(id), "{context}");
1229                assert_eq!(
1230                    projection.line(ix),
1231                    Some(DisplayRow::Data { row: id, depth: 0 }),
1232                    "{context}"
1233                );
1234                assert_eq!(projection.display_ix(id), Some(ix), "{context}");
1235                assert_eq!(projection.order_ix(id), Some(ix), "{context}");
1236                assert!(projection.contains(id), "{context}");
1237            }
1238            assert_eq!(projection.line(projection.len()), None);
1239            assert_eq!(projection.row(projection.len()), None);
1240            // Rows the view filters out are nowhere.
1241            for (id, _) in snapshot.iter() {
1242                let shown = projection.rows().contains(&id);
1243                assert_eq!(projection.contains(id), shown, "{context}");
1244                assert_eq!(projection.order_ix(id).is_some(), shown, "{context}");
1245                assert_eq!(projection.branch(id), None, "{context}");
1246            }
1247            assert!(!projection.is_grouped() && !projection.is_tree());
1248            assert!(projection.groups().is_empty());
1249            assert_eq!(projection.groups().iter().count(), 0);
1250            assert_eq!(projection.group_line(GroupId::new(0)), None);
1251            assert!(projection.totals().is_empty());
1252            assert_eq!(projection.totals().columns(), ColumnSet::EMPTY);
1253            assert_eq!(projection.totals().get(TradeColumn::Price.ix()), None);
1254            assert_eq!(projection.totals().row_count(), projection.row_count());
1255            let expanded: Vec<DisplayRow> = projection.expanded_lines().collect();
1256            let lines: Vec<DisplayRow> = (0..projection.len())
1257                .filter_map(|ix| projection.line(ix))
1258                .collect();
1259            assert_eq!(expanded, lines, "{context}");
1260            assert_eq!(
1261                projection.shown_rows().collect::<Vec<_>>(),
1262                projection.rows(),
1263                "{context}"
1264            );
1265
1266            // The default `aggregate` and `expand` keep the order: a flat
1267            // projection has nothing to aggregate or expand.
1268            let aggregated = engine.aggregate(&projection, &snapshot, &spec);
1269            assert_eq!(aggregated.rows(), projection.rows(), "{context}");
1270            assert_eq!(aggregated.store_version(), snapshot.version());
1271            let expanded = engine.expand(&projection, &snapshot, &spec);
1272            assert_eq!(expanded.rows(), projection.rows(), "{context}");
1273            assert_eq!(expanded.len(), projection.len(), "{context}");
1274            assert_eq!(expanded.store_version(), projection.store_version());
1275            let compacted = engine.compact(&projection, &snapshot);
1276            assert_eq!(compacted.rows(), projection.rows(), "{context}");
1277        }
1278    }
1279
1280    #[test]
1281    fn test_grouped_rows_follow_the_level_sort() {
1282        // Grouping by a column in key order lists the rows as a flat sort by
1283        // that column, then the view's sort, would.
1284        let engine = Rebuild::default().with_max_threads(1);
1285        for seed in 0..12 {
1286            let mut rng = fastrand::Rng::with_seed(seed);
1287            let snapshot = random_snapshot(&mut rng, 200);
1288            let spec = random_spec(&mut rng);
1289            let direction = [SortDirection::Ascending, SortDirection::Descending][rng.usize(..2)];
1290            let level =
1291                [TradeColumn::Group, TradeColumn::Mixed, TradeColumn::Price][rng.usize(..3)];
1292            let grouped = engine.rebuild(
1293                &snapshot,
1294                &spec
1295                    .clone()
1296                    .with_group_by([GroupBy::column(level).with_direction(direction)]),
1297            );
1298            let sort: Vec<_> = std::iter::once((level, direction))
1299                .chain(spec.sort().iter().copied())
1300                .collect();
1301            let flat = engine.rebuild(&snapshot, &spec.clone().with_sort(sort));
1302            assert_eq!(grouped.rows(), flat.rows(), "seed {seed}");
1303            assert!(grouped.is_grouped() && !grouped.is_tree());
1304            assert_eq!(grouped.row_count(), flat.row_count());
1305        }
1306    }
1307
1308    /// Takes an aggregate over every row, in order: a float sum, so the
1309    /// order shows in the last bits.
1310    struct OrderedSum;
1311
1312    impl AggregateFn<Trade> for OrderedSum {
1313        fn aggregate(&self, _: TradeColumn, rows: &mut dyn Iterator<Item = &Trade>) -> OwnedValue {
1314            OwnedValue::Float(rows.map(|trade| trade.price * 0.1).sum())
1315        }
1316    }
1317
1318    /// A grouped view of `Trade`s: one to three levels, aggregates of every
1319    /// kind, either group order and footer.
1320    fn random_grouped_spec(rng: &mut fastrand::Rng) -> ViewSpec<Trade> {
1321        let direction = |rng: &mut fastrand::Rng| {
1322            [SortDirection::Ascending, SortDirection::Descending][rng.usize(..2)]
1323        };
1324        let letter = || {
1325            GroupKey::new("letter", ColumnSet::first(4), |trade: &Trade| {
1326                CellValue::Text(trade.name.get(..1).unwrap_or_default())
1327            })
1328        };
1329        let levels: Vec<GroupBy<Trade>> = (0..rng.usize(1..=3))
1330            .map(|_| {
1331                let level = match rng.u8(0..5) {
1332                    0 => GroupBy::column(TradeColumn::Group),
1333                    1 => GroupBy::column(TradeColumn::Flag),
1334                    2 => GroupBy::column(TradeColumn::Mixed),
1335                    3 => GroupBy::column(TradeColumn::Price),
1336                    _ => GroupBy::key(letter()),
1337                };
1338                level.with_direction(direction(rng))
1339            })
1340            .collect();
1341        let spec = random_spec(rng).with_group_by(levels).with_aggregates([
1342            (TradeColumn::Price, Aggregate::Sum),
1343            (TradeColumn::Group, Aggregate::Avg),
1344            (TradeColumn::Name, Aggregate::Min),
1345            (TradeColumn::Mixed, Aggregate::Max),
1346            (TradeColumn::Flag, Aggregate::Count),
1347            (TradeColumn::Id, Aggregate::Custom(Arc::new(OrderedSum))),
1348        ]);
1349        let spec = if rng.bool() {
1350            spec.with_group_order(AggregateOrder::new(TradeColumn::Price, direction(rng)))
1351        } else {
1352            spec
1353        };
1354        if rng.bool() {
1355            spec.with_group_footer(EveryGroupFooter)
1356        } else {
1357            spec
1358        }
1359    }
1360
1361    /// Asserts two projections are the same, floats bit for bit.
1362    fn assert_same_projection(a: &Projection, b: &Projection, context: &str) {
1363        let same = |a: &OwnedValue, b: &OwnedValue| {
1364            std::mem::discriminant(a) == std::mem::discriminant(b) && a == b
1365        };
1366        let same_row = |a: AggregateRow<'_>, b: AggregateRow<'_>| {
1367            a.columns() == b.columns()
1368                && a.row_count() == b.row_count()
1369                && TradeColumn::all().iter().all(|column| {
1370                    match (a.get(column.ix()), b.get(column.ix())) {
1371                        (Some(a), Some(b)) => same(a, b),
1372                        (a, b) => a.is_none() && b.is_none(),
1373                    }
1374                })
1375        };
1376        assert_eq!(a.rows(), b.rows(), "{context}");
1377        assert_eq!(a.len(), b.len(), "{context}");
1378        for ix in 0..a.len() {
1379            assert_eq!(a.line(ix), b.line(ix), "{context}, line {ix}");
1380        }
1381        assert_eq!(a.row_count(), b.row_count(), "{context}");
1382        assert!(same_row(a.totals(), b.totals()), "{context}: totals");
1383        assert_eq!(a.groups().len(), b.groups().len(), "{context}");
1384        for (a, b) in a.groups().iter().zip(b.groups().iter()) {
1385            assert_eq!(a.path(), b.path(), "{context}");
1386            assert_eq!(a.parent(), b.parent(), "{context}");
1387            assert_eq!(a.rows(), b.rows(), "{context}");
1388            assert_eq!(a.row_count(), b.row_count(), "{context}");
1389            assert_eq!(
1390                (a.is_expanded(), a.has_footer()),
1391                (b.is_expanded(), b.has_footer()),
1392                "{context}"
1393            );
1394            assert!(same_row(a.aggregates(), b.aggregates()), "{context}: {a:?}");
1395        }
1396    }
1397
1398    #[test]
1399    fn test_grouped_rebuild_is_thread_independent() {
1400        // As `test_parallel_rebuild_matches_sequential`, with groups and
1401        // aggregates: chunk and run boundaries everywhere, the same
1402        // projection bit for bit, float sums included. Removed rows fade in
1403        // odd seeds.
1404        let mut fading = 0;
1405        for seed in 0..48 {
1406            let mut rng = fastrand::Rng::with_seed(seed);
1407            let count = [0, 1, 2, 7, 100, 1_000, 3_000][rng.usize(..7)];
1408            let snapshot = random_snapshot_with(&mut rng, count, seed % 2 == 1);
1409            for _ in 0..4 {
1410                let spec = random_grouped_spec(&mut rng);
1411                let Structure::Groups(grouping) = spec.structure() else {
1412                    unreachable!("a grouped view");
1413                };
1414                let sequential = group::rebuild_with(
1415                    &snapshot,
1416                    &spec,
1417                    grouping,
1418                    Parallelism {
1419                        threads: 1,
1420                        chunk_rows: CHUNK_ROWS,
1421                    },
1422                );
1423                let parallelism = Parallelism {
1424                    threads: rng.usize(2..=7),
1425                    chunk_rows: rng.usize(1..=97),
1426                };
1427                let parallel = group::rebuild_with(&snapshot, &spec, grouping, parallelism);
1428                assert_same_projection(
1429                    &parallel,
1430                    &sequential,
1431                    &format!("seed {seed}, {count} rows, {parallelism:?}"),
1432                );
1433                fading += sequential.rows().len() - sequential.row_count();
1434                for (order_ix, &id) in parallel.rows().iter().enumerate() {
1435                    assert_eq!(parallel.order_ix(id), Some(order_ix), "seed {seed}");
1436                }
1437            }
1438        }
1439        assert!(fading > 1_000, "{fading} fading rows");
1440    }
1441
1442    #[test]
1443    fn test_group_key_panic_resumes() {
1444        // A panicking `GroupKey` fails the rebuild on the calling thread
1445        // with its own message, as a panicking filter does.
1446        let mut rng = fastrand::Rng::with_seed(2);
1447        let snapshot = random_snapshot(&mut rng, 3_000);
1448        let spec = ViewSpec::new().with_group_by([GroupBy::key(GroupKey::new(
1449            "checked",
1450            ColumnSet::first(1),
1451            |trade: &Trade| {
1452                assert_ne!(trade.id, 1_500, "group key failed");
1453                CellValue::Int(trade.group)
1454            },
1455        ))]);
1456        let Structure::Groups(grouping) = spec.structure() else {
1457            unreachable!("a grouped view");
1458        };
1459        let parallelism = Parallelism {
1460            threads: 4,
1461            chunk_rows: 16,
1462        };
1463        let panic = panic::catch_unwind(panic::AssertUnwindSafe(|| {
1464            group::rebuild_with(&snapshot, &spec, grouping, parallelism)
1465        }))
1466        .expect_err("the group key panics");
1467        let message = panic
1468            .downcast_ref::<String>()
1469            .map(String::as_str)
1470            .unwrap_or_default();
1471        assert!(message.contains("group key failed"), "{message}");
1472    }
1473
1474    #[test]
1475    fn test_dependencies_with_groups() {
1476        let mut expected = ColumnSet::EMPTY;
1477        expected.insert(TradeColumn::Price.ix());
1478        let sorted =
1479            ViewSpec::<Trade>::new().with_sort([(TradeColumn::Price, SortDirection::Ascending)]);
1480        assert_eq!(sorted.dependencies(), expected);
1481        // Group columns, what a key reads and what the group order reads.
1482        let mut key_columns = ColumnSet::EMPTY;
1483        key_columns.insert(TradeColumn::Name.ix());
1484        let grouped = sorted
1485            .clone()
1486            .with_group_by([
1487                GroupBy::column(TradeColumn::Group),
1488                GroupBy::key(GroupKey::new("letter", key_columns, |trade: &Trade| {
1489                    CellValue::Text(trade.name.get(..1).unwrap_or_default())
1490                })),
1491            ])
1492            .with_aggregates([(TradeColumn::Mixed, Aggregate::Sum)])
1493            .with_group_order(AggregateOrder::new(
1494                TradeColumn::Flag,
1495                SortDirection::Ascending,
1496            ));
1497        for column in [TradeColumn::Group, TradeColumn::Name, TradeColumn::Flag] {
1498            expected.insert(column.ix());
1499        }
1500        assert_eq!(grouped.dependencies(), expected);
1501        // Aggregates alone change values, not the order.
1502        assert!(!grouped.dependencies().contains(TradeColumn::Mixed.ix()));
1503        assert!(
1504            grouped
1505                .aggregate_columns()
1506                .contains(TradeColumn::Mixed.ix())
1507        );
1508        // Without grouping, neither the levels nor the group order count.
1509        let flat = grouped.with_group_by([]);
1510        let mut price = ColumnSet::EMPTY;
1511        price.insert(TradeColumn::Price.ix());
1512        assert_eq!(flat.dependencies(), price);
1513    }
1514
1515    /// A weighted aggregate of a column that reads another column.
1516    struct Weighted;
1517
1518    impl AggregateFn<Trade> for Weighted {
1519        fn aggregate(&self, _: TradeColumn, _: &mut dyn Iterator<Item = &Trade>) -> OwnedValue {
1520            OwnedValue::Empty
1521        }
1522
1523        fn depends_on(&self, column: TradeColumn) -> ColumnSet {
1524            let mut columns = ColumnSet::EMPTY;
1525            columns.insert(column.ix());
1526            columns.insert(TradeColumn::Group.ix());
1527            columns
1528        }
1529
1530        fn name(&self) -> &str {
1531            "Weighted"
1532        }
1533    }
1534
1535    #[test]
1536    fn test_view_spec_structure() {
1537        let by_group = GroupBy::column(TradeColumn::Group);
1538        let by_letter = GroupBy::key(
1539            GroupKey::new("letter", ColumnSet::first(4), |trade: &Trade| {
1540                CellValue::Text(trade.name.get(..1).unwrap_or_default())
1541            })
1542            .with_title("First letter"),
1543        )
1544        .with_direction(SortDirection::Descending);
1545        let spec = ViewSpec::<Trade>::new()
1546            .with_aggregates([
1547                (TradeColumn::Price, Aggregate::Sum),
1548                (TradeColumn::Id, Aggregate::Count),
1549                (TradeColumn::Price, Aggregate::Max),
1550            ])
1551            .with_group_by([by_group.clone(), by_letter, by_group.clone()]);
1552
1553        // Repeated levels are dropped, the first stays.
1554        let levels: Vec<ColumnId> = spec.group_by().iter().map(GroupBy::id).collect();
1555        assert_eq!(levels, [ColumnId::from("group"), ColumnId::from("letter")]);
1556        assert_eq!(spec.group_by()[0].as_column(), Some(TradeColumn::Group));
1557        assert_eq!(spec.group_by()[1].as_column(), None);
1558        let key = spec.group_by()[1].as_key().expect("a key level");
1559        assert_eq!((key.id(), key.title()), ("letter", "First letter"));
1560        assert_eq!(spec.group_by()[1].direction(), SortDirection::Descending);
1561        assert!(spec.tree().is_none());
1562        // A repeated column keeps its last aggregate, in its first place.
1563        let aggregates: Vec<(TradeColumn, String)> = spec
1564            .aggregates()
1565            .iter()
1566            .map(|(column, aggregate)| (*column, format!("{aggregate:?}")))
1567            .collect();
1568        assert_eq!(
1569            aggregates,
1570            [
1571                (TradeColumn::Price, "Max".to_owned()),
1572                (TradeColumn::Id, "Count".to_owned())
1573            ]
1574        );
1575
1576        // A tree replaces the grouping, and a grouping the tree; the
1577        // aggregates outlive both.
1578        let tree = spec
1579            .clone()
1580            .with_tree(TreeSpec::new().with_column(TradeColumn::Name));
1581        assert!(tree.group_by().is_empty());
1582        assert_eq!(
1583            tree.tree().and_then(TreeSpec::column),
1584            Some(TradeColumn::Name)
1585        );
1586        assert!(matches!(
1587            tree.tree().unwrap().filter(),
1588            TreeFilter::KeepAncestors
1589        ));
1590        assert!(matches!(tree.tree().unwrap().totals(), TreeTotals::Leaves));
1591        assert_eq!(tree.aggregates().len(), 2);
1592        let regrouped = tree.with_group_by([by_group]);
1593        assert!(regrouped.tree().is_none());
1594        assert_eq!(regrouped.group_by().len(), 1);
1595        // An empty grouping is the flat view.
1596        let flat = regrouped.with_group_by([]);
1597        assert!(flat.group_by().is_empty() && flat.tree().is_none());
1598        assert!(matches!(flat.structure(), Structure::Flat));
1599        assert_eq!(flat.aggregates().len(), 2);
1600
1601        // Aggregate columns: the aggregated ones, and what a custom one
1602        // depends on.
1603        let mut expected = ColumnSet::EMPTY;
1604        expected.insert(TradeColumn::Price.ix());
1605        expected.insert(TradeColumn::Id.ix());
1606        assert_eq!(flat.aggregate_columns(), expected);
1607        let custom =
1608            flat.with_aggregates([(TradeColumn::Price, Aggregate::Custom(Arc::new(Weighted)))]);
1609        let mut expected = ColumnSet::EMPTY;
1610        expected.insert(TradeColumn::Price.ix());
1611        expected.insert(TradeColumn::Group.ix());
1612        assert_eq!(custom.aggregate_columns(), expected);
1613        assert_eq!(
1614            format!("{:?}", custom.aggregates()[0].1),
1615            r#"Custom("Weighted")"#
1616        );
1617        assert!(ViewSpec::<Trade>::new().aggregates().is_empty());
1618        assert!(ViewSpec::<Trade>::new().aggregate_columns().is_empty());
1619
1620        // The group order and footer: key order and none by default.
1621        let a = GroupPath::new([(ColumnId::from("group"), OwnedValue::Int(1))]);
1622        let b = GroupPath::new([(ColumnId::from("group"), OwnedValue::Int(2))]);
1623        let values = [OwnedValue::Float(2.0)];
1624        let low = [OwnedValue::Float(1.0)];
1625        let mut price = ColumnSet::EMPTY;
1626        price.insert(TradeColumn::Price.ix());
1627        let first = GroupOrderContext::new(&a, 3, AggregateRow::new(price, &values, 3));
1628        let second = GroupOrderContext::new(&b, 1, AggregateRow::new(price, &low, 1));
1629        let spec = ViewSpec::<Trade>::new();
1630        assert_eq!(spec.group_order().compare(&first, &second), Ordering::Equal);
1631        assert!(spec.group_order().depends_on().is_empty());
1632        let by_sum = spec.with_group_order(AggregateOrder::new(
1633            TradeColumn::Price,
1634            SortDirection::Descending,
1635        ));
1636        assert_eq!(
1637            by_sum.group_order().compare(&first, &second),
1638            Ordering::Less
1639        );
1640        assert_eq!(by_sum.group_order().depends_on(), price);
1641        assert_eq!(first.value(), &OwnedValue::Int(1));
1642        assert_eq!((first.depth(), first.row_count()), (0, 3));
1643        assert_eq!(first.path(), &a);
1644        assert!(by_sum.expansion() == &Expansion::default());
1645    }
1646}