Afficher la liste de désinstallation de Mole dès le clic
Quand on cliquait sur une app dans la page de désinstallation de Mole, la partie droite restait vide un instant, puis affichait « Analyse en cours », et ce n’est qu’après une demi-seconde à une seconde que les restes apparaissaient. Xcode demandait trois à quatre secondes à chaque clic, ChatGPT six, et le défilement de la liste dépliée de ChatGPT saccadait. Je voulais voir où passait vraiment ce temps, et j’ai regardé au passage comment quelques désinstalleurs open source traitent le même problème. Les changements étaient terminés le 5 octobre et sont sortis avec Mole 1.16.0 ; voici ce que j’ai mesuré à l’époque et les compromis que j’ai faits.
Ce n’était pas l’app elle-même qui était lente
Je m’attendais à ce que les grosses apps soient plus lentes, et ce n’était pas le cas : un petit utilitaire de 6,6 Mo avec six restes prenait quand même environ 0,56 seconde au premier clic. L’essentiel du temps partait dans quelques tâches fixes exécutées à chaque clic. Ce sont des médianes de trois mesures en build Release sur mon M5 Pro :
- La lecture du catalogue des casks Homebrew, un fichier de 5,6 Mo, prenait environ 200 ms à chaque fois, uniquement pour savoir si l’app avait été installée avec Homebrew.
- La signature de code de l’app était vérifiée, deux fois, à raison d’environ 1 ms par Mo d’exécutable ; MindNode y passait 260 ms par clic.
- La tâche d’analyse la plus lente était la liste des documents récents : environ 230 ms pour 775 entrées, presque entièrement passées à vérifier les chemins un par un.
Certaines apps ajoutaient leur propre coût. Le reçu d’installation de Xcode pèse 41 Mo et liste 125 000 chemins, et il était relu à chaque clic. ChatGPT laisse environ 15 000 dossiers temporaires, et il fallait vérifier pour chacun d’eux qu’il lui appartenait bien.
Ce qui a changé
- Le catalogue des casks est lu en arrière-plan dès l’ouverture de la page de désinstallation, pas au moment du clic.
- Le clic lit les informations de signature sans les vérifier et fixe l’app par son identité de fichier. La vérification complète tourne en arrière-plan une fois la liste affichée, et elle doit être terminée avant toute suppression.
- Les documents récents sont d’abord filtrés par nom ; seuls ceux qui correspondent passent par la vérification complète d’appartenance.
- Les racines de la bibliothèque sont listées en parallèle, l’identité des conteneurs est mise en cache par fichier de métadonnées, et les préfixes d’identifiant des programmes auxiliaires sont calculés une seule fois par analyse.
- Les reçus d’installation sont mis en cache par fichier de reçu, donc un deuxième clic sur Xcode ne relit pas ces 41 Mo.
- Les dossiers temporaires dont le nom seul prouve à qui ils appartiennent passent par un chemin rapide ; l’étape de suppression vérifie toujours chaque chemin.
- Les longues tâches d’analyse démarrent en premier, et les centaines de petites passent derrière.
- Une analyse lancée au survol s’arrête quand le pointeur s’en va, et une analyse lancée avant une désinstallation ne peut plus écrire de résultats périmés après coup.
Pour la vérification de signature, le plus simple aurait été de la supprimer. Je l’ai plutôt déplacée après le clic. Désormais, quand la vérification en arrière-plan échoue, l’app elle-même est quand même désinstallée, et seuls les types de restes trouvés grâce aux informations de signature restent en place, comme les conteneurs associés par l’identifiant d’équipe du développeur. Le résultat les indique par « Impossible de vérifier la signature de l’app », conservés plutôt qu’en échec. Une app remplacée par une autre après l’analyse est toujours refusée.
Combien plus rapide
Premier clic environ 1,5 seconde après l’ouverture de la page de désinstallation, build Release :
| App | Avant | Après |
|---|---|---|
| Shottr | 556 ms | 137 ms |
| MindNode | 1233 ms | 169 ms |
| Raycast | 716 ms | 171 ms |
| Parallels Desktop | 871 ms | 194 ms |
| VS Code | 584 ms | 232 ms |
| 605 ms | 238 ms | |
| Xcode | 3689 ms | 384 ms |
| ChatGPT | 6134 ms | 1530 ms |
La plupart des apps courantes passent maintenant sous 250 ms, si bien que les restes apparaissent en une fois, sans « Analyse en cours » avant. La vérification de signature en arrière-plan se termine 12 à 434 ms après l’affichage de la liste.
Les quinze mille lignes de ChatGPT
Avec des clics rapides, le défilement d’un ChatGPT déplié saccadait encore. Il affichait 15 175 lignes, dont 15 143 étaient des dossiers temporaires de la forme T/.com.openai.codex.XXXXXX, 1,0 Go au total, quelques-uns de plus à chaque lancement de l’app. Hina, une app que je développe, était dans le même cas : 323 de ses 344 lignes étaient des fichiers Hina-<Suite>-<UUID>.plist laissés par des tests, 0,01 Mo en tout. Dans les deux cas, une seule forme au nom aléatoire remplissait presque toute la liste.
Le nombre de lignes n’était pas la seule cause. La ligne dépliée retriait tous les restes à chaque redessin, et le survol redessinait toute la ligne de l’app, donc chaque ligne qui glissait sous un pointeur immobile déclenchait un nouveau tri. Sur les vraies données de ChatGPT, un redessin est passé de 309 à 354 ms à 56 à 58 ms, Hina de 33 à 36 ms à 17 à 19 ms, et la hauteur dépliée de ChatGPT est passée de 3200 pt à 1225 pt.
Des lignes partagent maintenant une seule ligne quand elles ont la même catégorie, le même dossier, le même propriétaire et la même sélection par défaut, et que leurs noms ne diffèrent que par une partie générée par l’app : un suffixe aléatoire, un UUID, une longue suite hexadécimale ou l’horodatage d’un rapport de plantage. La ligne commune garde le libellé de catégorie, affiche le chemin avec * à la place de la partie variable, suivi du nombre d’éléments et de la taille totale, avec une seule case à cocher, et elle ne se déplie pas. ChatGPT est descendu à 33 lignes sur 13 catégories, Hina à 24.
Les dossiers voisins qui ont leur propre nom gardent chacun leur ligne, comme les sept caches Electron de ChatGPT, GPUCache, Code Cache, Crashpad et les autres. Les fusionner aussi aurait laissé Hina avec environ sept lignes, ce qui ne ressemble plus à un nettoyage complet ; une douzaine de lignes, c’est à peu près juste. Un nom qui contient un mot de six lettres, comme T/<id>.helper, garde aussi sa propre ligne, car six lettres seules ne se distinguent pas d’un suffixe aléatoire.
Deux essais précédents sont restés écartés. L’un imbriquait un groupe repliable dans la ligne de l’app déjà dépliée ; essayé le 30 septembre et retiré le 2 octobre, parce qu’il mettait un dépliage dans un dépliage et répartissait des chemins semblables entre le groupe et des lignes isolées. L’autre affichait une ligne par catégorie ; utilisé à partir du 4 septembre et annulé une semaine plus tard, parce qu’il cachait des emplacements distincts derrière une seule ligne.
Comment font les autres désinstalleurs
Pearcleaner liste environ 62 racines en parallèle et compare par nom, sans preuve d’appartenance ni cache ; il est rapide parce qu’il en fait moins. Tencent Lemon calcule les restes de toutes les apps dès qu’on ouvre la page et les rafraîchit chaque jour, donc les clics sont instantanés, au prix d’une grosse analyse en arrière-plan à chaque visite, ce qui va contre la règle de Mole de n’ajouter aucun travail permanent. MangoDisk, Uninstally et Mole CLI utilisent des racines fixes avec un parallélisme limité.
Aucun n’avait de primitive de bas niveau plus rapide que getattrlistbulk et readdir, que Mole utilise déjà. J’ai aussi essayé Spotlight : chaque requête prenait 0,3 à 0,4 seconde et manquait la plupart des restes, n’en trouvant qu’un sur les quatre de VS Code, tandis que searchfs fonctionne sur APFS mais demande environ 15 secondes par requête. Le travail s’est donc résumé à retirer les tâches répétées de l’analyse que Mole avait déjà. Pour savoir comment lire cette liste de restes avant de supprimer quoi que ce soit, voir Désinstaller une app Mac : vérifier les restes avant de supprimer.