1use std::{sync::Arc, time::Duration};
10
11use crate::model::{ColumnIx, ColumnSet};
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15#[non_exhaustive]
16pub enum RowChange {
17 Inserted,
19 Updated,
21 Removed,
28}
29
30#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
32pub enum ChangeDirection {
33 Up,
35 Down,
37}
38
39#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
43pub struct Stamp(u32);
44
45impl Stamp {
46 pub const NONE: Self = Self(0);
48 const MAX_MS: u32 = (1 << 30) - 1;
49 const CHANGE_BITS: u32 = 0b11;
50 const INSERTED: u32 = 0b01;
51 const UP: u32 = 0b10;
52 const DOWN: u32 = 0b11;
53
54 pub fn new(ms: u32, change: RowChange) -> Self {
57 Self::with_direction(ms, change, None)
58 }
59
60 pub fn with_direction(ms: u32, change: RowChange, direction: Option<ChangeDirection>) -> Self {
66 debug_assert!(
67 change != RowChange::Removed,
68 "a removal stamp is written by the store for a fading row"
69 );
70 let bits = match (change, direction) {
71 (RowChange::Inserted, _) => Self::INSERTED,
72 (RowChange::Updated, Some(ChangeDirection::Up)) => Self::UP,
73 (RowChange::Updated, Some(ChangeDirection::Down)) => Self::DOWN,
74 _ => 0,
75 };
76 Self::packed(ms, bits)
77 }
78
79 pub(crate) fn removal(ms: u32) -> Self {
82 Self::packed(ms, 0)
83 }
84
85 fn packed(ms: u32, bits: u32) -> Self {
86 let ms = (ms & Self::MAX_MS).max(1);
88 Self((ms << 2) | bits)
89 }
90
91 pub fn is_set(self) -> bool {
93 self.0 != 0
94 }
95
96 pub fn ms(self) -> u32 {
98 self.0 >> 2
99 }
100
101 pub fn change(self) -> RowChange {
103 if self.0 & Self::CHANGE_BITS == Self::INSERTED {
104 RowChange::Inserted
105 } else {
106 RowChange::Updated
107 }
108 }
109
110 pub fn direction(self) -> Option<ChangeDirection> {
113 match self.0 & Self::CHANGE_BITS {
114 Self::UP => Some(ChangeDirection::Up),
115 Self::DOWN => Some(ChangeDirection::Down),
116 _ => None,
117 }
118 }
119}
120
121#[derive(Clone, Copy, Debug, PartialEq, Eq)]
131pub struct RowStamps<'a> {
132 header: &'a [Stamp; HEADER],
135 stamps: &'a [Stamp],
137}
138
139impl RowStamps<'_> {
140 const EMPTY: RowStamps<'static> = RowStamps {
141 header: &[Stamp::NONE; HEADER],
142 stamps: &[],
143 };
144
145 #[inline]
148 pub fn get(self, column: ColumnIx) -> Stamp {
149 let (ix, row) = (column.get(), self.header[ROW]);
150 if ix >= MAX_COLUMNS {
151 return row;
152 }
153 let word = ix / 32;
156 let (mask, bit) = (self.header[MASK + word].0, 1u32 << (ix % 32));
157 if mask & bit == 0 {
158 return row;
159 }
160 let first = (self.header[PLACES].0 >> (8 * word)) & 0xff;
161 let rank = first + (mask & (bit - 1)).count_ones();
162 self.stamps.get(rank as usize).copied().unwrap_or(row)
163 }
164
165 fn row(self) -> Stamp {
167 self.header[ROW]
168 }
169
170 fn columns(self) -> ColumnSet {
172 let bits = (0..MASK_WORDS).fold(0u128, |bits, word| {
173 bits | u128::from(self.header[MASK + word].0) << (32 * word)
174 });
175 ColumnSet::from_bits(bits)
176 }
177}
178
179const ROW: usize = 0;
183const MASK: usize = 1;
184const MASK_WORDS: usize = 4;
185const PLACES: usize = MASK + MASK_WORDS;
186const HEADER: usize = PLACES + 1;
187const MAX_COLUMNS: usize = 32 * MASK_WORDS;
189
190#[derive(Clone, Debug, PartialEq, Eq)]
198pub(crate) struct SparseStamps(Arc<[Stamp]>);
199
200impl SparseStamps {
201 pub(crate) fn whole_row(stamp: Stamp) -> Self {
203 Self(Arc::new(Self::header(stamp, ColumnSet::EMPTY)))
204 }
205
206 pub(crate) fn written(
211 existing: Option<&Self>,
212 columns: ColumnSet,
213 every: ColumnSet,
214 is_hot: impl Fn(Stamp) -> bool,
215 mut stamp: impl FnMut(ColumnIx) -> Stamp,
216 ) -> Option<Self> {
217 let columns = columns.intersection(every);
218 let old = existing.map_or(RowStamps::EMPTY, Self::view);
219 let old_columns = old.columns();
220 let mut touched = old_columns;
221 touched.union_with(columns);
222 let mut words = [Stamp::NONE; HEADER + MAX_COLUMNS];
224 let (mut kept, mut count, mut old_ix) = (ColumnSet::EMPTY, 0, 0);
225 for column in touched.iter() {
226 let own = old_columns.contains(column).then(|| {
227 old_ix += 1;
228 old.stamps.get(old_ix - 1).copied().unwrap_or(Stamp::NONE)
229 });
230 let value = if columns.contains(column) {
231 stamp(column)
232 } else {
233 match own {
234 Some(own) if is_hot(own) => own,
235 _ => continue,
236 }
237 };
238 words[HEADER + count] = value;
239 kept.insert(column);
240 count += 1;
241 }
242 let mut row = if is_hot(old.row()) {
243 old.row()
244 } else {
245 Stamp::NONE
246 };
247 if kept == every {
248 let first = words[HEADER];
251 let uniform = words[HEADER..HEADER + count]
252 .iter()
253 .all(|&stamp| stamp == first);
254 (row, kept, count) = if uniform {
255 (first, ColumnSet::EMPTY, 0)
256 } else {
257 (Stamp::NONE, kept, count)
258 };
259 }
260 if count == 0 && !row.is_set() {
261 return None;
262 }
263 words[..HEADER].copy_from_slice(&Self::header(row, kept));
264 Some(Self(Arc::from(&words[..HEADER + count])))
265 }
266
267 fn header(row: Stamp, columns: ColumnSet) -> [Stamp; HEADER] {
268 let bits = columns.bits();
269 let mut header = [Stamp::NONE; HEADER];
270 header[ROW] = row;
271 let mut before = 0;
272 for word in 0..MASK_WORDS {
273 let mask = (bits >> (32 * word)) as u32;
274 header[MASK + word] = Stamp(mask);
275 header[PLACES].0 |= before << (8 * word);
277 before += mask.count_ones();
278 }
279 header
280 }
281
282 #[inline]
284 pub(crate) fn view(&self) -> RowStamps<'_> {
285 match self.0.split_first_chunk::<HEADER>() {
286 Some((header, stamps)) => RowStamps { header, stamps },
287 None => RowStamps::EMPTY,
288 }
289 }
290
291 pub(crate) fn is_cold(&self, is_hot: impl Fn(Stamp) -> bool) -> bool {
293 let stamps = self.view();
294 !is_hot(stamps.row()) && !stamps.stamps.iter().any(|&stamp| is_hot(stamp))
295 }
296
297 pub(crate) fn heap_bytes(&self) -> usize {
299 2 * size_of::<usize>() + size_of_val::<[Stamp]>(&self.0)
300 }
301}
302
303#[derive(Clone, Copy, Debug, PartialEq, Eq)]
308pub struct HighlightPolicy {
309 duration_ms: u32,
310 steps: u32,
311 removal_fade_ms: u32,
312}
313
314impl Default for HighlightPolicy {
315 fn default() -> Self {
316 Self::new(Duration::from_secs(5), 8)
317 }
318}
319
320impl HighlightPolicy {
321 pub fn new(duration: Duration, steps: u32) -> Self {
324 let duration_ms = duration.as_millis().clamp(1, u128::from(Stamp::MAX_MS / 4)) as u32;
326 let steps = steps.clamp(1, duration_ms);
327 Self {
328 duration_ms,
329 steps,
330 removal_fade_ms: 0,
331 }
332 }
333
334 pub fn with_removal_fade(mut self, fade: Duration) -> Self {
341 self.removal_fade_ms = fade.as_millis().min(u128::from(Stamp::MAX_MS / 4)) as u32;
342 self
343 }
344
345 pub fn removal_fade(&self) -> Duration {
347 Duration::from_millis(self.removal_fade_ms.into())
348 }
349
350 pub fn duration_ms(&self) -> u32 {
352 self.duration_ms
353 }
354
355 pub fn steps(&self) -> u32 {
357 self.steps
358 }
359
360 fn step_ms(&self) -> u32 {
361 (self.duration_ms / self.steps).max(1)
362 }
363
364 pub fn tick(&self, now_ms: u32) -> u32 {
367 now_ms - now_ms % self.step_ms()
368 }
369
370 pub fn next_tick(&self, now_ms: u32) -> u32 {
373 self.tick(now_ms) + self.step_ms()
374 }
375
376 fn age(stamp: Stamp, now_ms: u32) -> u32 {
380 let age = (now_ms & Stamp::MAX_MS).wrapping_sub(stamp.ms()) & Stamp::MAX_MS;
381 if age > Stamp::MAX_MS / 2 { 0 } else { age }
382 }
383
384 pub fn intensity(&self, stamp: Stamp, tick_ms: u32) -> Option<f32> {
387 if !stamp.is_set() {
388 return None;
389 }
390 let age = Self::age(stamp, tick_ms);
391 if age >= self.duration_ms {
392 return None;
393 }
394 let step = (age / self.step_ms()).min(self.steps - 1);
396 Some(1.0 - step as f32 / self.steps as f32)
397 }
398
399 pub fn is_expired(&self, stamp: Stamp, now_ms: u32) -> bool {
401 !stamp.is_set() || Self::age(stamp, now_ms) >= self.duration_ms
402 }
403
404 pub(crate) fn is_highlight_over(&self, stamp: Stamp, now_ms: u32) -> bool {
409 self.is_expired(stamp, self.tick(now_ms))
410 }
411
412 pub fn removal_intensity(&self, removed: Stamp, tick_ms: u32) -> Option<f32> {
417 if self.is_removal_over(removed, tick_ms) {
418 return None;
419 }
420 let step_ms = self.step_ms();
421 let steps = self.removal_fade_ms.div_ceil(step_ms);
422 let step = Self::age(removed, tick_ms) / step_ms;
424 Some(1.0 - step as f32 / steps as f32)
425 }
426
427 pub(crate) fn is_removal_over(&self, removed: Stamp, now_ms: u32) -> bool {
430 !removed.is_set() || Self::age(removed, now_ms) >= self.removal_fade_ms
431 }
432
433 pub(crate) fn has_removal_fade(&self) -> bool {
435 self.removal_fade_ms > 0
436 }
437}
438
439#[cfg(test)]
440mod tests {
441 use super::*;
442
443 #[test]
444 fn test_highlight_policy_steps_on_global_tick() {
445 let policy = HighlightPolicy::new(Duration::from_millis(800), 8);
446 let stamp = Stamp::new(1_050, RowChange::Updated);
447 assert_eq!(stamp.ms(), 1_050);
448 assert_eq!(stamp.change(), RowChange::Updated);
449 assert_eq!(
450 Stamp::new(7, RowChange::Inserted).change(),
451 RowChange::Inserted
452 );
453
454 assert_eq!(policy.tick(1_099), 1_000);
456 assert_eq!(policy.next_tick(1_099), 1_100);
457 assert_eq!(policy.intensity(stamp, policy.tick(1_060)), Some(1.0));
459 assert_eq!(policy.intensity(stamp, 1_200), Some(1.0 - 1.0 / 8.0));
460 assert_eq!(policy.intensity(stamp, 1_849), Some(1.0 - 7.0 / 8.0));
461 assert_eq!(policy.intensity(stamp, 1_850), None);
462 assert!(policy.is_expired(stamp, 1_850));
463 assert_eq!(policy.intensity(Stamp::NONE, 1_000), None);
464
465 let wrap = 1u32 << 30;
467 let late = Stamp::new(wrap - 100, RowChange::Updated);
468 assert_eq!(policy.intensity(late, wrap + 200), Some(1.0 - 3.0 / 8.0));
469 assert!(!policy.is_expired(late, wrap + 200));
470 assert!(policy.is_expired(late, wrap + 800));
471
472 let uneven = HighlightPolicy::new(Duration::from_millis(1_000), 3);
474 let stamp = Stamp::new(10, RowChange::Updated);
475 assert!(uneven.intensity(stamp, 1_009).is_some_and(|i| i > 0.0));
476 }
477
478 #[test]
479 fn test_stamp_direction_bits() {
480 use ChangeDirection::{Down, Up};
481 use RowChange::{Inserted, Updated};
482
483 for (stamp, change, direction) in [
485 (Stamp::with_direction(1_050, Updated, None), Updated, None),
486 (Stamp::with_direction(1_050, Inserted, None), Inserted, None),
487 (
488 Stamp::with_direction(1_050, Updated, Some(Up)),
489 Updated,
490 Some(Up),
491 ),
492 (
493 Stamp::with_direction(1_050, Updated, Some(Down)),
494 Updated,
495 Some(Down),
496 ),
497 ] {
498 assert_eq!(stamp.ms(), 1_050);
499 assert_eq!(stamp.change(), change);
500 assert_eq!(stamp.direction(), direction);
501 assert!(stamp.is_set());
502 }
503 assert_eq!(
505 Stamp::with_direction(7, Inserted, Some(Up)),
506 Stamp::new(7, Inserted)
507 );
508 assert_eq!(Stamp::new(7, Updated).direction(), None);
509 assert_eq!(Stamp::NONE.direction(), None);
510 assert!(!Stamp::NONE.is_set());
511 assert_eq!(Stamp::removal(1_050), Stamp::new(1_050, Updated));
514 assert!(Stamp::removal(0).is_set());
515 let max = Stamp::with_direction(Stamp::MAX_MS, Updated, Some(Down));
517 assert_eq!((max.ms(), max.direction()), (Stamp::MAX_MS, Some(Down)));
518
519 let policy = HighlightPolicy::new(Duration::from_millis(800), 8);
521 let plain = Stamp::new(1_050, Updated);
522 for stamp in [
523 Stamp::new(1_050, Inserted),
524 Stamp::with_direction(1_050, Updated, Some(Up)),
525 Stamp::with_direction(1_050, Updated, Some(Down)),
526 ] {
527 for now in [1_060, 1_200, 1_849, 1_850, 5_000] {
528 let tick = policy.tick(now);
529 assert_eq!(policy.intensity(stamp, tick), policy.intensity(plain, tick));
530 assert_eq!(policy.is_expired(stamp, now), policy.is_expired(plain, now));
531 }
532 }
533 }
534
535 #[test]
536 fn test_removal_fade_steps_on_global_tick() {
537 let policy = HighlightPolicy::new(Duration::from_millis(800), 8);
539 assert_eq!(policy.removal_fade(), Duration::ZERO);
540 assert!(!policy.has_removal_fade());
541 let removed = Stamp::removal(1_050);
542 assert_eq!(policy.removal_intensity(removed, 1_050), None);
543 assert!(policy.is_removal_over(removed, 1_050));
544
545 let policy = policy.with_removal_fade(Duration::from_millis(250));
548 assert_eq!(policy.removal_fade(), Duration::from_millis(250));
549 assert!(policy.has_removal_fade());
550 assert_eq!(policy.duration_ms(), 800);
551 assert_eq!(
552 policy.removal_intensity(removed, policy.tick(1_060)),
553 Some(1.0)
554 );
555 assert_eq!(
556 policy.removal_intensity(removed, 1_200),
557 Some(1.0 - 1.0 / 3.0)
558 );
559 assert_eq!(
560 policy.removal_intensity(removed, 1_299),
561 Some(1.0 - 2.0 / 3.0)
562 );
563 assert!(!policy.is_removal_over(removed, 1_299));
564 assert_eq!(policy.removal_intensity(removed, 1_300), None);
565 assert!(policy.is_removal_over(removed, 1_300));
566 assert_eq!(policy.removal_intensity(Stamp::NONE, 1_100), None);
567
568 let short = policy.with_removal_fade(Duration::from_millis(40));
570 assert_eq!(short.removal_intensity(removed, 1_089), Some(1.0));
571 assert_eq!(short.removal_intensity(removed, 1_090), None);
572
573 let wrap = 1u32 << 30;
575 let late = Stamp::removal(wrap - 100);
576 assert_eq!(
577 policy.removal_intensity(late, wrap + 100),
578 Some(1.0 - 2.0 / 3.0)
579 );
580 assert!(policy.is_removal_over(late, wrap + 150));
581
582 let off = policy.with_removal_fade(Duration::ZERO);
584 assert_eq!(off, HighlightPolicy::new(Duration::from_millis(800), 8));
585 }
586
587 #[test]
588 #[cfg(debug_assertions)]
589 #[should_panic(expected = "a removal stamp is written by the store")]
590 fn test_removed_is_not_a_cell_stamp() {
591 Stamp::new(1_050, RowChange::Removed);
592 }
593
594 fn columns(ixs: &[u16]) -> ColumnSet {
595 let mut set = ColumnSet::EMPTY;
596 for &ix in ixs {
597 set.insert(ColumnIx::new(ix));
598 }
599 set
600 }
601
602 fn write(
604 policy: HighlightPolicy,
605 existing: Option<&SparseStamps>,
606 written: &[u16],
607 every: ColumnSet,
608 now_ms: u32,
609 stamp: Stamp,
610 ) -> Option<SparseStamps> {
611 SparseStamps::written(
612 existing,
613 columns(written),
614 every,
615 |stamp| !policy.is_highlight_over(stamp, now_ms),
616 |_| stamp,
617 )
618 }
619
620 fn held(stamps: &SparseStamps) -> usize {
622 let view = stamps.view();
623 usize::from(view.row().is_set()) + view.stamps.len()
624 }
625
626 #[test]
627 fn test_sparse_stamps_keep_written_columns() {
628 let policy = HighlightPolicy::new(Duration::from_millis(800), 8);
629 let every = ColumnSet::first(40);
630 let first = Stamp::new(10, RowChange::Updated);
631 let stamps = write(policy, None, &[3, 7], every, 10, first).expect("stamped");
632 let view = stamps.view();
633 assert_eq!(view.get(ColumnIx::new(3)), first);
634 assert_eq!(view.get(ColumnIx::new(7)), first);
635 assert_eq!(view.get(ColumnIx::new(0)), Stamp::NONE);
636 assert_eq!(view.get(ColumnIx::new(39)), Stamp::NONE);
637 assert_eq!(held(&stamps), 2);
638 assert_eq!(
640 stamps.heap_bytes(),
641 2 * size_of::<usize>() + (HEADER + 2) * 4
642 );
643
644 let second = Stamp::with_direction(20, RowChange::Updated, Some(ChangeDirection::Up));
646 let stamps = write(policy, Some(&stamps), &[7, 9], every, 20, second).expect("stamped");
647 let view = stamps.view();
648 assert_eq!(view.get(ColumnIx::new(3)), first);
649 assert_eq!(view.get(ColumnIx::new(7)), second);
650 assert_eq!(view.get(ColumnIx::new(9)), second);
651 assert_eq!(held(&stamps), 3);
652
653 assert_eq!(write(policy, None, &[40, 41], every, 20, second), None);
656 assert_eq!(
657 SparseStamps::written(None, ColumnSet::EMPTY, every, |_| true, |_| second),
658 None
659 );
660 }
661
662 #[test]
663 fn test_sparse_stamps_drop_expired_on_write() {
664 let policy = HighlightPolicy::new(Duration::from_millis(800), 8);
666 let every = ColumnSet::first(40);
667 let early = Stamp::new(100, RowChange::Updated);
668 let late = Stamp::new(150, RowChange::Updated);
669 let stamps = write(policy, None, &[1], every, 100, early).expect("stamped");
670 let stamps = write(policy, Some(&stamps), &[2], every, 150, late).expect("stamped");
671 assert_eq!(held(&stamps), 2);
672
673 let now = Stamp::new(960, RowChange::Updated);
677 let stamps = write(policy, Some(&stamps), &[3], every, 960, now).expect("stamped");
678 let view = stamps.view();
679 assert_eq!(view.get(ColumnIx::new(1)), Stamp::NONE);
680 assert_eq!(view.get(ColumnIx::new(2)), late);
681 assert_eq!(view.get(ColumnIx::new(3)), now);
682 assert_eq!(held(&stamps), 2);
683 let is_hot = |at: u32| move |stamp| !policy.is_highlight_over(stamp, at);
684 assert!(!stamps.is_cold(is_hot(960)));
685
686 let next = Stamp::new(1_000, RowChange::Updated);
688 let stamps = write(policy, Some(&stamps), &[4], every, 1_000, next).expect("stamped");
689 assert_eq!(held(&stamps), 2);
690 assert_eq!(stamps.view().get(ColumnIx::new(2)), Stamp::NONE);
691 assert_eq!(stamps.view().get(ColumnIx::new(3)), now);
692 assert!(!stamps.is_cold(is_hot(1_799)));
694 assert!(stamps.is_cold(is_hot(1_800)));
695 }
696
697 #[test]
698 fn test_sparse_stamps_whole_row() {
699 let policy = HighlightPolicy::new(Duration::from_millis(800), 8);
700 let every = ColumnSet::first(40);
701 let inserted = Stamp::new(10, RowChange::Inserted);
703 let stamps = SparseStamps::whole_row(inserted);
704 assert_eq!(held(&stamps), 1);
705 assert_eq!(stamps.heap_bytes(), 2 * size_of::<usize>() + HEADER * 4);
706 assert!((0..40).all(|ix| stamps.view().get(ColumnIx::new(ix)) == inserted));
707
708 let update = Stamp::with_direction(150, RowChange::Updated, Some(ChangeDirection::Down));
710 let stamps = write(policy, Some(&stamps), &[5], every, 150, update).expect("stamped");
711 assert_eq!(stamps.view().get(ColumnIx::new(5)), update);
712 assert_eq!(stamps.view().get(ColumnIx::new(4)), inserted);
713 assert_eq!(held(&stamps), 2);
714
715 let later = Stamp::new(900, RowChange::Updated);
717 let stamps = write(policy, Some(&stamps), &[6], every, 900, later).expect("stamped");
718 assert_eq!(stamps.view().get(ColumnIx::new(4)), Stamp::NONE);
719 assert_eq!(stamps.view().get(ColumnIx::new(5)), update);
720 assert_eq!(held(&stamps), 2);
721
722 let all: Vec<u16> = (0..40).collect();
725 let mark = Stamp::new(950, RowChange::Updated);
726 let marked = write(policy, Some(&stamps), &all, every, 950, mark).expect("stamped");
727 assert_eq!(held(&marked), 1);
728 assert!((0..40).all(|ix| marked.view().get(ColumnIx::new(ix)) == mark));
729 let mixed = SparseStamps::written(
730 Some(&stamps),
731 every,
732 every,
733 |_| true,
734 |column| Stamp::new(950 + column.get() as u32, RowChange::Updated),
735 )
736 .expect("stamped");
737 assert_eq!(held(&mixed), 40);
738 assert_eq!(mixed.view().row(), Stamp::NONE);
739 assert_eq!(mixed.view().get(ColumnIx::new(39)).ms(), 989);
740
741 let is_hot = |at: u32| move |stamp| !policy.is_highlight_over(stamp, at);
744 let flash = SparseStamps::whole_row(inserted);
745 assert!(!flash.is_cold(is_hot(899)));
746 assert!(flash.is_cold(is_hot(900)));
747 }
748
749 #[test]
750 fn test_sparse_stamps_past_64_and_128_columns() {
751 let policy = HighlightPolicy::new(Duration::from_millis(800), 8);
752 for width in [70, 100, 128] {
753 let every = ColumnSet::first(width);
754 let last = width as u16 - 1;
755 let written = [0, 31, 32, 63, 64, 65, last];
756 let stamps = SparseStamps::written(
758 None,
759 columns(&written),
760 every,
761 |stamp| !policy.is_highlight_over(stamp, 0),
762 |column| Stamp::new(column.get() as u32 + 1, RowChange::Updated),
763 )
764 .expect("stamped");
765 let view = stamps.view();
766 assert_eq!(view.columns(), columns(&written));
767 assert_eq!(view.header[PLACES].0 & 0xff_ff_ff, 2 << 8 | 4 << 16);
770 for ix in 0..width as u16 {
771 let expected = if written.contains(&ix) {
772 Stamp::new(u32::from(ix) + 1, RowChange::Updated)
773 } else {
774 Stamp::NONE
775 };
776 assert_eq!(view.get(ColumnIx::new(ix)), expected, "{width}: {ix}");
777 }
778 let update = Stamp::new(500, RowChange::Updated);
780 let stamps =
781 write(policy, Some(&stamps), &[last], every, 500, update).expect("stamped");
782 assert_eq!(stamps.view().get(ColumnIx::new(last)), update);
783 assert_eq!(stamps.view().get(ColumnIx::new(64)).ms(), 65);
784 assert_eq!(held(&stamps), 7);
785 }
786 }
787}