
2025/12/28 20:51
**プロジェクト概要** 私は、Macが熱的にスロットリング(温度上昇による性能低下)を開始した際に通知する macOS アプリケーションを開発しています。
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
記事は、Apple Silicon MacBook(特に4K 120 Hz外部ディスプレイを駆動するM2 Air、および熱放散が制限される14″ M4 Max)がサーマルスロットリングを経験する可能性があることを示しています。これは、iStat Menus が 100 % CPU 使用率とともに電力と周波数の低下を報告している点から明らかです。
ユーザーがこの圧力を検知できるように、著者は3つの検出方法を比較しています:
•
ProcessInfo.processInfo.thermalState(値 nominal、fair、serious、critical) – 「fair」は実際には powermetrics の中で「moderate」と「heavy」の両レベルに対応します。•
powermetrics -s thermal CLI は、より細かい「Current pressure level」(moderate, heavy, trapping)を報告します。• システムデーモン
thermald によって公開される Darwin 通知 com.apple.system.thermalpressurelevel は、OSThermalNotification.h で定義されており(nominal 0, moderate 1, heavy 2, trapping 3, sleeping 4)です。
通知チャネルを利用して、著者は MacThrottle を構築しました。これは SwiftUI のメニューバーアプリで、サーモメータアイコンとともに現在の熱状態を表示します。当初は
powermetrics を呼び出すために特権ヘルパーを実行していましたが、現在は通知を直接読み取り、root 権限を不要にしています。アプリはまた、SMC キー(古いシリコン向けには IOKit のフォールバック)を使用して温度とファン速度を取得し、10 分間のウィンドウでこれらのメトリクスをグラフ化します。状態遷移時に macOS 通知を送信し、
SMAppService.mainApp.register() でログイン項目として追加することも可能です。Apple Developer アカウントが無い場合はバイナリを手動でインストールする必要があります。ソースビルドはノタリゼーション制限のある Mac 用に提供されています。
日常ユーザー向けに、MacThrottle は Mac がスロットリングしているタイミングをリアルタイムで把握できるため、ワークロードやディスプレイ設定を調整できます。開発者およびシステムインテグレーターは公開されている
com.apple.system.thermalpressurelevel 通知を利用してカスタム監視ツールを構築し、macOS デバイス全体の電力管理を改善する可能性があります。本文
2025年12月27日 · 2204語・11分
MacThrottle を作った経緯についての物語です。
数年前から M2 MacBook Air に満足しています。
しかし外部ディスプレイ(特に 4K 120 Hz のような高負荷パネル)を接続すると、処理が遅くなりやすいことに気づきました。ファンがないため音はしませんが、すべての操作が遅くなったり応答がなくなる――これが熱スロットリングです。
iStat Menus で CPU 使用率が 100 % のままワット数が下がる様子を見ると、熱スロットリングだと分かります。
MX Power Gadget では、電力使用量とパフォーマンスコアの周波数が低下しているのに、利用率は 100 % を維持していることが一目で分かります。
同じ現象は、仕事用 MacBook Pro(14″ M4 Max)でも発生します。これは 14″ の熱設計が最大出力に対して小さく、最悪のバリアントです。以前使っていた 14″ M1 Pro MacBook Pro では、3 年間ファン音を聞いたことがありませんでした…
それでも Apple Silicon はパフォーマンスと電力使用量で Intel 時代より劇的に改善されているため、大好きです。
Apple Silicon SoC が熱スロットリングしているかどうかを判定する方法はある?
プログラムから thermal state を取得する
思ったよりも大変でした。macOS は複数の不揃いな手段でこの情報を公開しています。
Apple は Foundation の
を推奨しています:ProcessInfo.thermalState
➜ ~ swift -e 'import Foundation; print(["nominal","fair","serious","critical"][ProcessInfo.processInfo.thermalState.rawValue])' nominal
良さそうですが、別のツールも同じ情報を提供します(ただし root が必要です):
powermetrics.
➜ ~ sudo powermetrics -s thermal Password: Machine model: Mac14,2 OS version: 25B78 ... *** Sampled system activity ... *** **** Thermal pressure **** Current pressure level: Nominal
両方とも「Nominal」と報告しますが、実際に負荷テスト(
stress-ng --cpu 0 -t 600)を行うと二つの値が離れます。ProcessInfo.thermalState と powermetrics の granularity が異なり、状態数も違います。
経験上のマッピング:
| ProcessInfo.thermalState | powermetrics |
|---|---|
| nominal | nominal |
| fair | moderate |
| serious | heavy |
| critical | trapping |
「sleeping」状態は一度も触れたことがないので、正確に一致するかは不明ですが、技術的には上記のように定義されています。
実際、Mac が熱くなると
powermetrics は moderate になり、スロットリングが始まると heavy になります。ProcessInfo では両方とも fair に該当し、正確なスロットリング時点を知るには不十分です。
iOS と macOS の違いかと思いましたが、macOS ドキュメントでも同様に参照されています。Intel Mac ではより一貫していた可能性があります。
その他の CLI ツール
Dave MacLachlan(2020)の記事を見つけました。他にも thermal データ取得用 CLI があるものの、Apple Silicon の MacBook では動作しないようです。
➜ ~ sudo thermal levels Thermal levels are unsupported on this machine. ➜ ~ sudo pmset -g thermlog Note: No thermal warning level has been recorded ...
最も興味深いのは、
powermetrics が表示するデータが thermald から来ているということです。さらに thermald は現在の熱圧力を Darwin の通知システム(notifyd)に書き込みます。
➜ ~ notifyutil -g com.apple.system.thermalpressurelevel com.apple.system.thermalpressurelevel 0
各レベルは
OSThermalNotification.h に定義されています(Apple のヘッダ)。列挙体は次の通りです。
typedef enum { #if TARGET_OS_OSX || TARGET_OS_MACCATALYST kOSThermalPressureLevelNominal = 0, kOSThermalPressureLevelModerate, kOSThermalPressureLevelHeavy, kOSThermalPressureLevelTrapping, kOSThermalPressureLevelSleeping #endif } OSThermalPressureLevel;
面白いことに
OSThermalNotification.h はほとんど参照されておらず、Google 検索でも数件しかヒットしません。Bazel などで使われているようです。
通知システムを利用する(root不要)
この方法は root を必要とせず、
com.apple.system.thermalpressurelevel イベントにサブスクライブして 正確な熱状態 を取得できます。以下は Swift のサンプルです。
import Foundation @_silgen_name("notify_register_check") private func notify_register_check(_ name: UnsafePointer<CChar>, _ token: UnsafeMutablePointer<Int32>) -> UInt32 @_silgen_name("notify_get_state") private func notify_get_state(_ token: Int32, _ state: UnsafeMutablePointer<UInt64>) -> UInt32 @_silgen_name("notify_cancel") private func notify_cancel(_ token: Int32) -> UInt32 let notifyOK: UInt32 = 0 let name = "com.apple.system.thermalpressurelevel" var token: Int32 = 0 guard notify_register_check(name.withCString { $0 }, &token) == notifyOK else { fatalError("notify_register_check failed") } defer { _ = notify_cancel(token) } var state: UInt64 = 0 guard notify_get_state(token, &state) == notifyOK else { fatalError("notify_get_state failed") } let label = switch state { case 0: "nominal" case 1: "moderate" case 2: "heavy" case 3: "trapping" case 4: "sleeping" default: "unknown(\(state))" } print("\(state) \(label)")
実行すると:
➜ ~ swift thermal.swift 0 nominal
MacThrottle の構築
概要
Opus 4.5 を使って、Apple Silicon のダイが 110 °C を超えないように警告する小さなメニューバーアプリを作ることにしました。MacThrottle と名付けました。
単純な SwiftUI アプリでメニューバーに温度状態を表示し、オリジナルのサーモメータアイコン(色は緑から赤へ変化)で可視化します。全体で 20 種類のモノクロメタルアイコンがあり、サーモメータ内の色はシンプルにしています。
アプリは SwiftUI の
MenuBarExtra シーンを使用し、Dock アイコンは不要(Info.plist に LSUIElement=true を設定)。予想よりも簡単でした!
初期案:root ヘルパーで powermetrics
powermetrics最初は
powermetrics が必要だと考え、root で動くヘルパー(launchd デーモン)を設置する方針にしました。アプリ自体は権限を上げずに実行し、バックグラウンドで bash スクリプトを走らせます。
Launchd plist
<?xml version="1.0" encoding="UTF-8"?> <plist version="1.0"> <dict> <key>Label</key><string>com.macthrottle.thermal-monitor</string> <key>ProgramArguments</key> <array><string>/usr/local/bin/mac-throttle-thermal-monitor</string></array> <key>RunAtLoad</key><true/> <key>KeepAlive</key><true/> </dict> </plist>
Bash スクリプト
#!/bin/bash OUTPUT_FILE="/tmp/mac-throttle-thermal-state" while true; do THERMAL_OUTPUT=$(powermetrics -s thermal -n 1 -i 1 2>/dev/null | grep -i "Current pressure level") if echo "$THERMAL_OUTPUT" | grep -qi "sleeping"; then PRESSURE="sleeping" elif echo "$THERMAL_OUTPUT" | grep -qi "trapping"; then PRESSURE="trapping" elif echo "$THERMAL_OUTPUT" | grep -qi "heavy"; then PRESSURE="heavy" elif echo "$THERMAL_OUTPUT" | grep -qi "moderate"; then PRESSURE="moderate" elif echo "$THERMAL_OUTPUT" | grep -qi "nominal"; then PRESSURE="nominal" else PRESSURE="unknown" fi echo "{\"pressure\":\"$PRESSURE\",\"timestamp\":$(date +%s)}" > "$OUTPUT_FILE" chmod 644 "$OUTPUT_FILE" sleep 10 done
スクリプトは数秒ごとに熱状態を書き込み、アプリが読み取ります。
ヘルパーを通知で置き換える
notifyd を root なしで利用できることが分かったため、ヘルパーを完全に廃止しました。アプリは直接通知システムにサブスクライブします。これで実装は大幅に簡素化されました 🎉
温度とファン速度の表示
熱状態と温度・ファン速度の相関を確認するため、メニューバーアプリにグラフを追加しました。
温度読み取り
- 非公開 IOKit API:最大約 80 °C と報告。iStat Menus / MX Power Gadget は 100 °C 超。
- SMC(オープンソース版 iStat Menus):より正確だが、各 SoC にキーが異なるため不安定。
private let m1Keys = ["Tp01","Tp05","Tp09","Tp0D","Tp0H","Tp0L","Tp0P","Tp0X","Tp0b"] private let m2Keys = ["Tp01","Tp05","Tp09","Tp0D","Tp0X","Tp0b","Tp0f","Tp0j"] private let m3Keys = ["Tf04","Tf09","Tf0A","Tf0B","Tf0D","Tf0E","Tf44","Tf49","Tf4A","Tf4B"]
M3 キーは M4 Max MacBook Pro でも動作するため、SMC を優先し失敗した場合に IOKit にフォールバックします。
グラフデザイン
一目で熱履歴を確認できるコンパクトな可視化を目指しました。グラフは三層構成です:
- 背景セグメント:緑(nominal)、黄(moderate)、橙(heavy)、赤(critical)。
- 実線:CPU 温度、Y 軸は動的。
- 破線のシアン:ファン速度(ファン付き Mac でのみ)。
アプリは 2 秒ごとにポーリングし、10 分分だけデータを保持して clutter を防ぎます。
.onContinuousHover によるホバーツールチップも追加しました。120 Hz ディスプレイでは .drawingGroup を SwiftUI canvas に設定するまで滑らかでなく、GPU レンダリングに切り替えると再びスムーズになりました。
macOS 通知
熱状態が変化した際(例:スロットリング開始)にシステム通知を送る機能も実装しました。VS Code のインスタンスや Docker コンテナを停止するタイミングを把握でき、MacBook Air が過熱しているときはノイズになるかもしれませんが、温度上昇のサインを見逃しません。
ログイン時にアプリを起動
別途 plist を作成せずに
SMAppService を利用しました:
SMAppService.mainApp.register() // 自動起動有効化 SMAppService.mainApp.unregister() // 無効化 SMAppService.mainApp.status == .enabled // 現在の状態確認
利用方法
Apple Developer アカウントが無いため、リリースされたバイナリは notarization を受けていません。リリースからインストールする際には「プライバシーとセキュリティ」設定で追加の手順が必要です。
Mac が署名済みアプリを許可しない場合は、Xcode でソースコードからビルドしてください(README に手順があります)。
誰かに役立つ情報になれば幸いです!