Skip to main content

makepad-2-0-performance

CRITICAL: Use for Makepad 2.0 performance optimization and debugging. Triggers on: makepad performance, makepad debug, makepad profiling, makepad gc, new_batch, texture_caching, render optimization, draw batching, mod.gc, garbage collection, memory, debug logging, troubleshoot, ViewOptimize, PortalList, CachedView, render tree, invisible text, text disappears, UI freezes, scroll stuttering, 性能, 调试, 优化, 垃圾回收, 渲染, 批处理, 日志

Source facts

Repository
ZhangHanDong/makepad-skills
Last source activity
April 4, 2026 at 17:48
Detected SKILL.md language
English
Stars
748
Forks
88

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

File Explorer
2 files

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
makepad-2.0-performance
description
CRITICAL: Use for Makepad 2.0 performance optimization and debugging. Triggers on: makepad performance, makepad debug, makepad profiling, makepad gc, new_batch, texture_caching, render optimization, draw batching, mod.gc, garbage collection, memory, debug logging, troubleshoot, ViewOptimize, PortalList, CachedView, render tree, invisible text, text disappears, UI freezes, scroll stuttering, 性能, 调试, 优化, 垃圾回收, 渲染, 批处理, 日志
# Makepad 2.0 Performance & Debugging Skill ## 1. Overview Makepad 2.0 uses a unique rendering pipeline combined with the Splash script VM. Performance depends on understanding three critical subsystems: 1. **Draw Batching** - How Makepad groups GPU draw calls and why `new_batch: true` matters 2. **Garbage Collection** - The Splash VM's mark-sweep GC with per-type-bucket thresholds 3. **Render Triggers** - The `on_render` / `.render()` system that controls when sub-trees rebuild Unlike traditional retained-mode UI frameworks, Makepad uses an immediate-mode-inspired draw pipeline where widgets emit draw commands into a sorted batch list. Understanding this pipeline is essential for diagnosing invisible text, flickering, and performance regressions. --- ## 2. Draw Batching System ### How It Works Makepad automatically batches consecutive draw calls that use the **same shader** into a single GPU draw call. This is a major performance optimization, but it has a critical side effect: draw order can be surprising. ``` Draw pipeline (simplified): Widget tree: GPU batches (default): View (bg shader) Batch 1: all bg shaders Label (text) --> Batch 2: all text shaders View (bg shader) Label (text) Result: ALL backgrounds draw first, then ALL text draws second ``` When a View has `show_bg: true` AND contains text children, the text can end up **behind** the background because both text draws get batched together into a single draw call that executes before (or after) the background draw calls. ### `new_batch: true` Setting `new_batch: true` on a View forces Makepad to start a **new draw batch** at that point. This creates a `ViewOptimize::DrawList` internally, which ensures proper draw ordering within that View's subtree. ``` // PROBLEM: Label text is invisible - batched behind the background RoundedView{ width: Fill height: Fit draw_bg.color: #1e1e2e Label{text: "This text is INVISIBLE"} } // FIX: new_batch ensures background draws before text RoundedView{ width: Fill height: Fit new_batch: true draw_bg.color: #1e1e2e Label{text: "This text is VISIBLE"} } ``` ### When `new_batch: true` Is Required | Scenario | Required? | Why | |----------|-----------|-----| | View with `show_bg: true` containing Labels | YES | Text batches behind background | | View with hover animator + text children | YES | Hover bg covers text on activation | | Container of repeated items with backgrounds | YES | Each item and the container need it | | Transparent View (no `show_bg`) with Labels | NO | No background to overlap | | View with only non-text children (e.g., icons) | NO | Same shader type - no overlap issue | | Deeply nested Views each with backgrounds | YES on each | Each background layer needs its own batch | ### Hover Effects and `new_batch` This is the **number one mistake** with hoverable list items. When a View has `show_bg: true` with a hover animator that transitions from transparent (`#0000`) to opaque on hover, the text disappears on hover because the newly-opaque background covers the batched text. ``` // CORRECT: Hoverable item with new_batch let HoverItem = View{ width: Fill height: Fit new_batch: true show_bg: true draw_bg +: { color: uniform(#0000) color_hover: uniform(#fff2) hover: instance(0.0) } animator: Animator{ hover: { default: { from: {all: Forward{duration: 0.1}} apply: {draw_bg: {hover: 0.0}} } on: { from: {all: Forward{duration: 0.1}} apply: {draw_bg: {hover: 1.0}} } } } label := Label{text: "item" draw_text.color: #fff} } // Parent container of hover items also needs new_batch RoundedView{ flow: Down height: Fit new_batch: true draw_bg.color: #2a2a3d draw_bg.border_radius: 8.0 HoverItem{label.text: "First item"} HoverItem{label.text: "Second item"} } ``` ### ViewOptimize Internals The `new_batch` and `texture_caching` properties map to a `ViewOptimize` enum: ``` ViewOptimize::None - Default. No special draw ordering. ViewOptimize::DrawList - Created by new_batch: true. Starts a new DrawList2d. ViewOptimize::Texture - Created by texture_caching: true. Renders to offscreen texture. ``` Priority: `texture_caching` takes precedence over `new_batch` if both are set. --- ## 3. Texture Caching ### How It Works Setting `texture_caching: true` on a View renders its entire child sub-tree to an offscreen GPU texture. On subsequent frames, if nothing in the sub-tree has changed, Makepad can skip re-rendering the children and just blit the cached texture. ``` // Cache a complex but rarely-changing sidebar sidebar := View{ width: 280 height: Fill texture_caching: true flow: Down spacing: 4 // ... many child widgets ... } ``` ### Pre-Built Cached Views Makepad provides pre-styled cached views: | Widget | Description | |--------|-------------| | `CachedView` | Texture-cached rectangle container | | `CachedRoundedView` | Texture-cached rounded rectangle | ### When to Use Texture Caching **Good candidates:** - Complex static sidebars or toolbars that rarely change - Large widget sub-trees with many nested backgrounds and text - Decorative panels with shader effects **Bad candidates:** - Frequently updating views (e.g., animation targets, live data) - Small simple views (overhead exceeds benefit) - Views that contain scrolling content (CachedView wraps the whole content, not the viewport) ### Trade-offs | Benefit | Cost | |---------|------| | Reduces per-frame draw call count | Uses GPU memory for cached texture | | Avoids re-traversing large sub-trees | Texture must be invalidated on change | | Can eliminate batching issues (the texture resolves draw order) | DPI factor affects texture resolution | --- ## 4. Garbage Collection (mod.gc) ### Architecture The Splash VM uses a **mark-and-sweep garbage collector** with isolated heaps for different value types: ``` Heap Layout: +-- Objects (ScriptObject) -- Primary allocation type +-- Arrays (ScriptArray) -- Typed arrays and value arrays +-- Strings (ScriptString) -- Interned strings +-- Pods (ScriptPod) -- Pod values (vec2, vec3, vec4, etc.) +-- Handles (ScriptHandle) -- Native Rust handles +-- Regexes (ScriptRegex) -- Interned regex patterns ``` ### Automatic GC Triggering The GC uses a **growth-based heuristic** similar to Lua and V8: - **Growth Factor:** 2x - GC triggers when any heap category doubles since last GC - **Minimum Thresholds** (to avoid thrashing on small heaps): | Category | Minimum Before GC Can Trigger | |----------|-------------------------------| | Objects | 1,024 | | Strings | 256 | | Arrays | 128 | | Pods | 128 | | Handles | 64 | GC triggers when: `current_count >= MIN_THRESHOLD AND current_count >= last_gc_count * 2` ### Script API **`mod.gc.run()`** - Force a GC cycle immediately. Silent (no log output). **`mod.gc.run_status()`** - Force a GC cycle and print detailed statistics: ``` GC 142us: obj[S:1200 A:340 R:89] arr[S:45 A:12 R:3] str[S:890 A:120 R:15] ... ``` Where S=static (permanent), A=alive (survived), R=removed (freed). **`mod.gc.set_static(value)`** - Mark a value and its entire reachable object graph as **static**. Static objects: - Are never collected by GC (permanent) - Are skipped during GC mark phase (faster GC traversal) - Cannot be un-marked (irreversible within the VM lifetime) **`mod.gc.dump_tag(value)`** - Debug tool. Prints internal tag information for an object: type index, static flag, proto chain. ### Best Practices **Pattern: Static UI Trees** For large, stable UI tree definitions (like a Dock with many tabs), mark them as static immediately after definition. This is the standard pattern used in the Studio and UIZoo examples: ``` // Define a large widget tree let AppDock = Dock{ // ... tabs, splitters, content templates ... TabEditor := TabEditor{} TabFileTree := TabFileTree{} TabSettings := TabSettings{} } // Mark the entire tree as static - it will never be GC'd mod.gc.set_static(AppDock) // Run GC immediately to clean up any temporaries from tree construction mod.gc.run() // Now start the app startup() do #(App::script_component(vm)){ ui: Root{ main_window := Window{ body +: { // ... use AppDock here ... } } } } ``` **Pattern: Dynamic Content** For dynamic content (lists, user-generated items, chat messages), let the automatic GC handle cleanup: ``` // Dynamic data - no need to call mod.gc manually var todos = [] fn add_todo(text) { todos.push({text: text done: false}) ui.main_view.render() // Automatic GC will clean up old unreachable objects } fn delete_todo(index) { todos.splice(index, 1) ui.main_view.render() // Old todo object becomes unreachable, will be collected automatically } ``` **Pattern: Periodic Manual GC for Long-Running Apps** For apps that create and destroy many objects (e.g., chat applications with streaming responses): ``` var message_count = 0 fn on_new_message(msg) { messages.push(msg) message_count += 1 // Every 100 messages, run GC to reclaim temporary parsing objects if message_count % 100 == 0 { mod.gc.run() } ui.message_list.render() } ``` ### GC Mark Phase Details The mark phase traverses from roots: 1. Type check prototypes 2. Type defaults objects 3. Pod type defaults and objects 4. Root objects (held by Rust via `ScriptObjectRef`) 5. Root arrays (held by Rust via `ScriptArrayRef`) 6. Root handles (held by Rust via `ScriptHandleRef`) 7. Thread stacks (all live values on VM execution stacks) 8. Thread scopes 9. Method call contexts 10. Loop source values 11. Trap error/return/bail values 12. Script body scopes and tokenizer string literals 13. Native type table objects Static objects are skipped during traversal since they only reference other static values. --- ## 5. Render Optimization ### The `on_render` / `.render()` System Makepad 2.0 uses a pull-based rendering model for dynamic content. The `on_render` callback on a View only executes when `.render()` is called on that View. ``` // Define a reactive view counter_view := View{ on_render: || { Label{ text: "Count: " + state.counter draw_text.color: #fff } } } // In event handler - only re-render what changed fn increment() { state.counter += 1 ui.counter_view.render() // Only this view re-renders } ``` ### Rules for Efficient Rendering 1. **NEVER call `.render()` unnecessarily** - Each call completely rebuilds that sub-tree's widget output. 2. **Render only affected sub-trees** - If only a list changed, render only the list view, not the entire UI. 3. **Avoid rendering in tight loops** - Batch state changes, then render once: ``` // BAD: renders 100 times for i in 0..100 { items[i].value = compute(i) ui.item_list.render() // WASTEFUL - rebuilds list 100 times } // GOOD: render once after all changes for i in 0..100 { items[i].value = compute(i) } ui.item_list.render() // Render once with all changes applied ``` 4. **Use `on_startup` for initial render** - Trigger the first render when the app starts: ``` ui: Root{ on_startup: || { ui.main_view.render() } main_window := Window{ body +: { main_view := View{ on_render: || { // ... dynamic content ... } } } } } ``` ### Render Scope When `.render()` is called on a View, only that View's `on_render` callback executes. Child Views with their own `on_render` callbacks will NOT automatically re-render unless their `.render()` is also called (or they are reconstructed by the parent's `on_render`). --- ## 6. Debug Logging ### Rust-Side Logging Use the `log!` macro from Makepad's error log system: ```rust use makepad_widgets::*; // In Rust code log!("Button clicked, counter = {}", self.counter); log!("Widget action: {:?}", action); ``` ### Script-Side Logging In Splash scripts, you can use `log()` or string interpolation for debugging: ``` fn handle_click() { let value = compute_something() // Log values during development log("computed value: " + value) } ``` ### GC Status Logging Use `mod.gc.run_status()` to get a detailed breakdown of GC activity: ``` // Output example: // GC 142us: obj[S:1200 A:340 R:89] arr[S:45 A:12 R:3] str[S:890 A:120 R:15] // hdl[S:8 A:2 R:0] pod[S:200 A:45 R:10] rex[S:3 A:0 R:0] ``` Fields: - **Time** (142us) - GC cycle duration in microseconds - **S** (Static) - Objects permanently marked, never collected - **A** (Alive) - Objects that survived this GC cycle - **R** (Removed) - Objects freed in this cycle ### Tag Debugging
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub