docs: reorganize into en/ru and add English translation
This commit is contained in:
303
docs/ru/architecture/01-data-flow.md
Normal file
303
docs/ru/architecture/01-data-flow.md
Normal 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 из них, но время на их создание уже потрачено.
|
||||
68
docs/ru/architecture/02-performance.md
Normal file
68
docs/ru/architecture/02-performance.md
Normal 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`), прежде чем вкладываться в оптимизацию.
|
||||
Reference in New Issue
Block a user