tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
1//! stress_memo — Bloque 25 (memo). Como `stress_boundary`, pero el grid estático
2//! va en un `.memo(key, deps, builder)` en vez de `.repaint_boundary()`. Con
3//! `deps` constante, tras el primer frame el builder NO se llama: el `View` del
4//! grid no se reconstruye, no se re-monta y no se re-layoutea (se reusa el
5//! `Mounted` cacheado). Así se desploman `view`, `mount` Y `layout` para el
6//! subárbol — no sólo `paint` (que era lo del Bloque 23).
7//!
8//! ```bash
9//! LLIMPHI_PERF=20 cargo run -p llimphi-ui --example stress_grid --release -- --nodes 50000 --frames 0
10//! LLIMPHI_PERF=20 cargo run -p llimphi-ui --example stress_memo --release -- --nodes 50000 --frames 0
11//! ```
12//! En stress_memo, `view`/`mount`/`layout` deberían ser mucho más bajos (el grid
13//! sólo se construye una vez).
14
15use llimphi_ui::llimphi_layout::taffy::prelude::*;
16use llimphi_ui::llimphi_raster::peniko::Color;
17use llimphi_ui::{App, Handle, View};
18use std::time::Duration;
19
20#[derive(Clone)]
21enum Msg {
22 Tick,
23}
24
25struct Stress;
26
27struct Model {
28 frame: u64,
29 nodes: usize,
30 cols: usize,
31 quit_after: u64,
32 renders: std::cell::Cell<u64>,
33}
34
35fn arg_u64(name: &str, default: u64) -> u64 {
36 let mut args = std::env::args();
37 while let Some(a) = args.next() {
38 if a == name {
39 if let Some(v) = args.next() {
40 if let Ok(n) = v.parse::<u64>() {
41 return n;
42 }
43 }
44 }
45 }
46 default
47}
48
49/// Grid estático (colores fijos por índice). Lo produce el builder del memo, que
50/// sólo se llama cuando `deps` cambia (aquí nunca) → se construye una sola vez.
51fn grid_estatico(nodes: usize, cols: usize) -> View<Msg> {
52 let rows = nodes.div_ceil(cols);
53 let mut row_views = Vec::with_capacity(rows);
54 let mut made = 0usize;
55 for _r in 0..rows {
56 let mut cells = Vec::with_capacity(cols);
57 for _c in 0..cols {
58 if made >= nodes {
59 break;
60 }
61 let hue = (made % 255) as u8;
62 cells.push(
63 View::new(Style {
64 size: Size {
65 width: length(6.0_f32),
66 height: length(6.0_f32),
67 },
68 ..Default::default()
69 })
70 .fill(Color::from_rgba8(hue, 90, 200u8.wrapping_sub(hue), 255))
71 .radius(1.0),
72 );
73 made += 1;
74 }
75 row_views.push(
76 View::new(Style {
77 flex_direction: FlexDirection::Row,
78 gap: Size {
79 width: length(1.0_f32),
80 height: length(0.0_f32),
81 },
82 ..Default::default()
83 })
84 .children(cells),
85 );
86 }
87 View::new(Style {
88 flex_direction: FlexDirection::Column,
89 gap: Size {
90 width: length(0.0_f32),
91 height: length(1.0_f32),
92 },
93 ..Default::default()
94 })
95 .children(row_views)
96}
97
98impl App for Stress {
99 type Model = Model;
100 type Msg = Msg;
101
102 fn title() -> &'static str {
103 "llimphi · stress_memo"
104 }
105
106 fn init(handle: &Handle<Self::Msg>) -> Self::Model {
107 let nodes = arg_u64("--nodes", 5_000).max(1) as usize;
108 let cols = (nodes as f64).sqrt().ceil() as usize;
109 let quit_after = arg_u64("--frames", 240);
110 handle.spawn_periodic(Duration::from_millis(16), || Msg::Tick);
111 eprintln!(
112 "[stress_memo] nodes={nodes} cols={cols} — LLIMPHI_PERF=1; \
113 el grid va en un .memo (se construye UNA vez)"
114 );
115 Model {
116 frame: 0,
117 nodes,
118 cols,
119 quit_after,
120 renders: std::cell::Cell::new(0),
121 }
122 }
123
124 fn update(mut model: Self::Model, msg: Self::Msg, handle: &Handle<Self::Msg>) -> Self::Model {
125 match msg {
126 Msg::Tick => {
127 model.frame = model.frame.wrapping_add(1);
128 if model.quit_after != 0 && model.renders.get() >= model.quit_after {
129 handle.quit();
130 }
131 }
132 }
133 model
134 }
135
136 fn view(model: &Self::Model) -> View<Self::Msg> {
137 model.renders.set(model.renders.get() + 1);
138 // Header que cambia cada frame (fuerza el repintado). Fuera del memo.
139 let header = View::new(Style {
140 size: Size {
141 width: percent(1.0_f32),
142 height: length(28.0_f32),
143 },
144 ..Default::default()
145 })
146 .fill(Color::from_rgba8(30, 34, 44, 255))
147 .text(
148 format!("frame {}", model.frame),
149 16.0,
150 Color::from_rgba8(210, 214, 222, 255),
151 );
152
153 let nodes = model.nodes;
154 let cols = model.cols;
155 // Contenedor del grid con tamaño DEFINIDO (contrato del memo) + `.memo`
156 // con deps constante (0) → el grid se construye una sola vez.
157 let grid = View::new(Style {
158 size: Size {
159 width: percent(1.0_f32),
160 height: length(520.0_f32),
161 },
162 flex_shrink: 0.0,
163 ..Default::default()
164 })
165 .memo(1, 0, move || grid_estatico(nodes, cols));
166
167 View::new(Style {
168 flex_direction: FlexDirection::Column,
169 size: Size {
170 width: percent(1.0_f32),
171 height: percent(1.0_f32),
172 },
173 gap: Size {
174 width: length(0.0_f32),
175 height: length(4.0_f32),
176 },
177 ..Default::default()
178 })
179 .fill(Color::from_rgba8(16, 18, 24, 255))
180 .children(vec![header, grid])
181 }
182}
183
184fn main() {
185 llimphi_ui::run::<Stress>();
186}