1use std::fmt;
98use std::hash::{Hash, Hasher};
99
100#[derive(Debug, Clone, PartialEq)]
116pub enum DeltaEntry<K, V> {
117 Insert { key: K, value: V, logical_time: u64 },
120 Delete { key: K, logical_time: u64 },
123}
124
125impl<K, V> DeltaEntry<K, V> {
126 pub fn key(&self) -> &K {
128 match self {
129 DeltaEntry::Insert { key, .. } => key,
130 DeltaEntry::Delete { key, .. } => key,
131 }
132 }
133
134 pub fn logical_time(&self) -> u64 {
136 match self {
137 DeltaEntry::Insert { logical_time, .. } => *logical_time,
138 DeltaEntry::Delete { logical_time, .. } => *logical_time,
139 }
140 }
141
142 pub fn weight(&self) -> i8 {
144 match self {
145 DeltaEntry::Insert { .. } => 1,
146 DeltaEntry::Delete { .. } => -1,
147 }
148 }
149
150 pub fn is_insert(&self) -> bool {
152 matches!(self, DeltaEntry::Insert { .. })
153 }
154}
155
156#[derive(Debug, Clone)]
166pub struct DeltaBatch<K, V> {
167 pub epoch: u64,
169 pub entries: Vec<DeltaEntry<K, V>>,
171}
172
173impl<K: Eq + Hash, V> DeltaBatch<K, V> {
174 pub fn new(epoch: u64) -> Self {
176 Self {
177 epoch,
178 entries: Vec::new(),
179 }
180 }
181
182 pub fn len(&self) -> usize {
184 self.entries.len()
185 }
186
187 pub fn is_empty(&self) -> bool {
189 self.entries.is_empty()
190 }
191
192 pub fn insert(&mut self, key: K, value: V, logical_time: u64) {
194 self.entries.push(DeltaEntry::Insert {
195 key,
196 value,
197 logical_time,
198 });
199 }
200
201 pub fn delete(&mut self, key: K, logical_time: u64) {
203 self.entries.push(DeltaEntry::Delete { key, logical_time });
204 }
205}
206
207#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
213pub struct ViewId(pub u32);
214
215impl fmt::Display for ViewId {
216 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
217 write!(f, "V{}", self.0)
218 }
219}
220
221#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
229pub enum ViewDomain {
230 Style,
232 Layout,
234 Render,
236 FilteredList,
238 Custom,
240}
241
242impl ViewDomain {
243 pub fn as_str(&self) -> &'static str {
245 match self {
246 ViewDomain::Style => "style",
247 ViewDomain::Layout => "layout",
248 ViewDomain::Render => "render",
249 ViewDomain::FilteredList => "filtered_list",
250 ViewDomain::Custom => "custom",
251 }
252 }
253}
254
255#[derive(Debug, Clone)]
261pub struct PropagationResult {
262 pub view_id: ViewId,
264 pub domain: ViewDomain,
266 pub input_delta_size: usize,
268 pub output_delta_size: usize,
270 pub fell_back_to_full: bool,
272 pub materialized_size: usize,
274 pub duration_us: u64,
276}
277
278#[derive(Debug, Clone)]
287pub struct EpochEvidence {
288 pub epoch: u64,
290 pub views_processed: usize,
292 pub views_recomputed: usize,
294 pub total_delta_size: usize,
296 pub total_materialized_size: usize,
298 pub duration_us: u64,
300 pub per_view: Vec<PropagationResult>,
302}
303
304impl EpochEvidence {
305 pub fn delta_ratio(&self) -> f64 {
310 if self.total_materialized_size == 0 {
311 0.0
312 } else {
313 self.total_delta_size as f64 / self.total_materialized_size as f64
314 }
315 }
316
317 pub fn to_jsonl(&self) -> String {
319 format!(
320 "{{\"type\":\"ivm_epoch\",\"epoch\":{},\"views_processed\":{},\"views_recomputed\":{},\"total_delta_size\":{},\"total_materialized_size\":{},\"delta_ratio\":{:.4},\"duration_us\":{}}}",
321 self.epoch,
322 self.views_processed,
323 self.views_recomputed,
324 self.total_delta_size,
325 self.total_materialized_size,
326 self.delta_ratio(),
327 self.duration_us,
328 )
329 }
330}
331
332#[derive(Debug, Clone)]
342pub struct FallbackPolicy {
343 pub ratio_threshold: f64,
346 pub min_delta_for_fallback: usize,
350}
351
352impl Default for FallbackPolicy {
353 fn default() -> Self {
354 Self {
355 ratio_threshold: 0.5,
356 min_delta_for_fallback: 10,
357 }
358 }
359}
360
361impl FallbackPolicy {
362 pub fn should_fallback(&self, delta_size: usize, materialized_size: usize) -> bool {
364 if delta_size < self.min_delta_for_fallback {
365 return false;
366 }
367 if materialized_size == 0 {
368 return true; }
370 (delta_size as f64 / materialized_size as f64) > self.ratio_threshold
371 }
372}
373
374#[derive(Debug, Clone, PartialEq, Eq)]
380pub struct DagEdge {
381 pub from: ViewId,
383 pub to: ViewId,
385}
386
387#[derive(Debug, Clone)]
403pub struct DagTopology {
404 pub views: Vec<ViewDescriptor>,
406 pub edges: Vec<DagEdge>,
408 pub topo_order: Vec<ViewId>,
410}
411
412#[derive(Debug, Clone)]
414pub struct ViewDescriptor {
415 pub id: ViewId,
417 pub label: String,
419 pub domain: ViewDomain,
421 pub fallback_policy: FallbackPolicy,
423}
424
425impl DagTopology {
426 pub fn new() -> Self {
428 Self {
429 views: Vec::new(),
430 edges: Vec::new(),
431 topo_order: Vec::new(),
432 }
433 }
434
435 pub fn add_view(&mut self, label: impl Into<String>, domain: ViewDomain) -> ViewId {
437 let id = ViewId(self.views.len() as u32);
438 self.views.push(ViewDescriptor {
439 id,
440 label: label.into(),
441 domain,
442 fallback_policy: FallbackPolicy::default(),
443 });
444 id
445 }
446
447 pub fn add_edge(&mut self, from: ViewId, to: ViewId) {
453 assert!(
455 !self.can_reach(to, from),
456 "IVM DAG cycle detected: {} -> {} would create a cycle",
457 from,
458 to
459 );
460 self.edges.push(DagEdge { from, to });
461 }
462
463 fn can_reach(&self, from: ViewId, to: ViewId) -> bool {
465 let mut visited = vec![false; self.views.len()];
466 let mut stack = vec![from];
467 while let Some(current) = stack.pop() {
468 if current == to {
469 return true;
470 }
471 let idx = current.0 as usize;
472 if idx >= visited.len() || visited[idx] {
473 continue;
474 }
475 visited[idx] = true;
476 for edge in &self.edges {
477 if edge.from == current && !visited[edge.to.0 as usize] {
478 stack.push(edge.to);
479 }
480 }
481 }
482 false
483 }
484
485 pub fn compute_topo_order(&mut self) {
495 let n = self.views.len();
496 let mut in_degree = vec![0usize; n];
497 let mut adj: Vec<Vec<ViewId>> = vec![Vec::new(); n];
498
499 for edge in &self.edges {
500 in_degree[edge.to.0 as usize] += 1;
501 adj[edge.from.0 as usize].push(edge.to);
502 }
503
504 let mut queue: Vec<ViewId> = (0..n)
506 .filter(|&i| in_degree[i] == 0)
507 .map(|i| ViewId(i as u32))
508 .collect();
509 queue.sort();
510
511 let mut order = Vec::with_capacity(n);
512 while let Some(v) = queue.pop() {
513 order.push(v);
514 let mut neighbors = adj[v.0 as usize].clone();
516 neighbors.sort();
517 for next in neighbors {
518 in_degree[next.0 as usize] -= 1;
519 if in_degree[next.0 as usize] == 0 {
520 let pos = queue.partition_point(|&x| x > next);
522 queue.insert(pos, next);
523 }
524 }
525 }
526
527 assert_eq!(
528 order.len(),
529 n,
530 "IVM DAG has a cycle: only {} of {} views in topo order",
531 order.len(),
532 n
533 );
534
535 self.topo_order = order;
536 }
537
538 pub fn downstream(&self, view_id: ViewId) -> Vec<ViewId> {
540 self.edges
541 .iter()
542 .filter(|e| e.from == view_id)
543 .map(|e| e.to)
544 .collect()
545 }
546
547 pub fn upstream(&self, view_id: ViewId) -> Vec<ViewId> {
549 self.edges
550 .iter()
551 .filter(|e| e.to == view_id)
552 .map(|e| e.from)
553 .collect()
554 }
555
556 pub fn view_count(&self) -> usize {
558 self.views.len()
559 }
560
561 pub fn edge_count(&self) -> usize {
563 self.edges.len()
564 }
565}
566
567impl Default for DagTopology {
568 fn default() -> Self {
569 Self::new()
570 }
571}
572
573#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
579pub struct StyleKey(pub u32);
580
581#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
583pub struct LayoutKey(pub u32);
584
585#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
587pub struct CellKey {
588 pub row: u16,
589 pub col: u16,
590}
591
592#[derive(Debug, Clone, Copy, PartialEq, Eq)]
598pub struct ResolvedStyleValue {
599 pub style_hash: u64,
601}
602
603#[derive(Debug, Clone, PartialEq, Eq)]
607pub struct LayoutValue {
608 pub rects_hash: u64,
610 pub rect_count: u16,
612}
613
614#[derive(Debug, Clone, Copy, PartialEq, Eq)]
619pub struct CellValue {
620 pub cell_hash: u64,
622}
623
624#[derive(Debug, Clone)]
630pub struct IvmConfig {
631 pub force_full: bool,
634 pub default_fallback: FallbackPolicy,
636 pub emit_evidence: bool,
638}
639
640impl Default for IvmConfig {
641 fn default() -> Self {
642 Self {
643 force_full: false,
644 default_fallback: FallbackPolicy::default(),
645 emit_evidence: true,
646 }
647 }
648}
649
650impl IvmConfig {
651 pub fn from_env() -> Self {
653 let force = std::env::var("FRANKENTUI_FULL_RECOMPUTE")
654 .map(|v| matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes"))
655 .unwrap_or(false);
656 Self {
657 force_full: force,
658 ..Default::default()
659 }
660 }
661}
662
663pub fn fx_hash<T: Hash>(value: &T) -> u64 {
672 let mut h = std::collections::hash_map::DefaultHasher::new();
673 value.hash(&mut h);
674 h.finish()
675}
676
677pub trait IncrementalView<K: Clone + Eq + Hash, V: Clone + PartialEq> {
702 fn apply_delta(&mut self, batch: &DeltaBatch<K, V>) -> DeltaBatch<K, V>;
707
708 fn full_recompute(&self) -> Vec<(K, V)>;
713
714 fn materialized_size(&self) -> usize;
716
717 fn domain(&self) -> ViewDomain;
719
720 fn label(&self) -> &str;
722}
723
724pub struct StyleResolutionView {
740 label: String,
741 base_hash: u64,
743 overrides: std::collections::HashMap<StyleKey, u64>,
745 resolved: std::collections::HashMap<StyleKey, ResolvedStyleValue>,
747}
748
749impl StyleResolutionView {
750 pub fn new(label: impl Into<String>, base_hash: u64) -> Self {
752 Self {
753 label: label.into(),
754 base_hash,
755 overrides: std::collections::HashMap::new(),
756 resolved: std::collections::HashMap::new(),
757 }
758 }
759
760 pub fn set_base(&mut self, base_hash: u64) {
762 self.base_hash = base_hash;
763 }
764
765 pub fn base_hash(&self) -> u64 {
767 self.base_hash
768 }
769
770 fn resolve(&self, key: &StyleKey) -> ResolvedStyleValue {
772 let override_hash = self.overrides.get(key).copied().unwrap_or(0);
773 ResolvedStyleValue {
774 style_hash: self.base_hash ^ override_hash,
775 }
776 }
777}
778
779impl IncrementalView<StyleKey, ResolvedStyleValue> for StyleResolutionView {
780 fn apply_delta(
781 &mut self,
782 batch: &DeltaBatch<StyleKey, ResolvedStyleValue>,
783 ) -> DeltaBatch<StyleKey, ResolvedStyleValue> {
784 let mut output = DeltaBatch::new(batch.epoch);
785 let mut time = 0u64;
786
787 for entry in &batch.entries {
788 match entry {
789 DeltaEntry::Insert {
790 key,
791 value,
792 logical_time: _,
793 } => {
794 self.overrides.insert(*key, value.style_hash);
796 let new_resolved = self.resolve(key);
797 let old = self.resolved.insert(*key, new_resolved);
798 if old.as_ref() != Some(&new_resolved) {
800 output.insert(*key, new_resolved, time);
801 time += 1;
802 }
803 }
804 DeltaEntry::Delete {
805 key,
806 logical_time: _,
807 } => {
808 self.overrides.remove(key);
809 if self.resolved.remove(key).is_some() {
810 output.delete(*key, time);
811 time += 1;
812 }
813 }
814 }
815 }
816 output
817 }
818
819 fn full_recompute(&self) -> Vec<(StyleKey, ResolvedStyleValue)> {
820 self.overrides
821 .keys()
822 .map(|key| (*key, self.resolve(key)))
823 .collect()
824 }
825
826 fn materialized_size(&self) -> usize {
827 self.resolved.len()
828 }
829
830 fn domain(&self) -> ViewDomain {
831 ViewDomain::Style
832 }
833
834 fn label(&self) -> &str {
835 &self.label
836 }
837}
838
839type FilterPredicate<K, V> = dyn Fn(&K, &V) -> bool;
849
850pub struct FilteredListView<K: Clone + Eq + Hash, V: Clone + PartialEq> {
851 label: String,
852 filter: Box<FilterPredicate<K, V>>,
854 all_items: std::collections::HashMap<K, V>,
856 visible: std::collections::HashMap<K, V>,
858}
859
860impl<K: Clone + Eq + Hash, V: Clone + PartialEq> FilteredListView<K, V> {
861 pub fn new(label: impl Into<String>, filter: impl Fn(&K, &V) -> bool + 'static) -> Self {
863 Self {
864 label: label.into(),
865 filter: Box::new(filter),
866 all_items: std::collections::HashMap::new(),
867 visible: std::collections::HashMap::new(),
868 }
869 }
870
871 pub fn total_count(&self) -> usize {
873 self.all_items.len()
874 }
875
876 pub fn visible_count(&self) -> usize {
878 self.visible.len()
879 }
880}
881
882impl<K: Clone + Eq + Hash, V: Clone + PartialEq> IncrementalView<K, V> for FilteredListView<K, V> {
883 fn apply_delta(&mut self, batch: &DeltaBatch<K, V>) -> DeltaBatch<K, V> {
884 let mut output = DeltaBatch::new(batch.epoch);
885 let mut time = 0u64;
886
887 for entry in &batch.entries {
888 match entry {
889 DeltaEntry::Insert {
890 key,
891 value,
892 logical_time: _,
893 } => {
894 let was_visible = self.visible.contains_key(key);
895 self.all_items.insert(key.clone(), value.clone());
896 let now_visible = (self.filter)(key, value);
897
898 if now_visible {
899 let old = self.visible.insert(key.clone(), value.clone());
900 if !was_visible || old.as_ref() != Some(value) {
902 output.insert(key.clone(), value.clone(), time);
903 time += 1;
904 }
905 } else if was_visible {
906 self.visible.remove(key);
908 output.delete(key.clone(), time);
909 time += 1;
910 }
911 }
912 DeltaEntry::Delete {
913 key,
914 logical_time: _,
915 } => {
916 self.all_items.remove(key);
917 if self.visible.remove(key).is_some() {
918 output.delete(key.clone(), time);
919 time += 1;
920 }
921 }
922 }
923 }
924 output
925 }
926
927 fn full_recompute(&self) -> Vec<(K, V)> {
928 self.all_items
929 .iter()
930 .filter(|(k, v)| (self.filter)(k, v))
931 .map(|(k, v)| (k.clone(), v.clone()))
932 .collect()
933 }
934
935 fn materialized_size(&self) -> usize {
936 self.visible.len()
937 }
938
939 fn domain(&self) -> ViewDomain {
940 ViewDomain::FilteredList
941 }
942
943 fn label(&self) -> &str {
944 &self.label
945 }
946}
947
948#[cfg(test)]
953mod tests {
954 use super::*;
955
956 #[test]
959 fn delta_entry_insert_fields() {
960 let entry: DeltaEntry<u32, String> = DeltaEntry::Insert {
961 key: 42,
962 value: "hello".into(),
963 logical_time: 1,
964 };
965 assert_eq!(*entry.key(), 42);
966 assert_eq!(entry.logical_time(), 1);
967 assert_eq!(entry.weight(), 1);
968 assert!(entry.is_insert());
969 }
970
971 #[test]
972 fn delta_entry_delete_fields() {
973 let entry: DeltaEntry<u32, String> = DeltaEntry::Delete {
974 key: 42,
975 logical_time: 2,
976 };
977 assert_eq!(*entry.key(), 42);
978 assert_eq!(entry.logical_time(), 2);
979 assert_eq!(entry.weight(), -1);
980 assert!(!entry.is_insert());
981 }
982
983 #[test]
986 fn batch_operations() {
987 let mut batch = DeltaBatch::new(1);
988 assert!(batch.is_empty());
989 assert_eq!(batch.len(), 0);
990 assert_eq!(batch.epoch, 1);
991
992 batch.insert(1u32, "a".to_string(), 1);
993 batch.insert(2, "b".to_string(), 2);
994 batch.delete(1, 3);
995
996 assert_eq!(batch.len(), 3);
997 assert!(!batch.is_empty());
998 }
999
1000 #[test]
1003 fn fallback_below_min() {
1004 let policy = FallbackPolicy {
1005 ratio_threshold: 0.5,
1006 min_delta_for_fallback: 10,
1007 };
1008 assert!(!policy.should_fallback(5, 100));
1010 }
1011
1012 #[test]
1013 fn fallback_above_threshold() {
1014 let policy = FallbackPolicy {
1015 ratio_threshold: 0.5,
1016 min_delta_for_fallback: 10,
1017 };
1018 assert!(policy.should_fallback(60, 100));
1020 }
1021
1022 #[test]
1023 fn fallback_below_threshold() {
1024 let policy = FallbackPolicy {
1025 ratio_threshold: 0.5,
1026 min_delta_for_fallback: 10,
1027 };
1028 assert!(!policy.should_fallback(20, 100));
1030 }
1031
1032 #[test]
1033 fn fallback_empty_view() {
1034 let policy = FallbackPolicy::default();
1035 assert!(policy.should_fallback(10, 0));
1037 }
1038
1039 #[test]
1042 fn empty_dag() {
1043 let dag = DagTopology::new();
1044 assert_eq!(dag.view_count(), 0);
1045 assert_eq!(dag.edge_count(), 0);
1046 }
1047
1048 #[test]
1049 fn add_views_and_edges() {
1050 let mut dag = DagTopology::new();
1051 let style = dag.add_view("style", ViewDomain::Style);
1052 let layout = dag.add_view("layout", ViewDomain::Layout);
1053 let render = dag.add_view("render", ViewDomain::Render);
1054
1055 dag.add_edge(style, layout);
1056 dag.add_edge(layout, render);
1057
1058 assert_eq!(dag.view_count(), 3);
1059 assert_eq!(dag.edge_count(), 2);
1060 }
1061
1062 #[test]
1063 fn topological_order_linear() {
1064 let mut dag = DagTopology::new();
1065 let a = dag.add_view("style", ViewDomain::Style);
1066 let b = dag.add_view("layout", ViewDomain::Layout);
1067 let c = dag.add_view("render", ViewDomain::Render);
1068
1069 dag.add_edge(a, b);
1070 dag.add_edge(b, c);
1071 dag.compute_topo_order();
1072
1073 assert_eq!(dag.topo_order, vec![a, b, c]);
1074 }
1075
1076 #[test]
1077 fn topological_order_diamond() {
1078 let mut dag = DagTopology::new();
1080 let a = dag.add_view("source", ViewDomain::Style);
1081 let b = dag.add_view("branch_b", ViewDomain::Layout);
1082 let c = dag.add_view("branch_c", ViewDomain::Layout);
1083 let d = dag.add_view("sink", ViewDomain::Render);
1084
1085 dag.add_edge(a, b);
1086 dag.add_edge(a, c);
1087 dag.add_edge(b, d);
1088 dag.add_edge(c, d);
1089 dag.compute_topo_order();
1090
1091 assert_eq!(dag.topo_order[0], a);
1093 assert_eq!(dag.topo_order[3], d);
1094 let middle: Vec<ViewId> = dag.topo_order[1..3].to_vec();
1096 assert!(middle.contains(&b));
1097 assert!(middle.contains(&c));
1098 }
1099
1100 #[test]
1101 #[should_panic(expected = "cycle")]
1102 fn cycle_detection() {
1103 let mut dag = DagTopology::new();
1104 let a = dag.add_view("a", ViewDomain::Style);
1105 let b = dag.add_view("b", ViewDomain::Layout);
1106 dag.add_edge(a, b);
1107 dag.add_edge(b, a); }
1109
1110 #[test]
1111 fn downstream_upstream() {
1112 let mut dag = DagTopology::new();
1113 let a = dag.add_view("a", ViewDomain::Style);
1114 let b = dag.add_view("b", ViewDomain::Layout);
1115 let c = dag.add_view("c", ViewDomain::Render);
1116 dag.add_edge(a, b);
1117 dag.add_edge(a, c);
1118
1119 let down = dag.downstream(a);
1120 assert_eq!(down.len(), 2);
1121 assert!(down.contains(&b));
1122 assert!(down.contains(&c));
1123
1124 let up = dag.upstream(b);
1125 assert_eq!(up, vec![a]);
1126 }
1127
1128 #[test]
1131 fn epoch_evidence_delta_ratio() {
1132 let ev = EpochEvidence {
1133 epoch: 1,
1134 views_processed: 3,
1135 views_recomputed: 0,
1136 total_delta_size: 10,
1137 total_materialized_size: 200,
1138 duration_us: 42,
1139 per_view: vec![],
1140 };
1141 assert!((ev.delta_ratio() - 0.05).abs() < 0.001);
1142 }
1143
1144 #[test]
1145 fn epoch_evidence_jsonl() {
1146 let ev = EpochEvidence {
1147 epoch: 5,
1148 views_processed: 3,
1149 views_recomputed: 1,
1150 total_delta_size: 25,
1151 total_materialized_size: 500,
1152 duration_us: 100,
1153 per_view: vec![],
1154 };
1155 let jsonl = ev.to_jsonl();
1156 assert!(jsonl.contains("\"type\":\"ivm_epoch\""));
1157 assert!(jsonl.contains("\"epoch\":5"));
1158 assert!(jsonl.contains("\"views_recomputed\":1"));
1159 assert!(jsonl.contains("\"delta_ratio\":0.0500"));
1160 }
1161
1162 #[test]
1163 fn epoch_evidence_empty_materialized() {
1164 let ev = EpochEvidence {
1165 epoch: 1,
1166 views_processed: 0,
1167 views_recomputed: 0,
1168 total_delta_size: 0,
1169 total_materialized_size: 0,
1170 duration_us: 0,
1171 per_view: vec![],
1172 };
1173 assert!((ev.delta_ratio() - 0.0).abs() < f64::EPSILON);
1174 }
1175
1176 #[test]
1179 fn cell_key_ordering() {
1180 let a = CellKey { row: 0, col: 5 };
1181 let b = CellKey { row: 0, col: 10 };
1182 let c = CellKey { row: 1, col: 0 };
1183 assert!(a < b);
1184 assert!(b < c);
1185 }
1186
1187 #[test]
1188 fn style_key_hash() {
1189 let a = StyleKey(42);
1190 let b = StyleKey(42);
1191 assert_eq!(fx_hash(&a), fx_hash(&b));
1192 }
1193
1194 #[test]
1197 fn config_defaults() {
1198 let config = IvmConfig::default();
1199 assert!(!config.force_full);
1200 assert!(config.emit_evidence);
1201 assert!((config.default_fallback.ratio_threshold - 0.5).abs() < f64::EPSILON);
1202 }
1203
1204 #[test]
1205 fn config_from_env_default() {
1206 let config = IvmConfig::from_env();
1207 assert_eq!(config.default_fallback.min_delta_for_fallback, 10);
1208 }
1209
1210 #[test]
1213 fn view_domain_labels() {
1214 assert_eq!(ViewDomain::Style.as_str(), "style");
1215 assert_eq!(ViewDomain::Layout.as_str(), "layout");
1216 assert_eq!(ViewDomain::Render.as_str(), "render");
1217 assert_eq!(ViewDomain::FilteredList.as_str(), "filtered_list");
1218 assert_eq!(ViewDomain::Custom.as_str(), "custom");
1219 }
1220
1221 #[test]
1224 fn three_stage_pipeline_topology() {
1225 let mut dag = DagTopology::new();
1226
1227 let style = dag.add_view("StyleView", ViewDomain::Style);
1229 let layout = dag.add_view("LayoutView", ViewDomain::Layout);
1230 let render = dag.add_view("RenderView", ViewDomain::Render);
1231
1232 dag.add_edge(style, layout);
1233 dag.add_edge(layout, render);
1234 dag.compute_topo_order();
1235
1236 assert_eq!(dag.topo_order, vec![style, layout, render]);
1237 assert_eq!(dag.downstream(style), vec![layout]);
1238 assert_eq!(dag.downstream(layout), vec![render]);
1239 assert!(dag.downstream(render).is_empty());
1240 assert!(dag.upstream(style).is_empty());
1241 assert_eq!(dag.upstream(layout), vec![style]);
1242 assert_eq!(dag.upstream(render), vec![layout]);
1243 }
1244
1245 #[test]
1246 fn multi_source_pipeline() {
1247 let mut dag = DagTopology::new();
1249 let theme_style = dag.add_view("ThemeStyle", ViewDomain::Style);
1250 let content = dag.add_view("Content", ViewDomain::Custom);
1251 let layout = dag.add_view("Layout", ViewDomain::Layout);
1252 let render = dag.add_view("Render", ViewDomain::Render);
1253
1254 dag.add_edge(theme_style, layout);
1255 dag.add_edge(content, layout);
1256 dag.add_edge(layout, render);
1257 dag.compute_topo_order();
1258
1259 let layout_pos = dag.topo_order.iter().position(|&v| v == layout).unwrap();
1261 let render_pos = dag.topo_order.iter().position(|&v| v == render).unwrap();
1262 let theme_pos = dag
1263 .topo_order
1264 .iter()
1265 .position(|&v| v == theme_style)
1266 .unwrap();
1267 let content_pos = dag.topo_order.iter().position(|&v| v == content).unwrap();
1268
1269 assert!(theme_pos < layout_pos);
1270 assert!(content_pos < layout_pos);
1271 assert!(layout_pos < render_pos);
1272 }
1273
1274 #[test]
1275 fn filtered_list_side_branch() {
1276 let mut dag = DagTopology::new();
1279 let style = dag.add_view("Style", ViewDomain::Style);
1280 let content = dag.add_view("Content", ViewDomain::Custom);
1281 let filtered = dag.add_view("FilteredList", ViewDomain::FilteredList);
1282 let layout = dag.add_view("Layout", ViewDomain::Layout);
1283 let render = dag.add_view("Render", ViewDomain::Render);
1284
1285 dag.add_edge(style, layout);
1286 dag.add_edge(content, filtered);
1287 dag.add_edge(filtered, layout);
1288 dag.add_edge(layout, render);
1289 dag.compute_topo_order();
1290
1291 let filtered_pos = dag.topo_order.iter().position(|&v| v == filtered).unwrap();
1292 let layout_pos = dag.topo_order.iter().position(|&v| v == layout).unwrap();
1293 assert!(filtered_pos < layout_pos);
1294 }
1295
1296 #[test]
1299 fn propagation_result_construction() {
1300 let result = PropagationResult {
1301 view_id: ViewId(0),
1302 domain: ViewDomain::Style,
1303 input_delta_size: 5,
1304 output_delta_size: 3,
1305 fell_back_to_full: false,
1306 materialized_size: 100,
1307 duration_us: 15,
1308 };
1309 assert!(!result.fell_back_to_full);
1310 assert_eq!(result.output_delta_size, 3);
1311 }
1312
1313 #[test]
1314 fn propagation_result_fallback() {
1315 let result = PropagationResult {
1316 view_id: ViewId(1),
1317 domain: ViewDomain::Layout,
1318 input_delta_size: 80,
1319 output_delta_size: 100,
1320 fell_back_to_full: true,
1321 materialized_size: 100,
1322 duration_us: 200,
1323 };
1324 assert!(result.fell_back_to_full);
1325 }
1326
1327 #[test]
1330 fn style_view_apply_insert() {
1331 let mut view = StyleResolutionView::new("test_style", 0xABCD);
1332 let mut batch = DeltaBatch::new(1);
1333 batch.insert(StyleKey(0), ResolvedStyleValue { style_hash: 0x1234 }, 0);
1334
1335 let output = view.apply_delta(&batch);
1336 assert_eq!(output.len(), 1);
1337 assert!(output.entries[0].is_insert());
1338 assert_eq!(view.materialized_size(), 1);
1339 }
1340
1341 #[test]
1342 fn style_view_deduplicates_unchanged() {
1343 let mut view = StyleResolutionView::new("test", 0x0);
1344 let mut batch1 = DeltaBatch::new(1);
1345 batch1.insert(StyleKey(0), ResolvedStyleValue { style_hash: 0x42 }, 0);
1346 view.apply_delta(&batch1);
1347
1348 let mut batch2 = DeltaBatch::new(2);
1350 batch2.insert(StyleKey(0), ResolvedStyleValue { style_hash: 0x42 }, 0);
1351 let output = view.apply_delta(&batch2);
1352 assert!(output.is_empty(), "same style should produce no delta");
1353 }
1354
1355 #[test]
1356 fn style_view_delete() {
1357 let mut view = StyleResolutionView::new("test", 0x0);
1358 let mut batch1 = DeltaBatch::new(1);
1359 batch1.insert(StyleKey(0), ResolvedStyleValue { style_hash: 0x42 }, 0);
1360 view.apply_delta(&batch1);
1361 assert_eq!(view.materialized_size(), 1);
1362
1363 let mut batch2 = DeltaBatch::new(2);
1364 batch2.delete(StyleKey(0), 0);
1365 let output = view.apply_delta(&batch2);
1366 assert_eq!(output.len(), 1);
1367 assert!(!output.entries[0].is_insert()); assert_eq!(view.materialized_size(), 0);
1369 }
1370
1371 #[test]
1372 fn style_view_full_recompute_matches_incremental() {
1373 let mut view = StyleResolutionView::new("test", 0xFF00);
1374 let mut batch = DeltaBatch::new(1);
1375 batch.insert(StyleKey(0), ResolvedStyleValue { style_hash: 0x00FF }, 0);
1376 batch.insert(StyleKey(1), ResolvedStyleValue { style_hash: 0x0F0F }, 1);
1377 view.apply_delta(&batch);
1378
1379 let full = view.full_recompute();
1380 assert_eq!(full.len(), 2);
1381
1382 for (key, value) in &full {
1384 assert_eq!(value, view.resolved.get(key).unwrap());
1385 }
1386 }
1387
1388 #[test]
1389 fn style_view_base_change_affects_all() {
1390 let mut view = StyleResolutionView::new("test", 0x0);
1391 let mut batch = DeltaBatch::new(1);
1392 batch.insert(StyleKey(0), ResolvedStyleValue { style_hash: 0x42 }, 0);
1393 batch.insert(StyleKey(1), ResolvedStyleValue { style_hash: 0x99 }, 1);
1394 view.apply_delta(&batch);
1395
1396 view.set_base(0xFFFF);
1398 let full = view.full_recompute();
1399 for (_key, value) in &full {
1400 assert_ne!(value.style_hash, 0);
1402 }
1403 }
1404
1405 #[test]
1406 fn style_view_domain_and_label() {
1407 let view = StyleResolutionView::new("ThemeResolver", 0);
1408 assert_eq!(view.domain(), ViewDomain::Style);
1409 assert_eq!(view.label(), "ThemeResolver");
1410 }
1411
1412 #[test]
1415 fn filtered_view_insert_visible() {
1416 let mut view = FilteredListView::new("evens", |_k: &u32, v: &i32| *v % 2 == 0);
1418 let mut batch = DeltaBatch::new(1);
1419 batch.insert(1u32, 4i32, 0); batch.insert(2u32, 3i32, 1); let output = view.apply_delta(&batch);
1423 assert_eq!(output.len(), 1); assert_eq!(view.visible_count(), 1);
1425 assert_eq!(view.total_count(), 2);
1426 }
1427
1428 #[test]
1429 fn filtered_view_insert_then_filter_out() {
1430 let mut view = FilteredListView::new("test", |_k: &u32, v: &i32| *v > 10);
1431 let mut batch1 = DeltaBatch::new(1);
1432 batch1.insert(1u32, 20i32, 0); view.apply_delta(&batch1);
1434 assert_eq!(view.visible_count(), 1);
1435
1436 let mut batch2 = DeltaBatch::new(2);
1438 batch2.insert(1u32, 5i32, 0); let output = view.apply_delta(&batch2);
1440 assert_eq!(output.len(), 1);
1441 assert!(!output.entries[0].is_insert()); assert_eq!(view.visible_count(), 0);
1443 }
1444
1445 #[test]
1446 fn filtered_view_delete() {
1447 let mut view = FilteredListView::new("test", |_k: &u32, v: &i32| *v > 0);
1448 let mut batch1 = DeltaBatch::new(1);
1449 batch1.insert(1u32, 5i32, 0);
1450 view.apply_delta(&batch1);
1451 assert_eq!(view.visible_count(), 1);
1452
1453 let mut batch2 = DeltaBatch::new(2);
1454 batch2.delete(1u32, 0);
1455 let output = view.apply_delta(&batch2);
1456 assert_eq!(output.len(), 1);
1457 assert!(!output.entries[0].is_insert()); assert_eq!(view.visible_count(), 0);
1459 assert_eq!(view.total_count(), 0);
1460 }
1461
1462 #[test]
1463 fn filtered_view_full_recompute() {
1464 let mut view = FilteredListView::new("test", |_k: &u32, v: &i32| *v % 2 == 0);
1465 let mut batch = DeltaBatch::new(1);
1466 batch.insert(1u32, 2i32, 0);
1467 batch.insert(2u32, 3i32, 1);
1468 batch.insert(3u32, 4i32, 2);
1469 batch.insert(4u32, 5i32, 3);
1470 view.apply_delta(&batch);
1471
1472 let full = view.full_recompute();
1473 assert_eq!(full.len(), 2); let keys: Vec<u32> = full.iter().map(|(k, _)| *k).collect();
1475 assert!(keys.contains(&1));
1476 assert!(keys.contains(&3));
1477 }
1478
1479 #[test]
1480 fn filtered_view_deduplicates_unchanged() {
1481 let mut view = FilteredListView::new("test", |_k: &u32, v: &i32| *v > 0);
1482 let mut batch1 = DeltaBatch::new(1);
1483 batch1.insert(1u32, 5i32, 0);
1484 view.apply_delta(&batch1);
1485
1486 let mut batch2 = DeltaBatch::new(2);
1488 batch2.insert(1u32, 5i32, 0);
1489 let output = view.apply_delta(&batch2);
1490 assert!(output.is_empty(), "same value should produce no delta");
1491 }
1492
1493 #[test]
1494 fn filtered_view_domain_and_label() {
1495 let view: FilteredListView<u32, i32> =
1496 FilteredListView::new("EvenFilter", |_k: &u32, v: &i32| *v % 2 == 0);
1497 assert_eq!(view.domain(), ViewDomain::FilteredList);
1498 assert_eq!(view.label(), "EvenFilter");
1499 }
1500
1501 #[test]
1502 fn filtered_view_correctness_invariant() {
1503 let mut view = FilteredListView::new("test", |_k: &u32, v: &i32| *v > 0);
1505
1506 let mut batch1 = DeltaBatch::new(1);
1507 batch1.insert(1u32, 5i32, 0);
1508 batch1.insert(2u32, -3i32, 1);
1509 batch1.insert(3u32, 10i32, 2);
1510 view.apply_delta(&batch1);
1511
1512 let mut batch2 = DeltaBatch::new(2);
1513 batch2.delete(1u32, 0);
1514 batch2.insert(4u32, 7i32, 1);
1515 batch2.insert(2u32, 8i32, 2); view.apply_delta(&batch2);
1517
1518 let full = view.full_recompute();
1520 let mut full_map: std::collections::HashMap<u32, i32> = full.into_iter().collect();
1521 assert_eq!(full_map.len(), view.visible_count());
1522 for (k, v) in &view.visible {
1523 assert_eq!(full_map.remove(k).as_ref(), Some(v));
1524 }
1525 assert!(full_map.is_empty());
1526 }
1527
1528 #[test]
1529 fn filtered_view_delete_nonexistent_is_noop() {
1530 let mut view: FilteredListView<u32, i32> =
1531 FilteredListView::new("test", |_k: &u32, _v: &i32| true);
1532 let mut batch = DeltaBatch::new(1);
1533 batch.delete(999u32, 0);
1534 let output = view.apply_delta(&batch);
1535 assert!(output.is_empty());
1536 }
1537
1538 #[test]
1541 fn trait_object_dispatch() {
1542 let view: Box<dyn IncrementalView<StyleKey, ResolvedStyleValue>> =
1544 Box::new(StyleResolutionView::new("dynamic", 0x42));
1545 assert_eq!(view.materialized_size(), 0);
1546 assert_eq!(view.domain(), ViewDomain::Style);
1547 assert_eq!(view.label(), "dynamic");
1548 }
1549}