docs: reorganize into en/ru and add English translation

This commit is contained in:
Glint Dev
2026-07-24 22:04:29 +03:00
parent 938f05f59d
commit 35f92435a3
20 changed files with 4651 additions and 0 deletions

View File

@@ -0,0 +1,303 @@
# Поток данных в Glint Runtime
Как исходный код превращается в пиксели на экране.
```mermaid
flowchart TD
subgraph "Компиляция"
A1[".gltm markup"] --> P["Parser: glt crate"]
A2[".glts style"] --> P
P --> M["ModuleSoA — плоские массивы"]
M --> AST
AST --> C["Compiler: glt crate"]
C --> BC[".glbc bytecode"]
end
subgraph "Загрузка"
BC --> IR["Interpreter::run"]
IR --> R["Reader: парсит бинарник"]
R --> DOC["Document: дерево + стили + переменные"]
end
subgraph "Инициализация"
DOC --> BOOT["iced::application boot"]
BOOT --> RC["RheiContext: компилирует Rhai-скрипты"]
BOOT --> EV0["evaluate_vdom: собирает VDOM целиком"]
EV0 --> APP["GlintApp: готов к работе"]
end
subgraph "Круг жизни: каждый кадр"
APP --> LOOP{"iced event loop"}
LOOP -->|пришло событие| MSG[Message]
MSG --> UPD["GlintApp::update"]
UPD --> SET["меняет переменную"]
SET --> TV["tracker помечает зависимые элементы как dirty"]
TV --> DIRTY["забрать dirty_set"]
DIRTY --> VDOM["evaluate_vdom_incr: пересчитать только dirty"]
VDOM --> STYLE["StyleSheet: применить стили (с кэшем)"]
STYLE --> NEW_VDOM["новый VDOM"]
LOOP -->|по таймеру| VIEW["GlintApp::view"]
VIEW --> REND["render_element: Element → Iced-виджет"]
REND --> ICED["iced::Element дерево"]
ICED --> DIFF["Iced: сравнивает с предыдущим кадром"]
DIFF --> LAYOUT[Layout]
LAYOUT --> DRAW["GPU рисует"]
end
subgraph "Стили — отдельно"
STYLE --> SI["StyleIndex: ищет правила за O(1)\u2013O(K)"]
SI --> SC["StyleCache: не парсит одно и то же дважды"]
end
```
---
## Этап 1: Компиляция — из текста в байткод
Всё начинается с двух типов файлов:
- **`.gltm`** — разметка: кнопки, панели, тексты, слайдеры и так далее.
- **`.glts`** — стили: CSS-подобные правила, селекторы, цвета, отступы.
Их компилирует внешний crate **`glt`** (не часть этого репозитория). Он делает три вещи:
### 1.1 Парсинг
`Parser` читает `.gltm` и `.glts` и складывает всё в **`ModuleSoA`**.
**Что такое ModuleSoA?** SoA = Structure of Arrays (структура массивов). Вместо того чтобы хранить элементы как список структур:
```text
// Array of Structures (AoS) — как мы привыкли
Element { name: "Button", props: [...], children: [...] }
Element { name: "Text", props: [...], children: [...] }
```
компилятор хранит их как структуру с параллельными массивами:
```text
// Structure of Arrays (SoA) — эффективнее для компилятора
ModuleSoA {
type_names: ["Button", "Text", ...],
properties_vec: [ [...], [...], ...],
hierarchy: [parent_id, parent_id, ...],
}
```
Так компилятор проходит по всем именам разом (кэш процессора не простаивает),
быстрее ищет родительские связи и легче применяет оптимизации.
### 1.2 Построение AST
Из `ModuleSoA` строится AST-дерево. Здесь раскрываются компоненты, if/each ветки,
подставляются параметры.
### 1.3 Генерация байткода
`Compiler` обходит AST и превращает его в бинарный формат **`.glbc`**:
- заголовок с magic-байтами (`"glBc"`)
- пул строк (все имена, классы, тексты — одним блоком)
- байт-кодированные опкоды (см. `opcodes.rs`: `OP_ELEM_PUSH`, `OP_PROP`, `OP_IF`, `OP_EACH` и т.д.)
Результат — компактный бинарник, который можно быстро загрузить и скормить рантайму.
---
## Этап 2: Загрузка — из байткода в Document
Рантайм берёт `.glbc` и превращает его в структуры данных, с которыми можно работать.
### `Interpreter::run(bytecode) → Document`
Внутри `Reader` последовательно читает байткоп:
1. Проверяет magic-байты (это точно `.glbc`?)
2. Читает пул строк
3. Исполняет опкоды, на лету собирая дерево `Element`
Параллельно происходят две важные вещи:
**Стили:** каждая встреченная стилевая директива парсится в `StyleRule`,
потом из всех правил строится `StyleIndex` — каталог: «вот все правила для тэга Button,
вот для класса primary, вот для элемента с id=submit». Так поиск стиля
потом будет занимать не O(все правила), а O(пара штук).
**Зависимости:** каждое свойство вида `"text": "Hello $name"` — это подсказка:
элемент зависит от переменной `name`. `ReactiveTracker` сканирует все свойства,
находит `$var` и запоминает: «элемент ElementId(5) зависит от переменной "name"».
В итоге получается **`Document`**:
```rust
Document {
roots: Vec<Element>, // корневые элементы
components: HashMap<String, ComponentDef>, // компоненты
variables: HashMap<String, Value>, // начальные значения
stylesheet: StyleSheet, // таблица стилей
rhei_scripts: Vec<String>, // init-скрипты
tracker: ReactiveTracker, // кто от чего зависит
interner: Interner, // пул уникальных строк
}
```
---
## Этап 3: Инициализация — подготовка к жизни
`Document` готов, но его надо «завести». Это делает boot-функция Iced.
### 3.1 Клонирование
`doc.clone()` — все строки внутри Element имеют тип `&'a str` с исходным
временем жизни. После клонирования они становятся `&'static str` (рантайм
делает `Box::leak`, чтобы строки жили вечно — приложение работает до закрытия окна).
### 3.2 Компиляция Rhai
`RheiContext::new(scripts)`:
- Создаёт Rhai-движок (`Engine`)
- Компилирует все init-скрипты в AST и сохраняет их
- Собирает все функции из скриптов в глобальный модуль
- Потом `precompile_all_from_doc()` проходит по всему дереву Element и компилирует
каждый `__on:click { ... }` и каждое `!rhei:expr` в кэш.
**Теперь при клике не надо компилировать заново** — достаточно взять AST из кэша.
### 3.3 Запуск init-скриптов
`initialize()`: синхронизирует переменные с Rhai-скопом, выполняет init-скрипты,
забирает из скопа всё, что изменилось.
### 3.4 Первый VDOM
`evaluate_vdom()` — полный проход по дереву:
- Подставляет переменные в строки (`$name` → реальное значение)
- Вычисляет условия `@if`
- Раскрывает `@each` в реальное количество элементов
- Для каждого элемента находит подходящие стили и вычисляет `ComputedStyle`
- Присваивает `content_hash`
Результат: `GlintApp { doc, rhei, vdom_roots }`. Первый кадр готов к показу.
---
## Этап 4: Круг жизни — каждый кадр
Iced работает в цикле: событие → `update()``view()` → отрисовка.
### 4.1 Пришло событие: update()
Пользователь нажал кнопку, подвигал слайдер, ввёл текст — Iced присылает `Message`.
```rust
enum Message {
SliderChanged(Option<String>, f64), // слайдер: (привязанная переменная, новое значение)
InputChanged(Option<String>, String), // текстовое поле
ToggleChanged(Option<String>, bool), // чекбокс
EventTriggered(String), // клик по кнопке: запустить Rhai-скрипт
WindowScrolled(f32), // скролл окна
ScrollableScrolled(u64, f32), // скролл внутри контейнера
}
```
**GlintApp::update()** делает так:
1. **Меняет переменную.** Например, `SliderChanged("volume", 75)``variables["volume"] = 75.0`.
2. **Сообщает трекеру:** `tracker.on_variable_changed("volume")`. Трекер смотрит:
«от этой переменной зависят элементы с ID = 5, 12, 18». Он помечает их как dirty.
3. **Забирает dirty_set:** `tracker.take_dirty_set()`.
4. **Пересчитывает VDOM:** `evaluate_vdom_incr(roots, &dirty_set)`. Она проходит по дереву.
Если элемент в dirty_set — пересчитывает его (подстановка переменных, вычисление стилей).
Если нет — оставляет как есть. **Дети dirty-элемента тоже пересчитываются** (каскад).
### 4.2 По таймеру: view()
Даже если ничего не произошло, Iced вызывает `view()` каждый кадр (60 раз в секунду).
Нужно вернуть Iced-виджеты, которые он нарисует.
**render_element()** — рекурсивная функция, которая превращает Element в Iced-виджет:
- `Button``iced::button(...).on_press(...)`
- `Text``iced::text("...").size(16).color(...)`
- `Panel``iced::column[...].spacing(10)`, обёрнутый в контейнер с фоном и рамкой
- `Input``iced::text_input("placeholder", "value").on_input(...)`
- `Image``iced::image(path)` или `iced::svg(path)`
- Неизвестный тип → просто колонка с детьми
Каждый виджет оборачивается в **`apply_universal_box_model`**:
```text
контейнер [margin]
контейнер [padding, border, background]
scrollable (если overflow: scroll/auto)
контейнер [padding]
сам виджет
```
**Проблема:** `render_element` создаёт **все** виджеты с нуля каждый кадр, даже если
Element не изменился. Iced потом диффит новое дерево со старым — но само построение
дерева стоит ~7ms. Это главный резерв оптимизации.
### 4.3 Iced делает своё дело
Iced получает дерево `iced::Element`, сравнивает с предыдущим (diff), вычисляет
раскладку (layout) и рисует через GPU (wgpu). Всё это без участия нашего кода.
---
## Анатомия Element
```rust
Element {
type_name: "Button", // что это за элемент
properties: [("label", "Click"), ("color", "red"), ...], // его свойства
computed_style: ComputedStyle { color: Some(Red), padding: Some(8px), ... }, // вычисленный стиль
element_id: ElementId(42), // уникальный ID в дереве
content_hash: 0xABCD1234, // хэш содержимого (для кэша виджетов)
children: [Element, ...], // дочерние элементы
}
```
## Анатомия стилей
Стили хранятся в `StyleSheet` и работают в три этапа:
**1. Индекс (`StyleIndex`):** при загрузке все CSS-правила раскладываются по полочкам:
```text
Правило: "Button.primary#submit { color: red; padding: 10px }"
→ by_tag["Button"] = { RuleId(1) }
→ by_class["primary"] = { RuleId(1) }
→ by_id["submit"] = { RuleId(1) }
```
**2. Поиск:** когда нужно найти стили для элемента `Button.primary#submit`,
мы берём пересечение множеств из всех трёх полок. Вместо проверки 500 правил — 3 lookup'а.
**3. Кэш:** даже если стили найдены, `ComputedStyle::compute()` парсит все свойства
(цвет, отступы, шрифты — около 40 полей). Это дорого. `StyleCache` запоминает
результат: `hash(type_name, properties, эпоха) → ComputedStyle`. Если элемент
не менялся — берём готовый стиль из кэша, не парсим.
---
## Событийный цикл на примере слайдера
```
1. Пользователь двигает слайдер громкости
2. Iced: SliderChanged(Some("volume"), 75.0)
3. GlintApp::update:
a. variables["volume"] = Float(75.0)
b. tracker.on_variable_changed("volume")
→ грязные: ElementId(5) — текст с "$volume", ElementId(12) — ширина от "$volume"
c. evaluate_vdom_incr(roots, &{5, 12})
→ Element 5: пересчитать текст (новая громкость)
→ Element 12: пересчитать ширину
→ остальные 48 элементов: не трогать
4. GlintApp::view:
→ render_element для всех 50 root-элементов
→ рекурсивно для всех детей (даже для тех 48, что не менялись)
→ Iced получает полностью новое дерево из 200+ виджетов
5. Iced: диффит → находит 2 изменения → перерисовывает 2 области
```
**Узкое место:** шаг 4. VDOM пересчитал только 2 элемента из 50 (спасибо ReactiveTracker).
Но render_element создаёт виджеты для всех 200+ узлов. Iced потом всё равно диффит
и ничего не делает с 198 из них, но время на их создание уже потрачено.

