调试与检查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.
窗口内检查器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.
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
hit_behaviorThe hit node’s size and hit_behaviorsetText / setTextContent;这两个 API 自己会比较并标记 sizing / render dirtyDid you mutate TextProps fields without calling setText / setTextContent? Those APIs compare and mark sizing / render dirty themselvesnode.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独立面板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.
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.
四个顶层视图,六个详情页Four views, six detail tabs
选中 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.
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.
普通等级方法不记录调用位置。想点击日志跳到编辑器,使用 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
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:
Debug.debug.debugReleaseSafe.info.infoReleaseFast / 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.
测试入口Test entry points
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.



