tawasuyu

tawasuyu/llimphi

Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.

Rust 229TOML 101Markdown 82Shell 24052 Definiciones

⎇ c5a232cf24788ab75a850ffe81eca5a7af50e2f8 COMPUTO-FUERA-DEL-HILO-UI.md Markdown · 122 líneas · 5.8 KB Historia crudo

feat: llimphi standalone — framework UI soberano extraído del monorepo e65e9cc62 · Sergio ·

1# Cómputo pesado fuera del hilo de UI — regla dura de Llimphi
2
3> **PRIORIDAD URGENTE.** Patrón a aplicar a **todas** las apps Llimphi.
4> Origen: el "Not Responding" de cosmos (2026-05-31). Implementación de
5> referencia: `01_yachay/cosmos/cosmos-app-llimphi` (commits `added8b3`,
6> `9f221983`).
7
8## La regla
9
10Ningún `App::update`, `App::init` ni handler (`on_key`/`on_wheel`/…) debe
11ejecutar trabajo pesado **síncrono**. Bloquea el hilo de UI → la ventana no
12repinta, no responde, no cierra → "Not Responding". Es el antipatrón win32 de
13trabajo pesado en el message loop.
14
15Crítico: en winit, **`App::init()` corre dentro de `resumed`, DESPUÉS de crear
16la ventana**. Un cómputo pesado en init congela la ventana ya visible.
17
18Se nota brutal en **debug** (sin optimizar, 10–50× más lento; además debug
19*panica* en overflow donde release *wrappea*). Pero la mala arquitectura está
20igual en release: una carta pesada, una máquina lenta o un dataset grande la
21exponen.
22
23"Pesado" = efemérides/simulación, layout de árboles grandes, IO de disco/red,
24parse, embeddings, compresión… cualquier cosa que pueda pasar de ~unos ms.
25
26## El patrón (mover a un worker)
27
28```rust
29// 1) Mensaje de resultado: u64 = generación; Arc<T> porque Msg: Clone.
30enum Msg { /* … */ XComputed(u64, std::sync::Arc<Resultado>) }
31
32// 2) En el Model: el resultado es Option (None = "calculando…"),
33//    más un flag dirty y un contador de generación.
34struct Model { x: Option<Resultado>, x_dirty: bool, x_gen: u64, /* … */ }
35
36// 3) recompute_x sólo marca dirty (los helpers no tienen el Handle).
37fn recompute_x(m: &mut Model) { m.x_dirty = true; }
38
39// 4) Al FINAL de update() (que SÍ tiene el Handle): si está sucio, bumpear
40//    generación, clonar los inputs y despachar a un worker.
41if m.x_dirty {
42    m.x_dirty = false;
43    m.x_gen = m.x_gen.wrapping_add(1);
44    let gen = m.x_gen;
45    let input = m.input.clone();              // sólo lo que el worker necesita
46    handle.spawn(move || Msg::XComputed(gen, std::sync::Arc::new(compute(&input))));
47}
48
49// 5) Arm del resultado: aplicar SÓLO si la generación sigue vigente
50//    (un recálculo posterior ya dejó viejo a este). try_unwrap evita copiar
51//    (el Arc llega con refcount 1 porque el Msg no se clona en el camino).
52Msg::XComputed(gen, x) => {
53    if gen == m.x_gen {
54        m.x = Some(std::sync::Arc::try_unwrap(x).unwrap_or_else(|a| (*a).clone()));
55    }
56}
57
58// 6) En init: arrancar con None y despachar el primer cómputo a un worker
59//    (init tiene el Handle). La vista pinta "calculando…" mientras tanto.
60
61// 7) En la vista: match &model.x { Some(v) => panel(v), None => calculando() }
62```
63
64Notas:
65- El campo `Option<T>` exige `T: Clone` (para el fallback de `try_unwrap`).
66- La **generación** evita que un resultado tardío pise a uno más nuevo
67  (drags, toggles rápidos). Imprescindible si el recálculo puede dispararse
68  seguido.
69- Inputs al worker deben ser `Send` (clonar `Chart`, `Vec`, etc.).
70- No hace falta async-ear lo barato: en cosmos el render de la carta quedó
71  síncrono (con el solver acotado son ms); sólo el astro (144 muestras × 10
72  cuerpos) fue a worker.
73
74## Soluciones colaterales de la misma cacería (ya aplicadas, no revertir)
75
76- **Preferir Vulkan en `llimphi-hal`** (`Hal::new`, commit `9f221983`): pedir
77  adapter con `Backends::PRIMARY` y caer a `all()` (incluye GL) sólo si no hay
78  PRIMARY. El backend **GL de Mesa sobre Wayland segfaultea en el teardown**
79  (`eglTerminate → wl_proxy_marshal` sobre conexión muerta, exit 139 sin
80  panic). Es infra compartida → ya beneficia a todas las apps. No volver a
81  `InstanceDescriptor::default()`.
82- **Acotar solvers iterativos** (`cosmos-ephemeris`, Kepler, commit `added8b3`):
83  un `loop {}` con corte `dl.abs() < 1e-15` (pegado al epsilon de f64) entra en
84  ciclo límite y NO converge para ciertos inputs → loop infinito. Release
85  fusiona flops (FMA) y converge; debug no. **Todo solver Newton/bisección
86  lleva cota dura** (`for _ in 0..N`), no `loop {}`.
87
88## Cómo diagnosticar (sin ptrace; `ptrace_scope=1` bloquea gdb a no-hijos)
89
90- `/proc/$PID/wchan` del hilo principal: `do_epoll_wait` = ocioso sano;
91  `__futex_wait` = deadlock de lock; estado `R` sostenido = spin o cómputo en
92  el hilo de UI; `dma_fence`/`drm` = GPU; `poll` sobre fd `wayland-0` = frame
93  callback.
94- gdb **como PADRE** sí puede (lanzar la app *bajo* gdb): backtrace del spin/
95  segfault. La pila de wgpu revela el backend (`wgpu_hal::gles` vs vulkan).
96- Trazar con un `eprintln` ENTER/DONE para distinguir "una llamada que no
97  termina" (loop infinito) de "se llama repetidas veces" (storm de dispatch).
98- En debug arranca como `cargo run` (binario `target/debug`); el release puede
99  ocultar el bug (float/overflow distintos).
100
101## Checklist — auditar y aplicar a cada app
102
103Buscar trabajo pesado en `init`/`update`/handlers y moverlo a worker:
104
105- [x] `01_yachay/cosmos/cosmos-app-llimphi` (referencia)
106- [ ] `00_unanchay/pluma/pluma-app`
107- [ ] `00_unanchay/pluma/pluma-editor-llimphi`
108- [ ] `00_unanchay/pluma/pluma-notebook-llimphi`
109- [ ] `00_unanchay/puriy/puriy-llimphi`  (motor JS/render — alto riesgo)
110- [ ] `00_unanchay/khipu/khipu-app`
111- [ ] `00_unanchay/chaka/chaka-app-llimphi`
112- [ ] `01_yachay/dominium/dominium-app-llimphi`
113- [ ] `01_yachay/nakui/nakui-ui-llimphi`, `nakui-sheet-llimphi`, `nakui-explorer-llimphi`
114- [ ] `01_yachay/iniy/iniy-explorer-llimphi`
115- [ ] `01_yachay/tinkuy/tinkuy-llimphi`  (simulación — alto riesgo)
116- [ ] `02_ruway/ayni/ayni-llimphi`
117- [ ] `02_ruway/chasqui/chasqui-explorer-llimphi`, `chasqui-broker-explorer-llimphi`
118- [ ] `02_ruway/nada`, `02_ruway/mirada/*-llimphi`
119- [ ] `pineal-*` (charting — revisar si el cómputo de series corre en update)
120
121(Lista de partida: `grep -rl 'llimphi-ui' --include=Cargo.toml`. Los widgets/
122modules/demos rara vez hacen cómputo pesado; foco en las apps de dominio.)

