docs/advanced/devtools
应用能力 · Diagnostics

调试与检查

快速定位布局、命中、渲染和性能问题:一行 overlay、一个独立面板、一个有界的结构化 Console。

预计阅读 9 分钟

窗口内检查器

开发阶段在根节点上附加 overlay。悬停时它用虚线框标出节点矩形,并显示尺寸与组件名;overlay 自身是 pass-through 的,不拦截点击和滚动,悬停时也只做绘制级更新,不触发重新布局。

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 状态,不需要为调试再维护一份镜像模型。

按现象排查

现象先检查
对齐偏了父节点 rect、padding、gap、direction(Elements → Layout)
大片空白也能点中命中节点的尺寸与 hit_behavior
文本数据变了但画面没变是否直接改了 TextProps 字段却没调用 setText / setTextContent;这两个 API 自己会比较并标记 sizing / render dirty
改宽度后布局不动直接赋值 node.style 不会标脏;用 node.setStyle(alloc, .width, v),它按字段选择正确的 dirty 级别
页面越来越慢重复 mount、未随 Scope 释放的 Effect、每帧分配(Performance → Summary / Rebuild)

独立面板

需要 Elements、Components、Console 与 Performance 完整视图时,用 ui.devtools.mountPanel(cx, target, opts)。面板有自己的 Cx,观察另一个 target Cx;推荐用 MultiWindowApp 把它放在独立窗口,不遮挡产品 UI。面板使用期间 target 必须保持存活。

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 以编程方式操作面板。

Zenit DevTools Elements panel with a box selected and its layout details
左侧是实时 UI 树;选中节点后,右侧 Layout 显示 computed rect、padding、sizing 与 flex 属性。截图中的手型光标来自 Harness 的虚拟光标。

四个顶层视图,六个详情页

视图回答的问题
Elements真实节点层级、tag / id / text、布局与命中范围是什么?
Components哪些节点属于组件边界,状态和 owner Scope 在哪里?
Consoletarget Cx 捕获了哪些结构化日志,有没有淘汰或丢弃?
Performancetarget 正在渲染还是空闲?layout / render / cache / interaction 成本如何?

选中 Elements / Components 中的行后,可在 Layout、Style、State、Events、Render、Trace 之间切换。Style 中的部分数值可以实时编辑;Render / Trace 用于确认 dirty 原因和最近事件。树搜索支持 tag、#id 与组件名。配置 ui.devtools.source_link 后,组件行可以直接在编辑器中打开定义处。

Console 日志

每个 ui.Cx 都有自己的线程安全、有容量上限的 Console。同一条日志可以写到终端,同时保留为结构化事件,供 DevTools 和 E2E harness 读取;面板打开之前采集的历史也会显示。

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。

面板区域用途
Clear清空 target 的 Console 仓库本身,而不只是当前视图。
Filter不区分大小写地匹配 message 与 scope,与等级过滤组合生效。
Levels组合启用 Debug / Log / Info / Warn / Error。
日志列表显示等级、scope、消息、group 缩进和可选的源码位置;滚离底部后暂停自动跟随。
状态栏shown / captured / evicted / dropped,区分过滤、淘汰与丢日志。
Zenit DevTools Console with debug, info, warn and error entries
日志在面板打开之前就进入了 target Cx 的有界仓库;scope、等级和源码位置都保留。
Zenit DevTools Console filtered to the network scope
Filter 与 Levels 在 DevTools 本地组合,不删除 target 仓库里的事件。
真实的双窗口应用:虚拟光标从 Elements 切到 Console,聚焦 Filter 并输入 network。录像直接来自 DevTools 窗口的 Metal drawable。

普通等级方法不记录调用位置。想点击日志跳到编辑器,使用 writeAt(level, @src(), …),并用 ui.devtools.source_link.configure 设置源码根目录与编辑器命令(默认 code --goto)。

配置采集

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 按构建模式选择默认值:

构建模式终端内存采集
Debug.debug.debug
ReleaseSafe.info.info
ReleaseFast / ReleaseSmall.warnnull(关闭)

Console 对齐的是浏览器 Console 的日志采集与查看能力,不是 Zig / JavaScript REPL。Group 目前只提供缩进、不可交互折叠;table 目前以文本形式输出。完整 API、线程与生命周期约束及 harness 示例见仓库的 docs/CONSOLE.md。

Performance:区分空闲与卡住

Performance 页不是截取「最近 N 个渲染帧」然后冻结。它按 100ms 的墙钟时间桶持续观察 target:共 64 个桶,约 6.4 秒的滚动窗口;FPS 取最近 10 个桶(约 1 秒)的平均。target 超过 0.7 秒没有新帧时,读数明确显示 FPS: 0 — idle (not rendering):这是框架有意的省电停帧,不是 0 FPS 卡顿。

100MS BUCKETS
停帧后图表继续前进并补 0;读数先随空桶下降,超过 0.7 秒切换为 idle,不会把旧值伪装成当前值。
Zenit DevTools Performance showing FPS 0 idle (not rendering) with timing metrics
静态 target 已停帧;DevTools 自己以约 10Hz 低频刷新,让图表继续前进,layout、render、cache、focus 与 interaction 指标仍然可读。
读数口径
FPS + 滚动图墙钟时间桶;无帧的桶记 0,画成中性底线,不把旧值当作当前值
Timingtarget 上一帧 layout、render 命令生成等环节的 CPU 墙钟耗时
Summary / Interactionhit registry、mouse-hit、redraw streak、focus 与 interaction rebuild
Cache / Rebuildretained cache 命中 / 未命中,full / partial rebuild

测试入口

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

发布前检查

  • ✓

    关闭调试 UI。移除 inspector overlay,或用 debug 配置控制它只在开发构建中挂载。

  • ✓

    在真实窗口里验证输入。键盘、IME、剪贴板与 VoiceOver。

  • ✓

    跑构建门禁。headless、UI 与 render 相关的测试 step。

  • ✓

    确认 Console 策略。Release 的终端等级与采集开关符合预期,日志里没有敏感数据。

  • ✓

    固定版本。锁定 Zenit revision,并记录目标 Zig 版本。

zenit · 双授权开源项目可按 GPL-3.0-only 免费使用;闭源或商业产品需要商业授权。可联系作者:zongyi.xzy#gmail.com(# 换成 @)zenit 5f9add5+wip 2026-09-30