docs/advanced/devtools
Fonctionnalités · Diagnostics

DevTools

Traquez les problèmes de layout, de hit-testing, de rendu et de performance avec un overlay d’une ligne, un panneau autonome et une Console structurée et bornée.

9 min de lecture

Inspecteur dans la fenêtre

En développement, attachez l’overlay à votre racine. Au survol, il entoure le rect du nœud d’un cadre en pointillés et affiche sa taille et le nom du composant. L’overlay est pass-through — il n’intercepte ni les clics ni le défilement — et le survol ne déclenche que des mises à jour au niveau du dessin, jamais de 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;
}

L’état de l’overlay vit sur le scope que vous passez ; quand ce Scope est libéré, l’overlay disparaît avec lui. Utilisez-le dans la fenêtre du produit pour savoir « qui occupe cet espace ? » ; utilisez le panneau pour un diagnostic complet. Les deux lisent l’état réel de Node / Cx — aucun modèle miroir à maintenir pour le débogage.

Diagnostic par symptôme

SymptômeÀ vérifier d’abord
Contenu mal alignéRect, padding, gap et direction du parent (Elements → Layout)
Les zones vides sont cliquablesLa taille et le hit_behavior du nœud touché
Le texte a changé, pas l’écranAvez-vous modifié des champs de TextProps sans appeler setText / setTextContent ? Ces API comparent et marquent elles-mêmes sizing / render dirty
Le layout ignore une nouvelle largeurAffecter node.style directement ne marque rien comme dirty ; utilisez node.setStyle(alloc, .width, v), qui choisit le bon niveau de dirty pour chaque champ
Ralentit avec le tempsMounts répétés, Effects non libérés avec leur Scope, allocations à chaque frame (Performance → Summary / Rebuild)

Le panneau DevTools

Pour les vues complètes Elements, Components, Console et Performance, utilisez ui.devtools.mountPanel(cx, target, opts). Le panneau a son propre Cx et observe un Cx target distinct ; en général, on le place dans sa propre fenêtre avec MultiWindowApp pour qu’il ne masque jamais l’UI du produit. Le target doit rester vivant tant que le panneau est utilisé.

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

C’est ainsi qu’est structuré zig build devtools-probe dans le dépôt. ui.devtools expose aussi setViewMode, setTreeFilter, setConsoleFilter et consorts, pour que les sondes et les tests E2E pilotent le panneau par programme.

Zenit DevTools Elements panel with a box selected and its layout details
L’arbre d’UI en direct à gauche ; sélectionnez un nœud et l’onglet Layout à droite affiche son rect calculé, son padding, son sizing et ses propriétés flex. Le curseur en forme de main est le curseur virtuel du harness.

Quatre vues, six onglets de détail

VueRépond à
ElementsQuelle est la vraie hiérarchie des nœuds — tag / id / text, layout et zone de hit ?
ComponentsQuels nœuds forment les frontières de composant, et où sont leur état et leur Scope propriétaire ?
ConsoleQuels logs structurés le Cx target a-t-il capturés, et quelque chose a-t-il été évincé ou perdu ?
PerformanceLe target est-il en rendu ou inactif ? Que coûtent layout / render / cache / interaction ?

Une ligne Elements / Components sélectionnée, passez de Layout, Style, State, Events, Render à Trace. Certaines valeurs de Style sont modifiables en direct ; Render / Trace montrent pourquoi un nœud est devenu dirty et ses événements récents. La recherche dans l’arbre porte sur les tags, les #id et les noms de composant. Avec ui.devtools.source_link configuré, les lignes de composant peuvent ouvrir leur définition dans votre éditeur.

Journalisation Console

Chaque ui.Cx possède une Console thread-safe et bornée. Un seul appel peut écrire dans le terminal et conserver un événement structuré pour DevTools et le harness E2E ; l’historique capturé avant l’ouverture du panneau s’affiche aussi.

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

Les niveaux sont debug, log, info, warn et err. Les équivalents de la console du navigateur sont aussi là : group / groupEnd, count, time / timeEnd, assert, trace, inspect et table.

