# План улучшения архитектуры Glint Runtime ## Цель Обеспечить **плоский график производительности**: рендеринг 1000 строк байткода должен занимать ~0.04 секунды сегодня и столько же через годы, независимо от роста количества компонентов, стилей и страниц. Время обработки события должно быть пропорционально **размеру изменения**, а не **размеру всей системы**. --- ## Фаза 0: Бенчмарки и профилирование **Цель:** зафиксировать текущие метрики, чтобы объективно оценивать прогресс. - [ ] **0.1** Добавить Criterion в `Cargo.toml` - [ ] **0.2** Написать бенчмарки для: - `evaluate_vdom()` на `desktop.glbc` - `matching_rules()` — 10, 100, 1000 правил - `ComputedStyle::compute()` — пустой, 5, 20 свойств - `resolve_string()` — без `$`, с 1 `$var`, с 3 `$var` - `RheiContext::eval_expr()` — простое выражение, сложное - `RheiContext::execute_action()` — короткий скрипт - `Element::clone()` — замерить копирование глубокого дерева - [ ] **0.3** Запустить `perf record` / `flamegraph-rs` на горячем пути: - `cargo run -- run desktop.glbc` + интерактив - Выявить фактический bottleneck (гипотеза: style matching, string parsing) - [ ] **0.4** Записать baseline в `BENCHMARKS.md` или `README.md` **Файлы:** новый `benches/bench.rs`, `Cargo.toml` --- ## Фаза 1: Типизированные значения (Value enum) **Цель:** устранить постоянный round-trip через строки (parse/format на каждое свойство). **Текущая проблема:** `HashMap` для переменных. Каждый `SliderChanged` → `format!(...)`, потом `rhei.rs:139-142` — три парсинга подряд (`i64`, `f64`, `bool`). - [ ] **1.1** Определить `Value` enum в `interpreter/types.rs`: ```rust #[derive(Clone, Debug)] pub enum Value { Str(CompactStr), Int(i64), Float(f64), Bool(bool), Array(Vec), None, } ``` - [ ] **1.2** Заменить `HashMap` на `HashMap`: - `Document::variables` - `GlintApp::update()` — входящие значения - [ ] **1.3** Переписать `str_to_dyn()` и `dyn_to_str()` в `rhei.rs`: - Убрать парсинг, конвертировать напрямую `Value ↔ Dynamic` - [ ] **1.4** Переписать `resolve_string()`: - Если значение уже `Int`/`Float` — не парсить, форматировать один раз - [ ] **1.5** Переписать `evaluate_condition()`: - Числовые сравнения без парсинга строк - `resolve_string()` → преобразование к Value - [ ] **1.6** Переписать `is_truthy()`: работа с Value напрямую - [ ] **1.7** Обновить `renderer.rs`: - `SliderChanged`, `ToggleChanged`, `InputChanged` — принимать `Value`, не строку - Парсинг slider/toggle/input значений — один раз, а не на каждый чих - [ ] **1.8** Обновить `ComputedStyle::compute()` и `parse_*()` функции: - Принимать `&Value` где возможно, а не `&str` - `parse_color`, `parse_size`, `parse_length` — работа с Value - [ ] **1.9** Написать тесты для Value: конверсии, сравнения, форматирование **Изменяемые файлы:** `interpreter/types.rs`, `interpreter/mod.rs`, `interpreter/rhei.rs`, `interpreter/style.rs`, `app.rs`, `renderer.rs` --- ## Фаза 2: Интернирование строк (String Interning) **Цель:** ускорить сравнение строк (ключи свойств, названия типов, классы). Заменить million `== "padding-top"` на O(1) сравнение ID. - [ ] **2.1** Выбрать библиотеку intern-строк: - `lasso` (thread-safe, производительный) или `strena` (лёгкая) - Или самодельный `StringArena` с `HashMap<&str, usize>` - [ ] **2.2** Заменить `&'a str` на `InternedStr` (newtype вокруг `u32`/`usize`): - `Element::type_name` - Ключи в `Element::properties` - Селекторы, имена классов, ID - [ ] **2.3** `lookup()` в `style.rs`: - Сравнение ключей через ID вместо `==` - [ ] **2.4** `CompoundSelector::matches_element()`: - Сравнение `tag`, `id`, `classes` через internd-строки - [ ] **2.5** Все `HashMap` где ключи повторяются → `HashMap` **Изменяемые файлы:** `interpreter/types.rs`, `interpreter/style.rs`, `interpreter/mod.rs`, `renderer.rs` (много `el.type_name == "Button"`) --- ## Фаза 3: Граф зависимостей (Reactive Dependency Tracking) **Цель:** вместо полного `evaluate_vdom()` на каждое событие — пересчитывать только элементы, которые зависят от изменившейся переменной. **Это главный этап для плоского графика.** **Текущая проблема:** `app.rs:44-51` — на любое событие полный VDOM rebuild. Слайдер меняет `volume_level`, но пересчитываются все 500 элементов. - [ ] **3.1** Создать `interpreter/reactive.rs`: ```rust pub struct ReactiveTracker { subscribers: HashMap>, dependencies: HashMap>, dirty_set: HashSet, } ``` - [ ] **3.2** Анализ зависимостей при загрузке шаблона: - Сканировать `$var` в свойствах → регистрировать `ElementId → var` - Сканировать `!rhei:{expr}` → извлекать имена переменных через Rhai parser - `@if condition` → зависимости от переменных в condition - `@each source` → зависимости от переменных в source - Результат: каждый элемент знает, от каких переменных зависит - [ ] **3.3** Изменить `GlintApp::update()`: ```rust fn update(&mut self, msg: Message) { let affected = self.tracker.on_variable_changed(name, new_val); self.vdom_roots = Interpreter::evaluate_vdom_incr( &self.doc.roots, &affected, &self.tracker, ... ); } ``` - [ ] **3.4** `evaluate_vdom_incr()`: - Для элементов не в `affected` — возвращаем кэшированный VDOM - Для элементов в `affected` — пересчитываем и каскадно помечаем детей - `@if` — если условие не изменилось — не пересчитываем ветку - `@each` — если source не изменился — не пересчитываем - [ ] **3.5** Инвалидация стилей: - При изменении переменной, меняющей `class` или `id` элемента → сброс кэша стилей - Иначе стили не пересчитываем - [ ] **3.6** Кэш VDOM: - `HashMap>` — не клонируем, разделяем память - `Arc::make_mut()` при изменении (copy-on-write) - [ ] **3.7** Написать тест: - Сценарий: 1000 элементов, 1 зависимость → после изменения пересчитывается 1 элемент - Проверить, что dirty_set корректен **Изменяемые файлы:** новый `interpreter/reactive.rs`, `interpreter/mod.rs`, `interpreter/types.rs`, `app.rs`, `main.rs` --- ## Фаза 4: Индексированный матчинг стилей (Style Index) **Цель:** заменить O(N×M) на O(K) где K — число релевантных правил для элемента (1-5, не 500). **Текущая проблема:** `style.rs:487-505` — каждый элемент проверяется против ВСЕХ правил. 100 элементов × 500 правил = 50 000 проверок на событие. - [ ] **4.1** Построить индекс стилей при загрузке `Document`: ```rust pub struct StyleIndex { by_tag: HashMap>, by_class: HashMap>, by_id: HashMap>, by_tag_class: HashMap<(InternedStr, InternedStr), Vec>, by_tag_id: HashMap<(InternedStr, InternedStr), Vec>, complex_rules: Vec<(ComplexSelector, RuleId)>, rules: Vec, } ``` - [ ] **4.2** `matching_rules()` → `query_index()`: - Для элемента `Button.primary#submit`: 1. `by_tag["Button"]` → [1, 5, 12] 2. `by_class["primary"]` → [3, 5, 7] 3. `by_id["submit"]` → [5] 4. Пересечение → [5] 5. Проверить 1 сложное правило 6. Итого: 3 проверки вместо 500 - [ ] **4.3** Кэш совпадений: - `HashMap, u64)>` — инвалидируется по эпохе - epoch увеличивается при перезагрузке стилей - Псевдоклассы (`:hover`, `:active`) — кэш с ключом `(ElementId, PseudoClass)` - [ ] **4.4** `ComputedStyle::compute()`: - Убрать сортировку по specificity на каждый вызов - Правила уже отсортированы в индексе - `lookup()` — проход по нескольким правилам вместо прохода по `matched_sheets` - [ ] **4.5** Миграция `matching_pseudo_rules()`: - Использовать индекс + кэш - Вызывается из `renderer.rs` для hover/active — тысячи раз в секунду - Кэш на `(ElementId, pseudo)` с инвалидацией **Изменяемые файлы:** `interpreter/style.rs`, `interpreter/mod.rs`, `interpreter/types.rs`, `renderer.rs`, `main.rs` --- ## Фаза 5: Аренная аллокация и VNode (Flat VDOM) **Цель:** устранить глубокое клонирование `Element` и reduce аллокаций на каждый кадр. **Текущая проблема:** `Element` содержит `Vec` — рекурсивное клонирование. `child.clone()` в `@if` (mod.rs:347) копирует поддеревья целиком. - [ ] **5.1** Создать плоское представление VDOM: ```rust struct VNode<'a> { id: NodeId, type_name: InternedStr, properties: Range, children_range: Range, computed_style: ComputedStyle, } struct FlatVDom<'a> { nodes: Vec>, properties: Vec<(InternedStr, Value)>, arena: Bump, } ``` - [ ] **5.2** Добавить `bumpalo` или написать `BumpAlloc`: - Все строковые данные живут в арене - Очистка арены одним махом между кадрами - [ ] **5.3** `evaluate_vdom()` → возвращает `FlatVDom<'arena>` вместо `Vec`: - Вместо `push_prop("text", val.clone())` — allocate в арене - Вместо `Element::new(el.type_name)` — выделить VNode в `Vec` - [ ] **5.4** Structural sharing: - Ветки `@each` с одинаковыми телами разделяют `VNode` через `Arc` - `Arc::make_mut()` при изменении - [ ] **5.5** `render_element()` → работает с `&VNode`: - `get_prop()` — lookup в плоском массиве свойств - `children` — итерация по `children_range` - [ ] **5.6** Удалить `#[derive(Clone)]` из `Element` (или оставить для совместимости): - В горячем пути clone не используется - [ ] **5.7** Тест: проверить, что аллокаций на кадр стало < 10 (было ~1000+) **Изменяемые файлы:** `interpreter/types.rs`, `interpreter/mod.rs`, `renderer.rs`, `interpreter/style.rs`, `app.rs` --- ## Фаза 6: Кэширование Rhai AST **Цель:** убрать компиляцию Rhai скриптов при каждом выполнении. **Текущая проблема:** `rhei.rs:112` — `engine.compile(script)` при каждом `execute_action()`. Скрипты в `@on:click { ... }` компилируются каждый клик. - [ ] **6.1** Собрать все Rhai блоки при загрузке `Document`: - Из `rhei_scripts` (init-скрипты) - Из `__on:*` свойств (обработчики событий) - Из `!rhei:` выражений (компилируем, но не выполняем) - [ ] **6.2** `RheiContext`: ```rust pub struct RheiContext { engine: Engine, init_ast: AST, action_cache: HashMap, expr_cache: HashMap, } ``` - [ ] **6.3** `execute_action()`: - `self.action_cache.get(script)` вместо `engine.compile(script)` - Если нет — компилируем и кэшируем - [ ] **6.4** Очистка кэша: - Только при перезагрузке документа - Или `LruCache` если скриптов слишком много - [ ] **6.5** Тест: выполнить 1000 раз один и тот же action — время не должно расти **Изменяемые файлы:** `interpreter/rhei.rs`, `interpreter/mod.rs`, `main.rs` --- ## Фаза 7: Параллелизм (Rayon) **Цель:** распараллелить style matching и независимые ветки VDOM. **Текущая проблема:** всё выполняется последовательно, хотя style matching для разных элементов — embarrassingly parallel. - [ ] **7.1** Добавить `rayon` в `Cargo.toml` (feature gate: `parallel`) - [ ] **7.2** Параллельный style matching: - `matching_rules_batch(elements: &[Element]) -> Vec>` - `elements.par_iter().map(...)` - [ ] **7.3** Параллельный `@each`: - Итерации `@each` независимы - `items.par_iter().flat_map(|item| evaluate_vdom(body, ...))` - Feature gate: только для больших списков (> N элементов) - [ ] **7.4** Параллельный `ComputedStyle::compute_batch()`: - Векторизованный compute для нескольких элементов - [ ] **7.5** Тест: `@each` с 1000 итераций → ускорение ~4x на 8 ядрах **Изменяемые файлы:** `Cargo.toml`, `interpreter/style.rs`, `interpreter/mod.rs` --- ## Фаза 8: Мемоизация `ComputedStyle::compute()` **Цель:** не пересчитывать стили для элементов, чьи свойства не изменились. **Текущая проблема:** каждый кадр `ComputedStyle::compute()` парсит все ~40 полей через `lookup()`. - [ ] **8.1** Ввести хэш `(inline_properties_hash, stylesheet_epoch, element_id) → ComputedStyle` - [ ] **8.2** Кэш: `HashMap` + LRU eviction - [ ] **8.3** Инвалидация: - `stylesheet_epoch` — счётчик, увеличивается при изменении стилей - `inline_properties_hash` — хэш от значений свойств элемента - [ ] **8.4** Проверить hit rate на реальном UI: > 95%+ **Изменяемые файлы:** `interpreter/style.rs` --- ## Фаза 9: Инкрементальный рендеринг (keyed widgets) **Цель:** дать Iced'у возможность эффективно диффить виджеты, а не пересоздавать их. **Текущая проблема:** `view()` создаёт полностью новые Iced-виджеты каждый кадр. - [ ] **9.1** Ввести стабильные ID для каждого элемента VDOM - [ ] **9.2** `render_element()`: - Присваивать `iced::id()` на основе `ElementId` - Iced использует ID для сохранения состояния виджетов между кадрами - [ ] **9.3** Убирать из `view()` только изменившиеся элементы: - Если `VNode::hash == prev_hash` — вернуть кэшированный `iced::Element` - Кэш: `HashMap>` **Изменяемые файлы:** `renderer.rs`, `interpreter/types.rs` --- ## Фаза 10: Мониторинг и автоматические бенчмарки **Цель:** не допустить регрессий в будущем. - [ ] **10.1** Добавить `cargo bench` в CI - [ ] **10.2** Пороговые проверки: - `evaluate_vdom` на desktop.glbc < 5ms - `matching_rules` (100 правил) < 10μs - `ComputedStyle::compute` < 1μs - [ ] **10.3** Логирование производительности: - `--perf` флаг для `glint run` - Печатать: `VDOM: 1.2ms | Style: 0.3ms | Render: 2.1ms | Total: 3.6ms` - [ ] **10.4** Alert если total > 16ms (frame budget для 60 FPS) **Изменяемые файлы:** `Cargo.toml`, новый `src/perf.rs`, `cli.rs`, `.github/workflows/ci.yml` --- ## Сводная таблица влияния | Фаза | Описание | Ускорение | Плоский график | |------|----------|-----------|----------------| | 0 | Бенчмарки | — | — | | 1 | Value enum | 2-3× | Нет | | 2 | String interning | 1.5-2× | Нет | | **3** | **Reactive tracker** | **10-50×** | **Да (ключевое)** | | 4 | Style index | 5-10× (style) | Да | | 5 | Arena + Flat VDOM | 2-3× | Частично | | 6 | Rhai AST cache | 2-5× (click) | Да | | 7 | Parallelism | 2-4× | Нет | | 8 | Style memoization | 2-3× | Да | | 9 | Keyed widgets | 1.5-2× (render) | Частично | | 10 | CI benchmarks | — (контроль) | — | **Суммарно:** до 100× на горячих путях. --- ## Рекомендованный порядок имплементации 1. **Фаза 0** — профилирование (без этого нельзя) 2. **Фаза 1 + 2** — Value enum + String interning (фундамент) 3. **Фаза 3** — Reactive tracker (сердце архитектуры) 4. **Фаза 4** — Style index 5. **Фаза 5 + 8** — Arena + Memoization 6. **Фаза 6** — Rhai cache 7. **Фаза 7** — Parallelism 8. **Фаза 9** — Keyed widgets 9. **Фаза 10** — CI benchmarks