Files
Glint-Runtime/docs/ru/architecture/01-data-flow.md

304 lines
15 KiB
Markdown
Raw Permalink 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
Как исходный код превращается в пиксели на экране.
```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 из них, но время на их создание уже потрачено.