docs/advanced/devtools
Capacidades · Diagnostics

DevTools

Localiza problemas de layout, hit-testing, renderizado y rendimiento con un overlay de una línea, un panel independiente y una Console estructurada y acotada.

9 min de lectura

Inspector en la ventana

Durante el desarrollo, adjunta el overlay a tu raíz. Al pasar el cursor, delimita el rect del nodo con un recuadro discontinuo y muestra su tamaño y el nombre del componente. El overlay es pass-through —no intercepta clics ni scroll— y el hover solo provoca actualizaciones a nivel de pintado, nunca un 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;
}

El estado del overlay vive en el scope que pasas; cuando ese Scope se libera, el overlay desaparece con él. Úsalo en la ventana del producto para responder «¿quién ocupa este espacio?»; usa el panel para un diagnóstico completo. Ambos leen el estado real de Node / Cx: no hay ningún modelo espejo que mantener para depurar.

Diagnóstico por síntoma

SíntomaRevisa primero
Contenido desalineadoRect, padding, gap y direction del padre (Elements → Layout)
Las zonas vacías reciben clicsEl tamaño y el hit_behavior del nodo alcanzado
El texto cambió, la pantalla no¿Modificaste campos de TextProps sin llamar a setText / setTextContent? Esas API comparan y marcan sizing / render dirty por sí mismas
El layout ignora un ancho nuevoAsignar node.style directamente no marca nada como dirty; usa node.setStyle(alloc, .width, v), que elige el nivel de dirty correcto para cada campo
Se vuelve más lento con el tiempoMounts repetidos, Effects que no se liberan con su Scope, asignaciones por frame (Performance → Summary / Rebuild)

El panel de DevTools

Para las vistas completas de Elements, Components, Console y Performance, usa ui.devtools.mountPanel(cx, target, opts). El panel tiene su propio Cx y observa otro Cx target; lo habitual es ponerlo en su propia ventana con MultiWindowApp para que nunca tape la UI del producto. El target debe seguir vivo mientras el panel está en uso.

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();
}

Así está estructurado zig build devtools-probe en el repo. ui.devtools también expone setViewMode, setTreeFilter, setConsoleFilter y otras funciones para que los probes y las pruebas E2E controlen el panel por código.

Zenit DevTools Elements panel with a box selected and its layout details
A la izquierda, el árbol de UI en vivo; selecciona un nodo y la pestaña Layout de la derecha muestra su rect calculado, padding, sizing y propiedades flex. El cursor de mano de la captura es el cursor virtual del harness.

Cuatro vistas, seis pestañas de detalle

VistaResponde
Elements¿Cuál es la jerarquía real de nodos: tag / id / text, layout y área de hit?
Components¿Qué nodos forman los límites de componente y dónde están su estado y su Scope propietario?
Console¿Qué logs estructurados capturó el Cx target y se desalojó o descartó algo?
Performance¿El target está renderizando o inactivo? ¿Cuánto cuestan layout / render / cache / interaction?

Con una fila de Elements / Components seleccionada, alterna entre Layout, Style, State, Events, Render y Trace. Algunos valores de Style se pueden editar en vivo; Render / Trace muestran por qué un nodo quedó dirty y sus eventos recientes. La búsqueda en el árbol coincide con tags, #id y nombres de componente. Con ui.devtools.source_link configurado, las filas de componente pueden saltar a su definición en tu editor.

Registro con Console

Cada ui.Cx tiene su propia Console thread-safe y acotada. Una sola llamada puede imprimir en la terminal y guardar un evento estructurado para DevTools y el harness E2E; el historial capturado antes de abrir el panel también aparece.

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)});

Los niveles son debug, log, info, warn y err. También están los equivalentes de la consola del navegador: group / groupEnd, count, time / timeEnd, assert, trace, inspect y table.

