La semaine dernière, je profilais une application Swift avec Instruments. Rien d’extraordinaire : xctrace record, xctrace export, copier le XML dans le contexte de Claude Code, lui demander de trouver les hotspots.
Et Claude me répond : “Le XML est trop grand, je ne peux pas le traiter de manière fiable.”
33 553 lignes de XML. Pour un programme avec deux fonctions.
Le problème réel
xctrace export est un outil fantastique. Il te donne tout : chaque sample, chaque backtrace, chaque frame avec son binaire, son adresse mémoire, son UUID. C’est complet, précis et exhaustif.
Et c’est là tout le problème.
Lorsque je profile une application pour trouver des goulots d’étranglement, je n’ai pas besoin des 3 044 samples individuels. Je n’ai pas besoin de savoir que le sample numéro 1 847 a capturé le CPU à l’adresse 0x1027ec9a8 de libswiftCore.dylib à 00:02.847.882. J’ai besoin de savoir que heavyWork() prend 70 % du temps et que lightWork() en prend 30 %.
En termes simples : j’ai besoin de dix lignes, pas trente-trois mille.
Pourquoi XML est le bon format (mais pas le bruit)
Avant que quelqu’un dise “le problème, c’est d’utiliser XML en 2026” : non.
XML est le format idéal pour ce que fait xctrace. Réfléchis :
- Hiérarchique : un backtrace est un arbre de frames. Un sample contient un backtrace, un thread, un processus. XML modélise cela naturellement.
- Auto-descriptif : chaque élément a un nom, des attributs typés, et la structure peut être validée. Pas besoin de deviner ce que signifie le champ 7 d’une ligne CSV.
- Déduplication élégante : xctrace utilise un système
id/ref. Il définit une fonction une seule fois (par exemple,id="59" name="heavyWork()") et ensuite la référence ensuite avecref="59". C’est en gros un flyweight pattern sérialisé. - Facilement traitable avec des outils standards : XPath,
xmllint,xml.etree.ElementTree… Pas besoin d’un parser propriétaire.
Le XML de xctrace n’est pas du “remplissage inutile”. C’est une information structurée dont Instruments a besoin pour reconstruire des arbres de appels interactifs, comparer différents runs et filtrer par thread et par processus. Il est conçu pour un outil avec interface graphique qui peut gérer des nodites extensibles.
Le problème survient lorsque tu essaies d’intégrer ces informations dans le contexte limité d’un LLM. C’est comme lire l’intégralité du “Don Quichotte” pour trouver une phrase sur les moulins. Les données sont là, mais le rapport signal/bruit est catastrophique.
La solution : ztrace
Alors, j’ai créé ztrace. Un script Python qui prend un fichier .trace et produit un résumé compact.
L’idée est simple :
- Exécuter
xctrace export --tocpour obtenir les métadonnées (processus, durée, modèle). - Exécuter
xctrace export --xpathpour extraire la tabletime-profile. - Parser le XML en résolvant le système
id/ref. - Filtrer les frames système (tout ce qui provient de
/usr/lib/ou/System/). - Aggréger par fonction et générer un résumé.
Point clé : l’étape 3 est plus importante qu’elle n’y paraît. xctrace ne répète pas la définition complète d’un frame chaque fois qu’il apparaît dans un backtrace. Il le définit une fois avec id="59", puis utilise ref="59". Si tu ne résous pas les refs, tu perds la majorité des informations.
Le résultat
Avec le “fixture” de test (un programme trivial où heavyWork() représente ~70 % et 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 lignes à 13. Toutes les données nécessaires à un LLM pour te dire “optimise heavyWork(), ça utilise 70 % de la CPU” tiennent dans un tweet.
Ce que ztrace filtre (et pourquoi)
Tout ce que xctrace rapporte n’est pas actionnable. Lorsque je profile une application, je ne peux pas optimiser libdispatch.dylib. Je ne peux pas réécrire dyld4::PrebuiltLoader::loadDependents. Ces frames sont du bruit si je cherche des hotspots dans mon code.
ztrace applique plusieurs niveaux de filtre :
Binaires système : tout ce qui vit dans /usr/lib/ ou /System/ est ignoré. Ce sont des frames liés au système d’exploitation ou au runtime Swift.
Internes du runtime : des fonctions comme __swift_instantiateConcreteTypeFromMangledNameV2 ou DYLD-STUB$$sin sont techniquement dans ton binaire (liées statiquement), mais ce n’est pas du code que tu as écrit. Hors du résumé.
Symboles non résolus : Les apps en production (comme Spotify, par exemple) sont souvent stripped. Les frames apparaissent comme des adresses brutes : 0x104885404. ztrace les filtre et indique : “85 % des samples ne contiennent pas de symboles.” Tu sais alors que ton profil contient des données, mais qu’il te faut les fichiers dSYM pour exploiter cela.
Testé avec des applications réelles
Le test “fixture” est sympa mais artificiel. Est-ce que ça fonctionne sur une application réelle ? J’ai essayé avec Ghostty (un émulateur 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
Cela, c’est exploitable. On voit directement où se trouve les goulots : le renderer Metal (render pass, rebuild cells, draw frame) et le font shaping. Si tu optimisais Ghostty, tu saurais où concentrer tes efforts.
Et chaque fonction est associée à son module (ghostty), donc dans une application avec plusieurs frameworks, tu saurais directement si le problème est dans ton code ou dans une dépendance.
Le stack (et pourquoi pas Swift)
Le fichier CLAUDE.md original mentionnait Swift. Logique, me suis-je dit. Mais après avoir constaté que 95 % du travail consiste à parser du XML et formater du texte, je suis passé à Python.
xml.etree.ElementTree traite le XML en trois lignées. En Swift, XMLParser utilise la norme SAX – des callbacks, un état mutable, des délégués. Une vraie bricolage pour quelque chose qui devrait être aussi simple que “donne-moi l’arbre et laisse-moi l’explorer”.
Et surtout : un script Python se distribue avec facilité via uv tool install. Un binaire Swift fonctionne uniquement sur macOS/arm64. Et sachant que xctrace est uniquement disponible sur macOS, l’argument de la “portabilité” n’est pas pertinent ici. La distribution via uv est bien plus propre que de compiler et copier des binaires.
Perspectives
Voici ce qui pourrait être amélioré dans une version future (v0.1.1 ? ou vNext ?) :
ztrace record: enregistrer et résumer en une seule commande.- Filtres configurables : exclusion ciblée de certains modules ou ajustement de la profondeur des call stacks.
- Comparaison de traces : visualiser les changements avant/après optimisation.
- Support pour Allocations : analyser la mémoire en plus de la CPU.
Le repository est disponible sur GitHub, si vous souhaitez le tester.
Intégration dans un workflow quotidien
L’objectif de ztrace n’est pas de l’utiliser manuellement. C’est que Claude Code l’emploie automatiquement pendant le profilage.
Ajoutez ceci à votre fichier CLAUDE.md (global ou propre à un projet) :
| |
Et désormais, chaque fois que Claude Code doit analyser quelque chose, le workflow est :
| |
Sans ztrace, la deuxième étape produirait 30 000 lignes de XML qui submergent le contexte ou noient le signal dans le bruit. Avec ztrace, Claude reçoit exactement les bonnes informations pour te dire “70 % de la CPU est utilisée par heavyWork(), ligne 42 dans Renderer.swift.”
Le méta-point
ztrace existe parce que les LLMs sont inefficaces lorsqu’il s’agit de traiter des données brutes à grande échelle. Ils sont parfaits pour raisonner sur des informations compactes et prétraitées. Donner 33 000 lignes de XML à Claude, c’est comme donner à un médecin une IRM brute au format DICOM en lui demandant un diagnostic. Le médecin a besoin de l’image traitée, pas des octets.
La prochaine fois qu’un LLM te dit “le résultat est trop grand”, la solution n’est pas de chercher un modèle avec un contexte plus grand. C’est de fournir un résumé meilleur. Un pipeline qui transforme les données brutes en informations exploitables avant de les transmettre au modèle.
C’est ce que font les ingénieurs au fond : transformer le bruit en signal. Avec ou sans IA dans le mix.