---
title: "调试与检查 — zenit Zig UI 文档"
description: "快速定位布局、命中、渲染和性能问题：一行 overlay、一个独立面板、一个有界的结构化 Console。"
url: https://zenit.z.express/zh/docs/advanced/devtools
language: zh-CN
alternate_en: https://zenit.z.express/docs/advanced/devtools.md
alternate_es: https://zenit.z.express/es/docs/advanced/devtools.md
alternate_ja: https://zenit.z.express/ja/docs/advanced/devtools.md
alternate_ko: https://zenit.z.express/ko/docs/advanced/devtools.md
alternate_fr: https://zenit.z.express/fr/docs/advanced/devtools.md
alternate_de: https://zenit.z.express/de/docs/advanced/devtools.md
project: zenit v0.1.0-alpha (Zig 0.15.2, macOS)
source: https://github.com/version-next/zenit
---

# 调试与检查

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

## 窗口内检查器

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

`main.zig`

```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](https://zenit.z.express/zh/docs/advanced/multi-window) 把它放在独立窗口，不遮挡产品 UI。面板使用期间 target 必须保持存活。

`main.zig`

```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](https://zenit.z.express/media/devtools-elements.png?v=1c99ec3edc)](https://zenit.z.express/media/devtools-elements.png?v=1c99ec3edc)

左侧是实时 UI 树；选中节点后，右侧 Layout 显示 computed rect、padding、sizing 与 flex 属性。截图中的手型光标来自 Harness 的虚拟光标。

### 四个顶层视图，六个详情页

| 视图 | 回答的问题 |
| --- | --- |
| **Elements** | 真实节点层级、tag / id / text、布局与命中范围是什么？ |
| **Components** | 哪些节点属于组件边界，状态和 owner Scope 在哪里？ |
| **Console** | target Cx 捕获了哪些结构化日志，有没有淘汰或丢弃？ |
| **Performance** | target 正在渲染还是空闲？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`

```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](https://zenit.z.express/media/devtools-console.png?v=1411f611cd)](https://zenit.z.express/media/devtools-console.png?v=1411f611cd)

日志在面板打开之前就进入了 target Cx 的有界仓库；scope、等级和源码位置都保留。

[![Zenit DevTools Console filtered to the network scope](https://zenit.z.express/media/devtools-console-filter.png?v=acfa3d9446)](https://zenit.z.express/media/devtools-console-filter.png?v=acfa3d9446)

Filter 与 Levels 在 DevTools 本地组合，不删除 target 仓库里的事件。

[Video](https://zenit.z.express/media/devtools-console.mp4?v=05e8c163e3)

真实的双窗口应用：虚拟光标从 Elements 切到 Console，聚焦 Filter 并输入 network。录像直接来自 DevTools 窗口的 Metal drawable。

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

### 配置采集

`main.zig`

```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` | `.warn` | `null`（关闭） |

> WARNING
> 
> **Release 默认不采集。** ReleaseFast / ReleaseSmall 下，DevTools 看不到历史日志。生产构建若需要，必须显式设置 `capture_level`，并避免记录 token、密码和个人数据。

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](https://zenit.z.express/media/devtools-performance.png?v=41f18a12a0)](https://zenit.z.express/media/devtools-performance.png?v=41f18a12a0)

静态 target 已停帧；DevTools 自己以约 10Hz 低频刷新，让图表继续前进，layout、render、cache、focus 与 interaction 指标仍然可读。

| 读数 | 口径 |
| --- | --- |
| FPS + 滚动图 | 墙钟时间桶；无帧的桶记 0，画成中性底线，不把旧值当作当前值 |
| Timing | target 上一帧 layout、render 命令生成等环节的 CPU 墙钟耗时 |
| Summary / Interaction | hit registry、mouse-hit、redraw streak、focus 与 interaction rebuild |
| Cache / Rebuild | retained cache 命中 / 未命中，full / partial rebuild |

## 测试入口

```sh
zig build test-headless
zig build test-ui
zig build test-render
zig build hello-button
```

> WARNING
> 
> **不要对内部文件单独运行 zig test。** Zenit 模块之间有跨目录导入，请使用 `build.zig` 定义的测试 step，否则可能遇到 `import of file outside module path`。完整列表见 [命令速查](https://zenit.z.express/zh/docs/reference/commands)；真实窗口测试见 [E2E 自动化](https://zenit.z.express/zh/docs/advanced/e2e)。

## 发布前检查

-   **关闭调试 UI。**移除 inspector overlay，或用 debug 配置控制它只在开发构建中挂载。
    
-   **在真实窗口里验证输入。**键盘、IME、剪贴板与 VoiceOver。
    
-   **跑构建门禁。**headless、UI 与 render 相关的测试 step。
    
-   **确认 Console 策略。**Release 的终端等级与采集开关符合预期，日志里没有敏感数据。
    
-   **固定版本。**锁定 Zenit revision，并记录目标 Zig 版本。
