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. 03Splash 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 を組み合わせて使用します。パフォーマンスを理解するには、次の 3 つの重要なサブシステムを把握する必要があります。

  1. 描画バッチ処理 - Makepad が GPU 描画呼び出しをどのようにまとめるか、また new_batch: true が重要な理由
  2. ガベージコレクション - Splash VM の、型ごとのバケットしきい値を使用するマーク・アンド・スイープ GC
  3. レンダリングトリガー - サブツリーをいつ再構築するかを制御する on_render / .render() システム

従来の保持モード UI フレームワークとは異なり、Makepad は即時モードに着想を得た描画パイプラインを使用します。このパイプラインでは、ウィジェットが描画コマンドをソート済みのバッチリストに出力します。テキストが表示されない、ちらつく、パフォーマンスが低下するといった問題を診断するには、このパイプラインを理解することが不可欠です。


2. 描画バッチ処理システム

仕組み

Makepad は、同じシェーダーを使用する連続した描画呼び出しを、1 回の 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 が設定され、かつテキストの子要素が含まれている場合、テキストが背景の後ろに配置されることがあります。テキストの描画がどちらも 1 回の描画呼び出しにまとめられ、その呼び出しが背景の描画呼び出しより前(または後)に実行されるためです。

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 が設定され、Label を含む Viewはいテキストのバッチが背景の後ろに配置されます
ホバーアニメーターとテキストの子要素がある Viewはい有効化時にホバー背景がテキストを覆います
背景のある項目を繰り返し表示するコンテナーはい各項目とコンテナーで必要です
Label を含む透明な View(show_bg なし)いいえ重なる背景がありません
テキスト以外の子要素(アイコンなど)のみを含む Viewいいえシェーダーの種類が同じなので、重なりの問題はありません
背景のある深くネストされた 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 ...
}
事前定義済みのキャッシュ View

Makepad には、スタイルがあらかじめ設定されたキャッシュ View が用意されています。

ウィジェット説明
CachedViewテクスチャキャッシュされた矩形コンテナー
CachedRoundedViewテクスチャキャッシュされた角丸矩形
テクスチャキャッシュを使用する場合

適した例:

  • ほとんど変更されない、複雑で静的なサイドバーやツールバー
  • ネストされた背景やテキストを多数含む、大きなウィジェットサブツリー
  • シェーダー効果を使った装飾パネル

適さない例:

  • 頻繁に更新される View(アニメーションの対象やライブデータなど)
  • シンプルで小さな View(オーバーヘッドが利点を上回ります)
  • スクロール可能なコンテンツを含む View(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 以降にいずれかのヒープカテゴリが 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 ツリー

多数のタブを持つ Dock など、大きくて安定した UI ツリー定義は、定義直後に静的としてマークします。これは 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 の on_render コールバックは、その View に対して .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() の呼び出しは絶対に避けてください — 呼び出すたびに、そのサブツリーのウィジェット出力が完全に再構築されます。

  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 は、子 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 に show_bg: true とともに new_batch: true を追加する
ホバー時にテキストが消えるホバーアニメーション中のバッチの重なりホバー可能な View に new_batch: true を追加する
UI がフリーズする/カクつく.render() の呼び出し過多状態変更をまとめ、変更されたサブツリーのみをレンダリングする
メモリ使用量が際限なく増えるGC が実行されていない、または大きな静的リーク安定したツリーには mod.gc.set_static() を使い、動的コンテンツは自動 GC に任せる
初回読み込みが遅い起動時の大規模なスクリプト評価モジュールに分割し、遅延読み込みのパターンを使う
スクロールがカクつく描画するアイテムが多すぎる仮想化描画に PortalList を使う
ホバーに反応しないView に show_bg: true がないホバー時のマウスイベントを受け取るには、View に 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 トレイトを使用)

Rust 主導の描画では、Widget トレイトを実装します。

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: falseN/A描画を完全にスキップ

優先順位: 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 を確認する
ウィジェットが見つからない
  • script_mod で #(WidgetName::register_widget(vm)) が登録されていることを確認します
  • カスタム登録より前に crate::makepad_widgets::script_mod(vm) が呼び出されていることを確認します
  • ウィジェットのクレートが Cargo.toml の依存関係に含まれていることを確認します
スクリプトエラー
  • 実行中の値をデバッグするには 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 は実行中のアプリケーションに接続し、次の機能を提供します:

  • スクリーンショット - 現在のフレームの状態をキャプチャします
  • ウィジェットツリーダンプ - 実行中のウィジェット階層を調べます
  • ウィジェットクエリ - ID で特定のウィジェットを検索します
  • パフォーマンス監視 - フレーム時間、描画呼び出し数
パフォーマンスのデバッグに 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. ウィジェットツリーをキャプチャして、レンダリング構造を確認します:
{"WidgetTreeDump":{"build_id":BUILD_ID}}
  1. スクリーンショットを撮って、表示状態を確認します:
{"Screenshot":{"build_id":BUILD_ID}}
負荷の高いレンダリングパスを特定する
  • WidgetTreeDump を使って、ツリー内のウィジェット数を確認します
  • 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

コメント