Files
Glint-Runtime/ARCHITECTURE-IMPROVEMENT-PLAN.md

380 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# План улучшения архитектуры 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<String, String>` для переменных. Каждый `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<Value>),
None,
}
```
- [ ] **1.2** Заменить `HashMap<String, String>` на `HashMap<String, Value>`:
- `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<String, String>` где ключи повторяются → `HashMap<InternedStr, Value>`
**Изменяемые файлы:** `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<InternedStr, HashSet<ElementId>>,
dependencies: HashMap<ElementId, HashSet<InternedStr>>,
dirty_set: HashSet<ElementId>,
}
```
- [ ] **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<ElementId, Arc<VNode>>` — не клонируем, разделяем память
- `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<InternedStr, Vec<RuleId>>,
by_class: HashMap<InternedStr, Vec<RuleId>>,
by_id: HashMap<InternedStr, Vec<RuleId>>,
by_tag_class: HashMap<(InternedStr, InternedStr), Vec<RuleId>>,
by_tag_id: HashMap<(InternedStr, InternedStr), Vec<RuleId>>,
complex_rules: Vec<(ComplexSelector, RuleId)>,
rules: Vec<StyleRule>,
}
```
- [ ] **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<ElementId, (Vec<RuleId>, 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<Element>` — рекурсивное клонирование.
`child.clone()` в `@if` (mod.rs:347) копирует поддеревья целиком.
- [ ] **5.1** Создать плоское представление VDOM:
```rust
struct VNode<'a> {
id: NodeId,
type_name: InternedStr,
properties: Range<PropertyIdx>,
children_range: Range<NodeIdx>,
computed_style: ComputedStyle,
}
struct FlatVDom<'a> {
nodes: Vec<VNode<'a>>,
properties: Vec<(InternedStr, Value)>,
arena: Bump,
}
```
- [ ] **5.2** Добавить `bumpalo` или написать `BumpAlloc`:
- Все строковые данные живут в арене
- Очистка арены одним махом между кадрами
- [ ] **5.3** `evaluate_vdom()` → возвращает `FlatVDom<'arena>` вместо `Vec<Element>`:
- Вместо `push_prop("text", val.clone())` — allocate в арене
- Вместо `Element::new(el.type_name)` — выделить VNode в `Vec<VNode>`
- [ ] **5.4** Structural sharing:
- Ветки `@each` с одинаковыми телами разделяют `VNode` через `Arc<VNode>`
- `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<InternedStr, AST>,
expr_cache: HashMap<InternedStr, AST>,
}
```
- [ ] **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<Vec<&HashMap>>`
- `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<u64, ComputedStyle>` + 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<ElementId, Arc<iced::Element>>`
**Изменяемые файлы:** `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