v0.1.0-alpha
官网Home
docs/advanced/devtools
应用能力 · DiagnosticsCapabilities · Diagnostics

调试与检查DevTools

快速定位布局、命中、渲染和性能问题:一行 overlay、一个独立面板、一个有界的结构化 Console。Track down layout, hit-testing, rendering and performance issues with a one-line overlay, a standalone panel and a bounded, structured Console.

预计阅读 9 分钟9 min read

窗口内检查器In-window inspector

开发阶段在根节点上附加 overlay。悬停时它用虚线框标出节点矩形,并显示尺寸与组件名;overlay 自身是 pass-through 的,不拦截点击和滚动,悬停时也只做绘制级更新,不触发重新布局。During development, attach the overlay to your root. On hover it outlines the node’s rect with a dashed box and shows its size and component name. The overlay is pass-through — it doesn’t intercept clicks or scrolling — and hovering only triggers paint-level updates, never a relayout.

main.zig
fn mountUi(cx: *ui.Cx, scope: *ui.Scope) anyerror!*ui.Node {
    const root = try mountProductUi(cx, scope);

    // Development only: hover highlight with size + component name.
    _ = try ui.devtools.overlay.attach(cx, scope, root, .{});
    return root;
}

overlay 的状态挂在传入的 scope 上,Scope 释放时 overlay 随之移除。它适合在产品窗口里快速确认「谁占了这块空间」;完整诊断用独立面板。两者都读取真实的 Node / Cx 状态,不需要为调试再维护一份镜像模型。The overlay’s state lives on the scope you pass; when that Scope is disposed, the overlay goes with it. Use it in the product window to answer “who owns this space?”; use the panel for full diagnostics. Both read the real Node / Cx state — no mirror model to maintain for debugging.

按现象排查Diagnose by symptom

现象Symptom先检查Check first
对齐偏了Misaligned content父节点 rect、padding、gap、direction(Elements → Layout)Parent rect, padding, gap, direction (Elements → Layout)
大片空白也能点中Empty areas are clickable命中节点的尺寸与 hit_behaviorThe hit node’s size and hit_behavior
文本数据变了但画面没变Text data changed, screen didn’t是否直接改了 TextProps 字段却没调用 setText / setTextContent;这两个 API 自己会比较并标记 sizing / render dirtyDid you mutate TextProps fields without calling setText / setTextContent? Those APIs compare and mark sizing / render dirty themselves
改宽度后布局不动Layout ignores a new width直接赋值 node.style 不会标脏;用 node.setStyle(alloc, .width, v),它按字段选择正确的 dirty 级别Assigning node.style directly marks nothing dirty; use node.setStyle(alloc, .width, v), which picks the right dirty level per field
页面越来越慢Gets slower over time重复 mount、未随 Scope 释放的 Effect、每帧分配(Performance → Summary / Rebuild)Repeated mounts, Effects not released with their Scope, per-frame allocation (Performance → Summary / Rebuild)

独立面板The DevTools panel

需要 Elements、Components、Console 与 Performance 完整视图时,用 ui.devtools.mountPanel(cx, target, opts)。面板有自己的 Cx,观察另一个 target Cx;推荐用 MultiWindowApp 把它放在独立窗口,不遮挡产品 UI。面板使用期间 target 必须保持存活。For the full Elements, Components, Console and Performance views, use ui.devtools.mountPanel(cx, target, opts). The panel has its own Cx and observes a separate target Cx; the usual setup puts it in its own window with MultiWindowApp so it never covers the product UI. The target must stay alive while the panel is in use.

main.zig
const std = @import("std");
const ui = @import("ui");
const zenit_app = @import("zenit_app");

// mountProductUi: your app's ordinary mount function (see above).
var g_target_cx: ?*ui.Cx = null;

fn mountDevTools(cx: *ui.Cx, scope: *ui.Scope) anyerror!*ui.Node {
    _ = scope;
    const target = g_target_cx orelse return error.TargetNotReady;
    return ui.devtools.mountPanel(cx, target, .{ .title = "My App DevTools" });
}

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();

    var application = zenit_app.MultiWindowApp.init(gpa.allocator(), .{});
    defer application.deinit();

    const product = try application.createWindowWith(.{
        .window = .{ .width = 900, .height = 640, .title = "My App" },
    }, mountProductUi);
    g_target_cx = product.cx;

    const tools = try application.createWindowWith(.{
        .window = .{ .width = 760, .height = 560, .title = "DevTools" },
    }, mountDevTools);
    _ = ui.devtools.setViewMode(tools.cx, "performance"); // optional start tab

    try application.run();
}

这就是仓库 zig build devtools-probe 的结构。ui.devtools 还提供 setViewMode、setTreeFilter、setConsoleFilter 等函数,供探针和 E2E 以编程方式操作面板。This is how the repo’s zig build devtools-probe is structured. ui.devtools also exposes setViewMode, setTreeFilter, setConsoleFilter and friends so probes and E2E tests can drive the panel programmatically.