Zona del panelUso
ClearVacía el propio almacén de Console del target, no solo la vista.
FilterCoincidencia sin distinguir mayúsculas en message y scope; se combina con el filtro de nivel.
LevelsActiva Debug / Log / Info / Warn / Error en cualquier combinación.
Lista de logsNivel, scope, mensaje, sangría de group y ubicación opcional en el código; el seguimiento automático se pausa al desplazarte fuera del final.
Barra de estadoshown / captured / evicted / dropped: distingue filtrado, desalojo y pérdida.
Zenit DevTools Console with debug, info, warn and error entries
Los logs llegaron al almacén acotado del Cx target antes de abrir el panel; se conservan el scope, el nivel y la ubicación en el código.
Zenit DevTools Console filtered to the network scope
Filter y Levels se combinan localmente en DevTools; no se elimina nada del almacén del target.
Una app real de dos ventanas: el cursor virtual pasa de Elements a Console, enfoca Filter y escribe network. Grabado directamente desde el Metal drawable de la ventana de DevTools.

Los métodos de nivel simples no registran el punto de llamada. Para hacer clic en una línea de log y abrir tu editor, usa writeAt(level, @src(), …) y define la raíz del código y el comando del editor con ui.devtools.source_link.configure (por defecto: code --goto).

Configurar la captura

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,
    },
});

Sin un console explícito, zenit_app elige valores por defecto según el modo de build:

Modo de buildTerminalCaptura
Debug.debug.debug
ReleaseSafe.info.info
ReleaseFast / ReleaseSmall.warnnull (desactivada)

La Console replica la parte de captura y visualización de la consola del navegador; no es un REPL de Zig / JavaScript. Por ahora los groups se sangran pero no se pueden plegar de forma interactiva, y table se muestra como texto. La API completa, las reglas de hilos y ciclo de vida y los ejemplos del harness están en docs/CONSOLE.md del repo.

Performance: inactivo frente a bloqueado

La vista Performance no se congela en «los últimos N frames renderizados». Sigue observando el target en buckets de 100 ms de tiempo real —64 en total, una ventana móvil de unos 6.4 s— y calcula los FPS como la media de los últimos 10 buckets (aprox. 1 s). Cuando el target no produce ningún frame durante más de 0.7 s, la lectura indica FPS: 0 — idle (not rendering): el framework omite frames a propósito para ahorrar energía, no está atascado a 0 FPS.

100MS BUCKETS
Cuando se detienen los frames, el gráfico sigue avanzando y registra ceros; la lectura baja a medida que llegan buckets vacíos y pasa a idle tras 0.7 s: nunca hace pasar un valor antiguo por actual.
Zenit DevTools Performance showing FPS 0 idle (not rendering) with timing metrics
El target estático dejó de renderizar; DevTools se refresca a unos 10 Hz para que el gráfico siga avanzando, y las métricas de layout, render, cache, focus e interaction siguen siendo legibles.
LecturaSignificado
FPS + gráfico móvilBuckets de tiempo real; un bucket sin frames registra 0 y se dibuja como línea base neutra, nunca como un valor antiguo
TimingTiempo real de CPU del último frame del target: layout, generación de comandos de render, etc.
Summary / InteractionHit registry, mouse-hit, redraw streak, focus y rebuilds de interaction
Cache / RebuildAciertos / fallos de la retained cache, rebuilds full / partial

Puntos de entrada de pruebas

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

Antes de publicar

  • ✓

    Desactiva la UI de depuración. Quita el overlay del inspector o móntalo solo con una configuración de debug.

  • ✓

    Verifica la entrada en una ventana real. Teclado, IME, portapapeles y VoiceOver.

  • ✓

    Ejecuta las compuertas de build. Los steps de prueba headless, UI y render.

  • ✓

    Revisa la política de Console. El nivel de terminal y la captura en Release son los que pretendes, y los logs no contienen datos sensibles.

  • ✓

    Fija las versiones. Bloquea la revisión de zenit y anota la versión de Zig objetivo.

zenit · Doble licenciaGratis para proyectos de código abierto bajo GPL-3.0-only; los productos cerrados o comerciales necesitan una licencia comercial.Contacta con el autor: zongyi.xzy#gmail.com (cambia # por @)zenit 5f9add5+wip 2026-09-30