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/ref no qual define um frame pela primeira vez (id="59" name="heavyWork()") e depois faz referência com ref="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:

  1. Executar xctrace export --toc para obter metadados (processo, duração, template)
  2. Executar xctrace export --xpath para extrair a tabela time-profile
  3. Fazer o parsing do XML resolvendo o sistema de id/ref
  4. Filtrar frames de sistema (tudo o que vive em /usr/lib/ ou /System/)
  5. 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):

1
2
3
4
5
### Profiling (xctrace)

- Use `ztrace summary <file.trace>` para ler traces. NUNCA leia o XML bruto de xctrace export.
- Fluxo: `xctrace record``ztrace summary`
- Flags: `--threshold 0.5` (mais funções), `--depth 10` (stacks mais profundos)

E, a partir daí, toda vez que o Claude Code precisar fazer análise de desempenho, o fluxo é:

1
2
3
4
5
6
7
# 1. Gravar
xctrace record --template 'Time Profiler' --time-limit 5s --launch -- .build/debug/MyApp

# 2. Resumir (10 linhas que cabem no contexto)
ztrace summary MyApp.trace

# 3. Claude lê o resumo e sugere otimizações

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.