mcpField
M
makepad-2.0-performance

makepad-2.0-performance

github.com/zhanghandong开发75252 次安装29 周前更新
代码仓库

关于 makepad-2.0-performance

面向 Makepad 2.0 开发者的性能与调试技能,聚焦 UI 渲染管线和 Splash VM。内容说明绘制批处理、纹理缓存、垃圾回收、局部渲染及诊断工具。

主要功能

  1. 01讲解绘制批处理与顺序
  2. 02指导使用纹理缓存
  3. 03介绍 Splash VM 垃圾回收
  4. 04优化指定子树的渲染
  5. 05涵盖日志与 GC 诊断

适用场景

  1. 01排查文本被背景遮挡
  2. 02减少动态视图的重复渲染
  3. 03诊断长时间运行应用的内存回收

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/zhanghandong/makepad-skills --skill makepad-2.0-performance

安装后在 Claude Code 中输入/makepad-2.0-performance确认已加载。

附带文件references/optimization-guide.md

详细介绍

Makepad 2.0 性能与调试技能

1. 概述

Makepad 2.0 使用独特的渲染管线,并结合 Splash 脚本 VM。性能取决于是否理解以下三个关键子系统:

  1. 绘制批处理 - Makepad 如何对 GPU 绘制调用进行分组,以及为什么 new_batch: true 很重要
  2. 垃圾回收 - Splash VM 的标记-清除 GC,采用按类型分桶的阈值
  3. 渲染触发器 - 控制何时重建子树的 on_render / .render() 系统

与传统的保留模式 UI 框架不同,Makepad 使用受即时模式启发的绘制管线:控件会将绘制命令写入经过排序的批次列表。理解此管线对于诊断文本不可见、闪烁和性能回退至关重要。


2. 绘制批处理系统

工作原理

Makepad 会自动将使用相同着色器的连续绘制调用合并为一次 GPU 绘制调用。这是一项重要的性能优化,但有一个关键的副作用:绘制顺序可能出人意料。

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

当 View 设置了 show_bg: true 且包含文本子项时,文本可能会被绘制在背景后面,因为文本绘制可能会被合并到同一次绘制调用中,而这次调用会在背景绘制调用之前(或之后)执行。

new_batch: true

在 View 上设置 new_batch: true 会强制 Makepad 从该位置开始一个新的绘制批次。这会在内部创建一个 ViewOptimize::DrawList,确保该 View 子树中的绘制顺序正确。

// 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"}
}
何时需要 new_batch: true
场景是否需要?原因
设置了 show_bg: true 且包含 Labels 的 View是文本批次会被绘制在背景后面
带有悬停动画器和文本子项的 View是激活时悬停背景会覆盖文本
包含多个带背景项的容器是每个项和容器都需要设置
透明 View(没有 show_bg)且包含 Labels否没有背景会发生重叠
只包含非文本子项(例如图标)的 View否着色器类型相同,不存在重叠问题
每层都有背景的深层嵌套 View每层都需要每个背景层都需要独立批次
悬停效果与 new_batch