Zenit DevTools Elements panel with a box selected and its layout details
左侧是实时 UI 树;选中节点后,右侧 Layout 显示 computed rect、padding、sizing 与 flex 属性。截图中的手型光标来自 Harness 的虚拟光标。The live UI tree on the left; select a node and the Layout tab on the right shows its computed rect, padding, sizing and flex properties. The hand cursor in the shot is the harness’s virtual cursor.

四个顶层视图,六个详情页Four views, six detail tabs

视图View回答的问题Answers
Elements真实节点层级、tag / id / text、布局与命中范围是什么?What is the real node hierarchy — tag / id / text, layout and hit area?
Components哪些节点属于组件边界,状态和 owner Scope 在哪里?Which nodes form component boundaries, and where are their state and owner Scope?
Consoletarget Cx 捕获了哪些结构化日志,有没有淘汰或丢弃?Which structured logs did the target Cx capture, and was anything evicted or dropped?
Performancetarget 正在渲染还是空闲?layout / render / cache / interaction 成本如何?Is the target rendering or idle? What do layout / render / cache / interaction cost?

选中 Elements / Components 中的行后,可在 Layout、Style、State、Events、Render、Trace 之间切换。Style 中的部分数值可以实时编辑;Render / Trace 用于确认 dirty 原因和最近事件。树搜索支持 tag、#id 与组件名。配置 ui.devtools.source_link 后,组件行可以直接在编辑器中打开定义处。With an Elements / Components row selected, switch between Layout, Style, State, Events, Render and Trace. Some Style values are live-editable; Render / Trace show why a node went dirty and its recent events. Tree search matches tags, #id and component names. With ui.devtools.source_link configured, component rows can jump to their definition in your editor.

Console 日志Console logging

每个 ui.Cx 都有自己的线程安全、有容量上限的 Console。同一条日志可以写到终端,同时保留为结构化事件,供 DevTools 和 E2E harness 读取;面板打开之前采集的历史也会显示。Every ui.Cx owns a thread-safe, bounded Console. One call can print to the terminal and keep a structured event for DevTools and the E2E harness; history captured before the panel opened shows up too.

logging.zig
const log = cx.console();

log.info("application ready", .{});
log.scoped("network").warn("retry {d}", .{attempt});

// Plain level methods don't record a call site; writeAt does.
log.writeAt(.err, @src(), "save failed: {s}", .{@errorName(err)});

等级依次为 debug、log、info、warn、err。此外还有与浏览器 Console 对应的 group / groupEnd、count、time / timeEnd、assert、trace、inspect 与 table。Levels are debug, log, info, warn and err. Browser-console counterparts are there too: group / groupEnd, count, time / timeEnd, assert, trace, inspect and table.

面板区域Panel area用途Purpose
Clear清空 target 的 Console 仓库本身,而不只是当前视图。Clears the target’s Console store itself, not just the view.
Filter不区分大小写地匹配 message 与 scope,与等级过滤组合生效。Case-insensitive match on message and scope; combines with the level filter.
Levels组合启用 Debug / Log / Info / Warn / Error。Toggle Debug / Log / Info / Warn / Error in any combination.
日志列表Log list显示等级、scope、消息、group 缩进和可选的源码位置;滚离底部后暂停自动跟随。Level, scope, message, group indentation and optional source location; auto-follow pauses once you scroll away from the bottom.
状态栏Status barshown / captured / evicted / dropped,区分过滤、淘汰与丢日志。shown / captured / evicted / dropped — tells filtering, eviction and loss apart.
Zenit DevTools Console with debug, info, warn and error entries
日志在面板打开之前就进入了 target Cx 的有界仓库;scope、等级和源码位置都保留。The logs reached the target Cx’s bounded store before the panel opened; scope, level and source location are kept.
Zenit DevTools Console filtered to the network scope
Filter 与 Levels 在 DevTools 本地组合,不删除 target 仓库里的事件。Filter and Levels combine locally in DevTools; nothing is removed from the target’s store.
真实的双窗口应用:虚拟光标从 Elements 切到 Console,聚焦 Filter 并输入 network。录像直接来自 DevTools 窗口的 Metal drawable。A real two-window app: the virtual cursor switches from Elements to Console, focuses Filter and types network. Recorded straight from the DevTools window’s Metal drawable.

普通等级方法不记录调用位置。想点击日志跳到编辑器,使用 writeAt(level, @src(), …),并用 ui.devtools.source_link.configure 设置源码根目录与编辑器命令(默认 code --goto)。Plain level methods don’t record a call site. To click a log line and land in your editor, use writeAt(level, @src(), …) and set the source root and editor command with ui.devtools.source_link.configure (default: code --goto).

配置采集Configuring capture

