move example template files to examples/, clean up root
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,2 +1,4 @@
|
|||||||
/target
|
/target
|
||||||
*.glbc
|
*.glbc
|
||||||
|
STYLE-SYSTEM-COMPLETION-PLAN.md
|
||||||
|
ARCHITECTURE-IMPROVEMENT-PLAN.md
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
# План улучшения архитектуры Glint Runtime
|
# План улучшения архитектуры Glint Runtime — остаток
|
||||||
|
|
||||||
## Цель
|
Всё, что не реализовано из исходного плана. Реализованные фазы (3-8, 10, 9.1-9.2) удалены.
|
||||||
|
|
||||||
Обеспечить **плоский график производительности**: рендеринг 1000 строк байткода должен занимать
|
|
||||||
~0.04 секунды сегодня и столько же через годы, независимо от роста количества компонентов,
|
|
||||||
стилей и страниц. Время обработки события должно быть пропорционально **размеру изменения**,
|
|
||||||
а не **размеру всей системы**.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -13,367 +8,63 @@
|
|||||||
|
|
||||||
**Цель:** зафиксировать текущие метрики, чтобы объективно оценивать прогресс.
|
**Цель:** зафиксировать текущие метрики, чтобы объективно оценивать прогресс.
|
||||||
|
|
||||||
- [ ] **0.1** Добавить Criterion в `Cargo.toml`
|
- [ ] **0.3** Запустить `perf record` / `flamegraph-rs` на горячем пути и выявить bottleneck
|
||||||
- [ ] **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`
|
- [ ] **0.4** Записать baseline в `BENCHMARKS.md` или `README.md`
|
||||||
|
|
||||||
**Файлы:** новый `benches/bench.rs`, `Cargo.toml`
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Фаза 1: Типизированные значения (Value enum)
|
## Фаза 1: Типизированные значения (Value enum)
|
||||||
|
|
||||||
**Цель:** устранить постоянный round-trip через строки (parse/format на каждое свойство).
|
**Цель:** устранить постоянный round-trip через строки (parse/format на каждое свойство).
|
||||||
|
|
||||||
**Текущая проблема:** `HashMap<String, String>` для переменных. Каждый `SliderChanged` →
|
**Статус:** `Value` enum, конверсии `Value↔Dynamic` и `resolve_string` с Value сделаны.
|
||||||
`format!(...)`, потом `rhei.rs:139-142` — три парсинга подряд (`i64`, `f64`, `bool`).
|
**Осталось:** `HashMap<String, String>` в стилях не тронут — `ComputedStyle::compute()` и
|
||||||
|
`parse_*()` всё ещё принимают `&str`. Стили — главный потребитель parse/format.
|
||||||
|
|
||||||
- [ ] **1.1** Определить `Value` enum в `interpreter/types.rs`:
|
- [ ] **1.2** Заменить `HashMap<String, String>` на `HashMap<String, Value>` в стилях:
|
||||||
```rust
|
- `StyleRule::properties`
|
||||||
#[derive(Clone, Debug)]
|
- Все matched_sheets
|
||||||
pub enum Value {
|
- [ ] **1.8** Переписать `parse_color`, `parse_size`, `parse_length` на работу с `&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: конверсии, сравнения, форматирование
|
- [ ] **1.9** Написать тесты для Value: конверсии, сравнения, форматирование
|
||||||
|
|
||||||
**Изменяемые файлы:** `interpreter/types.rs`, `interpreter/mod.rs`, `interpreter/rhei.rs`,
|
|
||||||
`interpreter/style.rs`, `app.rs`, `renderer.rs`
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Фаза 2: Интернирование строк (String Interning)
|
## Фаза 2: Интернирование строк (String Interning)
|
||||||
|
|
||||||
**Цель:** ускорить сравнение строк (ключи свойств, названия типов, классы).
|
**Цель:** ускорить сравнение строк (ключи свойств, названия типов, классы).
|
||||||
Заменить million `== "padding-top"` на O(1) сравнение ID.
|
Заменить миллион `== "padding-top"` на O(1) сравнение ID.
|
||||||
|
|
||||||
- [ ] **2.1** Выбрать библиотеку intern-строк:
|
**Статус:** `InternedStr(u32)` и `Interner` определены в `types.rs`, но нигде не используются
|
||||||
- `lasso` (thread-safe, производительный) или `strena` (лёгкая)
|
в горячем пути. `Element::type_name` — `&str`, ключи пропертей — `Cow<str>`,
|
||||||
- Или самодельный `StringArena` с `HashMap<&str, usize>`
|
селекторы — `String`. Сравнения — через `==`.
|
||||||
- [ ] **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`,
|
- [ ] **2.2** Заменить `&'a str` на `InternedStr` в `Element::type_name` и ключах
|
||||||
`renderer.rs` (много `el.type_name == "Button"`)
|
- [ ] **2.3** `lookup()` в `style.rs` — сравнение через ID вместо `==`
|
||||||
|
- [ ] **2.4** `CompoundSelector::matches_element()` — сравнение через InternedStr
|
||||||
|
- [ ] **2.5** `HashMap<String, String>` → `HashMap<InternedStr, Value>` где ключи повторяются
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Фаза 3: Граф зависимостей (Reactive Dependency Tracking)
|
## Фаза 9.3: Кэш Iced-виджетов
|
||||||
|
|
||||||
**Цель:** вместо полного `evaluate_vdom()` на каждое событие — пересчитывать только
|
**Цель:** не пересоздавать `iced::Element` для элементов с неизменившимся `content_hash`.
|
||||||
элементы, которые зависят от изменившейся переменной. **Это главный этап для плоского графика.**
|
|
||||||
|
|
||||||
**Текущая проблема:** `app.rs:44-51` — на любое событие полный VDOM rebuild. Слайдер
|
**Статус:** 9.1 (`content_hash` на `Element`) и 9.2 (стабильные `iced::widget::Id`) сделаны.
|
||||||
меняет `volume_level`, но пересчитываются все 500 элементов.
|
9.3 отложен: `iced::Element` не `Clone`, хранит `Box<dyn Widget>`, кэширование требует
|
||||||
|
unsafe transmute lifetime или перестройки renderer.
|
||||||
|
|
||||||
- [ ] **3.1** Создать `interpreter/reactive.rs`:
|
- [ ] **9.3** Реализовать кэш `HashMap<ElementId, (u64, iced::Element)>` в `GlintApp`:
|
||||||
```rust
|
- При совпадении `content_hash` — возвращать сохранённый виджет
|
||||||
pub struct ReactiveTracker {
|
- Иначе — рендерить, кэшировать, обновлять hash
|
||||||
subscribers: HashMap<InternedStr, HashSet<ElementId>>,
|
- Unsafe transmute `'static → '_` допустим, т.к. данные живут в `Element<'static>`
|
||||||
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` — каждый элемент проверяется против ВСЕХ правил.
|
| 0 | Профилирование | — |
|
||||||
100 элементов × 500 правил = 50 000 проверок на событие.
|
| 1 | Value enum в стилях | 2-3× |
|
||||||
|
| 2 | String interning | 1.5-2× |
|
||||||
- [ ] **4.1** Построить индекс стилей при загрузке `Document`:
|
| 9.3 | Кэш виджетов | 1.5-2× (render) |
|
||||||
```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
|
|
||||||
|
|||||||
BIN
desktop.glbc
BIN
desktop.glbc
Binary file not shown.
BIN
display.glbc
BIN
display.glbc
Binary file not shown.
432
examples/glt/main.glts
Normal file
432
examples/glt/main.glts
Normal file
@@ -0,0 +1,432 @@
|
|||||||
|
// =============================================================================
|
||||||
|
// Glint Design System — Core Fixed Stylesheet (main.glts)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
$bg-app = #11111b
|
||||||
|
$bg-panel = #1e1e2e
|
||||||
|
$bg-surface = #313244
|
||||||
|
$bg-input = #181825
|
||||||
|
$accent = #b4befe
|
||||||
|
$text-main = #cdd6f4
|
||||||
|
$text-muted = #9399b2
|
||||||
|
$border-glow = #45475a
|
||||||
|
|
||||||
|
@mixin text-body {
|
||||||
|
font-size: 14px
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin standard-card {
|
||||||
|
background: $bg-panel
|
||||||
|
border-color: $border-glow
|
||||||
|
border-width: 1px
|
||||||
|
border-radius: 12px
|
||||||
|
}
|
||||||
|
|
||||||
|
Window {
|
||||||
|
background: $bg-app
|
||||||
|
}
|
||||||
|
|
||||||
|
Panel {
|
||||||
|
background: transparent
|
||||||
|
border-width: 0px
|
||||||
|
padding: 0px
|
||||||
|
gap: 12px
|
||||||
|
direction: vertical
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-card {
|
||||||
|
@use standard-card
|
||||||
|
padding: 14px
|
||||||
|
gap: 12px
|
||||||
|
align-items: center
|
||||||
|
content-align: center
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Служебные Layout-классы ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
.layout-viewport {
|
||||||
|
width: fill
|
||||||
|
padding: 16px
|
||||||
|
gap: 16px
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-row {
|
||||||
|
direction: horizontal
|
||||||
|
gap: 16px
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-col {
|
||||||
|
direction: vertical
|
||||||
|
gap: 16px
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-clean {
|
||||||
|
width: fill
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Стили атомарных компонентов ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
Header {
|
||||||
|
color: $accent
|
||||||
|
font-size: 20px
|
||||||
|
padding: 2px
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
@use text-body
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
color: $text-muted
|
||||||
|
font-size: 12px
|
||||||
|
}
|
||||||
|
|
||||||
|
Input {
|
||||||
|
background-color: $bg-input
|
||||||
|
color: $text-main
|
||||||
|
font-size: 14px
|
||||||
|
padding: 10px
|
||||||
|
border-radius: 8px
|
||||||
|
border-width: 1px
|
||||||
|
border-color: $border-glow
|
||||||
|
width: 240px
|
||||||
|
}
|
||||||
|
|
||||||
|
Toggle {
|
||||||
|
color: $text-main
|
||||||
|
font-size: 14px
|
||||||
|
}
|
||||||
|
|
||||||
|
Divider {
|
||||||
|
background: $border-glow
|
||||||
|
border-width: 1px
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
border-radius: 6px
|
||||||
|
border-width: 1px
|
||||||
|
padding: 8px
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-custom {
|
||||||
|
background: #000000
|
||||||
|
}
|
||||||
|
|
||||||
|
Image {
|
||||||
|
border-radius: 8px
|
||||||
|
border-width: 1px
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Секция тестирования продвинутой геометрии Box-Model ──────────────────────
|
||||||
|
|
||||||
|
.geometry-box {
|
||||||
|
@use standard-card
|
||||||
|
background: #24253a
|
||||||
|
border-color: #f38ba8
|
||||||
|
border-width: 2px
|
||||||
|
padding-top: 24px
|
||||||
|
padding-bottom: 12px
|
||||||
|
padding-left: 16px
|
||||||
|
padding-right: 40px
|
||||||
|
width: 320px
|
||||||
|
}
|
||||||
|
|
||||||
|
.margin-test-item {
|
||||||
|
background: #a6e3a1
|
||||||
|
padding: 8px
|
||||||
|
margin-top: 12px
|
||||||
|
margin-bottom: 4px
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Секция тестирования каскадного наследования (Inheritance) ────────────────
|
||||||
|
|
||||||
|
.inheritance-box {
|
||||||
|
@use standard-card
|
||||||
|
background: #181825
|
||||||
|
padding: 12px
|
||||||
|
color: #f9e2af
|
||||||
|
font-size: 18px
|
||||||
|
}
|
||||||
|
|
||||||
|
.override-style {
|
||||||
|
color: #f38ba8
|
||||||
|
font-size: 12px
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Секция распределения весов Flex-Grow (Fill portions) ────────────────────
|
||||||
|
|
||||||
|
.flex-row-bar {
|
||||||
|
background: $bg-surface
|
||||||
|
direction: horizontal
|
||||||
|
padding: 8px
|
||||||
|
gap: 8px
|
||||||
|
width: fill
|
||||||
|
}
|
||||||
|
|
||||||
|
.fill-10 {
|
||||||
|
background: #f38ba8
|
||||||
|
width: 10%
|
||||||
|
}
|
||||||
|
|
||||||
|
.fixed-120 {
|
||||||
|
background: #f9e2af
|
||||||
|
width: 50px
|
||||||
|
color: #11111b
|
||||||
|
}
|
||||||
|
|
||||||
|
.fill-20 {
|
||||||
|
background: #a6e3a1
|
||||||
|
width: 20%
|
||||||
|
color: #11111b
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── СТИЛИ ДЛЯ ПРОВЕРКИ POSITION: FIXED ───────────────────────────────────────
|
||||||
|
|
||||||
|
.fixed-toast {
|
||||||
|
position: fixed
|
||||||
|
top: 20px
|
||||||
|
right: 20px
|
||||||
|
background: #a6e3a1
|
||||||
|
border-radius: 8px
|
||||||
|
padding: 12px
|
||||||
|
border-width: 1px
|
||||||
|
border-color: #a6e3a1
|
||||||
|
color: #000
|
||||||
|
font-weight: bold
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-close {
|
||||||
|
background: transparen
|
||||||
|
border-width: 0px
|
||||||
|
padding: 2px
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.scroll-test-box {
|
||||||
|
@use standard-card
|
||||||
|
background: #313244
|
||||||
|
height: 150px
|
||||||
|
width: 250px
|
||||||
|
overflow-y: scroll
|
||||||
|
padding: 10px
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ── Стили для тестирования Sticky Эффекта ────────────────────
|
||||||
|
|
||||||
|
.sticky-container {
|
||||||
|
@use standard-card
|
||||||
|
background: #181825
|
||||||
|
padding: 12px
|
||||||
|
gap: 8px
|
||||||
|
width: fill
|
||||||
|
height: 200px
|
||||||
|
overflow-y: scroll
|
||||||
|
}
|
||||||
|
|
||||||
|
.sticky-header {
|
||||||
|
position: sticky
|
||||||
|
top: 0px
|
||||||
|
background: #fab387
|
||||||
|
padding: 10px 14px
|
||||||
|
border-radius: 6px
|
||||||
|
color: #11111b
|
||||||
|
font-weight: bold
|
||||||
|
font-size: 14px
|
||||||
|
width: fill
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-spacer {
|
||||||
|
gap: 100px
|
||||||
|
padding: 10px
|
||||||
|
width: fill
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Стили для тестирования Opacity ────────────────────────────
|
||||||
|
|
||||||
|
.opacity-fade {
|
||||||
|
background: #f38ba8
|
||||||
|
padding: 12px
|
||||||
|
border-radius: 8px
|
||||||
|
opacity: 0.3
|
||||||
|
}
|
||||||
|
|
||||||
|
.opacity-mid {
|
||||||
|
background: #a6e3a1
|
||||||
|
padding: 12px
|
||||||
|
border-radius: 8px
|
||||||
|
opacity: 0.6
|
||||||
|
}
|
||||||
|
|
||||||
|
.opacity-subtle {
|
||||||
|
background: #89b4fa
|
||||||
|
padding: 12px
|
||||||
|
border-radius: 8px
|
||||||
|
opacity: 0.85
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Стили для тестирования Font-Weight ─────────────────────────
|
||||||
|
|
||||||
|
.fw-light {
|
||||||
|
font-weight: 300
|
||||||
|
color: $text-main
|
||||||
|
}
|
||||||
|
|
||||||
|
.fw-normal {
|
||||||
|
font-weight: 400
|
||||||
|
color: $text-main
|
||||||
|
}
|
||||||
|
|
||||||
|
.fw-bold {
|
||||||
|
font-weight: 700
|
||||||
|
color: $text-main
|
||||||
|
}
|
||||||
|
|
||||||
|
.fw-black {
|
||||||
|
font-weight: 900
|
||||||
|
color: $text-main
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Стили для тестирования Text-Align ──────────────────────────
|
||||||
|
|
||||||
|
.ta-box {
|
||||||
|
background: #313244
|
||||||
|
padding: 8px
|
||||||
|
border-radius: 6px
|
||||||
|
width: fill
|
||||||
|
}
|
||||||
|
|
||||||
|
.ta-left {
|
||||||
|
text-align: left
|
||||||
|
}
|
||||||
|
|
||||||
|
.ta-center {
|
||||||
|
text-align: center
|
||||||
|
}
|
||||||
|
|
||||||
|
.ta-right {
|
||||||
|
text-align: right
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Стили для тестирования Line-Height ─────────────────────────
|
||||||
|
|
||||||
|
.lh-box {
|
||||||
|
background: #313244
|
||||||
|
padding: 8px
|
||||||
|
border-radius: 6px
|
||||||
|
width: 300px
|
||||||
|
}
|
||||||
|
|
||||||
|
.lh-tight {
|
||||||
|
line-height: 0.8
|
||||||
|
}
|
||||||
|
|
||||||
|
.lh-normal {
|
||||||
|
line-height: 1.2
|
||||||
|
}
|
||||||
|
|
||||||
|
.lh-loose {
|
||||||
|
line-height: 2.0
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Стили для тестирования селекторов через запятую ────────────
|
||||||
|
|
||||||
|
.warning-text, .error-text {
|
||||||
|
font-weight: bold
|
||||||
|
padding: 8px
|
||||||
|
border-radius: 6px
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-text {
|
||||||
|
color: #f9e2af
|
||||||
|
background: #3d3520
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-text {
|
||||||
|
color: #f38ba8
|
||||||
|
background: #3d2020
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Стили для тестирования Display: None ───────────────────────
|
||||||
|
|
||||||
|
.hidden-box {
|
||||||
|
background: #f38ba8
|
||||||
|
padding: 12px
|
||||||
|
border-radius: 8px
|
||||||
|
display: none
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-section-header {
|
||||||
|
color: $accent
|
||||||
|
font-size: 16px
|
||||||
|
padding: 4px 0
|
||||||
|
margin-top: 8px
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-row {
|
||||||
|
direction: horizontal
|
||||||
|
gap: 12px
|
||||||
|
align-items: center
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Стили для тестирования POSITION: ABSOLUTE ────────────────
|
||||||
|
|
||||||
|
.abs-demo-container {
|
||||||
|
position: relative
|
||||||
|
background: #313244
|
||||||
|
padding: 16px
|
||||||
|
margin-top: 8px
|
||||||
|
border-radius: 8px
|
||||||
|
width: fill
|
||||||
|
gap: 8px
|
||||||
|
min-height: 60px
|
||||||
|
}
|
||||||
|
|
||||||
|
.abs-demo-badge {
|
||||||
|
position: absolute
|
||||||
|
top: 10px
|
||||||
|
right: 4px
|
||||||
|
background: #f38ba8
|
||||||
|
color: #11111b
|
||||||
|
font-weight: bold
|
||||||
|
font-size: 10px
|
||||||
|
padding: 4px 8px
|
||||||
|
border-radius: 4px
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Полноценная демка absolute-позиционирования ──────────────
|
||||||
|
|
||||||
|
.absolute-demo-section {
|
||||||
|
@use standard-card
|
||||||
|
background: #181825
|
||||||
|
padding: 12px
|
||||||
|
gap: 8px
|
||||||
|
width: fill
|
||||||
|
}
|
||||||
|
|
||||||
|
.abs-stage {
|
||||||
|
position: relative
|
||||||
|
background: #24253a
|
||||||
|
border-color: $accent
|
||||||
|
border-width: 1px
|
||||||
|
border-radius: 8px
|
||||||
|
padding: 16px
|
||||||
|
gap: 6px
|
||||||
|
width: fill
|
||||||
|
min-height: 80px
|
||||||
|
}
|
||||||
|
|
||||||
|
.abs-overlay {
|
||||||
|
position: absolute
|
||||||
|
top: 10px
|
||||||
|
right: 10px
|
||||||
|
background: #f38ba8
|
||||||
|
border-radius: 6px
|
||||||
|
padding: 8px 12px
|
||||||
|
color: #11111b
|
||||||
|
font-weight: bold
|
||||||
|
font-size: 12px
|
||||||
|
gap: 4px
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user