tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
⎇ 168cd187519c0ad575d3ce63baee2f164c38d9d1
llimphi-ui/src/censo.rs
Ñawpa willay
Pi imata llamkarqan
ch'uya
chore: refresco desde el monorepo — vello 0.9 / wgpu 29, 120 miembros
1//! Censo del reparto `View`/`ViewRare` contra el árbol de una app **real**,
2//! headless.
3//!
4//! Bloque 21, palanca 2b de `PLAN-NUCLEO-RETENIDO.md`. El ahorro del reparto
5//! depende de una sola cifra —la fracción de nodos que lleva alguna prop rara y
6//! paga la caja— y esa cifra la da el árbol de la app, no un benchmark: contra
7//! `stress_grid` daba −74 % y contra la galería real, −25,9 %. Toda la
8//! corrección del reparto salió de medir esto.
9//!
10//! No abre ventana ni toca la GPU: arma el Model con un `Handle` de tests, llama
11//! `App::view` una vez y recorre el árbol. Sale por stdout, es determinista, y
12//! cuesta lo que cuesta un `view()` — el modo barato de evidencia que pide la
13//! Regla 8 del repo.
14//!
15//! Enchufarlo en una app es una línea en su `main`:
16//!
17//! ```ignore
18//! if std::env::args().any(|a| a == "--censo") {
19//! llimphi_ui::censo_app::<MiApp>();
20//! return;
21//! }
22//! ```
23
24use crate::{censo_rare, mascaras_rare, tam_rare, App, CensoRare, Handle, View, ViewRare};
25
26/// Corre el censo de `A` e imprime el informe. Ver el módulo.
27pub fn censo_app<A: App>() {
28 // `init` sólo recibe el handle para despachar; acá los Msg se tiran porque
29 // no hay bucle que los aplique.
30 let modelo = A::init(&Handle::<A::Msg>::for_test_with(|_| {}));
31 let vista: View<A::Msg> = A::view(&modelo);
32 informe::<A::Msg>(&vista, A::title());
33}
34
35/// El informe sobre un árbol ya construido — separado de [`censo_app`] para que
36/// una app con un Model caro de armar pueda pasar el suyo.
37pub fn informe<Msg>(vista: &View<Msg>, nombre: &str) {
38 let c = censo_rare(vista);
39 let (antes, ahora) = (c.bytes_antes(), c.bytes_ahora::<Msg>());
40 let base_view = core::mem::size_of::<View<Msg>>();
41 let base_rare = core::mem::size_of::<ViewRare<Msg>>();
42
43 println!("{nombre} — censo de props raras (árbol real, headless)");
44 println!(" nodos {}", c.nodos);
45 println!(
46 " con caja ViewRare {} ({:.1} %)",
47 c.con_rare,
48 100.0 * c.fraccion()
49 );
50 println!(
51 " size_of View / Rare {base_view} B / {base_rare} B (antes del reparto: {} B por nodo)",
52 CensoRare::VIEW_ANTES,
53 );
54 println!(
55 " bytes del árbol {antes} → {ahora} ({:+.1} %)",
56 100.0 * (ahora as f64 - antes as f64) / antes as f64
57 );
58 println!(
59 " asignaciones extra {} (una caja por nodo que usa alguna prop rara)",
60 c.con_rare
61 );
62 if c.con_rare > 0 {
63 println!(" qué prop hace rara a cada caja:");
64 for (campo, n) in crate::censo_campos(vista) {
65 println!(" {n:>5} {campo}");
66 }
67 }
68
69 // ¿Convendría devolver alguna prop a `View`? La marginal por campo NO
70 // alcanza: lo que importa es cuántos nodos quedarían SIN ninguna prop rara
71 // (ésos dejan de pagar la caja entera), y eso depende del conjunto por nodo.
72 // Con la máscara se evalúa exacto; se elige por búsqueda voraz.
73 let mascaras = mascaras_rare(vista);
74 let tam = tam_rare::<Msg>();
75 let bytes_con = |s: u64| -> usize {
76 let crecio: usize = (0..tam.len()).filter(|k| s & (1 << k) != 0).map(|k| tam[k]).sum();
77 let cajas = mascaras.iter().filter(|m| *m & !s != 0).count();
78 c.nodos * (base_view + crecio) + cajas * (base_rare - crecio)
79 };
80 let mut s = 0u64;
81 let mut mejor = bytes_con(0);
82 loop {
83 let cand = (0..tam.len())
84 .filter(|k| s & (1 << k) == 0)
85 .map(|k| (bytes_con(s | (1 << k)), k))
86 .min();
87 match cand {
88 Some((b, k)) if b < mejor => {
89 mejor = b;
90 s |= 1 << k;
91 }
92 _ => break,
93 }
94 }
95 if s == 0 {
96 println!(" reparto óptimo para este árbol: el vigente (no conviene mover ningún campo)");
97 return;
98 }
99 let elegidas: Vec<&str> = (0..tam.len())
100 .filter(|k| s & (1 << k) != 0)
101 .map(|k| crate::NOMBRES_RARE[k])
102 .collect();
103 let crecio: usize = (0..tam.len()).filter(|k| s & (1 << k) != 0).map(|k| tam[k]).sum();
104 let cajas = mascaras.iter().filter(|m| *m & !s != 0).count();
105 println!(" este árbol preferiría OTRO reparto (búsqueda voraz sobre la máscara):");
106 println!(" volver inline {elegidas:?}");
107 println!(" View {base_view} B → {} B", base_view + crecio);
108 println!(" cajas {} → {cajas}", c.con_rare);
109 println!(
110 " bytes del árbol {ahora} → {mejor} ({:+.1} % contra el View viejo)",
111 100.0 * (mejor as f64 - antes as f64) / antes as f64
112 );
113 println!(
114 " contracara: un árbol uniforme SIN props raras (tipo stress_grid)\n\
115 \x20 pasaría de {base_view} a {} B por nodo, {:+.1} %",
116 base_view + crecio,
117 100.0 * crecio as f64 / base_view as f64
118 );
119}