文書
life / research / 2026-09-16
作成
秘書 あおい
宛先
shika さん
件名
PC 発熱の原因調査と、恒久対策の実装
thermal audit / expo-idle-guard / 2026-09-16

Mac 発熱調査と自動停止の番人

「PC が熱を帯びている」という申告から、実測で熱源を 2 つ特定しました。11 時間 47 分にわたり 1 コアを焼き続けていた放置済みの Expo 開発サーバーと、445MB の 4K 動画を再生し続けていた OS 標準の「動く壁紙」です。両方を止め、前者については未使用時に自動停止する番人を実装して launchd に載せました。

観測時刻はすべて JST。調査 2026-09-16 10:53〜11:18。対象機は Darwin 25.6.0(macOS 26 系)。

10h28m放置 Expo サーバーの累積 CPU
経過 11h50m の 88%
2h59m動く壁紙の累積 CPU
2 日で消費
457MB削除した壁紙アセット
うち動画本体 445MB
4.82 → 1.51load average(1分値)
10:53 → 11:17

1. 何が熱源だったか

瞬間の CPU 使用率ではなく 累積 CPU 時間(ps の TIME 列)で並べ替えると、犯人は一目で出ました。これが今回いちばん再利用できる手順です。

$ ps -Ao pid,user,time,%cpu,etime,command | (累積CPU秒に変換して降順)

 10h28m 16157  node .../shushu-app/node_modules/.bin/expo start --dev-client --lan --port 8081 --clear
  9h34m  6020  .../Virtualization.framework/.../com.apple.Virtualization.VirtualMachine
  5h18m   406  .../SkyLight.framework/Resources/WindowServer -daemon
  2h59m   678  .../WallpaperAerialsExtension.appex/Contents/MacOS/WallpaperAerialsExtension
  1h51m  6063  .../Docker Desktop Helper (Renderer)

熱源 A: 放置された Expo 開発サーバー

項目実測値
PID16157
起動2026-09-15 23:06:14
経過11時間50分
累積 CPU10時間28分(経過の 88%)
親プロセスlaunchd(PID 1)= 起動元のターミナルは閉じられていた
配下jest-worker × 6 が同時に残存

待機中の Metro が 1 コアを張り付かせるのは異常値です。watchman が未インストールで、ファイル監視が Node 自前の経路に落ちていたことが有力な原因ですが、この点は未確認です(サーバーが調査中に別セッションから再起動され、暴走状態の再現ができなくなったため)。

本質的な問題は暴走そのものではなく「孤児化」です。ターミナルを閉じても親が launchd に移るだけでプロセスは生き残ります。誰も見ていない・誰も止めない・止まる条件も無い。だから 11 時間走り続けました。次節の番人はここに効きます。

熱源 B: OS 標準の「動く壁紙」

当初「開発中の機能が使っているかもしれない」という見立てでしたが、調査の結果これは macOS 標準のエアリアル壁紙で、開発物とは無関係でした(リポジトリ全文検索でヒット 0。唯一の一致はポケモンのメガプテラの記事)。

項目実測値
実体/System/Library/ExtensionKit/Extensions/WallpaperAerialsExtension.appex
削除可否不可(SIP 有効。csrutil status = enabled)
動いていた理由壁紙設定が macOS 既定のまま(Provider: default、LastSet 2024-11-10 から未変更)
再生していた動画4C108785-….mov = "Tahoe Day" 445MB(6016×6016)
アセット合計457MB

2. 実施した対処

止めたもの

  • 壁紙を全 Space で静止画(Mac Blue.heic)に変更 → 動画の再生が停止(拡張プロセス自体は常駐するが、✅ 実測 11:20:50〜11:21:41 の 2 分間、累積 CPU が 0:00.26 から 1 ミリ秒も動かない休眠状態)
  • アセット 457MB を削除(45 秒後の再確認で再ダウンロードなし)
  • Expo サーバーは調査中に別セッションが再起動済み(現在 CPU 0.2%)

触れていないもの

  • スクリーンセーバ(Drift)— 壁紙の設定単位を linked から individual に分けて、Idle 側は元のまま残した
  • Supabase ローカルスタック 11 コンテナ(累積 9h34m)— 開発に使用中のため判断待ち
  • iOS シミュレータ・claude セッション 5 本

3. 番人: bin/expo-idle-guard.sh

「シミュレータでの動作確認に使っている、もしくは iPhone が刺さっている、のどちらも満たさないなら自動停止する」という指示に対する実装です。5 分おきに launchd が起動します。

