Skip to main content

ravenrs_widgets/grid/
remote.rs

1//! Rows that live on a server. The grid holds a few chunks of them and asks for more
2//! only once scrolling settles, so that neither the program's memory nor the database
3//! pays for rows nobody is looking at.
4
5use std::cell::RefCell;
6use std::collections::{BTreeMap, BTreeSet};
7use std::fmt;
8use std::rc::Rc;
9
10use ravenrs_reactive::State;
11
12use super::model::{Row, SortDirection};
13use super::query::GridQuery;
14
15const DEFAULT_CHUNK: usize = 100;
16const DEFAULT_MAX_CHUNKS: usize = 8;
17const DEFAULT_IN_FLIGHT: usize = 2;
18/// Seconds scrolling must rest before anything is asked for: a scrollbar dragged
19/// across a million rows asks only for where it stopped.
20const SCROLL_SETTLE: f64 = 0.12;
21/// Seconds typing in a filter must rest before the data is asked for again.
22const QUERY_SETTLE: f64 = 0.3;
23
24/// What a [`RemoteRows`] loader must bring.
25#[derive(Clone, Debug, PartialEq)]
26#[non_exhaustive]
27#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
28pub struct GridRequest {
29    /// The first row wanted, from the start of the result.
30    pub offset: usize,
31    /// How many rows, at most.
32    pub limit: usize,
33    /// Sort columns (zero-based) in priority order.
34    pub sort: Vec<(usize, SortDirection)>,
35    /// Filter text per column (index = column).
36    pub filters: Vec<String>,
37    /// What is being searched for.
38    pub search: String,
39    /// The row just before `offset`, when it is at hand. With it a query can continue
40    /// from there (`WHERE key > ?`) and not walk past `offset` rows, which is what
41    /// makes `OFFSET` slow on a big table.
42    pub after: Option<Row>,
43    /// Whether the total is still unknown. Answer `None` to leave it so: a total can
44    /// cost the database more than all the rows ever shown.
45    pub wants_total: bool,
46}
47
48/// What a [`RemoteRows`] loader brought.
49#[derive(Clone, Debug, Default, PartialEq)]
50#[non_exhaustive]
51#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
52pub struct GridPage {
53    /// The rows, at most as many as were asked for.
54    pub rows: Vec<Row>,
55    /// How many rows match in all, when it is known.
56    pub total: Option<usize>,
57    /// There are more rows after these. Without a total, the grid keeps going while this holds.
58    pub has_more: bool,
59    /// Aggregates per column, for the footer.
60    pub totals: Vec<Option<f64>>,
61}
62
63#[cfg_attr(feature = "live", ravenrs_live::expose)]
64impl GridPage {
65    /// These rows, with more after them or not.
66    pub fn new(rows: Vec<Row>, has_more: bool) -> Self {
67        Self { rows, has_more, ..Self::default() }
68    }
69
70    /// Says how many rows match in all.
71    pub fn total(mut self, total: usize) -> Self {
72        self.total = Some(total);
73        self
74    }
75
76    /// Gives the aggregates per column for the footer.
77    pub fn totals(mut self, totals: Vec<Option<f64>>) -> Self {
78        self.totals = totals;
79        self
80    }
81}
82
83struct Chunk {
84    rows: Vec<Row>,
85    used: u64,
86}
87
88#[derive(Default)]
89struct Held {
90    chunks: BTreeMap<usize, Chunk>,
91    in_flight: BTreeSet<usize>,
92    /// Bumped when the query changes, so that an old answer is not taken for a new one.
93    generation: u64,
94    total: Option<usize>,
95    /// One past the last row known to exist.
96    end: usize,
97    has_more: bool,
98    totals: Vec<Option<f64>>,
99    clock: u64,
100    wanted: (usize, usize),
101    moved_at: f64,
102    query: Option<GridQuery>,
103    queried_at: f64,
104    error: Option<String>,
105    started: bool,
106    peak_rows: usize,
107}
108
109/// Answers one [`GridRequest`], now or later. An answer to a question nobody is asking
110/// any more, because the filters changed meanwhile, is dropped.
111#[derive(Clone)]
112#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
113pub struct GridResponder {
114    held: Rc<RefCell<Held>>,
115    tick: State<u64>,
116    generation: u64,
117    chunk: usize,
118    chunk_size: usize,
119    max_chunks: usize,
120}
121
122impl fmt::Debug for GridResponder {
123    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124        f.debug_struct("GridResponder").field("chunk", &self.chunk).finish_non_exhaustive()
125    }
126}
127
128#[cfg_attr(feature = "live", ravenrs_live::expose)]
129impl GridResponder {
130    /// Gives the rows. Call it on the UI thread.
131    pub fn send(self, page: GridPage) {
132        {
133            let mut held = self.held.borrow_mut();
134            if held.generation != self.generation {
135                return;
136            }
137            held.in_flight.remove(&self.chunk);
138            held.error = None;
139            let start = self.chunk * self.chunk_size;
140            held.end = held.end.max(start + page.rows.len());
141            held.has_more = page.has_more;
142            if page.total.is_some() {
143                held.total = page.total;
144            } else if !page.has_more {
145                held.total = Some(start + page.rows.len());
146            }
147            if !page.totals.is_empty() {
148                held.totals = page.totals;
149            }
150            held.clock += 1;
151            let used = held.clock;
152            held.chunks.insert(self.chunk, Chunk { rows: page.rows, used });
153            evict(&mut held, self.max_chunks, self.chunk_size);
154            let rows: usize = held.chunks.values().map(|c| c.rows.len()).sum();
155            held.peak_rows = held.peak_rows.max(rows);
156        }
157        self.tick.update(|t| *t += 1);
158    }
159
160    /// Says what went wrong. The grid offers to try again.
161    pub fn fail(self, message: impl Into<String>) {
162        {
163            let mut held = self.held.borrow_mut();
164            if held.generation != self.generation {
165                return;
166            }
167            held.in_flight.remove(&self.chunk);
168            held.error = Some(message.into());
169        }
170        self.tick.update(|t| *t += 1);
171    }
172}
173
174/// Drops the chunks used longest ago, never one on the screen.
175fn evict(held: &mut Held, max_chunks: usize, chunk_size: usize) {
176    let (first, last) = (held.wanted.0 / chunk_size.max(1), held.wanted.1 / chunk_size.max(1));
177    while held.chunks.len() > max_chunks.max(last.saturating_sub(first) + 1) {
178        let oldest = held
179            .chunks
180            .iter()
181            .filter(|(chunk, _)| **chunk < first || **chunk > last)
182            .min_by_key(|(_, c)| c.used)
183            .map(|(chunk, _)| *chunk);
184        let Some(oldest) = oldest else { return };
185        held.chunks.remove(&oldest);
186    }
187}
188
189type Loader = Rc<dyn Fn(GridRequest, GridResponder)>;
190
191/// The rows of a [`DataGrid`](crate::DataGrid) on a server. The grid asks `loader` for
192/// a chunk at a time, only for what is on the screen, at most two at once, and keeps a
193/// limited number of them, so a table of any size costs the same to look at.
194///
195/// ```
196/// use ravenrs_widgets::{DataGrid, GridColumn, GridPage, RemoteRows, Value};
197///
198/// let rows = RemoteRows::new(|request, answer| {
199///     // Ask the database here, off the UI thread if it takes a while.
200///     let chunk = (request.offset..request.offset + request.limit)
201///         .map(|n| vec![Value::Number(n as f64)])
202///         .collect();
203///     answer.send(GridPage::new(chunk, true));
204/// });
205/// let grid = DataGrid::remote(rows, [GridColumn::number("N")]);
206/// ```
207#[derive(Clone)]
208#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
209pub struct RemoteRows {
210    held: Rc<RefCell<Held>>,
211    tick: State<u64>,
212    loader: Loader,
213    chunk_size: usize,
214    max_chunks: usize,
215    in_flight: usize,
216}
217
218impl fmt::Debug for RemoteRows {
219    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
220        f.debug_struct("RemoteRows").field("chunk_size", &self.chunk_size).finish_non_exhaustive()
221    }
222}
223
224#[cfg_attr(feature = "live", ravenrs_live::expose)]
225impl RemoteRows {
226    /// Rows that `loader` brings, in chunks of 100, keeping 8 of them.
227    pub fn new(loader: impl Fn(GridRequest, GridResponder) + 'static) -> Self {
228        Self {
229            held: Rc::new(RefCell::new(Held { has_more: true, ..Held::default() })),
230            tick: State::new(0),
231            loader: Rc::new(loader),
232            chunk_size: DEFAULT_CHUNK,
233            max_chunks: DEFAULT_MAX_CHUNKS,
234            in_flight: DEFAULT_IN_FLIGHT,
235        }
236    }
237
238    /// How many rows to ask for at a time.
239    pub fn chunk_size(mut self, rows: usize) -> Self {
240        self.chunk_size = rows.max(1);
241        self
242    }
243
244    /// How many chunks to keep. The memory it takes is this times the chunk size, at most.
245    pub fn max_chunks(mut self, chunks: usize) -> Self {
246        self.max_chunks = chunks.max(1);
247        self
248    }
249
250    /// How many questions the server gets at once.
251    pub fn max_in_flight(mut self, requests: usize) -> Self {
252        self.in_flight = requests.max(1);
253        self
254    }
255
256    /// The most rows it has held at any moment.
257    pub fn peak_rows(&self) -> usize {
258        self.held.borrow().peak_rows
259    }
260
261    /// Forgets everything and asks again, as when the data changed on the server.
262    pub fn refresh(&self) {
263        {
264            let mut held = self.held.borrow_mut();
265            held.generation += 1;
266            held.chunks.clear();
267            held.in_flight.clear();
268            held.total = None;
269            held.end = 0;
270            held.has_more = true;
271            held.started = false;
272            held.error = None;
273        }
274        self.tick.update(|t| *t += 1);
275    }
276
277    /// Subscribes the build to what arrives.
278    pub(crate) fn watch(&self) {
279        self.tick.get();
280    }
281
282    /// How many rows the grid shows: all of them when the total is known, or what has
283    /// come so far and a little more, so that the scrollbar has somewhere to go.
284    pub(crate) fn len(&self) -> usize {
285        let held = self.held.borrow();
286        match held.total {
287            Some(total) => total,
288            None if held.has_more => held.end + self.chunk_size,
289            None => held.end,
290        }
291    }
292
293    #[cfg(test)]
294    pub(crate) fn total(&self) -> Option<usize> {
295        self.held.borrow().total
296    }
297
298    pub(crate) fn error(&self) -> Option<String> {
299        self.held.borrow().error.clone()
300    }
301
302    pub(crate) fn totals(&self) -> Vec<Option<f64>> {
303        self.held.borrow().totals.clone()
304    }
305
306    /// The row at `index`, if it is here.
307    /// Every row at hand, with its index.
308    pub(crate) fn held_rows(&self) -> Vec<(usize, Row)> {
309        let held = self.held.borrow();
310        let mut rows = Vec::new();
311        for (chunk, content) in &held.chunks {
312            let start = chunk * self.chunk_size;
313            rows.extend(content.rows.iter().enumerate().map(|(i, row)| (start + i, row.clone())));
314        }
315        rows
316    }
317
318    pub(crate) fn row(&self, index: usize) -> Option<Row> {
319        let mut held = self.held.borrow_mut();
320        held.clock += 1;
321        let used = held.clock;
322        let chunk = held.chunks.get_mut(&(index / self.chunk_size))?;
323        chunk.used = used;
324        chunk.rows.get(index % self.chunk_size).cloned()
325    }
326
327    /// Takes note of what is on the screen and asks for what is missing, if things have
328    /// settled. Returns when to look again, if something is still waiting to settle.
329    pub(crate) fn want(&self, first: usize, last: usize, query: &GridQuery, now: f64) -> Option<f64> {
330        let mut requests = Vec::new();
331        let wake = {
332            let mut held = self.held.borrow_mut();
333            let same_query = held.query.as_ref().is_some_and(|q| {
334                q.sort == query.sort && q.filters == query.filters && q.search == query.search
335            });
336            if !same_query {
337                let was_running = held.query.is_some();
338                held.query = Some(query.clone());
339                if was_running {
340                    held.generation += 1;
341                    held.chunks.clear();
342                    held.in_flight.clear();
343                    held.total = None;
344                    held.end = 0;
345                    held.has_more = true;
346                    held.queried_at = now;
347                    held.started = true;
348                    held.error = None;
349                }
350            }
351            if held.wanted != (first, last) {
352                held.wanted = (first, last);
353                held.moved_at = now;
354            }
355            // The very first screen has nothing to wait for.
356            let ready = (!held.started)
357                || (now - held.moved_at >= SCROLL_SETTLE && now - held.queried_at >= QUERY_SETTLE);
358            if !ready {
359                Some((held.moved_at + SCROLL_SETTLE).max(held.queried_at + QUERY_SETTLE))
360            } else if held.error.is_some() {
361                None
362            } else {
363                held.started = true;
364                let size = self.chunk_size;
365                let end = last.max(first + 1) - 1;
366                // One chunk past the screen, so that a slow scroll never sees a blank row.
367                let until = end / size + 1;
368                let limit = held.total.map(|t| t.div_ceil(size));
369                for chunk in first / size..=until {
370                    if limit.is_some_and(|l| chunk >= l)
371                        || held.chunks.contains_key(&chunk)
372                        || held.in_flight.contains(&chunk)
373                        || held.in_flight.len() >= self.in_flight
374                    {
375                        continue;
376                    }
377                    held.in_flight.insert(chunk);
378                    let after = chunk
379                        .checked_sub(1)
380                        .and_then(|before| held.chunks.get(&before))
381                        .and_then(|c| c.rows.last().cloned());
382                    let request = GridRequest {
383                        offset: chunk * size,
384                        limit: size,
385                        sort: query.sort.clone(),
386                        filters: query.filters.clone(),
387                        search: query.search.clone(),
388                        after,
389                        wants_total: held.total.is_none(),
390                    };
391                    requests.push((chunk, request, held.generation));
392                }
393                None
394            }
395        };
396        for (chunk, request, generation) in requests {
397            (self.loader)(
398                request,
399                GridResponder {
400                    held: self.held.clone(),
401                    tick: self.tick.clone(),
402                    generation,
403                    chunk,
404                    chunk_size: self.chunk_size,
405                    max_chunks: self.max_chunks,
406                },
407            );
408        }
409        wake
410    }
411
412    /// Clears a failure, so that the next look asks again.
413    pub(crate) fn retry(&self) {
414        self.held.borrow_mut().error = None;
415        self.tick.update(|t| *t += 1);
416    }
417}
418
419#[cfg(test)]
420mod tests {
421    use std::cell::Cell;
422
423    use super::super::model::Value;
424    use super::*;
425
426    const SERVER_ROWS: usize = 1_000_000;
427
428    /// A server that answers at once, and counts what it was asked.
429    fn server(calls: &Rc<Cell<usize>>) -> RemoteRows {
430        let calls = calls.clone();
431        RemoteRows::new(move |request, answer| {
432            calls.set(calls.get() + 1);
433            let end = (request.offset + request.limit).min(SERVER_ROWS);
434            let rows = (request.offset..end).map(|n| vec![Value::Number(n as f64)]).collect();
435            answer.send(GridPage::new(rows, end < SERVER_ROWS));
436        })
437    }
438
439    #[test]
440    fn it_asks_only_for_what_is_on_the_screen() {
441        let calls = Rc::new(Cell::new(0));
442        let rows = server(&calls);
443        rows.want(0, 30, &GridQuery::default(), 0.0);
444        assert!(calls.get() <= 2, "{} questions", calls.get());
445        assert_eq!(rows.row(5), Some(vec![Value::Number(5.0)]));
446        assert_eq!(rows.row(5000), None);
447    }
448
449    #[test]
450    fn dragging_the_scrollbar_across_everything_asks_only_where_it_stopped() {
451        let calls = Rc::new(Cell::new(0));
452        let rows = server(&calls);
453        let q = GridQuery::default();
454        rows.want(0, 30, &q, 0.0);
455        let before = calls.get();
456        for step in 1..500 {
457            let at = step * 1000;
458            assert!(rows.want(at, at + 30, &q, f64::from(step as u32) * 0.016).is_some());
459        }
460        assert_eq!(calls.get(), before, "nothing while it moves");
461        rows.want(499_000, 499_030, &q, 100.0);
462        assert!(calls.get() - before <= 2);
463        assert!(rows.row(499_010).is_some());
464    }
465
466    #[test]
467    fn memory_stays_within_its_limit_however_far_it_goes() {
468        let calls = Rc::new(Cell::new(0));
469        let rows = server(&calls).chunk_size(100).max_chunks(4);
470        let q = GridQuery::default();
471        let mut now = 0.0;
472        for at in (0..200_000).step_by(100) {
473            now += 1.0;
474            rows.want(at, at + 30, &q, now);
475            now += 1.0;
476            rows.want(at, at + 30, &q, now);
477        }
478        assert!(rows.peak_rows() <= 400, "{} rows held", rows.peak_rows());
479    }
480
481    #[test]
482    fn typing_in_a_filter_asks_once_it_rests() {
483        let calls = Rc::new(Cell::new(0));
484        let rows = server(&calls);
485        let mut q = GridQuery::default();
486        rows.want(0, 30, &q, 0.0);
487        let before = calls.get();
488        for (i, text) in ["a", "ab", "abc"].iter().enumerate() {
489            q.search = (*text).to_owned();
490            rows.want(0, 30, &q, 1.0 + i as f64 * 0.05);
491        }
492        assert_eq!(calls.get(), before);
493        rows.want(0, 30, &q, 5.0);
494        assert!(calls.get() > before);
495    }
496
497    #[test]
498    fn an_answer_to_an_old_question_is_dropped() {
499        let waiting: Rc<RefCell<Vec<GridResponder>>> = Rc::default();
500        let held = waiting.clone();
501        let rows = RemoteRows::new(move |_, answer| held.borrow_mut().push(answer)).max_in_flight(1);
502        let mut q = GridQuery::default();
503        rows.want(0, 30, &q, 0.0);
504        let old = waiting.borrow_mut().remove(0);
505        q.search = "new".into();
506        rows.want(0, 30, &q, 1.0);
507        rows.want(0, 30, &q, 5.0);
508        old.send(GridPage::new(vec![vec![Value::Text("stale".into())]], false));
509        assert_eq!(rows.row(0), None);
510        assert_eq!(rows.total(), None);
511    }
512
513    #[test]
514    fn the_server_gets_a_limited_number_of_questions_at_once() {
515        let waiting: Rc<RefCell<Vec<GridResponder>>> = Rc::default();
516        let held = waiting.clone();
517        let rows =
518            RemoteRows::new(move |_, answer| held.borrow_mut().push(answer)).chunk_size(10).max_in_flight(2);
519        rows.want(0, 100, &GridQuery::default(), 0.0);
520        assert_eq!(waiting.borrow().len(), 2);
521    }
522
523    #[test]
524    fn without_a_total_it_grows_as_rows_arrive_and_stops_at_the_end() {
525        let rows = RemoteRows::new(|request, answer| {
526            let n = if request.offset >= 100 { 40 } else { 100 };
527            let page = (0..n).map(|i| vec![Value::Number(f64::from(i))]).collect();
528            answer.send(GridPage::new(page, n == 100));
529        });
530        let q = GridQuery::default();
531        rows.want(0, 10, &q, 0.0);
532        rows.want(95, 110, &q, 1.0);
533        rows.want(95, 110, &q, 2.0);
534        assert_eq!(rows.total(), Some(140), "a short page was the last");
535        assert_eq!(rows.len(), 140);
536    }
537
538    #[test]
539    fn it_can_continue_from_the_row_before_instead_of_counting_rows() {
540        let seen: Rc<RefCell<Vec<GridRequest>>> = Rc::default();
541        let log = seen.clone();
542        let rows = RemoteRows::new(move |request, answer| {
543            log.borrow_mut().push(request.clone());
544            let page = (request.offset..request.offset + request.limit)
545                .map(|n| vec![Value::Number(n as f64)])
546                .collect();
547            answer.send(GridPage::new(page, true));
548        });
549        let q = GridQuery::default();
550        rows.want(0, 10, &q, 0.0);
551        rows.want(150, 160, &q, 1.0);
552        rows.want(150, 160, &q, 2.0);
553        let next = seen.borrow().iter().find(|r| r.offset == 200).cloned().expect("the next chunk");
554        assert_eq!(next.after, Some(vec![Value::Number(199.0)]));
555    }
556
557    #[test]
558    fn a_failure_waits_for_someone_to_try_again() {
559        let calls = Rc::new(Cell::new(0));
560        let count = calls.clone();
561        let rows = RemoteRows::new(move |_, answer| {
562            count.set(count.get() + 1);
563            answer.fail("no connection");
564        })
565        .max_in_flight(1);
566        let q = GridQuery::default();
567        rows.want(0, 10, &q, 0.0);
568        assert_eq!(rows.error().as_deref(), Some("no connection"));
569        rows.want(0, 10, &q, 9.0);
570        assert_eq!(calls.get(), 1, "it does not hammer a server that is down");
571        rows.retry();
572        rows.want(0, 10, &q, 10.0);
573        assert_eq!(calls.get(), 2);
574    }
575}