这是可悬停列表项中最常见的错误。当 View 设置了 show_bg: true,并带有一个从透明(#0000)过渡到悬停时不透明的悬停动画器时,悬停时文本会消失,因为新变为不透明的背景会覆盖批处理后的文本。

// 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 内部机制

new_batch 和 texture_caching 属性映射到一个 ViewOptimize 枚举:

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.

优先级:如果两者都已设置,texture_caching 的优先级高于 new_batch。


3. 纹理缓存

工作原理

在 View 上设置 texture_caching: true 会将整个子树渲染到离屏 GPU 纹理中。后续帧中,如果子树没有任何变化,Makepad 就可以跳过对子项的重新渲染,只需绘制缓存纹理。

// Cache a complex but rarely-changing sidebar
sidebar := View{
    width: 280 height: Fill
    texture_caching: true
    flow: Down spacing: 4
    // ... many child widgets ...
}
预构建的缓存视图

Makepad 提供了预先设置样式的缓存视图:

控件描述
CachedView使用纹理缓存的矩形容器
CachedRoundedView使用纹理缓存的圆角矩形
何时使用纹理缓存

适合的场景:

  • 很少变化的复杂静态侧边栏或工具栏
  • 包含许多嵌套背景和文本的大型控件子树
  • 带有着色器效果的装饰面板

不适合的场景:

  • 频繁更新的视图(例如动画目标、实时数据)
  • 简单的小型视图(开销大于收益)
  • 包含滚动内容的视图(CachedView 会包装整个内容,而非视口)
权衡
优势成本
减少每帧的绘制调用次数缓存纹理会占用 GPU 内存
避免重新遍历大型子树内容变化时必须使纹理缓存失效
可以消除批处理问题(纹理会解决绘制顺序问题)DPI 系数会影响纹理分辨率

4. 垃圾回收(mod.gc)

架构

Splash VM 使用标记-清除垃圾回收器,并为不同值类型隔离堆:

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
自动触发 GC

GC 使用基于增长的启发式策略,类似于 Lua 和 V8:

  • 增长因子: 2 倍——任意堆类别自上次 GC 后增长到两倍时,触发 GC
  • 最小阈值(避免小堆频繁触发):
类别GC 可触发前的最小数量
对象1,024
字符串256
数组128
Pod128
句柄64

满足以下条件时触发 GC:current_count >= MIN_THRESHOLD AND current_count >= last_gc_count * 2

脚本 API

mod.gc.run() - 立即强制执行一次 GC。不会输出日志。

mod.gc.run_status() - 强制执行一次 GC 并打印详细统计信息:

GC 142us: obj[S:1200 A:340 R:89] arr[S:45 A:12 R:3] str[S:890 A:120 R:15] ...

其中 S=静态(永久),A=存活(已保留),R=已移除(已释放)。

mod.gc.set_static(value) - 将一个值及其整个可达对象图标记为静态。静态对象:

  • 永不被 GC 回收(永久保留)
  • 在 GC 标记阶段跳过(加快 GC 遍历)
  • 无法取消标记(在 VM 生命周期内不可逆)

mod.gc.dump_tag(value) - 调试工具。打印对象的内部标签信息:类型索引、静态标志、原型链。

最佳实践

模式:静态 UI 树

对于大型且稳定的 UI 树定义(例如包含许多标签页的 Dock),定义完成后立即将其标记为静态。这是 Studio 和 UIZoo 示例中使用的标准模式:

// 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 ...
            }
        }
    }
}

模式:动态内容

对于动态内容(列表、用户生成的项目、聊天消息),让自动 GC 负责清理:

// 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
}

模式:长时间运行的应用定期手动触发 GC

对于会创建和销毁大量对象的应用(例如带有流式响应的聊天应用):

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 标记阶段详情

标记阶段从根开始遍历:

  1. 类型检查原型
  2. 类型默认对象
  3. Pod 类型默认值和对象
  4. 根对象(由 Rust 通过 ScriptObjectRef 持有)
  5. 根数组(由 Rust 通过 ScriptArrayRef 持有)
  6. 根句柄(由 Rust 通过 ScriptHandleRef 持有)
  7. 线程栈(VM 执行栈上的所有存活值)
  8. 线程作用域
  9. 方法调用上下文
  10. 循环源值
  11. 陷阱错误/返回/退出值
  12. 脚本体作用域和分词器字符串字面量
  13. 原生类型表对象

遍历时会跳过静态对象,因为它们只引用其他静态值。


5. 渲染优化

on_render / .render() 系统

Makepad 2.0 对动态内容采用拉取式渲染模型。只有在对某个 View 调用 .render() 时,该 View 上的 on_render 回调才会执行。

// 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
}
高效渲染规则
  1. 绝不要无故调用 .render() - 每次调用都会完全重建该子树的 widget 输出。

  2. 仅渲染受影响的子树 - 如果只有列表发生变化,只渲染列表视图,而非整个 UI。

  3. 避免在紧密循环中渲染 - 批量修改状态,然后只渲染一次:

// 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
  1. 使用 on_startup 进行初始渲染 - 应用启动时触发首次渲染:
ui: Root{
    on_startup: || {
        ui.main_view.render()
    }
    main_window := Window{
        body +: {
            main_view := View{
                on_render: || {
                    // ... dynamic content ...
                }
            }
        }
    }
}
渲染作用域