「使っている」の判定

条件判定方法実測所要
(a) シミュレータが起動中xcrun simctl list devices booted に (Booted)0.28 秒
(b) iOS 実機が USB 接続ioreg -rc IOUSBHostDevice に SupportsIPhoneOS0.015 秒
(c) ビルド進行中expo run:ios / xcodebuild / eas build の存在—

1 つでも真なら停止しません。(c) は指示に無い追加条件ですが、調査中に実際に事故りかけたため入れました。10:55 に別セッションが expo run:ios を開始した直後で、ビルドは 8081 の Metro を掴むため、巻き込むと「原因不明で、しかも再現しないビルド失敗」になります。

実機判定に xcrun devicectl(USB と Wi-Fi を transportType で区別できる)を採らず ioreg にしたのは、devicectl が数秒かかり Xcode の在否に依存するためです。代償として Wi-Fi ペアリングのみの端末は「刺さっていない」と判定されますが、これは指示の文言どおりです。

止め方

  1. 1 回の不在では止めない。連続 2 回(=最短 5 分・最長 15 分)観測して初めて停止する。シミュレータ再起動や端末の抜き差しで一瞬不在になるのは正常で、そこで殺すと人は仕組みごと無効化する
  2. 起動から 10 分未満は見送る。起動処理中を殺さない
  3. プロセスグループごと TERM → 10 秒待つ → KILL。npx → npm → node → jest-worker×6 の一族をまとめて落とす。PGID が安全でないときは個別 PID に落とす
  4. 黙って殺さない。停止ログに「何を・いつ起動した・累積 CPU いくつのものを」を残し、後から妥当性を判断できるようにする

受け入れテスト

bin/expo-idle-guard.test.sh — 20 項目すべて通過。本物の node プロセスを立てて実際に停止できることまで確認しています(モックの戻り値照合では「プロセスグループごと落とす」の検証にならないため)。

🔴 このテストは一度、本番環境を壊しました。初版はスタブで「シミュレータなし・実機なし」を装った瞬間、番人がマシン全体を走査して、テスト用の偽サーバーだけでなく別セッションが使っていた本物の Expo サーバー(PID 90932、ポート 8081)まで停止させました。

対策として本体に EXPO_GUARD_MATCH(走査範囲をコマンドラインの部分一致で限定する口)を追加し、テストは必ず自分の作業場に閉じるようにしました。本番では空のまま=マシン全体が対象です。テストが本番環境を壊せる時点でテストではありません。

実装中に踏んだもう 1 つの罠

--install が終了コード 1 で落ちました。原因は say "ラベル : $LABEL(5分おき)" の 1 行で、bash が全角括弧まで変数名の一部として読み、set -u 下で未定義変数エラーになったものです。LaunchAgent 自体は読み込みに成功していたため、「失敗したように見えて半分成功している」という最悪の壊れ方をしました。日本語でログを書く以上必ず起きるので、変数は ${VAR} で囲む規約を設計の芯に書き込んであります。

4. 使い方

bin/expo-idle-guard.sh --status      # いまの判定材料とストライク数を見る
bin/expo-idle-guard.sh --dry-run     # 判定だけして停止はしない(状態も書き換えない)
bin/expo-idle-guard.sh --uninstall   # launchd から外す
bash bin/expo-idle-guard.test.sh     # 受け入れテスト(20項目)

tail -f ~/.local/state/personal-os/logs/expo-idle-guard.log   # 停止の記録

ログは「使用中」のときは 1 行も書きません(沈黙)。STRIKE・RESET・STOP・SKIP のときだけ記録します。

5. 残っている判断

対象実測提案
Supabase ローカルスタック(11 コンテナ)VM 累積 CPU 9h34m / 45 時間連続稼働使わない時間帯は supabase stop。常時稼働が要るかは事業側の判断
iOS シミュレータiPhone 17 Pro が起動中実機で確認しているなら落とす
claude セッション 5 本最古 1 日 18 時間半 / 付随 MCP 10 プロセスCPU はほぼ 0。メモリ整理の観点で古いものを閉じる
エアリアル拡張の常駐プロセス休眠(累積 CPU 0.26 秒で停止)SIP により削除不可。動画も消えたため実害なし。完全に消すには OS の拡張機能管理が要る
watchman 未インストールMetro のファイル監視が Node 自前経路暴走の再現ができていないため未確認。再発したら導入を検討