//! stress_grid — Bloque 21 (PLAN-NUCLEO-RETENIDO.md): app sintética de N nodos //! para medir el camino completo de redraw con el perfilador `LLIMPHI_PERF`. //! //! Cada tick (~16 ms) muta el modelo → invalida `last_render` → fuerza un frame //! de camino COMPLETO (mount + layout + paint), que es justo lo que el //! perfilador muestrea. El color de cada celda depende del frame, así el árbol //! cambia de verdad cada cuadro (nada de cache-hit). //! //! Uso (baseline del Bloque 21): //! ```bash //! LLIMPHI_PERF=1 cargo run -p llimphi-ui --example stress_grid --release -- --nodes 5000 //! LLIMPHI_PERF=1 cargo run -p llimphi-ui --example stress_grid --release -- --nodes 50000 //! LLIMPHI_PERF=1 cargo run -p llimphi-ui --example stress_grid --release -- --nodes 200000 //! ``` use llimphi_ui::llimphi_layout::taffy::prelude::*; use llimphi_ui::llimphi_raster::peniko::Color; use llimphi_ui::{App, Handle, View}; use std::time::Duration; #[derive(Clone)] enum Msg { Tick, } struct Stress; struct Model { frame: u64, nodes: usize, cols: usize, /// Se cierra solo tras estos frames RENDERIZADOS (para no imprimir /// infinito). `0` = nunca (corre hasta cerrar la ventana). Ver `--frames`. quit_after: u64, /// Frames realmente renderizados (incrementado en `view`). El auto-quit se /// basa en esto, no en los Ticks: a muchos nodos el render es más lento que /// el tick de 16 ms y contar Ticks cerraría antes de que el perfilador /// (reporta cada N renders) imprima una línea. renders: std::cell::Cell, } /// Lee un flag `-- ` de la línea de comando. fn arg_u64(name: &str, default: u64) -> u64 { let mut args = std::env::args(); while let Some(a) = args.next() { if a == name { if let Some(v) = args.next() { if let Ok(n) = v.parse::() { return n; } } } } default } impl App for Stress { type Model = Model; type Msg = Msg; fn title() -> &'static str { "llimphi · stress_grid" } fn init(handle: &Handle) -> Self::Model { let nodes = arg_u64("--nodes", 5_000).max(1) as usize; let cols = (nodes as f64).sqrt().ceil() as usize; // Default: se cierra solo tras 240 frames (~4 reportes de 60). `--frames 0` // corre indefinido (cierras la ventana a mano). let quit_after = arg_u64("--frames", 240); // Feed continuo: garantiza frames de camino completo para muestrear. handle.spawn_periodic(Duration::from_millis(16), || Msg::Tick); eprintln!( "[stress_grid] nodes={nodes} cols={cols} — LLIMPHI_PERF=1; \ sale solo a los {quit_after} frames (--frames 0 = infinito)" ); Model { frame: 0, nodes, cols, quit_after, renders: std::cell::Cell::new(0), } } fn update(mut model: Self::Model, msg: Self::Msg, handle: &Handle) -> Self::Model { match msg { Msg::Tick => { model.frame = model.frame.wrapping_add(1); if model.quit_after != 0 && model.renders.get() >= model.quit_after { handle.quit(); } } } model } fn view(model: &Self::Model) -> View { model.renders.set(model.renders.get() + 1); let cols = model.cols.max(1); let rows = model.nodes.div_ceil(cols); let f = model.frame; let mut row_views = Vec::with_capacity(rows); let mut made = 0usize; for _r in 0..rows { let mut cells = Vec::with_capacity(cols); for _c in 0..cols { if made >= model.nodes { break; } let i = made as u64; // Color animado por frame → el árbol cambia de verdad cada cuadro. let hue = ((i.wrapping_add(f)) % 255) as u8; cells.push( View::new(Style { size: Size { width: length(6.0_f32), height: length(6.0_f32), }, ..Default::default() }) .fill(Color::from_rgba8(hue, 90, 200u8.wrapping_sub(hue), 255)) .radius(1.0), ); made += 1; } row_views.push( View::new(Style { flex_direction: FlexDirection::Row, gap: Size { width: length(1.0_f32), height: length(0.0_f32), }, ..Default::default() }) .children(cells), ); } View::new(Style { flex_direction: FlexDirection::Column, size: Size { width: percent(1.0_f32), height: percent(1.0_f32), }, gap: Size { width: length(0.0_f32), height: length(1.0_f32), }, ..Default::default() }) .fill(Color::from_rgba8(16, 18, 24, 255)) .children(row_views) } } fn main() { llimphi_ui::run::(); }