docs: reorganize into en/ru and add English translation
This commit is contained in:
303
docs/en/architecture/01-data-flow.md
Normal file
303
docs/en/architecture/01-data-flow.md
Normal file
@@ -0,0 +1,303 @@
|
||||
# Data Flow in Glint Runtime
|
||||
|
||||
How source code turns into pixels on the screen.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph "Compilation"
|
||||
A1[".gltm markup"] --> P["Parser: glt crate"]
|
||||
A2[".glts style"] --> P
|
||||
P --> M["ModuleSoA — flat arrays"]
|
||||
M --> AST
|
||||
AST --> C["Compiler: glt crate"]
|
||||
C --> BC[".glbc bytecode"]
|
||||
end
|
||||
|
||||
subgraph "Loading"
|
||||
BC --> IR["Interpreter::run"]
|
||||
IR --> R["Reader: parses the binary"]
|
||||
R --> DOC["Document: tree + styles + variables"]
|
||||
end
|
||||
|
||||
subgraph "Initialization"
|
||||
DOC --> BOOT["iced::application boot"]
|
||||
BOOT --> RC["RheiContext: compiles Rhai scripts"]
|
||||
BOOT --> EV0["evaluate_vdom: builds the full VDOM"]
|
||||
EV0 --> APP["GlintApp: ready to run"]
|
||||
end
|
||||
|
||||
subgraph "Lifecycle: each frame"
|
||||
APP --> LOOP{"iced event loop"}
|
||||
|
||||
LOOP -->|event received| MSG[Message]
|
||||
MSG --> UPD["GlintApp::update"]
|
||||
UPD --> SET["changes a variable"]
|
||||
SET --> TV["tracker marks dependent elements as dirty"]
|
||||
TV --> DIRTY["collect dirty_set"]
|
||||
DIRTY --> VDOM["evaluate_vdom_incr: recalculate only dirty"]
|
||||
VDOM --> STYLE["StyleSheet: apply styles (with cache)"]
|
||||
STYLE --> NEW_VDOM["new VDOM"]
|
||||
|
||||
LOOP -->|on timer| VIEW["GlintApp::view"]
|
||||
VIEW --> REND["render_element: Element → Iced widget"]
|
||||
REND --> ICED["iced::Element tree"]
|
||||
ICED --> DIFF["Iced: compares with previous frame"]
|
||||
DIFF --> LAYOUT[Layout]
|
||||
LAYOUT --> DRAW["GPU draws"]
|
||||
end
|
||||
|
||||
subgraph "Styles — separate"
|
||||
STYLE --> SI["StyleIndex: looks up rules in O(1)–O(K)"]
|
||||
SI --> SC["StyleCache: doesn't parse the same thing twice"]
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Stage 1: Compilation — from text to bytecode
|
||||
|
||||
It all starts with two file types:
|
||||
|
||||
- **`.gltm`** — markup: buttons, panels, texts, sliders, and so on.
|
||||
- **`.glts`** — styles: CSS-like rules, selectors, colors, margins.
|
||||
|
||||
They are compiled by the external **`glt`** crate (not part of this repository). It does three things:
|
||||
|
||||
### 1.1 Parsing
|
||||
|
||||
`Parser` reads `.gltm` and `.glts` and stores everything in **`ModuleSoA`**.
|
||||
|
||||
**What is ModuleSoA?** SoA = Structure of Arrays. Instead of storing elements as a list of structs:
|
||||
|
||||
```text
|
||||
// Array of Structures (AoS) — the usual way
|
||||
Element { name: "Button", props: [...], children: [...] }
|
||||
Element { name: "Text", props: [...], children: [...] }
|
||||
```
|
||||
|
||||
the compiler stores them as a struct with parallel arrays:
|
||||
|
||||
```text
|
||||
// Structure of Arrays (SoA) — more efficient for the compiler
|
||||
ModuleSoA {
|
||||
type_names: ["Button", "Text", ...],
|
||||
properties_vec: [ [...], [...], ...],
|
||||
hierarchy: [parent_id, parent_id, ...],
|
||||
}
|
||||
```
|
||||
|
||||
This way the compiler iterates over all names at once (CPU cache stays hot),
|
||||
finds parent relationships faster, and applies optimizations more easily.
|
||||
|
||||
### 1.2 Building the AST
|
||||
|
||||
An AST tree is built from `ModuleSoA`. Components, if/each branches,
|
||||
and parameters are resolved here.
|
||||
|
||||
### 1.3 Bytecode generation
|
||||
|
||||
`Compiler` walks the AST and turns it into the **`.glbc`** binary format:
|
||||
- header with magic bytes (`"glBc"`)
|
||||
- string pool (all names, classes, texts — one contiguous block)
|
||||
- byte-encoded opcodes (see `opcodes.rs`: `OP_ELEM_PUSH`, `OP_PROP`, `OP_IF`, `OP_EACH`, etc.)
|
||||
|
||||
The result is a compact binary that can be loaded quickly and fed to the runtime.
|
||||
|
||||
---
|
||||
|
||||
## Stage 2: Loading — from bytecode to Document
|
||||
|
||||
The runtime takes `.glbc` and turns it into data structures that can be worked with.
|
||||
|
||||
### `Interpreter::run(bytecode) → Document`
|
||||
|
||||
Internally, `Reader` reads the bytecode sequentially:
|
||||
1. Checks magic bytes (is this really `.glbc`?)
|
||||
2. Reads the string pool
|
||||
3. Executes opcodes, building the `Element` tree on the fly
|
||||
|
||||
Two important things happen in parallel:
|
||||
|
||||
**Styles:** each encountered style directive is parsed into a `StyleRule`,
|
||||
then all rules are built into a `StyleIndex` — a catalog: "here are all rules for tag Button,
|
||||
here for class primary, here for element with id=submit". This way style lookup
|
||||
will later take not O(all rules), but O(a couple of items).
|
||||
|
||||
**Dependencies:** every property like `"text": "Hello $name"` is a hint:
|
||||
the element depends on the variable `name`. `ReactiveTracker` scans all properties,
|
||||
finds `$var` and remembers: "element ElementId(5) depends on variable 'name'".
|
||||
|
||||
The result is a **`Document`**:
|
||||
```rust
|
||||
Document {
|
||||
roots: Vec<Element>, // root elements
|
||||
components: HashMap<String, ComponentDef>, // components
|
||||
variables: HashMap<String, Value>, // initial values
|
||||
stylesheet: StyleSheet, // style sheet
|
||||
rhei_scripts: Vec<String>, // init scripts
|
||||
tracker: ReactiveTracker, // who depends on what
|
||||
interner: Interner, // unique string pool
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Stage 3: Initialization — preparing for life
|
||||
|
||||
`Document` is ready, but it needs to be "started". The Iced boot function does this.
|
||||
|
||||
### 3.1 Cloning
|
||||
|
||||
`doc.clone()` — all strings inside Element have type `&'a str` with the original
|
||||
lifetime. After cloning they become `&'static str` (the runtime
|
||||
calls `Box::leak` so strings live forever — the application runs until the window is closed).
|
||||
|
||||
### 3.2 Rhai compilation
|
||||
|
||||
`RheiContext::new(scripts)`:
|
||||
- Creates a Rhai engine (`Engine`)
|
||||
- Compiles all init scripts into AST and saves them
|
||||
- Collects all functions from the scripts into a global module
|
||||
- Then `precompile_all_from_doc()` walks the entire Element tree and compiles
|
||||
every `__on:click { ... }` and every `!rhei:expr` into cache.
|
||||
**Now on click there's no need to recompile** — just grab the AST from cache.
|
||||
|
||||
### 3.3 Running init scripts
|
||||
|
||||
`initialize()`: synchronizes variables with the Rhai scope, executes init scripts,
|
||||
pulls everything that changed from the scope.
|
||||
|
||||
### 3.4 First VDOM
|
||||
|
||||
`evaluate_vdom()` — a full traversal of the tree:
|
||||
- Substitutes variables into strings (`$name` → actual value)
|
||||
- Evaluates `@if` conditions
|
||||
- Expands `@each` into the actual number of elements
|
||||
- For each element, finds matching styles and computes `ComputedStyle`
|
||||
- Assigns `content_hash`
|
||||
|
||||
Result: `GlintApp { doc, rhei, vdom_roots }`. The first frame is ready to display.
|
||||
|
||||
---
|
||||
|
||||
## Stage 4: Lifecycle — each frame
|
||||
|
||||
Iced runs in a loop: event → `update()` → `view()` → rendering.
|
||||
|
||||
### 4.1 Event received: update()
|
||||
|
||||
The user clicked a button, moved a slider, entered text — Iced sends a `Message`.
|
||||
|
||||
```rust
|
||||
enum Message {
|
||||
SliderChanged(Option<String>, f64), // slider: (bound variable, new value)
|
||||
InputChanged(Option<String>, String), // text input
|
||||
ToggleChanged(Option<String>, bool), // checkbox
|
||||
EventTriggered(String), // button click: run Rhai script
|
||||
WindowScrolled(f32), // window scroll
|
||||
ScrollableScrolled(u64, f32), // scroll inside container
|
||||
}
|
||||
```
|
||||
|
||||
**GlintApp::update()** does the following:
|
||||
|
||||
1. **Changes the variable.** For example, `SliderChanged("volume", 75)` → `variables["volume"] = 75.0`.
|
||||
2. **Notifies the tracker:** `tracker.on_variable_changed("volume")`. The tracker checks:
|
||||
"elements with IDs 5, 12, 18 depend on this variable". It marks them as dirty.
|
||||
3. **Collects the dirty_set:** `tracker.take_dirty_set()`.
|
||||
4. **Recalculates VDOM:** `evaluate_vdom_incr(roots, &dirty_set)`. It walks the tree.
|
||||
If an element is in dirty_set — recalculates it (variable substitution, style computation).
|
||||
If not — leaves it as is. **Children of dirty elements are also recalculated** (cascade).
|
||||
|
||||
### 4.2 On timer: view()
|
||||
|
||||
Even if nothing happened, Iced calls `view()` every frame (60 times per second).
|
||||
It needs to return Iced widgets for rendering.
|
||||
|
||||
**render_element()** — a recursive function that turns an Element into an Iced widget:
|
||||
|
||||
- `Button` → `iced::button(...).on_press(...)`
|
||||
- `Text` → `iced::text("...").size(16).color(...)`
|
||||
- `Panel` → `iced::column[...].spacing(10)`, wrapped in a container with background and border
|
||||
- `Input` → `iced::text_input("placeholder", "value").on_input(...)`
|
||||
- `Image` → `iced::image(path)` or `iced::svg(path)`
|
||||
- Unknown type → just a column with children
|
||||
|
||||
Each widget is wrapped in **`apply_universal_box_model`**:
|
||||
```text
|
||||
container [margin]
|
||||
container [padding, border, background]
|
||||
scrollable (if overflow: scroll/auto)
|
||||
container [padding]
|
||||
the widget itself
|
||||
```
|
||||
|
||||
**Problem:** `render_element` creates **all** widgets from scratch every frame, even if
|
||||
the Element hasn't changed. Iced then diffs the new tree against the old one — but building
|
||||
the tree itself takes ~7ms. This is the main optimization opportunity.
|
||||
|
||||
### 4.3 Iced does its thing
|
||||
|
||||
Iced receives the `iced::Element` tree, compares it with the previous one (diff), computes
|
||||
the layout, and renders via GPU (wgpu). All of this happens without our code.
|
||||
|
||||
---
|
||||
|
||||
## Element anatomy
|
||||
|
||||
```rust
|
||||
Element {
|
||||
type_name: "Button", // what kind of element
|
||||
properties: [("label", "Click"), ("color", "red"), ...], // its properties
|
||||
computed_style: ComputedStyle { color: Some(Red), padding: Some(8px), ... }, // computed style
|
||||
element_id: ElementId(42), // unique ID in the tree
|
||||
content_hash: 0xABCD1234, // content hash (for widget cache)
|
||||
children: [Element, ...], // child elements
|
||||
}
|
||||
```
|
||||
|
||||
## Style anatomy
|
||||
|
||||
Styles are stored in `StyleSheet` and work in three stages:
|
||||
|
||||
**1. Index (`StyleIndex`):** at load time, all CSS rules are sorted into buckets:
|
||||
```text
|
||||
Rule: "Button.primary#submit { color: red; padding: 10px }"
|
||||
→ by_tag["Button"] = { RuleId(1) }
|
||||
→ by_class["primary"] = { RuleId(1) }
|
||||
→ by_id["submit"] = { RuleId(1) }
|
||||
```
|
||||
|
||||
**2. Lookup:** when we need to find styles for a `Button.primary#submit` element,
|
||||
we take the intersection of sets from all three buckets. Instead of checking 500 rules — 3 lookups.
|
||||
|
||||
**3. Cache:** even when styles are found, `ComputedStyle::compute()` parses all properties
|
||||
(color, margins, fonts — about 40 fields). This is expensive. `StyleCache` remembers
|
||||
the result: `hash(type_name, properties, epoch) → ComputedStyle`. If the element
|
||||
hasn't changed — we get the ready-made style from cache, no parsing.
|
||||
|
||||
---
|
||||
|
||||
## Event loop with a slider example
|
||||
|
||||
```
|
||||
1. User moves the volume slider
|
||||
2. Iced: SliderChanged(Some("volume"), 75.0)
|
||||
3. GlintApp::update:
|
||||
a. variables["volume"] = Float(75.0)
|
||||
b. tracker.on_variable_changed("volume")
|
||||
→ dirty: ElementId(5) — text with "$volume", ElementId(12) — width from "$volume"
|
||||
c. evaluate_vdom_incr(roots, &{5, 12})
|
||||
→ Element 5: recalculate text (new volume)
|
||||
→ Element 12: recalculate width
|
||||
→ remaining 48 elements: don't touch
|
||||
4. GlintApp::view:
|
||||
→ render_element for all 50 root elements
|
||||
→ recursively for all children (even for those 48 that didn't change)
|
||||
→ Iced receives a completely new tree of 200+ widgets
|
||||
5. Iced: diffs → finds 2 changes → redraws 2 areas
|
||||
```
|
||||
|
||||
**Bottleneck:** step 4. VDOM recalculated only 2 out of 50 elements (thanks to ReactiveTracker).
|
||||
But render_element creates widgets for all 200+ nodes. Iced then diffs anyway
|
||||
and does nothing with 198 of them, but the time to create them has already been spent.
|
||||
Reference in New Issue
Block a user