Making Mole's Uninstall List Appear on Click
Clicking an app on Mole's uninstall page used to leave the right side blank for a moment, then show "Scanning", and only after half a second to a second did the leftovers appear. Xcode took three to four seconds on every click, ChatGPT six, and scrolling through ChatGPT's expanded list stuttered. I wanted to see where that time actually went, and looked at how a few open-source uninstallers handle the same problem. The changes were finished on October 5 and shipped in Mole 1.16.0; what follows is what I measured at the time and the trade-offs I made.
The app itself was not the slow part
I expected bigger apps to be slower, and they were not: a 6.6 MB utility with six leftovers still took about 0.56 seconds on its first click. Most of the time went into a few fixed jobs that ran on every click. These are Release-build medians of three runs on my M5 Pro:
- Reading Homebrew's cask catalog, a 5.6 MB file, took about 200 ms each time, only to tell whether the app was installed with Homebrew.
- The app's code signature was validated, twice, at roughly 1 ms per MB of executable; MindNode spent 260 ms on it per click.
- The slowest scan task was the recent-documents lists: about 230 ms for 775 entries, nearly all of it spent checking paths one by one.
Some apps added their own cost on top. Xcode's installer receipt is 41 MB and lists 125,000 paths, and it was parsed again on every click. ChatGPT leaves about 15,000 temporary folders behind, and each one had to be checked for ownership on its own.
What changed
- The cask catalog is read in the background as soon as the uninstall page opens, not on the click.
- The click reads the signing information without validating it and pins the app by file identity. Full validation runs in the background after the list appears, and it must finish before anything is deleted.
- Recent-documents entries are filtered by name first; only those that match go through the full ownership check.
- The Library roots are listed in parallel, container identities are cached per metadata file, and helper-ID prefixes are worked out once per scan.
- Installer receipts are cached per receipt file, so a second click on Xcode does not parse those 41 MB again.
- Temporary folders whose name alone proves who owns them take a fast path; the deletion step still checks every path.
- Long scan tasks start first, and the hundreds of small ones queue behind them.
- A scan started by hovering stops when the pointer leaves, and a scan started before an uninstall can no longer write stale results afterwards.
The easy option for the signature check was to drop it. I moved it after the click instead. When the background validation fails now, the app itself is still uninstalled, and only the kinds of leftovers that were found through the signing information stay behind, such as containers matched by the developer's team ID. The result lists them as "Couldn't verify the app's signature", kept rather than failed. An app that was replaced by a different one after the scan is still refused.
How much faster
First click about 1.5 seconds after opening the uninstall page, Release build:
| App | Before | After |
|---|---|---|
| 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 |
Most common apps now land under 250 ms, so the leftovers appear in one step without a "Scanning" label first. The background signature validation finishes 12 to 434 ms after the list appears.
ChatGPT's fifteen thousand rows
With the clicks fast, scrolling an expanded ChatGPT still stuttered. It listed 15,175 rows, and 15,143 of them were temporary folders shaped like T/.com.openai.codex.XXXXXX, 1.0 GB in total, a few more left behind every time the app launches. Hina, an app I make, was similar: 323 of its 344 rows were Hina-<Suite>-<UUID>.plist files left by test runs, 0.01 MB together. In both cases one randomly named shape filled almost the whole list.
The row count was not the only cause. The expanded row re-sorted every leftover each time it redrew, and hovering redrew the whole app row, so every row that slid under a still pointer triggered another sort. Rendering the real ChatGPT data, one redraw went from 309 to 354 ms down to 56 to 58 ms, Hina from 33 to 36 ms to 17 to 19 ms, and ChatGPT's expanded height shrank from 3200 pt to 1225 pt.
Rows now share one line when they have the same category, folder, owner and default selection, and their names differ only in a part the app generated: a random suffix, a UUID, a long hex run or a crash-report timestamp. The shared line keeps the category label, shows the path with * in place of the changing part, followed by the item count and total size, with a single checkbox, and it does not expand. ChatGPT went down to 33 rows across 13 categories, Hina to 24.
Siblings with names of their own keep separate rows, such as ChatGPT's seven Electron caches, GPUCache, Code Cache, Crashpad and the rest. Merging those as well would have left Hina with about seven rows, which no longer looks like a thorough cleanup; a dozen or so is about right. A name carrying a six-letter word, such as T/<id>.helper, also stays on its own row, because six letters alone cannot be told apart from a random suffix.
Two earlier attempts stayed rejected. One nested a foldable group inside the already expanded app row; it was tried on September 30 and removed on October 2, because it put folding inside folding and split similar paths between the group and loose rows. The other showed one row per category; it was used on September 4 and reversed a week later, because it hid separate locations behind one line.
How other uninstallers do it
Pearcleaner lists about 62 roots concurrently and matches by name, with no ownership proof and no cache; it is fast because it does less. Tencent Lemon works out every app's leftovers when you enter the page and refreshes daily, so clicks are instant, at the cost of a large background scan on every visit, which goes against Mole's rule of adding no resident work. MangoDisk, Uninstally and the Mole CLI use fixed roots with bounded concurrency.
None of them had a faster underlying primitive than the getattrlistbulk and readdir Mole already uses. I tried Spotlight too: each query took 0.3 to 0.4 seconds and missed most leftovers, finding one of VS Code's four, while searchfs works on APFS but takes about 15 seconds per query. So the work came down to removing repeated jobs from the scan Mole already had. For how to read that leftover list before you delete anything, see Mac uninstall leftovers: review before you delete.