Mole のアンインストール一覧をクリックですぐ出すまで
Mole のアンインストール画面でアプリをクリックすると、右側がいったん空になり、次に「スキャン中」と出て、0.5 秒から 1 秒ほどたってようやく残存ファイルが並んでいました。Xcode はクリックのたびに 3、4 秒、ChatGPT は 6 秒かかり、ChatGPT を展開してスクロールするとカクつきもありました。この時間が実際どこに消えているのかをきちんと見たくて、いくつかのオープンソースのアンインストーラーが同じ問題をどう扱っているかも調べました。変更は 10 月 5 日に終わり、Mole 1.16.0 の正式版で届けています。以下は当時の計測と、そのときの判断です。
遅いのはアプリそのものではありませんでした
最初は大きなアプリほど遅いのだろうと思っていましたが、測ってみると違いました。残存ファイルが 6 件しかない 6.6 MB の小さなツールでも、最初のクリックで 0.56 秒ほどかかります。時間の大半は、クリックのたびに毎回やっていたいくつかの決まった処理に使われていました。以下は私の M5 Pro で Release ビルドを使い、3 回測った中央値です。
- Homebrew の cask カタログの読み込み。5.6 MB あるファイルで、毎回 200 ms ほどかかり、目的はそのアプリが Homebrew で入れたものかどうかを判定することだけでした。
- アプリのコード署名の検証。しかも 2 回やっていて、実行ファイル 1 MB あたり約 1 ms、MindNode ではクリックごとに 260 ms かかっていました。
- 一番遅いスキャン項目は最近使った書類のリストで、775 件に 230 ms ほど、そのほとんどがパスを 1 件ずつ確かめる時間でした。
さらにアプリごとの事情が上乗せされます。Xcode のインストールレシートは 41 MB で 12 万 5000 件のパスが記録されていて、クリックのたびに解析し直していました。ChatGPT は一時フォルダを 1 万 5000 個ほど残していて、その一つひとつについて本当に ChatGPT のものかを確認する必要がありました。
変えたところ
- cask カタログはクリックを待たず、アンインストール画面を開いた時点でバックグラウンドで読み込みます。
- クリック時は署名情報を読むだけで検証はせず、ファイルの同一性でそのアプリを固定します。完全な署名検証はリストが出たあとにバックグラウンドで走らせ、削除の前には必ず終わらせます。
- 最近使った書類はまず名前で絞り込み、名前が合うものだけ完全な所有者チェックを通します。
- ライブラリ配下のルートは並列で一覧し、コンテナの識別情報はメタデータファイルごとにキャッシュし、補助プログラムの ID プレフィックスはスキャン 1 回につき 1 度だけ計算します。
- インストールレシートはレシートファイルごとにキャッシュするので、2 回目に Xcode をクリックしたときにはあの 41 MB を解析し直しません。
- 名前だけで持ち主がはっきりわかる一時フォルダは速い経路を通します。実際に削除するときは、引き続きパスを 1 件ずつ確認します。
- 時間のかかるスキャンを先に始め、何百もある小さな処理はその後ろに並べます。
- マウスを乗せたときに始まる先読みスキャンはポインタが離れると止まり、アンインストール前に始まったスキャンがアンインストール後に古い結果を書き戻すこともなくなりました。
署名検証については、一番手っ取り早いのは外してしまうことでしたが、私は削除せずにクリックのあとへ移すほうを選びました。今はバックグラウンドの検証に失敗しても、アプリ本体はそのままアンインストールされ、署名情報を手がかりに見つけた種類の残存ファイルだけが残ります。たとえば開発元のチーム ID で照合したコンテナです。結果には「アプリの署名を確認できませんでした」と表示され、削除しなかった項目として扱われ、失敗にはなりません。スキャンのあとでアプリが別のものに入れ替わっていた場合は、これまでどおりアンインストールを拒否します。
どれくらい速くなったか
アンインストール画面を開いて約 1.5 秒後の最初のクリック、こちらも Release ビルドです。
| アプリ | 変更前 | 変更後 |
|---|---|---|
| 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 |
よく使われるアプリの多くは 250 ms 以内に収まり、残存ファイルは「スキャン中」を一瞬はさむことなく一度で並びます。バックグラウンドの署名検証は、リストが出てから 12 から 434 ms のうちに終わります。
ChatGPT の 1 万 5000 行
クリックが速くなっても、ChatGPT を展開してスクロールするとまだカクつきました。数えてみると 15,175 行あり、そのうち 15,143 行が T/.com.openai.codex.XXXXXX のような一時フォルダで、合計 1.0 GB、アプリを起動するたびに少しずつ増えていきます。私が作っている Hina も似ていて、344 行のうち 323 行がテスト実行の残した Hina-<Suite>-<UUID>.plist で、合わせても 0.01 MB でした。どちらも、ランダムな名前のもの一種類がリストのほぼ全部を埋めていました。
カクつきの原因は行数だけではありませんでした。展開した行は再描画のたびに残存ファイルを全部並べ替えていて、マウスを動かさずにリストの上に置いているだけでも、ポインタの下を行が通り過ぎるたびに行全体が再描画され、そのたびにまた並べ替えが走っていました。実際の ChatGPT のデータで測ると、1 回の再描画は 309〜354 ms から 56〜58 ms に、Hina は 33〜36 ms から 17〜19 ms に下がり、ChatGPT を展開したときの高さも 3200 pt から 1225 pt に縮みました。
今は、同じカテゴリ、同じフォルダ、同じ持ち主で、初期のチェック状態も同じで、名前がアプリ自身の生成した部分だけで違う行は 1 行にまとめます。その部分とは、ランダムなサフィックス、UUID、長い 16 進数の並び、クラッシュレポートのタイムスタンプのことです。まとめた行はカテゴリ名を残し、パスの変わる部分を * で置き換え、その後ろに件数と合計サイズを書き、チェックボックスは 1 つだけで、展開はしません。ChatGPT は 13 カテゴリ 33 行に、Hina は 24 行になりました。
自分の名前を持つ兄弟フォルダは、これまでどおり 1 行ずつです。たとえば ChatGPT の 7 つの Electron キャッシュ、GPUCache、Code Cache、Crashpad などです。これもまとめると Hina は 7 行ほどになってしまい、かえってきちんと掃除できたように見えません。十数行くらいがちょうどいい量でした。T/<id>.helper のように 6 文字の単語を含む名前も 1 行のままにしています。6 文字だけでは、それがランダムなサフィックスかどうかを見分けられないからです。
以前に試した 2 つのやり方には、今回も戻していません。1 つは展開したアプリの中にさらに折りたたみのグループを入れる形で、9 月 30 日に試して 10 月 2 日には外しました。折りたたみの中に折りたたみがあり、似たパスがグループとばらの行の両方に分かれてしまったからです。もう 1 つはカテゴリごとに 1 行だけ出す形で、9 月 4 日に使い、1 週間後に戻しました。別々の場所にあるものを 1 行の裏に隠してしまったからです。
ほかのアンインストーラーはどうしているか
Pearcleaner は 62 ほどのルートを並列で一覧し、名前で照合していて、持ち主の証明もキャッシュもありません。速いのはやっていることが少ないからです。Tencent Lemon は画面に入った時点で全アプリの残存ファイルを計算し、1 日 1 回更新するので、クリックは一瞬です。その代わり開くたびに大きなバックグラウンドスキャンが走り、常駐の負荷を増やさないという Mole の方針とは合いません。MangoDisk、Uninstally、Mole CLI は、固定のルートに上限つきの並列処理という形です。
調べた範囲では、Mole がすでに使っている getattrlistbulk と readdir より速い低レベルの手段を持つツールはありませんでした。Spotlight も試しましたが、1 回の問い合わせに 0.3〜0.4 秒かかり、残存ファイルの大半を取りこぼし、VS Code の 4 件のうち 1 件しか見つかりませんでした。searchfs は APFS でも動きますが、1 回に 15 秒ほどかかります。結局、今あるスキャンから重複した処理を取り除くことに尽きました。削除する前にこの残存ファイルのリストをどう読むかは、Mac アプリの残存ファイルは、削除前に一覧を確認の記事に続きます。