Zone du panneauRôle
ClearVide le stockage Console du target lui-même, pas seulement la vue.
FilterCorrespondance insensible à la casse sur message et scope ; se combine avec le filtre de niveau.
LevelsActivez Debug / Log / Info / Warn / Error dans n’importe quelle combinaison.
Liste des logsNiveau, scope, message, indentation de group et emplacement source facultatif ; le suivi automatique se met en pause dès que vous quittez le bas.
Barre d’étatshown / captured / evicted / dropped — distingue filtrage, éviction et perte.
Zenit DevTools Console with debug, info, warn and error entries
Les logs sont arrivés dans le stockage borné du Cx target avant l’ouverture du panneau ; scope, niveau et emplacement source sont conservés.
Zenit DevTools Console filtered to the network scope
Filter et Levels se combinent localement dans DevTools ; rien n’est retiré du stockage du target.
Une vraie app à deux fenêtres : le curseur virtuel passe d’Elements à Console, active Filter et tape network. Enregistré directement depuis le Metal drawable de la fenêtre DevTools.

Les méthodes de niveau simples n’enregistrent pas le site d’appel. Pour cliquer sur une ligne de log et arriver dans votre éditeur, utilisez writeAt(level, @src(), …) et définissez la racine des sources et la commande d’éditeur avec ui.devtools.source_link.configure (par défaut : code --goto).

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

Sans console explicite, zenit_app choisit des valeurs par défaut selon le mode de build :

Mode de buildTerminalCapture
Debug.debug.debug
ReleaseSafe.info.info
ReleaseFast / ReleaseSmall.warnnull (désactivée)

La Console reprend la partie capture et consultation de la console du navigateur ; ce n’est pas un REPL Zig / JavaScript. Les groups sont pour l’instant indentés mais non repliables, et table se rabat sur du texte. L’API complète, les règles de threads et de durée de vie et des exemples de harness se trouvent dans docs/CONSOLE.md du dépôt.

Performance : inactif ou bloqué

La vue Performance ne se fige pas sur « les N dernières frames rendues ». Elle observe le target en continu par buckets de 100 ms de temps réel — 64 au total, soit une fenêtre glissante d’environ 6.4 s — et calcule les FPS comme la moyenne des 10 derniers buckets (environ 1 s). Quand le target ne produit aucune frame pendant plus de 0.7 s, l’affichage indique FPS: 0 — idle (not rendering) : le framework saute délibérément des frames pour économiser l’énergie, il n’est pas bloqué à 0 FPS.

100MS BUCKETS
Une fois les frames arrêtées, le graphique continue d’avancer en enregistrant des zéros ; l’affichage baisse à mesure qu’arrivent des buckets vides et passe à idle après 0.7 s — il ne fait jamais passer une valeur périmée pour actuelle.
Zenit DevTools Performance showing FPS 0 idle (not rendering) with timing metrics
Le target statique a cessé de rendre ; DevTools se rafraîchit à environ 10 Hz pour que le graphique avance, et les métriques layout, render, cache, focus et interaction restent lisibles.
AffichageSignification
FPS + graphique glissantBuckets de temps réel ; un bucket sans frame enregistre 0 et s’affiche comme une ligne de base neutre, jamais comme une valeur périmée
TimingTemps CPU réel de la dernière frame du target : layout, génération des commandes de rendu, etc.
Summary / InteractionHit registry, mouse-hit, redraw streak, focus et rebuilds d’interaction
Cache / RebuildHits / misses du retained cache, rebuilds full / partial

Points d’entrée des tests

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

Avant la publication

  • ✓

    Désactivez l’UI de débogage. Retirez l’overlay d’inspection, ou conditionnez-le à une configuration de debug.

  • ✓

    Vérifiez la saisie dans une vraie fenêtre. Clavier, IME, presse-papiers et VoiceOver.

  • ✓

    Passez les contrôles de build. Les steps de test headless, UI et render.

  • ✓

    Vérifiez la politique de Console. Le niveau terminal et la capture en Release sont ceux voulus, et les logs ne contiennent aucune donnée sensible.

  • ✓

    Figez les versions. Verrouillez la révision de zenit et notez la version de Zig ciblée.

zenit · Double licenceGratuit pour les projets open source sous GPL-3.0-only ; les produits propriétaires ou commerciaux nécessitent une licence commerciale.Contacter l’auteur : zongyi.xzy#gmail.com (remplacez # par @)zenit 5f9add5+wip 2026-09-30