Na semana passada, eu estava analisando o desempenho de um aplicativo Swift usando o Instruments. Nada fora do comum: xctrace record, xctrace export, copiar o XML no contexto do Claude Code, pedir para detectar os hotspots.
E o Claude me responde: “O arquivo XML é muito grande, não consigo processá-lo de forma confiável.”
33.553 linhas de XML. Tudo isso para um programa com duas funções.
O verdadeiro problema
xctrace export é uma ferramenta incrível. Ele te dá tudo: cada sample, cada backtrace, cada frame com seu binário, seu endereço de memória, seu UUID. É algo completo, preciso e exaustivo.
E é exatamente esse o problema.
Quando analiso o desempenho de um aplicativo para encontrar gargalos, eu não preciso dos 3.044 samples individuais. Eu não preciso saber que o sample número 1.847 capturou a CPU no endereço 0x1027ec9a8 do libswiftCore.dylib às 00:02.847.882. Eu preciso saber que heavyWork() consome 70% do tempo e lightWork() 30%.
Em outras palavras: eu preciso de 10 linhas, não 33 mil.
Por que XML é o formato certo (mas o ruído não)
Antes que alguém fale “o problema é usar XML em pleno 2026”: pode parar por aí.
O XML é o formato ideal para o que o xctrace faz. Pense bem:
- Hierárquico: um backtrace é uma árvore de frames. Um sample contém um backtrace, uma thread, um processo. O XML modela isso de forma natural.
- Autoexplicativo: cada elemento tem nome, atributos tipados e a estrutura é validável. Você não precisa adivinhar o que significa o campo 7 de uma linha CSV.
- Deduplicação elegante: o xctrace utiliza um sistema de
id/refno qual define um frame pela primeira vez (id="59" name="heavyWork()") e depois faz referência comref="59". É basicamente um flyweight pattern serializado. - Processável com ferramentas padrão: XPath,
xmllint,xml.etree.ElementTree… você não precisa de um parser proprietário.
O XML do xctrace não é bloat. É informação estruturada que o Instruments usa para reconstruir árvores de chamadas interativas, comparar execuções, filtrar por threads e processos. Ele foi projetado para uma ferramenta com interface gráfica que pode expandir e colapsar nós.
O problema surge quando você tenta colocar essas informações na janela de contexto de um LLM. É como tentar ler o livro “Dom Quixote” inteiro só para encontrar a história dos moinhos de vento. A informação está lá, mas o índice sinal/ruído é brutal.
A solução: ztrace
Então eu criei o ztrace. Um script Python que pega um pacote .trace e gera um resumo compacto.
A ideia é simples:
- Executar
xctrace export --tocpara obter metadados (processo, duração, template) - Executar
xctrace export --xpathpara extrair a tabelatime-profile - Fazer o parsing do XML resolvendo o sistema de
id/ref - Filtrar frames de sistema (tudo o que vive em
/usr/lib/ou/System/) - Agregar por função e criar o resumo
Atenção para isso: o passo 3 é mais importante do que parece. O xctrace não repete a definição completa de um frame toda vez que ele aparece em um backtrace. Ele define uma vez com id="59" e depois usa ref="59". Se você não resolver os refs, você perde a maior parte da informação.
O resultado
Com o fixture de teste (um programa trivial com heavyWork() ~70% e lightWork() ~30%):
$ ztrace summary sample.trace
Process: hotspot Duration: 3.8s Template: Time Profiler
Samples: 3044 Total CPU: 3044ms
SELF TIME
69.4% 2113ms hotspot heavyWork()
29.7% 905ms hotspot lightWork()
TOTAL TIME (callers with significant overhead)
99.9% 3041ms main
CALL STACKS
69.4% 2113ms main > heavyWork()
29.7% 904ms main > lightWork()
De 33.553 linhas para 13. Todas as informações que um LLM precisa para dizer “otimize o heavyWork(), está consumindo 70% da CPU” cabem em um tweet.
O que é filtrado (e por quê)
Nem tudo que o xctrace reporta é acionável. Quando eu analiso um aplicativo, eu não consigo otimizar libdispatch.dylib. Eu não posso reescrever dyld4::PrebuiltLoader::loadDependents. Esses frames são ruídos quando eu estou tentando encontrar hotspots no meu código.
O ztrace filtra em várias camadas:
Binários do sistema: tudo o que está em /usr/lib/ ou /System/ é descartado. São só frames do sistema operacional ou do runtime do Swift.
Elementos internos do runtime: funções como __swift_instantiateConcreteTypeFromMangledNameV2 ou DYLD-STUB$$sin tecnicamente estão no seu binário (linkadas estaticamente), mas não são código que você escreveu. Fora.
Símbolos não resolvidos: aplicativos em produção (como Spotify, por exemplo) costumam estar stripped. Os frames aparecem como endereços brutos, algo como 0x104885404. O ztrace filtra e avisa: “85% dos samples do usuário não têm símbolos”. Assim você sabe que o perfil tem dados, mas que será necessário obter os dSYMs para usá-los.
Testando com aplicativos reais
O fixture é bonitinho, mas artificial. Será que funciona em um aplicativo de verdade? Testei com o Ghostty (um emulador de terminal):
Process: ghostty Duration: 3.8s Template: Time Profiler
Samples: 295 Total CPU: 295ms
SELF TIME
53.2% 157ms ghostty main
3.7% 11ms ghostty renderer.metal.RenderPass.begin
3.1% 9ms ghostty renderer.generic.Renderer(renderer.Metal).rebuildCells
2.7% 8ms ghostty renderer.generic.Renderer(renderer.Metal).drawFrame
2.4% 7ms ghostty renderer.generic.Renderer(renderer.Metal).updateFrame
2.0% 6ms ghostty heap.PageAllocator.alloc
1.7% 5ms ghostty terminal.page.Page.clonePartialRowFrom
1.7% 5ms ghostty font.shaper.coretext.Shaper.shape
Isso sim é acionável. Dá pra ver de cara: o renderer Metal (render pass, rebuild cells, draw frame) e o font shaping estão consumindo mais tempo. Se você estivesse otimizando o Ghostty, saberia exatamente por onde começar.
E cada função vem com seu módulo (ghostty), então em um app com múltiplos frameworks você saberia se o gargalo está no seu código ou em uma dependência.
A stack (e por que não Swift)
O arquivo original CLAUDE.md dizia para usar Swift. Achei que fazia sentido com o caso de uso. Mas depois de perceber que 95% do trabalho é apenas fazer o parsing do XML e formatar texto, mudei para Python.
xml.etree.ElementTree faz o parsing do XML em três linhas. Em Swift, XMLParser é puro SAX — callbacks, estado mutável, delegados. Uma gambiarra para algo que deveria ser apenas “me dê a árvore e deixe eu navegar”.
Além disso: com um script em Python você pode distribuir via uv tool install. Um binário em Swift só funciona no macOS/arm64. E, dado que o xctrace só roda no macOS, “multiplataforma” não é realmente uma vantagem para o Swift aqui. Fora que a distribuição via uv é incomparavelmente mais prática do que compilar e copiar binários.
O que vem a seguir
Esta é a versão 0.1. O que ainda falta:
ztrace record: gravar e resumir com um único comando (conveniência, não essencial)- Filtros configuráveis: capacidade de desativar módulos específicos, ajustar profundidade de chamadas
- Comparação de traces: antes/depois de uma otimização, em formato diff
- Suporte para Allocations: não só para CPU, mas também para memória
O repositório está no GitHub para quem quiser testar.
Integrando no dia a dia
A ideia do ztrace não é ser usado manualmente. É fazer com que o Claude Code use-o automaticamente na análise de desempenho.
Você só precisa adicionar isso no seu arquivo CLAUDE.md (global ou do projeto):
| |
E, a partir daí, toda vez que o Claude Code precisar fazer análise de desempenho, o fluxo é:
| |
Sem o ztrace, o passo 2 geraria 30.000 linhas de XML que ou sobrecarregariam a janela de contexto ou enterrariam o sinal no meio do ruído. Com o ztrace, o Claude recebe exatamente o que precisa para te dizer “70% da CPU está em heavyWork(), linha 42 do Renderer.swift”.
O ponto principal
O ztrace existe porque os LLMs não são bons em processar dados brutos em grande escala. Eles são ótimos para raciocinar sobre dados processados e compactos. Passar 33 mil linhas de XML para o Claude é como dar para um médico uma ressonância magnética no formato DICOM bruto e esperar que ele faça um diagnóstico. O médico precisa da imagem renderizada, não dos bytes.
Da próxima vez que um LLM te disser “o arquivo é muito grande”, a solução não é um modelo com mais contexto. É um resumo melhor. Um pipeline que transforma dados brutos em informações úteis antes que eles cheguem ao modelo.
No final das contas, isso é o que fazemos como engenheiros: convertemos ruído em sinal. Com ou sem IA no meio.