View File

@@ -0,0 +1,68 @@
# Анализ производительности
Измерения с флагом `--perf` на `desktop.glbc`.
## Результаты замеров
**Steady state** (нет событий, idle):
```
VDOM: 6.6ms | Style: 0.3ms | Render: 7.2ms | Total: 14.1ms
```
**При событиях** (перетаскивание слайдера, пиковые значения):
```
VDOM: 17.4ms | Style: 0.8ms | Render: 15.2ms | Total: 33.5ms ⚠️
```
**60 FPS frame budget: 16ms.** В покое укладываемся (14ms), при событиях — нет (до 33ms).
## Анализ bottleneck'ов
### Style matching — НЕ bottleneck (0.3-0.8ms)
Style matching занимает менее 1ms даже на пике. Это результат работы:
- **Phase 4** (StyleIndex) — O(K) вместо O(N×M)
- **Phase 8** (StyleCache) — мемоизация computed style
### VDOM eval — основной потребитель (6-17ms)
В покое ~6.6ms — это полный проход по дереву Element'ов. При событиях до 17ms:
- `evaluate_vdom_incr` пересчитывает dirty-элементы (Phase 3)
- Каждое событие может делать dirty целые поддеревья
- Внутри: `resolve_string`, `resolve_prop`, `compute_cached`, рекурсивный проход
### Render — второй потребитель (7-15ms)
**Здесь главный резерв оптимизации.** `render_element` создаёт ВСЕ Iced-виджеты
каждый кадр с нуля, даже если Element не изменился. Iced затем диффит новое дерево
со старым — но само построение дерева стоит ~7ms.
### Сценарий: слайдер
1. `SliderChanged``age` и `volume_level` меняются
2. `tracker.on_variable_changed` → dirty_set для зависимых элементов
3. `evaluate_vdom_incr` пересчитывает dirty-элементы и их детей
4. `view()``render_element` для ВСЕХ элементов (полный перерендер)
5. Итог: VDOM 13ms + Render 14ms = 27ms — пропуск кадра
Первые несколько кадров после события — самые тяжёлые (VDOM ~13ms), затем
стабилизируются (~7ms), так как dirty_set постепенно очищается.
## Рекомендации
1. **Phase 9.3 — кэш виджетов** — сократит Render с 7ms до ~0ms для неизменившихся
элементов. Если изменился 1 элемент из 50, перерендеривать нужно только его.
Это снизит общее время с 14ms до ~7ms в покое.
2. **Phase 1 — Value enum в стилях**`ComputedStyle::compute()` и `parse_*()`
принимают `&str` и парсят каждое свойство. Если передавать `&Value` — парсинг
не нужен. Потенциально ускорит и style matching, и VDOM eval.
3. **Phase 2 — InternedStr** — сравнения строк (`type_name == "Button"`,
`key == "padding-top"`) происходят тысячами за кадр. Замена на сравнение u32
даст 1.5-2× в VDOM и render путях.
4. **Phase 0.3 — flamegraph** — подтвердить гипотезы замерами профилировщика
(`perf record`), прежде чем вкладываться в оптимизацию.