对某个 View 调用 .render() 时,仅执行该 View 的 on_render 回调。具有自身 on_render 回调的子 View 不会自动重新渲染,除非也对它们调用 .render()(或由父级的 on_render 重新构建)。


6. 调试日志

Rust 端日志

使用 Makepad 错误日志系统中的 log! 宏:

use makepad_widgets::*;

// In Rust code
log!("Button clicked, counter = {}", self.counter);
log!("Widget action: {:?}", action);
脚本端日志

在 Splash 脚本中,可以使用 log() 或字符串插值进行调试:

fn handle_click() {
    let value = compute_something()
    // Log values during development
    log("computed value: " + value)
}
GC 状态日志

使用 mod.gc.run_status() 获取 GC 活动的详细信息:

// 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]

字段:

  • Time(142us)- GC 周期持续时间,单位为微秒
  • S(Static)- 永久标记、永不回收的对象
  • A(Alive)- 在此 GC 周期中存活的对象
  • R(Removed)- 在此周期中释放的对象
标签调试

如需深入调试特定对象,请使用 mod.gc.dump_tag(value):

let my_widget = View{...}
mod.gc.dump_tag(my_widget)
// Output: obj 4523 type_index=Some(12) is_static=false proto=Some(89) ...

7. 常见性能问题及修复方法

问题原因修复方法
文本不可见缺少 new_batch在父级 View 中添加 new_batch: true,并设置 show_bg: true
悬停时文本消失悬停动画期间批次重叠在可悬停的 View 中添加 new_batch: true
UI 卡顿/停顿.render() 调用过多批量处理状态更改,仅渲染发生更改的子树
内存持续增长GC 未运行或存在大量静态泄漏对稳定树使用 mod.gc.set_static(),让自动 GC 处理动态内容
初始加载缓慢启动时脚本求值量过大拆分为多个模块,采用延迟加载模式
滚动卡顿渲染的项目过多使用 PortalList 进行虚拟化渲染
悬停无响应View 缺少 show_bg: trueView 需要设置 show_bg: true 才能接收鼠标悬停事件
运行时找不到 Widget命名运算符错误对具名/可寻址子项使用 :=(而非 :)
样式覆盖未生效缺少合并运算符使用 +: 合并属性;使用 : 会完全替换属性
布局高度收缩为零缺少 height: Fit所有容器都需要显式设置 height: Fit 或固定高度

8. 用于大型列表的 PortalList

为什么使用 PortalList

PortalList 会对渲染进行虚拟化——只绘制视口中可见的项目。对于包含 100 个以上项目的列表,必须使用它。否则,无论项目是否可见,每一帧都会绘制所有项目。

脚本端 PortalList(使用 on_render)

对于由 Splash 驱动的列表,请使用模板定义 PortalList,并使用 on_render:

