diff --git a/docs/semantic-analysis-optimization.md b/docs/semantic-analysis-optimization.md new file mode 100644 index 00000000000..c24976f2199 --- /dev/null +++ b/docs/semantic-analysis-optimization.md @@ -0,0 +1,407 @@ +# Semantic Analysis Performance Optimization Analysis + +This document analyzes allocation hotspots in the Slint compiler's semantic analysis phase and proposes optimization strategies. + +## Executive Summary + +The semantic analysis phase shows Vec-related allocation overhead in three main areas: +1. **Element duplication during inlining** - O(n) clones per element, recursive +2. **Exports sorting** - O(n²) insertion sort pattern +3. **Incremental Vec growth** - No pre-allocation for children, states, transitions + +--- + +## 1. Critical Hotspots + +### 1.1 Element Duplication in Inlining Pass + +**Location:** `internal/compiler/passes/inlining.rs:349-405` + +This is the most allocation-heavy operation. Each element duplication clones ~15 fields: + +```rust +fn duplicate_element_with_mapping(element: &ElementRc, ...) -> ElementRc { + let elem = element.borrow(); + let new = Rc::new(RefCell::new(Element { + base_type: elem.base_type.clone(), // Clone + id: elem.id.clone(), // Clone (SmolStr - cheap) + property_declarations: elem.property_declarations.clone(), // BTreeMap clone! + bindings: elem.bindings.iter() + .map(|b| duplicate_binding(...)) + .collect(), // New BTreeMap + N clones + change_callbacks: elem.change_callbacks.clone(), // BTreeMap> clone! + property_analysis: elem.property_analysis.clone(), + children: elem.children.iter() + .map(|x| duplicate_element_with_mapping(...)) // RECURSIVE + .collect(), // New Vec + states: elem.states.clone(), // Vec clone + transitions: elem.transitions.iter() + .map(|t| duplicate_transition(...)) + .collect(), // New Vec + N clones + debug: elem.debug.clone(), // Vec clone + // ... more fields + })); +} +``` + +**Impact:** For a component with 100 elements, each with 5 properties and 2 children on average: +- ~1,500 BTreeMap/Vec clones +- ~500 binding duplications +- All recursive, so memory pressure compounds + +**Optimization Options:** + +| Option | Description | Effort | Impact | +|--------|-------------|--------|--------| +| **Arena allocation** | Use `bumpalo` or typed arena for elements during compilation | High | High | +| **Copy-on-write** | Use `Cow<>` or `Arc` for fields that rarely change | Medium | Medium | +| **Lazy cloning** | Clone only when mutated (track "dirty" flag) | Medium | High | +| **Pre-sized Vecs** | Use `Vec::with_capacity()` based on source element size | Low | Low | + +### 1.2 O(n²) Export Sorting + +**Location:** `internal/compiler/object_tree.rs:2950-2959` + +```rust +let mut sorted_exports_with_duplicates: Vec<(ExportedName, _)> = Vec::new(); + +let mut extend_exports = |it: &mut dyn Iterator<...>| { + for (name, compo_or_type) in it { + let pos = sorted_exports_with_duplicates + .partition_point(|(existing_name, _)| existing_name.name <= name.name); // O(log n) + sorted_exports_with_duplicates.insert(pos, (name, compo_or_type)); // O(n)! + } +}; + +extend_exports(&mut ...); // Called 3 times +``` + +**Impact:** For 50 exports, this is ~2,500 element shifts (50 × 50 / 2). + +**Optimization Options:** + +| Option | Description | Effort | Impact | +|--------|-------------|--------|--------| +| **Collect then sort** | Collect all into Vec, then call `.sort_by()` once | Low | High | +| **BTreeMap intermediate** | Use BTreeMap for sorted insertion, convert to Vec | Low | Medium | + +**Suggested fix:** +```rust +let mut exports: Vec<(ExportedName, _)> = doc.ExportsList() + .filter(...) + .flat_map(...) + .chain(doc.ExportsList().flat_map(...)) // Combine all iterators + .chain(doc.ExportsList().flat_map(...)) + .collect(); + +exports.sort_by(|(a, _), (b, _)| a.name.cmp(&b.name)); +exports.dedup_by(|(a, _), (b, _)| a.name == b.name); +``` + +### 1.3 PropertyPath Cloning in Binding Analysis + +**Location:** `internal/compiler/passes/binding_analysis.rs:105` + +```rust +fn relative(&self, second: &PropertyPath) -> Self { + // ... + let mut elements = self.elements.clone(); // Full Vec clone + loop { + if let Some(last) = elements.pop() { + // May only pop a few elements + } + } + // ... +} +``` + +**Impact:** Called frequently during binding dependency analysis. Clones full path even when only truncating. + +**Optimization Options:** + +| Option | Description | Effort | Impact | +|--------|-------------|--------|--------| +| **Slice view** | Return slice indices instead of cloned Vec | Medium | High | +| **Truncate in place** | Pass `&mut self` and truncate, or use `Cow` | Low | Medium | +| **SmallVec** | Use `SmallVec<[_; 4]>` for typical short paths | Low | Medium | + +--- + +## 2. Data Structure Analysis + +### 2.1 Element Struct + +**Location:** `internal/compiler/object_tree.rs:813-882` + +```rust +pub struct Element { + pub id: SmolStr, // 24 bytes, inline small strings + pub base_type: ElementType, // enum, variable size + pub bindings: BTreeMap>, // Heap + pub change_callbacks: BTreeMap>>, // Double heap! + pub property_analysis: RefCell>, + pub children: Vec, // Heap, grows incrementally + pub property_declarations: BTreeMap, + pub states: Vec, // Heap + pub transitions: Vec, // Heap + pub debug: Vec, // Heap + // ... 15 more fields +} +``` + +**Issues:** +1. `change_callbacks: BTreeMap>>` - Triple indirection (map → RefCell → Vec) +2. `children` grows via `.push()` without capacity hints +3. Many small Vecs allocated separately + +**Optimization Options:** + +| Field | Current | Proposed | Rationale | +|-------|---------|----------|-----------| +| `children` | `Vec` | `SmallVec<[ElementRc; 4]>` | Most elements have ≤4 children | +| `states` | `Vec` | `SmallVec<[State; 2]>` | Most elements have 0-2 states | +| `transitions` | `Vec` | `SmallVec<[Transition; 2]>` | Most have 0-2 transitions | +| `debug` | `Vec` | `SmallVec<[ElementDebugInfo; 1]>` | Usually exactly 1 | +| `change_callbacks` | `BTreeMap>>` | `BTreeMap>` | Usually 1 callback per property | + +### 2.2 Expression Enum + +**Location:** `internal/compiler/expression_tree.rs:600-760` + +Heavy use of `Box` for recursive variants: + +```rust +pub enum Expression { + BinaryExpression { lhs: Box, rhs: Box, op: char }, + UnaryOp { sub: Box, op: char }, + Condition { condition: Box, true_expr: Box, false_expr: Box }, + StructFieldAccess { base: Box, name: SmolStr }, + // ... 13 more Box-using variants +} +``` + +**Impact:** Deep expression trees like `a + b + c + d + e` create 8+ heap allocations. + +**Optimization Options:** + +| Option | Description | Effort | Impact | +|--------|-------------|--------|--------| +| **Arena allocation** | Allocate expressions from arena, store indices | High | High | +| **Flattened representation** | Store as bytecode-like Vec, index-based | High | High | +| **Inline small expressions** | Use `enum { Inline(SmallExpr), Boxed(Box) }` | Medium | Medium | + +### 2.3 Component Struct + +**Location:** `internal/compiler/object_tree.rs:452-493` + +```rust +pub struct Component { + pub optimized_elements: RefCell>, + pub popup_windows: RefCell>, + pub timers: RefCell>, + pub menu_item_tree: RefCell>>, + pub exported_global_names: RefCell>, + pub private_properties: RefCell>, + pub init_code: RefCell, // Contains 3 more Vecs +} +``` + +**Issue:** 7+ `RefCell>` fields, each requiring: +1. RefCell borrow checking overhead +2. Separate heap allocation per Vec +3. No capacity hints + +**Optimization Options:** + +| Option | Description | Effort | Impact | +|--------|-------------|--------|--------| +| **Consolidate Vecs** | Use single Vec with tagged enum for different types | Medium | Medium | +| **SmallVec** | Use `SmallVec<[T; N]>` for typically-small collections | Low | Medium | +| **Remove RefCell** | Use indices + arena for mutation without RefCell | High | High | + +--- + +## 3. Incremental Vec Growth Patterns + +### 3.1 Children Population + +**Location:** `internal/compiler/object_tree.rs:1693-1734` + +```rust +// Inside a loop iterating node.children() +r.borrow_mut().children.push(Element::from_sub_element_node(...)); +// ... +r.borrow_mut().children.push(rep); // For repeated elements +// ... +r.borrow_mut().children.push(rep); // For conditional elements +``` + +**Issue:** Vec grows 1 element at a time. Rust's Vec doubles capacity, but still causes multiple reallocations. + +**Optimization:** +```rust +// Before the loop: +let child_count = node.children().filter(|n| /* is element */).count(); +r.borrow_mut().children.reserve(child_count); +``` + +### 3.2 States and Transitions + +**Location:** `internal/compiler/object_tree.rs:1762-1797` + +```rust +// Inside loops +r.borrow_mut().transitions.push(t); +r.borrow_mut().states.push(s); +``` + +**Optimization:** +```rust +let state_count = node.State().count(); +let transition_count = node.Transition().count() + + node.State().flat_map(|s| s.Transition()).count(); + +let mut elem = r.borrow_mut(); +elem.states.reserve(state_count); +elem.transitions.reserve(transition_count); +``` + +--- + +## 4. Recommended Optimization Priority + +### High Priority (Low effort, High impact) + +1. **Fix O(n²) export sorting** - Replace insertion sort with collect-then-sort + - File: `object_tree.rs:2950-2959` + - Estimated complexity: ~20 lines changed + +2. **Pre-allocate children Vec** - Count children before populating + - File: `object_tree.rs:1693` + - Estimated complexity: ~5 lines added + +3. **SmallVec for Element fields** - Replace `Vec` with `SmallVec` for: + - `children: SmallVec<[ElementRc; 4]>` + - `states: SmallVec<[State; 2]>` + - `transitions: SmallVec<[Transition; 2]>` + - `debug: SmallVec<[ElementDebugInfo; 1]>` + - Estimated complexity: Type changes + Cargo.toml dependency + +### Medium Priority (Medium effort, High impact) + +4. **PropertyPath slice optimization** - Avoid cloning in `relative()` + - File: `binding_analysis.rs:105` + - Estimated complexity: ~30 lines refactored + +5. **SmallVec for PropertyPath::elements** + - Typical paths are short (1-4 elements) + +6. **change_callbacks simplification** - Remove RefCell from values + - `BTreeMap>` + +### Lower Priority (High effort, High impact) + +7. **Arena allocation for Elements** - Use `typed-arena` or `bumpalo` + - Eliminates per-element Rc overhead + - Requires significant refactoring + +8. **Lazy element cloning in inlining** - Copy-on-write semantics + - Only clone when mutating + +9. **Expression arena** - Allocate all expressions from arena + - Eliminates Box overhead for expression trees + +--- + +## 5. Benchmarks + +Benchmarks for the semantic analysis phase are located in `internal/compiler/benches/semantic_analysis.rs`. + +### Running Benchmarks + +```bash +# Run all benchmarks +cargo bench -p i-slint-compiler + +# Run specific category +cargo bench -p i-slint-compiler -- full_compilation + +# Run specific benchmark +cargo bench -p i-slint-compiler -- "full_compilation::nested_components" + +# Quick test (verify benchmarks work without timing) +cargo bench -p i-slint-compiler -- --test +``` + +### Benchmark Categories + +| Category | Benchmarks | What it Measures | +|----------|------------|------------------| +| `lexing` | simple, many_children, many_properties | Token allocation in lexer | +| `parsing` | simple, many_children, many_properties | Syntax tree creation | +| `full_compilation` | 8 scenarios (see below) | End-to-end compilation | +| `expression_complexity` | binary chains, struct field access | Expression tree allocations | + +### Stress Test Scenarios + +Each scenario is designed to stress a specific allocation pattern: + +| Benchmark | Parameters | Hotspot Targeted | +|-----------|------------|------------------| +| `many_children` | 10, 50, 100, 200 | `children: Vec` growth | +| `many_properties` | 10, 50, 100 | `property_declarations` BTreeMap | +| `many_exports` | 5, 20, 60 | O(n²) export sorting (realistic: app, std-widgets, material lib) | +| `many_states` | 5, 10, 20 | `states`/`transitions` Vec allocation | +| `nested_components` | 5, 10, 15 | Inlining pass element duplication | +| `deep_expressions` | 5, 10, 20 | `Box` chain allocation | +| `binding_chain` | 10, 50, 100 | Binding analysis dependency tracking | +| `struct_field_access_chain` | 3, 5, 8 | Nested `StructFieldAccess` expressions | + +### Baseline Results + +Representative results on a typical development machine (times will vary): + +``` +full_compilation::simple_component ~10ms (baseline) +full_compilation::nested_components/5 ~10ms +full_compilation::nested_components/10 ~11ms +full_compilation::nested_components/15 ~12ms (+20% vs baseline) +full_compilation::many_children/100 ~10ms +full_compilation::many_exports/60 ~10ms (largest real-world case) +``` + +### Measuring Allocations + +For detailed allocation profiling, use `dhat` or a custom allocator: + +```rust +// Add to benchmark or test +#[global_allocator] +static ALLOC: divan::AllocProfiler = divan::AllocProfiler::system(); +``` + +Or use external tools: +```bash +# macOS: Instruments with Allocations template +# Linux: heaptrack or valgrind --tool=massif +# Cross-platform: cargo-instruments, samply +``` + +--- + +## 6. SmallVec Sizing Recommendations + +Based on typical Slint component structure: + +| Collection | Typical Size | Recommended SmallVec | +|------------|--------------|---------------------| +| Element.children | 2-4 | `SmallVec<[_; 4]>` | +| Element.states | 0-2 | `SmallVec<[_; 2]>` | +| Element.transitions | 0-2 | `SmallVec<[_; 2]>` | +| Element.debug | 1 | `SmallVec<[_; 1]>` | +| PropertyPath.elements | 1-3 | `SmallVec<[_; 4]>` | +| InitCode.constructor_code | 0-5 | `SmallVec<[_; 4]>` | +| Component.popup_windows | 0-1 | `SmallVec<[_; 1]>` | +| Component.timers | 0-2 | `SmallVec<[_; 2]>` | + +Note: SmallVec inline storage should be sized to fit common cases without exceeding reasonable stack usage (~64-128 bytes per SmallVec). diff --git a/internal/compiler/Cargo.toml b/internal/compiler/Cargo.toml index 70b05e5d789..0a13b95bde0 100644 --- a/internal/compiler/Cargo.toml +++ b/internal/compiler/Cargo.toml @@ -85,6 +85,12 @@ i-slint-parser-test-macro = { path = "./parser-test-macro" } regex = "1.3.7" spin_on = { workspace = true } rayon = { workspace = true } +divan = "0.1.14" + +[[bench]] +name = "semantic_analysis" +harness = false +required-features = ["rust"] [package.metadata.docs.rs] features = [ diff --git a/internal/compiler/benches/semantic_analysis.rs b/internal/compiler/benches/semantic_analysis.rs new file mode 100644 index 00000000000..6c47203adc9 --- /dev/null +++ b/internal/compiler/benches/semantic_analysis.rs @@ -0,0 +1,556 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + +//! Benchmarks for the compiler's semantic analysis phase. +//! +//! These benchmarks measure the performance of various compilation stages, +//! focusing on areas identified as allocation hotspots. +//! +//! Run with: cargo bench -p i-slint-compiler --features rust +//! +//! To run a specific benchmark: +//! cargo bench -p i-slint-compiler --features rust -- full_compilation +//! +//! To run proc-macro simulation (measures full slint! macro overhead): +//! cargo bench -p i-slint-compiler --features rust -- proc_macro_simulation +//! +//! To get allocation statistics, set DIVAN_BYTES=1 + +use i_slint_compiler::CompilerConfiguration; +use i_slint_compiler::diagnostics::{BuildDiagnostics, SourceFile, SourceFileInner}; +use i_slint_compiler::object_tree::Document; +use i_slint_compiler::parser; +use std::path::PathBuf; +use std::rc::Rc; + +#[global_allocator] +static ALLOC: divan::AllocProfiler = divan::AllocProfiler::system(); + +/// Minimal valid Slint document - baseline for proc-macro overhead +const EMPTY_COMPONENT: &str = "export component Empty {}"; + +/// Simple component for baseline measurements +const SIMPLE_COMPONENT: &str = r#" +export component Simple inherits Rectangle { + width: 100px; + height: 100px; + background: blue; + + Text { + text: "Hello"; + color: white; + } +} +"#; + +/// Component with many children to stress children Vec allocation +fn generate_many_children(count: usize) -> String { + let mut s = String::from("export component ManyChildren inherits Rectangle {\n"); + for i in 0..count { + s.push_str(&format!( + " rect{i}: Rectangle {{ x: {x}px; y: {y}px; width: 10px; height: 10px; }}\n", + x = (i % 10) * 15, + y = (i / 10) * 15 + )); + } + s.push_str("}\n"); + s +} + +/// Component with many properties to stress property declaration allocations +fn generate_many_properties(count: usize) -> String { + let mut s = String::from("export component ManyProps inherits Rectangle {\n"); + for i in 0..count { + s.push_str(&format!(" in-out property prop{i}: {i};\n")); + } + s.push_str(" width: 100px;\n"); + s.push_str(" height: 100px;\n"); + s.push_str("}\n"); + s +} + +/// Component with deep expression trees to stress Box allocations +fn generate_deep_expressions(depth: usize) -> String { + let mut expr = String::from("1"); + for i in 2..=depth { + expr = format!("({expr} + {i})"); + } + format!( + r#" +export component DeepExpr inherits Rectangle {{ + property result: {expr}; + width: 100px; + height: 100px; +}} +"# + ) +} + +/// Component with many states to stress states Vec allocation +fn generate_many_states(count: usize) -> String { + let mut s = String::from( + r#" +export component ManyStates inherits Rectangle { + in-out property current-state: 0; + width: 100px; + height: 100px; + background: gray; +"#, + ); + for i in 0..count { + s.push_str(&format!( + r#" + states [ + state{i} when current-state == {i}: {{ + background: rgb({r}, {g}, {b}); + }} + ] +"#, + r = (i * 7) % 256, + g = (i * 13) % 256, + b = (i * 23) % 256 + )); + } + s.push_str("}\n"); + s +} + +/// Component with nested sub-components to stress inlining +fn generate_nested_components(depth: usize) -> String { + let mut s = String::new(); + for i in (0..depth).rev() { + if i == depth - 1 { + s.push_str(&format!( + r#" +component Level{i} inherits Rectangle {{ + width: 10px; + height: 10px; + background: red; +}} +"# + )); + } else { + s.push_str(&format!( + r#" +component Level{i} inherits Rectangle {{ + Level{next} {{ }} + width: parent.width + 10px; + height: parent.height + 10px; +}} +"#, + next = i + 1 + )); + } + } + s.push_str( + r#" +export component NestedComponents inherits Rectangle { + Level0 { } + width: 200px; + height: 200px; +} +"#, + ); + s +} + +/// Component with many exports to stress export sorting +fn generate_many_exports(count: usize) -> String { + let mut s = String::new(); + // Generate components in non-alphabetical order to stress sorting + let mut indices: Vec = (0..count).collect(); + indices.sort_by(|a, b| ((b * 17) % count).cmp(&((a * 17) % count))); + + for i in indices { + s.push_str(&format!( + r#" +export component Export{i:04} inherits Rectangle {{ + width: 10px; + height: 10px; +}} +"# + )); + } + s +} + +/// Component with bindings that have dependencies (for binding analysis) +fn generate_binding_chain(length: usize) -> String { + let mut s = String::from( + r#" +export component BindingChain inherits Rectangle { + property start: 1; +"#, + ); + for i in 0..length { + s.push_str(&format!(" property step{i}: start + {i};\n")); + } + s.push_str(&format!(" property end: step{};\n", length.saturating_sub(1))); + s.push_str(" width: 100px;\n"); + s.push_str(" height: 100px;\n"); + s.push_str("}\n"); + s +} + +/// Parse source code into a syntax node +fn parse_source(source: &str) -> parser::SyntaxNode { + let mut diagnostics = BuildDiagnostics::default(); + let tokens = i_slint_compiler::lexer::lex(source); + let source_file: SourceFile = + Rc::new(SourceFileInner::new(PathBuf::from("bench.slint"), source.to_string())); + parser::parse_tokens(tokens, source_file, &mut diagnostics) +} + +/// Full compilation including all passes +fn compile_full(source: &str) -> (Document, BuildDiagnostics) { + let diagnostics = BuildDiagnostics::default(); + let node = parse_source(source); + + let config = CompilerConfiguration::new(i_slint_compiler::generator::OutputFormat::Interpreter); + + let (doc, diag, _loader) = + spin_on::spin_on(i_slint_compiler::compile_syntax_node(node, diagnostics, config)); + (doc, diag) +} + +// ============================================================================ +// Benchmarks +// ============================================================================ + +mod parsing { + use super::*; + + #[divan::bench] + fn simple_component() { + divan::black_box(parse_source(SIMPLE_COMPONENT)); + } + + #[divan::bench(args = [10, 50, 100, 200])] + fn many_children(n: usize) { + let source = generate_many_children(n); + divan::black_box(parse_source(&source)); + } + + #[divan::bench(args = [10, 50, 100])] + fn many_properties(n: usize) { + let source = generate_many_properties(n); + divan::black_box(parse_source(&source)); + } +} + +mod lexing { + use super::*; + + #[divan::bench] + fn simple_component() { + divan::black_box(i_slint_compiler::lexer::lex(SIMPLE_COMPONENT)); + } + + #[divan::bench(args = [10, 50, 100, 200])] + fn many_children(n: usize) { + let source = generate_many_children(n); + divan::black_box(i_slint_compiler::lexer::lex(&source)); + } + + #[divan::bench(args = [10, 50, 100])] + fn many_properties(n: usize) { + let source = generate_many_properties(n); + divan::black_box(i_slint_compiler::lexer::lex(&source)); + } +} + +mod full_compilation { + use super::*; + + #[divan::bench] + fn simple_component() { + divan::black_box(compile_full(SIMPLE_COMPONENT)); + } + + #[divan::bench(args = [10, 50, 100])] + fn many_children(n: usize) { + let source = generate_many_children(n); + divan::black_box(compile_full(&source)); + } + + #[divan::bench(args = [10, 50, 100])] + fn many_properties(n: usize) { + let source = generate_many_properties(n); + divan::black_box(compile_full(&source)); + } + + #[divan::bench(args = [5, 10, 20])] + fn deep_expressions(depth: usize) { + let source = generate_deep_expressions(depth); + divan::black_box(compile_full(&source)); + } + + #[divan::bench(args = [5, 10, 15])] + fn nested_components(depth: usize) { + let source = generate_nested_components(depth); + divan::black_box(compile_full(&source)); + } + + #[divan::bench(args = [10, 50, 100])] + fn binding_chain(length: usize) { + let source = generate_binding_chain(length); + divan::black_box(compile_full(&source)); + } + + /// Realistic export counts: typical app (5), std-widgets (20), material library (60) + #[divan::bench(args = [5, 20, 60])] + fn many_exports(n: usize) { + let source = generate_many_exports(n); + divan::black_box(compile_full(&source)); + } + + #[divan::bench(args = [5, 10, 20])] + fn many_states(n: usize) { + let source = generate_many_states(n); + divan::black_box(compile_full(&source)); + } +} + +mod expression_complexity { + use super::*; + + /// Stress test for binary expression allocation + #[divan::bench(args = [10, 25, 50])] + fn binary_expression_chain(n: usize) { + let source = generate_deep_expressions(n); + divan::black_box(compile_full(&source)); + } + + /// Stress test for struct field access chains + #[divan::bench(args = [3, 5, 8])] + fn struct_field_access_chain(depth: usize) { + let mut struct_def = String::from("export struct Level0 { value: int }\n"); + for i in 1..depth { + struct_def.push_str(&format!( + "export struct Level{i} {{ inner: Level{prev} }}\n", + prev = i - 1 + )); + } + let access = (0..depth - 1).fold(String::from("data"), |acc, _| format!("{acc}.inner")); + let source = format!( + r#" +{struct_def} +export component FieldAccess inherits Rectangle {{ +property data; +property result: {access}.value; +width: 100px; +height: 100px; +}} +"#, + last = depth - 1 + ); + divan::black_box(compile_full(&source)); + } +} + +/// Benchmark simulating the proc-macro pipeline (compile + Rust code generation). +/// +/// This module measures the full cost of processing a slint! macro invocation, +/// providing a baseline for tracking down slow proc-macro expansion in rust-analyzer. +mod proc_macro_simulation { + use super::*; + + /// Compile and generate Rust code (simulates what the slint! proc-macro does) + fn compile_to_rust(source: &str) -> (proc_macro2::TokenStream, BuildDiagnostics) { + let diagnostics = BuildDiagnostics::default(); + let node = parse_source(source); + + let config = CompilerConfiguration::new(i_slint_compiler::generator::OutputFormat::Rust); + + let (doc, diag, loader) = + spin_on::spin_on(i_slint_compiler::compile_syntax_node(node, diagnostics, config)); + + let rust_code = i_slint_compiler::generator::rust::generate(&doc, &loader.compiler_config) + .expect("Rust code generation failed"); + + (rust_code, diag) + } + + /// Baseline benchmark: empty component through full proc-macro pipeline. + /// This measures the minimum overhead of proc-macro expansion. + #[divan::bench] + fn empty_component() { + divan::black_box(compile_to_rust(EMPTY_COMPONENT)); + } +} + +/// Detailed phase benchmarks to identify hotspots in compilation. +mod phase_breakdown { + use super::*; + use std::cell::RefCell; + use std::rc::Rc; + + /// Phase 1: Just parsing (lexing + parsing) + #[divan::bench] + fn phase1_parsing() { + divan::black_box(parse_source(EMPTY_COMPONENT)); + } + + /// Phase 2: Create TypeLoader (includes style resolution) + #[divan::bench] + fn phase2_prepare_compile() { + let mut diag = BuildDiagnostics::default(); + let config = CompilerConfiguration::new(i_slint_compiler::generator::OutputFormat::Rust); + divan::black_box(i_slint_compiler::typeloader::TypeLoader::new( + i_slint_compiler::typeregister::TypeRegister::builtin(), + config, + &mut diag, + )); + } + + /// Phase 3: Load dependencies (slint-widgets.slint and its deps) + #[divan::bench] + fn phase3_load_dependencies() { + let mut diag = BuildDiagnostics::default(); + let config = CompilerConfiguration::new(i_slint_compiler::generator::OutputFormat::Rust); + let mut loader = i_slint_compiler::typeloader::TypeLoader::new( + i_slint_compiler::typeregister::TypeRegister::builtin(), + config, + &mut diag, + ); + let doc_node: i_slint_compiler::parser::syntax_nodes::Document = + parse_source(EMPTY_COMPONENT).into(); + let type_registry = Rc::new(RefCell::new( + i_slint_compiler::typeregister::TypeRegister::new(&loader.global_type_registry), + )); + divan::black_box(spin_on::spin_on(loader.load_dependencies_recursively( + &doc_node, + &mut diag, + &type_registry, + ))); + } + + /// Phase 4: Full compile_syntax_node (parsing + loading + passes) + #[divan::bench] + fn phase4_compile_syntax_node() { + let diagnostics = BuildDiagnostics::default(); + let node = parse_source(EMPTY_COMPONENT); + let config = CompilerConfiguration::new(i_slint_compiler::generator::OutputFormat::Rust); + divan::black_box(spin_on::spin_on(i_slint_compiler::compile_syntax_node( + node, + diagnostics, + config, + ))); + } + + /// Phase 5: Just Rust code generation (given already compiled doc) + #[divan::bench] + fn phase5_rust_codegen() { + // First compile to get the document + let diagnostics = BuildDiagnostics::default(); + let node = parse_source(EMPTY_COMPONENT); + let config = CompilerConfiguration::new(i_slint_compiler::generator::OutputFormat::Rust); + let (doc, _diag, loader) = + spin_on::spin_on(i_slint_compiler::compile_syntax_node(node, diagnostics, config)); + + // Now benchmark just the code generation + divan::black_box( + i_slint_compiler::generator::rust::generate(&doc, &loader.compiler_config).unwrap(), + ); + } + + /// Phase 4a: Document::from_node (creates object tree from syntax) + #[divan::bench] + fn phase4a_document_from_node() { + let mut diag = BuildDiagnostics::default(); + let config = CompilerConfiguration::new(i_slint_compiler::generator::OutputFormat::Rust); + let mut loader = i_slint_compiler::typeloader::TypeLoader::new( + i_slint_compiler::typeregister::TypeRegister::builtin(), + config, + &mut diag, + ); + let doc_node: i_slint_compiler::parser::syntax_nodes::Document = + parse_source(EMPTY_COMPONENT).into(); + let type_registry = Rc::new(RefCell::new( + i_slint_compiler::typeregister::TypeRegister::new(&loader.global_type_registry), + )); + let (foreign_imports, reexports) = spin_on::spin_on(loader.load_dependencies_recursively( + &doc_node, + &mut diag, + &type_registry, + )); + + // Benchmark just Document::from_node + divan::black_box(i_slint_compiler::object_tree::Document::from_node( + doc_node, + foreign_imports, + reexports, + &mut diag, + &type_registry, + )); + } + + /// Phase 4b: run_passes (all compiler passes) + #[divan::bench] + fn phase4b_run_passes() { + let mut diag = BuildDiagnostics::default(); + let config = CompilerConfiguration::new(i_slint_compiler::generator::OutputFormat::Rust); + let mut loader = i_slint_compiler::typeloader::TypeLoader::new( + i_slint_compiler::typeregister::TypeRegister::builtin(), + config, + &mut diag, + ); + let doc_node: i_slint_compiler::parser::syntax_nodes::Document = + parse_source(EMPTY_COMPONENT).into(); + let type_registry = Rc::new(RefCell::new( + i_slint_compiler::typeregister::TypeRegister::new(&loader.global_type_registry), + )); + let (foreign_imports, reexports) = spin_on::spin_on(loader.load_dependencies_recursively( + &doc_node, + &mut diag, + &type_registry, + )); + let mut doc = i_slint_compiler::object_tree::Document::from_node( + doc_node, + foreign_imports, + reexports, + &mut diag, + &type_registry, + ); + + // Benchmark just run_passes + divan::black_box(spin_on::spin_on(i_slint_compiler::passes::run_passes( + &mut doc, + &mut loader, + false, + &mut diag, + ))); + } + + /// Phase 4b1: Just import StyleMetrics and Palette (start of run_passes) + #[divan::bench] + fn phase4b1_import_style_components() { + let mut diag = BuildDiagnostics::default(); + let config = CompilerConfiguration::new(i_slint_compiler::generator::OutputFormat::Rust); + let mut loader = i_slint_compiler::typeloader::TypeLoader::new( + i_slint_compiler::typeregister::TypeRegister::builtin(), + config, + &mut diag, + ); + + // Benchmark just the import_component calls + let mut build_diags_to_ignore = BuildDiagnostics::default(); + let _style_metrics = spin_on::spin_on(loader.import_component( + "slint-widgets.slint", + "StyleMetrics", + &mut build_diags_to_ignore, + )); + let _palette = spin_on::spin_on(loader.import_component( + "slint-widgets.slint", + "Palette", + &mut build_diags_to_ignore, + )); + + // avoid the unused variables being optimized away + divan::black_box((_style_metrics, _palette)); + } +} + +fn main() { + divan::main(); +}