tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
1//! stress_grid — Bloque 21 (PLAN-NUCLEO-RETENIDO.md): app sintética de N nodos
2//! para medir el camino completo de redraw con el perfilador `LLIMPHI_PERF`.
3//!
4//! Cada tick (~16 ms) muta el modelo → invalida `last_render` → fuerza un frame
5//! de camino COMPLETO (mount + layout + paint), que es justo lo que el
6//! perfilador muestrea. El color de cada celda depende del frame, así el árbol
7//! cambia de verdad cada cuadro (nada de cache-hit).
8//!
9//! Uso (baseline del Bloque 21):
10//! ```bash
11//! LLIMPHI_PERF=1 cargo run -p llimphi-ui --example stress_grid --release -- --nodes 5000
12//! LLIMPHI_PERF=1 cargo run -p llimphi-ui --example stress_grid --release -- --nodes 50000
13//! LLIMPHI_PERF=1 cargo run -p llimphi-ui --example stress_grid --release -- --nodes 200000
14//! ```
15
16use llimphi_ui::llimphi_layout::taffy::prelude::*;
17use llimphi_ui::llimphi_raster::peniko::Color;
18use llimphi_ui::{App, Handle, View};
19use std::time::Duration;
20
21#[derive(Clone)]
22enum Msg {
23 Tick,
24}
25
26struct Stress;
27
28struct Model {
29 frame: u64,
30 nodes: usize,
31 cols: usize,
32 /// Se cierra solo tras estos frames RENDERIZADOS (para no imprimir
33 /// infinito). `0` = nunca (corre hasta cerrar la ventana). Ver `--frames`.
34 quit_after: u64,
35 /// Frames realmente renderizados (incrementado en `view`). El auto-quit se
36 /// basa en esto, no en los Ticks: a muchos nodos el render es más lento que
37 /// el tick de 16 ms y contar Ticks cerraría antes de que el perfilador
38 /// (reporta cada N renders) imprima una línea.
39 renders: std::cell::Cell<u64>,
40}
41
42/// Lee un flag `--<name> <valor>` de la línea de comando.
43fn arg_u64(name: &str, default: u64) -> u64 {
44 let mut args = std::env::args();
45 while let Some(a) = args.next() {
46 if a == name {
47 if let Some(v) = args.next() {
48 if let Ok(n) = v.parse::<u64>() {
49 return n;
50 }
51 }
52 }
53 }
54 default
55}
56
57impl App for Stress {
58 type Model = Model;
59 type Msg = Msg;
60
61 fn title() -> &'static str {
62 "llimphi · stress_grid"
63 }
64
65 fn init(handle: &Handle<Self::Msg>) -> Self::Model {
66 let nodes = arg_u64("--nodes", 5_000).max(1) as usize;
67 let cols = (nodes as f64).sqrt().ceil() as usize;
68 // Default: se cierra solo tras 240 frames (~4 reportes de 60). `--frames 0`
69 // corre indefinido (cierras la ventana a mano).
70 let quit_after = arg_u64("--frames", 240);
71 // Feed continuo: garantiza frames de camino completo para muestrear.
72 handle.spawn_periodic(Duration::from_millis(16), || Msg::Tick);
73 eprintln!(
74 "[stress_grid] nodes={nodes} cols={cols} — LLIMPHI_PERF=1; \
75 sale solo a los {quit_after} frames (--frames 0 = infinito)"
76 );
77 Model {
78 frame: 0,
79 nodes,
80 cols,
81 quit_after,
82 renders: std::cell::Cell::new(0),
83 }
84 }
85
86 fn update(mut model: Self::Model, msg: Self::Msg, handle: &Handle<Self::Msg>) -> Self::Model {
87 match msg {
88 Msg::Tick => {
89 model.frame = model.frame.wrapping_add(1);
90 if model.quit_after != 0 && model.renders.get() >= model.quit_after {
91 handle.quit();
92 }
93 }
94 }
95 model
96 }
97
98 fn view(model: &Self::Model) -> View<Self::Msg> {
99 model.renders.set(model.renders.get() + 1);
100 let cols = model.cols.max(1);
101 let rows = model.nodes.div_ceil(cols);
102 let f = model.frame;
103 let mut row_views = Vec::with_capacity(rows);
104 let mut made = 0usize;
105 for _r in 0..rows {
106 let mut cells = Vec::with_capacity(cols);
107 for _c in 0..cols {
108 if made >= model.nodes {
109 break;
110 }
111 let i = made as u64;
112 // Color animado por frame → el árbol cambia de verdad cada cuadro.
113 let hue = ((i.wrapping_add(f)) % 255) as u8;
114 cells.push(
115 View::new(Style {
116 size: Size {
117 width: length(6.0_f32),
118 height: length(6.0_f32),
119 },
120 ..Default::default()
121 })
122 .fill(Color::from_rgba8(hue, 90, 200u8.wrapping_sub(hue), 255))
123 .radius(1.0),
124 );
125 made += 1;
126 }
127 row_views.push(
128 View::new(Style {
129 flex_direction: FlexDirection::Row,
130 gap: Size {
131 width: length(1.0_f32),
132 height: length(0.0_f32),
133 },
134 ..Default::default()
135 })
136 .children(cells),
137 );
138 }
139 View::new(Style {
140 flex_direction: FlexDirection::Column,
141 size: Size {
142 width: percent(1.0_f32),
143 height: percent(1.0_f32),
144 },
145 gap: Size {
146 width: length(0.0_f32),
147 height: length(1.0_f32),
148 },
149 ..Default::default()
150 })
151 .fill(Color::from_rgba8(16, 18, 24, 255))
152 .children(row_views)
153 }
154}
155
156fn main() {
157 llimphi_ui::run::<Stress>();
158}