Depurar Zig é diferente de depurar uma aplicação escondida atrás de runtime pesado. O binário mostra mais do que você espera: stack trace, símbolo DWARF, erro de allocator, endereço de falha, custo de função, syscall lenta e diferença real entre Debug, ReleaseSafe, ReleaseFast e ReleaseSmall. Essa transparência é uma vantagem enorme, desde que você use as ferramentas certas.
Este guia reúne um fluxo prático para depuração e profiling em Zig em 2026. Vamos cobrir modos de build, stack traces, GDB e LLDB, Valgrind, perf, flamegraphs, Tracy, detecção de vazamento de memória, leitura de core dump e o que levar para produção sem transformar observabilidade em overhead permanente. Ele complementa os guias de benchmarking em Zig, observabilidade em Zig, OpenTelemetry em Zig, eBPF para observabilidade Linux e servidor HTTP em produção.
Resposta rápida: qual ferramenta usar?
| Problema | Primeira ferramenta | Por quê |
|---|---|---|
Crash local ou panic | Stack trace do Zig + GDB/LLDB | Mostra arquivo, linha, frames e variáveis sem instrumentação pesada |
| Vazamento ou uso inválido de memória | GeneralPurposeAllocator em Debug, depois Valgrind/ASan quando fizer sentido | Pega leak simples cedo e confirma comportamento nativo fora do teste |
| Função lenta em CPU | perf record + flamegraph | Mede o binário real no Linux, inclusive syscalls e bibliotecas C |
| Latência intermitente | Tracy ou logs estruturados com IDs de operação | Mostra timeline, zonas e correlação entre etapas |
| Regressão após refatoração | Benchmark pequeno + ReleaseFast/ReleaseSafe comparados | Evita otimizar ruído ou modo de build errado |
| Incidente em produção | logs, métricas, health checks, core dump e símbolos | Investiga sem recompilar às cegas |
A regra prática é simples: comece pelo mecanismo mais barato que responde à pergunta. Não abra Tracy para um erro óbvio de índice. Não rode benchmark sintético para um problema de DNS. Não use ReleaseFast para investigar UB que só aparece quando as verificações de segurança somem.
Modos de build mudam o diagnóstico
Antes de culpar a ferramenta, confirme o modo de compilação. Zig muda bastante entre desenvolvimento, produção segura e otimização agressiva.
| Modo | Uso recomendado | O que você ganha | O que você perde |
|---|---|---|---|
Debug | desenvolvimento e reprodução local | safety checks, stack traces completos, asserts úteis | performance realista |
ReleaseSafe | produção conservadora e staging | otimização com várias verificações de segurança | parte do detalhe de debug, dependendo da configuração |
ReleaseFast | hot path já validado | máxima performance | checks removidos; bugs podem virar corrupção silenciosa |
ReleaseSmall | binário mínimo, embarcado, CLI distribuída | tamanho reduzido | menos informação para inspeção |
Para depurar, compile primeiro em Debug:
zig build -Doptimize=Debug
zig build test -Doptimize=Debug
Para comparar comportamento de produção:
zig build -Doptimize=ReleaseSafe
zig build -Doptimize=ReleaseFast
Se um bug só aparece em ReleaseFast, trate como sinal de comportamento indefinido, dado não inicializado, corrida, lifetime errado ou pressuposto quebrado. Reproduza em ReleaseSafe com asserts extras antes de tentar adivinhar pela performance.
Stack traces e erros explícitos
Zig costuma entregar stack traces legíveis quando o binário mantém símbolos e quando o erro passa por caminhos rastreáveis. Isso já resolve muitos problemas sem debugger interativo.
Exemplo mínimo:
const std = @import("std");
fn divide(a: i32, b: i32) i32 {
return @divTrunc(a, b);
}
pub fn main() void {
const value = divide(10, 0);
std.debug.print("valor={d}\n", .{value});
}
Em Debug, o erro aponta para a linha relevante. Em projetos maiores, preserve nomes de funções descritivos e evite engolir erros com catch unreachable fora de limites muito controlados. Um try que propaga erro com contexto claro costuma ser mais útil que uma mensagem genérica no topo.
Quando capturar erro para logar, registre operação, entrada sanitizada, duração e causa. Para serviços HTTP, use o mesmo ID de requisição nos logs e nas métricas. Esse padrão conversa com o guia de observabilidade em Zig e evita que depuração local vire uma disciplina separada da produção.
GDB e LLDB para inspeção interativa
Zig gera DWARF, então GDB e LLDB funcionam bem para inspeção de frames, variáveis, ponteiros e chamadas C. O fluxo típico é:
zig build-exe src/main.zig -O Debug -femit-bin=zig-debug-demo
gdb ./zig-debug-demo
# ou
lldb ./zig-debug-demo
Com GDB:
break main
run
bt
frame 2
info locals
print minha_struct
next
step
continue
Com LLDB:
breakpoint set --name main
run
thread backtrace
frame variable
next
step
continue
Dicas práticas:
- mantenha um caso de reprodução pequeno antes de abrir o debugger;
- coloque breakpoints em fronteiras: parser, alocação, chamada externa, handler HTTP, função de serialização;
- use watchpoints quando uma variável muda sem você saber onde;
- confirme se o binário inspecionado é o mesmo que falhou;
- ao depurar código com C, instale símbolos das bibliotecas quando possível.
Para aplicações servidoras, também vale iniciar o processo com configuração local mínima e chamar uma rota específica com curl. Isso reduz ruído e facilita repetir o mesmo passo após cada hipótese.
Allocators como ferramenta de depuração
Em Zig, allocator não é detalhe: é parte do contrato. Isso torna vazamentos e lifetimes mais visíveis.
Durante desenvolvimento, use GeneralPurposeAllocator e verifique deinit():
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer {
const leaked = gpa.deinit();
if (leaked == .leak) {
std.debug.print("vazamento detectado\n", .{});
}
}
const allocator = gpa.allocator();
const buffer = try allocator.alloc(u8, 1024);
defer allocator.free(buffer);
@memset(buffer, 0);
}
Use ArenaAllocator para lifetimes de requisição, parsing ou jobs curtos. Isso simplifica limpeza quando todos os objetos morrem juntos. Mas não use arena como desculpa para esconder crescimento ilimitado: em serviço long-running, arena por requisição deve ser destruída ao fim da operação.
Checklist de memória:
- quem aloca também documenta quem libera;
- buffers de resposta têm limite explícito;
- parsing de JSON define tamanho máximo;
- cache tem política de expiração;
- testes exercitam caminhos de erro, não só sucesso;
- logs não imprimem segredos nem buffers enormes.
Valgrind, sanitizers e confirmação externa
O allocator de Zig pega muitos vazamentos em desenvolvimento, mas Valgrind ainda é útil para confirmar comportamento nativo, especialmente quando há interoperabilidade com C.
zig build -Doptimize=Debug
valgrind --leak-check=full --show-leak-kinds=all ./zig-out/bin/app
Quando o projeto usa bibliotecas C, rode um teste que passe pela fronteira C/Zig. Vazamento pode estar em uma função de inicialização, em callback, em buffer retornado por C ou em caminho de erro que pula limpeza.
Sanitizers também podem ajudar dependendo da versão do Zig, target e dependências. Se o build com sanitizer ficar instável ou não suportado para o target, não trate isso como bloqueio para investigar: volte para Debug, ReleaseSafe, Valgrind e teste de reprodução menor.
perf para CPU real no Linux
Quando a pergunta é CPU, perf costuma ser o melhor primeiro passo em Linux. Ele mede o binário real, com otimização real, no kernel real.
zig build -Doptimize=ReleaseFast
perf record -F 99 -g -- ./zig-out/bin/app --bench
perf report
Para flamegraph:
perf script > perf.out
# depois gere o flamegraph com a ferramenta que você usa no ambiente
O que procurar:
- função aparecendo larga no topo sem motivo claro;
- parser ou formatação consumindo mais CPU que a regra de negócio;
- alocação em hot path;
- syscalls frequentes demais;
- lock ou espera que parece CPU por amostragem ruim;
- diferença grande entre entrada pequena e entrada real.
Não otimize o primeiro frame chamativo sem confirmar a hipótese. Às vezes a função aparece porque tudo passa por ela; o problema real está no número de chamadas, não no custo de cada chamada. Adicione contadores ou métricas antes de reescrever arquitetura.
Tracy para timeline e zonas
Tracy é valioso quando você precisa enxergar tempo por zona, frame, thread ou operação. Ele é mais intrusivo que perf, mas muito bom para responder perguntas como: “qual etapa de uma requisição atrasou?”, “o parser está bloqueando?”, “o worker passa tempo esperando IO?”
Use Tracy quando:
- o problema é latência, não apenas CPU total;
- você precisa comparar fases de uma operação;
- há múltiplas threads ou workers;
- a sequência temporal importa.
Instrumente zonas pequenas e nomeadas. Evite marcar cada linha. O excesso de zona aumenta overhead e transforma a visualização em ruído. Bons nomes são de domínio: parse_config, fetch_api, render_template, write_cache, handle_request, flush_metrics.
Em produção, mantenha essa instrumentação desligada por padrão ou limitada a builds especiais. Para sinal contínuo, prefira logs estruturados, métricas e traces leves como discutido no guia de OpenTelemetry em Zig.
Core dumps e falhas que só aparecem fora da sua máquina
Alguns bugs só aparecem no host real. Nesses casos, configure core dump e preserve o binário com símbolos correspondente ao commit em produção.
Fluxo de investigação:
- registrar commit, versão do Zig, target, flags e ambiente;
- guardar binário e core dump;
- abrir com GDB ou LLDB;
- extrair backtrace, thread atual e variáveis locais;
- reproduzir localmente com a menor entrada possível;
- adicionar teste ou assert antes do fix.
Exemplo com GDB:
gdb ./zig-out/bin/app core.1234
(gdb) bt
(gdb) info threads
(gdb) thread apply all bt
Nunca dependa apenas de “reiniciou e parou de acontecer”. Se o processo caiu, transforme a queda em caso reproduzível, teste ou pelo menos alerta mais preciso.
Debug local e observabilidade de produção devem se encontrar
A fronteira entre debugging e observabilidade é prática. Debugger responde “o que acontece agora nesta execução?”. Observabilidade responde “o que aconteceu em muitas execuções, inclusive quando ninguém estava olhando?”.
Para serviços Zig, um conjunto mínimo saudável inclui:
- logs estruturados com nível, operação e ID de correlação;
- métricas de latência, taxa de erro, fila, memória e conexões;
- health check separado de readiness;
- build info exposto de forma segura;
- limites claros para body, resposta, timeout e concorrência;
- core dumps ou crash reports controlados;
- alerta baseado em sintoma, não em ruído interno.
Esse conjunto reduz a necessidade de entrar no servidor para “dar uma olhada”. Quando um incidente acontece, você já sabe qual rota, qual versão, qual dependência e qual sintoma merecem depuração local.
Checklist antes de chamar de otimizado
Antes de declarar vitória em performance:
- o teste usa dados parecidos com produção?
- o modo de build é o modo que você pretende comparar?
- há benchmark antes e depois?
perfconfirma a hipótese?- alocação em hot path foi medida, não presumida?
- a mudança mantém legibilidade e segurança?
- o ganho compensa a complexidade?
zig build testcontinua passando?- logs, métricas e limites continuam intactos?
Zig facilita otimizações profundas, mas também facilita micro-otimizações desnecessárias. O melhor fluxo é medir, mudar pouco, medir de novo e registrar o motivo. Para um binário de produção, previsibilidade vale tanto quanto velocidade bruta.
Perguntas frequentes
Devo depurar sempre em Debug?
Comece em Debug, porque os checks e stack traces ajudam muito. Depois confirme em ReleaseSafe ou ReleaseFast se o bug depende de otimização. Problemas que só aparecem em ReleaseFast merecem suspeita extra de comportamento indefinido, dado não inicializado ou lifetime incorreto.
Valgrind substitui o GeneralPurposeAllocator?
Não. Use o GeneralPurposeAllocator durante desenvolvimento e testes porque ele integra naturalmente com Zig. Use Valgrind para confirmação externa, interoperabilidade com C e investigações em que você quer observar o processo nativo por fora.
perf ou Tracy?
Use perf para CPU real e amostragem no Linux. Use Tracy para timeline, zonas, threads e latência por etapa. Em muitos casos, os dois se complementam: perf aponta onde a CPU foi embora; Tracy mostra quando e em qual fase isso aconteceu.
Posso deixar instrumentação pesada em produção?
Evite. Logs, métricas e traces leves podem ficar sempre ligados com limites de cardinalidade e amostragem. Profiling pesado, Tracy detalhado e dumps amplos devem ser acionados com cuidado, em ambiente controlado ou build específico.
Como isso se conecta com observabilidade?
Depuração corrige um caso. Observabilidade ajuda a encontrar e priorizar os casos. Um serviço Zig bem instrumentado entrega logs, métricas e health checks suficientes para você reproduzir localmente o problema certo, em vez de adivinhar pelo sintoma errado.