list := PortalList{
    width: Fill height: Fill
    flow: Down spacing: 4
    scroll_bar: ScrollBar{}
    Item := View{
        width: Fill height: Fit
        padding: 8
        new_batch: true
        draw_bg.color: #2a2a3d
        label := Label{text: "" draw_text.color: #ddd}
    }
}
Rust 端 PortalList(使用 Widget trait)

对于由 Rust 驱动的渲染,请实现 Widget trait:

impl Widget for MyList {
    fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep {
        while let Some(item) = self.view.draw_walk(cx, scope, walk).step() {
            if let Some(mut list) = item.borrow_mut::<PortalList>() {
                list.set_item_range(cx, 0, self.items.len());

                while let Some(item_id) = list.next_visible_item(cx) {
                    let template = id!(Item);
                    let item = list.item(cx, item_id, template);
                    item.label(ids!(label)).set_text(cx, &self.items[item_id].text);
                    item.draw_all(cx, &mut Scope::empty());
                }
            }
        }
        DrawStep::done()
    }
}
FlatList 与 PortalList
特性FlatListPortalList
虚拟化否是
适用范围少于 100 个项目任意数量的项目
内存用量所有项目都在内存中仅可见项目
滚动性能随项目数量增加而下降恒定

9. ViewOptimize 选项摘要

属性ViewOptimize 值效果
(默认)None标准批量绘制
new_batch: trueDrawList新建绘制批次,确保绘制顺序正确
texture_caching: trueTexture将子项渲染到离屏纹理
visible: false不适用完全跳过渲染

优先级: 如果同时设置了 texture_caching 和 new_batch,则以 texture_caching 为准(变为 ViewOptimize::Texture)。


10. 调试检查清单

UI 未显示
  • 检查所有容器是否设置了 height: Fit(在 Fit 父级中,默认的 height: Fill 会导致高度为 0)
  • 检查根容器是否设置了 width: Fill(最外层元素不要使用固定像素宽度)
  • 确认脚本顶部有 use mod.prelude.widgets.*
文本不可见
  • 对任何包含文本且设置了 show_bg: true 的 View,添加 new_batch: true
  • 检查 draw_text.color 是否为透明色或与背景色相同
  • 确认 Label 是直接子项,或位于经过适当批次处理的子项中
悬停时文本消失
  • 在可悬停的 View 中添加 new_batch: true
  • 确保可悬停项目的容器也设置了 new_batch: true
点击无效
  • 检查 := 与 : 的区别——对需要引用的具名/动态子项使用 :=
  • 确认可接收鼠标事件的 View 已设置 show_bg: true
  • 如果需要键盘事件,请检查 grab_key_focus
未找到 Widget
  • 在 script_mod 中验证 #(WidgetName::register_widget(vm)) 注册
  • 检查是否在自定义注册之前调用了 crate::makepad_widgets::script_mod(vm)
  • 确认 Cargo.toml 中已将 widget crate 添加为依赖项
脚本错误
  • 使用 log() 在执行期间调试值
  • 使用 mod.gc.run_status() 检查堆统计信息
  • 使用 mod.gc.dump_tag(value) 检查对象内部结构
样式未生效
  • 使用 +: 合并运算符扩展现有样式:draw_bg +: { color: #fff }
  • 仅当要完全替换某个属性时才使用 :
  • 检查点路径语法:draw_bg.color: #fff 是 draw_bg +: { color: #fff } 的简写

11. 使用 Studio 进行性能分析

Makepad Studio 内置了性能分析器,可用于监控应用性能。

Studio 远程协议

Studio 可以连接到正在运行的应用,并提供:

  • 屏幕截图 - 捕获当前帧状态
  • Widget 树转储 - 检查实时 Widget 层级结构
  • Widget 查询 - 按 ID 查找特定 Widget
  • 性能监控 - 帧耗时、绘制调用次数
使用 Studio 调试性能
  1. 启动 Studio 远程连接:
cargo run -p cargo-makepad --release -- studio --studio=127.0.0.1:8001
  1. 通过 Studio 运行应用:
{"Run":{"mount":"makepad","process":"makepad-example-myapp","args":[]}}
  1. 捕获 Widget 树,以识别渲染结构:
{"WidgetTreeDump":{"build_id":BUILD_ID}}
  1. 截取屏幕截图,以确认视觉状态:
{"Screenshot":{"build_id":BUILD_ID}}
识别渲染热点路径
  • 使用 WidgetTreeDump 查看树中有多少个 Widget
  • 查找可从 texture_caching 中受益的深层嵌套 View 层级结构
  • 找出应使用 PortalList 而非手动循环的重复项
  • 检查设置了 new_batch: true 但可能不需要的 View(每个新批次都会创建一个新的绘制列表)

12. 快速参考卡

View 上的性能属性
// Force new GPU draw batch (fixes text-behind-background)
new_batch: true

// Cache children to GPU texture (reduces draw calls for stable subtrees)
texture_caching: true

// Hide without removing from tree (skip rendering entirely)
visible: false
GC API
mod.gc.set_static(value)    // Mark value tree as permanent
mod.gc.run()                // Force GC cycle (silent)
mod.gc.run_status()         // Force GC cycle with log output
mod.gc.dump_tag(value)      // Debug: print object tag info
渲染 API
ui.widget_name.render()     // Trigger on_render for specific widget
GC 阈值(自动触发)
Objects:  >= 1024 AND >= 2x since last GC
Strings:  >= 256  AND >= 2x since last GC
Arrays:   >= 128  AND >= 2x since last GC
Pods:     >= 128  AND >= 2x since last GC
Handles:  >= 64   AND >= 2x since last GC

评论

支持 Markdown