main.zig
const app = try zenit_app.App.init(allocator, .{
    .console = .{
        .terminal_level = .info, // null disables the terminal sink
        .capture_level = .debug, // null disables in-memory capture
        .max_entries = 10_000,
        .max_bytes = 8 * 1024 * 1024,
        .max_entry_bytes = 64 * 1024,
    },
});

不传 console 时,zenit_app 按构建模式选择默认值:Without an explicit console, zenit_app picks defaults by build mode:

构建模式Build mode终端Terminal内存采集Capture
Debug.debug.debug
ReleaseSafe.info.info
ReleaseFast / ReleaseSmall.warnnull(关闭)null (off)

Console 对齐的是浏览器 Console 的日志采集与查看能力,不是 Zig / JavaScript REPL。Group 目前只提供缩进、不可交互折叠;table 目前以文本形式输出。完整 API、线程与生命周期约束及 harness 示例见仓库的 docs/CONSOLE.md。The Console mirrors the browser console’s capture-and-view side; it is not a Zig / JavaScript REPL. Groups currently indent but can’t be collapsed interactively, and table falls back to text. The full API, threading and lifetime rules, and harness examples are in the repo’s docs/CONSOLE.md.

Performance:区分空闲与卡住Performance: idle vs. stuck

Performance 页不是截取「最近 N 个渲染帧」然后冻结。它按 100ms 的墙钟时间桶持续观察 target:共 64 个桶,约 6.4 秒的滚动窗口;FPS 取最近 10 个桶(约 1 秒)的平均。target 超过 0.7 秒没有新帧时,读数明确显示 FPS: 0 — idle (not rendering):这是框架有意的省电停帧,不是 0 FPS 卡顿。The Performance view doesn’t freeze on “the last N rendered frames”. It keeps observing the target in 100 ms wall-clock buckets — 64 of them, a rolling window of about 6.4 s — and computes FPS as the mean of the last 10 buckets (about 1 s). When the target produces no frame for more than 0.7 s, the readout says FPS: 0 — idle (not rendering): the framework is deliberately skipping frames to save power, not stuck at 0 FPS.

100MS BUCKETS
停帧后图表继续前进并补 0;读数先随空桶下降,超过 0.7 秒切换为 idle,不会把旧值伪装成当前值。After frames stop, the chart keeps moving and records zeros; the readout drops as empty buckets arrive and flips to idle after 0.7 s — it never passes off a stale value as current.
Zenit DevTools Performance showing FPS 0 idle (not rendering) with timing metrics
静态 target 已停帧;DevTools 自己以约 10Hz 低频刷新,让图表继续前进,layout、render、cache、focus 与 interaction 指标仍然可读。The static target has stopped rendering; DevTools refreshes itself at about 10 Hz so the chart keeps moving, and layout, render, cache, focus and interaction metrics stay readable.
读数Readout口径Meaning
FPS + 滚动图FPS + rolling chart墙钟时间桶;无帧的桶记 0,画成中性底线,不把旧值当作当前值Wall-clock buckets; a bucket with no frames records 0 and draws as a neutral baseline, never a stale value
TimingTimingtarget 上一帧 layout、render 命令生成等环节的 CPU 墙钟耗时CPU wall time of the target’s last frame: layout, render-command generation and so on
Summary / InteractionSummary / Interactionhit registry、mouse-hit、redraw streak、focus 与 interaction rebuildHit registry, mouse-hit, redraw streak, focus and interaction rebuilds
Cache / RebuildCache / Rebuildretained cache 命中 / 未命中,full / partial rebuildRetained-cache hits / misses, full / partial rebuilds

测试入口Test entry points

Terminal
zig build test-headless
zig build test-ui
zig build test-render
zig build hello-button

发布前检查Before release

  • ✓

    关闭调试 UI。移除 inspector overlay,或用 debug 配置控制它只在开发构建中挂载。Turn off debug UI. Remove the inspector overlay, or gate it behind a debug-only config.

  • ✓

    在真实窗口里验证输入。键盘、IME、剪贴板与 VoiceOver。Verify input in a real window. Keyboard, IME, clipboard and VoiceOver.

  • ✓

    跑构建门禁。headless、UI 与 render 相关的测试 step。Run the build gates. The headless, UI and render test steps.

  • ✓

    确认 Console 策略。Release 的终端等级与采集开关符合预期,日志里没有敏感数据。Check the Console policy. Release terminal level and capture are what you intend, and logs carry no sensitive data.

  • ✓

    固定版本。锁定 Zenit revision,并记录目标 Zig 版本。Pin versions. Lock the Zenit revision and record the target Zig version.

zenit · 双授权Dual-licensed开源项目可按 GPL-3.0-only 免费使用;闭源或商业产品需要商业授权。Free for open-source projects under GPL-3.0-only; closed-source or commercial products need a commercial license.可联系作者:Contact the author: zongyi.xzy#gmail.com(# 换成 @) (replace # with @)zenit 5f9add5+wip 2026-09-30