COMPUTO-FUERA-DEL-HILO-UI.md

Cómputo pesado fuera del hilo de UI — regla dura de Llimphi

PRIORIDAD URGENTE. Patrón a aplicar a todas las apps Llimphi. Origen: el "Not Responding" de cosmos (2026-05-31). Implementación de referencia: 01_yachay/cosmos/cosmos-app-llimphi (commits added8b3, 9f221983).

La regla

Ningún App::update, App::init ni handler (on_key/on_wheel/…) debe ejecutar trabajo pesado síncrono. Bloquea el hilo de UI → la ventana no repinta, no responde, no cierra → "Not Responding". Es el antipatrón win32 de trabajo pesado en el message loop.

Crítico: en winit, App::init() corre dentro de resumed, DESPUÉS de crear la ventana. Un cómputo pesado en init congela la ventana ya visible.

Se nota brutal en debug (sin optimizar, 10–50× más lento; además debug panica en overflow donde release wrappea). Pero la mala arquitectura está igual en release: una carta pesada, una máquina lenta o un dataset grande la exponen.

"Pesado" = efemérides/simulación, layout de árboles grandes, IO de disco/red, parse, embeddings, compresión… cualquier cosa que pueda pasar de ~unos ms.

El patrón (mover a un worker)

// 1) Mensaje de resultado: u64 = generación; Arc<T> porque Msg: Clone.
enum Msg { /* … */ XComputed(u64, std::sync::Arc<Resultado>) }

// 2) En el Model: el resultado es Option (None = "calculando…"),
//    más un flag dirty y un contador de generación.
struct Model { x: Option<Resultado>, x_dirty: bool, x_gen: u64, /* … */ }

// 3) recompute_x sólo marca dirty (los helpers no tienen el Handle).
fn recompute_x(m: &mut Model) { m.x_dirty = true; }

// 4) Al FINAL de update() (que SÍ tiene el Handle): si está sucio, bumpear
//    generación, clonar los inputs y despachar a un worker.
if m.x_dirty {
    m.x_dirty = false;
    m.x_gen = m.x_gen.wrapping_add(1);
    let gen = m.x_gen;
    let input = m.input.clone();              // sólo lo que el worker necesita
    handle.spawn(move || Msg::XComputed(gen, std::sync::Arc::new(compute(&input))));
}

// 5) Arm del resultado: aplicar SÓLO si la generación sigue vigente
//    (un recálculo posterior ya dejó viejo a este). try_unwrap evita copiar
//    (el Arc llega con refcount 1 porque el Msg no se clona en el camino).
Msg::XComputed(gen, x) => {
    if gen == m.x_gen {
        m.x = Some(std::sync::Arc::try_unwrap(x).unwrap_or_else(|a| (*a).clone()));
    }
}

// 6) En init: arrancar con None y despachar el primer cómputo a un worker
//    (init tiene el Handle). La vista pinta "calculando…" mientras tanto.

// 7) En la vista: match &model.x { Some(v) => panel(v), None => calculando() }

Notas:

Soluciones colaterales de la misma cacería (ya aplicadas, no revertir)

Cómo diagnosticar (sin ptrace; ptrace_scope=1 bloquea gdb a no-hijos)

Checklist — auditar y aplicar a cada app

Buscar trabajo pesado en init/update/handlers y moverlo a worker:

(Lista de partida: grep -rl 'llimphi-ui' --include=Cargo.toml. Los widgets/ modules/demos rara vez hacen cómputo pesado; foco en las apps de dominio.)