diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index af111e3..ed098e7 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -54,7 +54,15 @@ Things that are easy to get wrong and are handled on purpose: - Process CPU time and wall time are both in mach absolute units, so their ratio needs no timebase conversion. - Core types come from `hw.perflevelN`; logical CPUs are numbered from the lowest - performance level up. + performance level up. The reported names distinguish super, performance and efficiency cores; + groups are never inferred from the chip brand. Invalid or incomplete core counts fall back to + one generic group covering all logical CPUs. Two-group S/P and P/E fixtures and three-group + S/P/E fixtures verify the indices and workload descriptions; new-chip hardware still needs + its own runtime validation. +- CPU snapshots include macOS `ProcessInfo.thermalState` through the existing sampling loop, + without another timer or observer. The CPU detail shows nominal, fair, serious or critical + thermal pressure independently of SMC temperature. Temperature headroom uses 100°C only as + a reference, not as a device-specific throttling threshold. ## SMC diff --git a/Packages/XStatsKit/Sources/Localization/Resources/ar.tsv b/Packages/XStatsKit/Sources/Localization/Resources/ar.tsv index f2ddc51..610a7a1 100644 --- a/Packages/XStatsKit/Sources/Localization/Resources/ar.tsv +++ b/Packages/XStatsKit/Sources/Localization/Resources/ar.tsv @@ -799,7 +799,6 @@ uv 下载的 Python 包与构建缓存,由 uv 自行清理 ذاكرة حزم P 核心 {}({})· {} النواة {} ({}) · {} 核心分工 أنواع النوى 核心数 عدد النوى -核心最高温度;余量是离 100°C 还差多少度,越接近 0 越可能因过热降频 أعلى حرارة للنوى؛ الهامش هو عدد الدرجات المتبقية حتى 100°C. كلما اقترب من 0 زاد احتمال خفض التردد بسبب الحرارة 核心热力图 خريطة حمل النوى 核心负载 حمل النوى 检查 فحص @@ -1195,7 +1194,6 @@ Apple 智能本机模型当前不可用。 نموذج Apple Intelligence على 通知 الإشعارات 通知可以正常显示。发生你打开的状况时,会像这样提醒你。 تعمل الإشعارات. سيصلك تنبيه كهذا عند حدوث حالة فعّلتها. 通过 {} 登录 تم تسجيل الدخول باستخدام {} -速度与省电介于两者之间 متوسطة بين السرعة والكفاءة 部分网站直连、部分经代理,每个网站走哪个出口见下方。 تتصل بعض المواقع مباشرة وأخرى عبر وكيل. راجع منفذ خروج كل موقع أدناه. 部分项目需要“完全磁盘访问权限”才能扫描(Safari 缓存、废纸篓)。 تتطلب بعض العناصر الوصول الكامل إلى القرص لفحصها (ذاكرات Safari المؤقتة وسلة المهملات). 配置已保存,上传或下载时将验证连接 حُفظ الإعداد. سيُتحقق من الاتصال عند الرفع أو التنزيل @@ -1673,3 +1671,11 @@ IP 归属地 موقع عنوان IP 音量 مستوى الصوت 显示链路正在恢复… جارٍ استعادة اتصال الشاشة… 对比度 التباين +热压力:正常 الضغط الحراري: طبيعي +热压力:偏高 الضغط الحراري: مرتفع +热压力:严重 الضغط الحراري: شديد +热压力:临界 الضغط الحراري: حرج +热压力:未知 الضغط الحراري: غير معروف +高效处理多线程任务 يعالج المهام متعددة الخيوط بكفاءة +由 macOS 报告的系统热状态,与 CPU 温度读数独立。 الحالة الحرارية للنظام كما يبلغ عنها macOS، وهي مستقلة عن قراءات درجة حرارة المعالج. +核心最高温度;余量以 100°C 为参考,不代表设备实际降频阈值。 أعلى درجة حرارة للأنوية؛ يستخدم الهامش 100°C كمرجع، وليس كحد فعلي لخفض تردد الجهاز. diff --git a/Packages/XStatsKit/Sources/Localization/Resources/de.tsv b/Packages/XStatsKit/Sources/Localization/Resources/de.tsv index 2789d9d..272a8ba 100644 --- a/Packages/XStatsKit/Sources/Localization/Resources/de.tsv +++ b/Packages/XStatsKit/Sources/Localization/Resources/de.tsv @@ -799,7 +799,6 @@ uv 下载的 Python 包与构建缓存,由 uv 自行清理 Von uv geladener Py 核心 {}({})· {} Kern {} ({}) · {} 核心分工 Kerntypen 核心数 Kerne -核心最高温度;余量是离 100°C 还差多少度,越接近 0 越可能因过热降频 Wärmster Kern; der Spielraum ist der Abstand zu 100 °C. Je näher an 0, desto wahrscheinlicher wird thermische Drosselung 核心热力图 Kern-Heatmap 核心负载 Kernauslastung 检查 Prüfen @@ -1195,7 +1194,6 @@ Apple 智能本机模型当前不可用。 Das lokale Apple Intelligence-Modell 通知 Mitteilungen 通知可以正常显示。发生你打开的状况时,会像这样提醒你。 Mitteilungen funktionieren. Bei aktivierten Ereignissen werden Sie auf diese Weise benachrichtigt. 通过 {} 登录 Mit {} angemeldet -速度与省电介于两者之间 Bei Geschwindigkeit und Effizienz zwischen beiden 部分网站直连、部分经代理,每个网站走哪个出口见下方。 Einige Websites verbinden sich direkt, andere über einen Proxy. Die Ausgänge der Websites stehen unten. 部分项目需要“完全磁盘访问权限”才能扫描(Safari 缓存、废纸篓)。 Einige Objekte benötigen zum Scannen Festplattenvollzugriff (Safari-Caches, Papierkorb). 配置已保存,上传或下载时将验证连接 Konfiguration gesichert. Die Verbindung wird beim Hoch- oder Herunterladen geprüft @@ -1673,3 +1671,11 @@ IP 归属地 IP-Standort 音量 Lautstärke 显示链路正在恢复… Displayverbindung wird wiederhergestellt… 对比度 Kontrast +热压力:正常 Thermische Belastung: Normal +热压力:偏高 Thermische Belastung: Erhöht +热压力:严重 Thermische Belastung: Hoch +热压力:临界 Thermische Belastung: Kritisch +热压力:未知 Thermische Belastung: Unbekannt +高效处理多线程任务 Bearbeitet Multithread-Aufgaben effizient +由 macOS 报告的系统热状态,与 CPU 温度读数独立。 Von macOS gemeldeter thermischer Systemzustand, unabhängig von CPU-Temperaturwerten. +核心最高温度;余量以 100°C 为参考,不代表设备实际降频阈值。 Höchste Kerntemperatur; der Abstand zu 100°C dient nur als Referenz, nicht als tatsächliche Drosselungsschwelle des Geräts. diff --git a/Packages/XStatsKit/Sources/Localization/Resources/es.tsv b/Packages/XStatsKit/Sources/Localization/Resources/es.tsv index 51b6c15..befc899 100644 --- a/Packages/XStatsKit/Sources/Localization/Resources/es.tsv +++ b/Packages/XStatsKit/Sources/Localization/Resources/es.tsv @@ -799,7 +799,6 @@ uv 下载的 Python 包与构建缓存,由 uv 自行清理 Cachés de paquetes 核心 {}({})· {} Núcleo {} ({}) · {} 核心分工 Tipos de núcleo 核心数 Núcleos -核心最高温度;余量是离 100°C 还差多少度,越接近 0 越可能因过热降频 Núcleo más caliente; el margen indica los grados hasta 100 °C. Cuanto más cerca de 0, más probable es la limitación térmica 核心热力图 Mapa térmico de núcleos 核心负载 Carga de núcleos 检查 Comprobar @@ -1195,7 +1194,6 @@ Apple 智能本机模型当前不可用。 El modelo de Apple Intelligence en el 通知 Notificaciones 通知可以正常显示。发生你打开的状况时,会像这样提醒你。 Las notificaciones funcionan. Recibirás una alerta así cuando ocurra alguna condición activada. 通过 {} 登录 Sesión iniciada con {} -速度与省电介于两者之间 Velocidad y eficiencia intermedias 部分网站直连、部分经代理,每个网站走哪个出口见下方。 Algunos sitios conectan directamente y otros por proxy. Consulta abajo la salida de cada uno. 部分项目需要“完全磁盘访问权限”才能扫描(Safari 缓存、废纸篓)。 Algunos elementos requieren acceso total al disco para analizarse (cachés de Safari, Papelera). 配置已保存,上传或下载时将验证连接 Configuración guardada. La conexión se verificará al subir o descargar @@ -1673,3 +1671,11 @@ IP 归属地 Ubicación IP 音量 Volumen 显示链路正在恢复… Restableciendo la conexión de pantalla… 对比度 Contraste +热压力:正常 Presión térmica: Normal +热压力:偏高 Presión térmica: Elevada +热压力:严重 Presión térmica: Grave +热压力:临界 Presión térmica: Crítica +热压力:未知 Presión térmica: Desconocida +高效处理多线程任务 Procesa eficientemente tareas multihilo +由 macOS 报告的系统热状态,与 CPU 温度读数独立。 Estado térmico del sistema informado por macOS, independiente de las lecturas de temperatura de la CPU. +核心最高温度;余量以 100°C 为参考,不代表设备实际降频阈值。 Temperatura máxima de los núcleos; el margen usa 100°C como referencia, no como umbral real de limitación del dispositivo. diff --git a/Packages/XStatsKit/Sources/Localization/Resources/fr.tsv b/Packages/XStatsKit/Sources/Localization/Resources/fr.tsv index 5e4caca..86c7ee7 100644 --- a/Packages/XStatsKit/Sources/Localization/Resources/fr.tsv +++ b/Packages/XStatsKit/Sources/Localization/Resources/fr.tsv @@ -799,7 +799,6 @@ uv 下载的 Python 包与构建缓存,由 uv 自行清理 Caches de paquets P 核心 {}({})· {} Cœur {} ({}) · {} 核心分工 Types de cœurs 核心数 Cœurs -核心最高温度;余量是离 100°C 还差多少度,越接近 0 越可能因过热降频 Cœur le plus chaud ; la marge est l’écart jusqu’à 100 °C. Plus elle approche de 0, plus le risque de bridage thermique augmente 核心热力图 Carte thermique des cœurs 核心负载 Charge des cœurs 检查 Vérifier @@ -1195,7 +1194,6 @@ Apple 智能本机模型当前不可用。 Le modèle Apple Intelligence sur l 通知 Notifications 通知可以正常显示。发生你打开的状况时,会像这样提醒你。 Les notifications fonctionnent. Vous recevrez ce type d’alerte lorsqu’un événement activé se produira. 通过 {} 登录 Connecté avec {} -速度与省电介于两者之间 Intermédiaire en vitesse et en efficacité 部分网站直连、部分经代理,每个网站走哪个出口见下方。 Certains sites se connectent directement, d’autres via un proxy. Les sorties utilisées figurent ci-dessous. 部分项目需要“完全磁盘访问权限”才能扫描(Safari 缓存、废纸篓)。 Certains éléments nécessitent l’accès complet au disque pour être analysés (caches Safari, Corbeille). 配置已保存,上传或下载时将验证连接 Configuration enregistrée. La connexion sera vérifiée lors de l’envoi ou du téléchargement @@ -1673,3 +1671,11 @@ IP 归属地 Localisation IP 音量 Volume 显示链路正在恢复… Rétablissement de la connexion à l’écran… 对比度 Contraste +热压力:正常 Pression thermique : normale +热压力:偏高 Pression thermique : élevée +热压力:严重 Pression thermique : importante +热压力:临界 Pression thermique : critique +热压力:未知 Pression thermique : inconnue +高效处理多线程任务 Traite efficacement les tâches multithreads +由 macOS 报告的系统热状态,与 CPU 温度读数独立。 État thermique du système signalé par macOS, indépendant des mesures de température du CPU. +核心最高温度;余量以 100°C 为参考,不代表设备实际降频阈值。 Température maximale des cœurs ; la marge utilise 100°C comme référence, pas comme seuil réel de limitation de l’appareil. diff --git a/Packages/XStatsKit/Sources/Localization/Resources/ja.tsv b/Packages/XStatsKit/Sources/Localization/Resources/ja.tsv index 419e82c..50f3b24 100644 --- a/Packages/XStatsKit/Sources/Localization/Resources/ja.tsv +++ b/Packages/XStatsKit/Sources/Localization/Resources/ja.tsv @@ -799,7 +799,6 @@ uv 下载的 Python 包与构建缓存,由 uv 自行清理 uvのPythonパッ 核心 {}({})· {} コア {}({})· {} 核心分工 コアの役割 核心数 コア数 -核心最高温度;余量是离 100°C 还差多少度,越接近 0 越可能因过热降频 コアの最高温度。余裕は100°Cまでの残り温度で、0に近いほど過熱による性能低下が起きやすくなります 核心热力图 コアのヒートマップ 核心负载 コア負荷 检查 確認 @@ -1195,7 +1194,6 @@ Apple 智能本机模型当前不可用。 Apple Intelligenceのオンデバイ 通知 通知 通知可以正常显示。发生你打开的状况时,会像这样提醒你。 通知は正常に表示されています。有効にした状況が発生すると、このようにお知らせします。 通过 {} 登录 {}でサインイン済み -速度与省电介于两者之间 速度と電力効率が両者の中間 部分网站直连、部分经代理,每个网站走哪个出口见下方。 直接接続するサイトとプロキシ経由のサイトがあります。各サイトの出口は以下をご覧ください。 部分项目需要“完全磁盘访问权限”才能扫描(Safari 缓存、废纸篓)。 一部項目(Safariキャッシュ、ゴミ箱)のスキャンにはフルディスクアクセスが必要です。 配置已保存,上传或下载时将验证连接 設定を保存しました。アップロード・ダウンロード時に接続を検証します @@ -1673,3 +1671,11 @@ IP 归属地 IPの所在地 音量 音量 显示链路正在恢复… ディスプレイの接続を復旧中… 对比度 コントラスト +热压力:正常 熱負荷:正常 +热压力:偏高 熱負荷:やや高い +热压力:严重 熱負荷:高い +热压力:临界 熱負荷:危険 +热压力:未知 熱負荷:不明 +高效处理多线程任务 マルチスレッド処理を効率的に実行 +由 macOS 报告的系统热状态,与 CPU 温度读数独立。 macOS が報告するシステムの熱状態です。CPU の温度測定とは別の情報です。 +核心最高温度;余量以 100°C 为参考,不代表设备实际降频阈值。 コアの最高温度。余裕は 100°C を基準とした参考値で、実際の周波数制限のしきい値ではありません。 diff --git a/Packages/XStatsKit/Sources/Localization/Resources/ko.tsv b/Packages/XStatsKit/Sources/Localization/Resources/ko.tsv index a842d87..f5b9d67 100644 --- a/Packages/XStatsKit/Sources/Localization/Resources/ko.tsv +++ b/Packages/XStatsKit/Sources/Localization/Resources/ko.tsv @@ -799,7 +799,6 @@ uv 下载的 Python 包与构建缓存,由 uv 自行清理 uv의 Python 패키 核心 {}({})· {} 코어 {}({}) · {} 核心分工 코어 역할 核心数 코어 수 -核心最高温度;余量是离 100°C 还差多少度,越接近 0 越可能因过热降频 코어 최고 온도입니다. 여유는 100°C까지 남은 온도로 0에 가까울수록 과열로 성능이 낮아질 가능성이 높습니다 核心热力图 코어 히트맵 核心负载 코어 부하 检查 확인 @@ -1195,7 +1194,6 @@ Apple 智能本机模型当前不可用。 Apple Intelligence 온디바이스 通知 알림 通知可以正常显示。发生你打开的状况时,会像这样提醒你。 알림이 정상 작동합니다. 켜 둔 상황이 발생하면 이렇게 알려 드립니다. 通过 {} 登录 {}(으)로 로그인됨 -速度与省电介于两者之间 속도와 전력 효율이 둘 사이 部分网站直连、部分经代理,每个网站走哪个出口见下方。 일부 사이트는 직접 연결하고 일부는 프록시를 통합니다. 사이트별 출구는 아래를 확인하세요. 部分项目需要“完全磁盘访问权限”才能扫描(Safari 缓存、废纸篓)。 일부 항목(Safari 캐시, 휴지통)을 스캔하려면 전체 디스크 접근 권한이 필요합니다. 配置已保存,上传或下载时将验证连接 구성을 저장했습니다. 업로드 또는 다운로드 시 연결을 확인합니다 @@ -1673,3 +1671,11 @@ IP 归属地 IP 위치 音量 음량 显示链路正在恢复… 디스플레이 연결 복구 중… 对比度 명암 +热压力:正常 열 상태: 정상 +热压力:偏高 열 상태: 다소 높음 +热压力:严重 열 상태: 심각 +热压力:临界 열 상태: 위험 +热压力:未知 열 상태: 알 수 없음 +高效处理多线程任务 멀티스레드 작업을 효율적으로 처리 +由 macOS 报告的系统热状态,与 CPU 温度读数独立。 macOS가 보고한 시스템 열 상태이며, CPU 온도 측정값과 별개입니다. +核心最高温度;余量以 100°C 为参考,不代表设备实际降频阈值。 가장 높은 코어 온도입니다. 여유는 100°C를 기준으로 한 참고값이며, 기기의 실제 성능 제한 임계값이 아닙니다. diff --git a/Packages/XStatsKit/Sources/Localization/Resources/zh-Hant.tsv b/Packages/XStatsKit/Sources/Localization/Resources/zh-Hant.tsv index 581b3cd..4502fbf 100644 --- a/Packages/XStatsKit/Sources/Localization/Resources/zh-Hant.tsv +++ b/Packages/XStatsKit/Sources/Localization/Resources/zh-Hant.tsv @@ -800,7 +800,6 @@ uv 下载的 Python 包与构建缓存,由 uv 自行清理 uv 下載的 Python 核心 {}({})· {} 核心 {}({})· {} 核心分工 核心分工 核心数 核心數 -核心最高温度;余量是离 100°C 还差多少度,越接近 0 越可能因过热降频 核心最高溫度;餘量是離 100°C 還差多少度,越接近 0 越可能因過熱降頻 核心热力图 核心熱力圖 核心负载 核心負載 检查 檢查 @@ -1196,7 +1195,6 @@ Apple 智能本机模型当前不可用。 Apple 智能本機模型目前無法 通知 通知 通知可以正常显示。发生你打开的状况时,会像这样提醒你。 通知可以正常顯示。發生你打開的狀況時,會像這樣提醒你。 通过 {} 登录 通過 {} 登錄 -速度与省电介于两者之间 速度與省電介於兩者之間 部分网站直连、部分经代理,每个网站走哪个出口见下方。 部分網站直連、部分經代理,每個網站走哪個出口見下方。 部分项目需要“完全磁盘访问权限”才能扫描(Safari 缓存、废纸篓)。 部分項目需要“完全磁盤訪問權限”才能掃描(Safari 緩存、廢紙簍)。 配置已保存,上传或下载时将验证连接 配置已保存,上傳或下載時將驗證連接 @@ -1674,3 +1672,11 @@ IP 归属地 IP 歸屬地 音量 音量 显示链路正在恢复… 顯示連線正在恢復… 对比度 對比度 +热压力:正常 熱壓力:正常 +热压力:偏高 熱壓力:偏高 +热压力:严重 熱壓力:嚴重 +热压力:临界 熱壓力:臨界 +热压力:未知 熱壓力:未知 +高效处理多线程任务 高效處理多執行緒任務 +由 macOS 报告的系统热状态,与 CPU 温度读数独立。 由 macOS 回報的系統熱狀態,與 CPU 溫度讀數獨立。 +核心最高温度;余量以 100°C 为参考,不代表设备实际降频阈值。 核心最高溫度;餘量以 100°C 為參考,不代表裝置實際降頻門檻。 diff --git a/Packages/XStatsKit/Sources/Localization/Translations.swift b/Packages/XStatsKit/Sources/Localization/Translations.swift index 0739906..cfd1681 100644 --- a/Packages/XStatsKit/Sources/Localization/Translations.swift +++ b/Packages/XStatsKit/Sources/Localization/Translations.swift @@ -65,6 +65,14 @@ extension Translations { """# static let english = #""" +热压力:正常 Thermal pressure: Normal +热压力:偏高 Thermal pressure: Elevated +热压力:严重 Thermal pressure: Serious +热压力:临界 Thermal pressure: Critical +热压力:未知 Thermal pressure: Unknown +高效处理多线程任务 Efficiently handles multithreaded work +由 macOS 报告的系统热状态,与 CPU 温度读数独立。 System thermal state reported by macOS, independent of CPU temperature readings. +核心最高温度;余量以 100°C 为参考,不代表设备实际降频阈值。 Highest core temperature; headroom uses 100°C as a reference, not the device’s actual throttling threshold. GitHub 项目 GitHub Repository 赞助 XStats Support XStats Apple 智能 Apple Intelligence @@ -947,7 +955,6 @@ uv 下载的 Python 包与构建缓存,由 uv 自行清理 Python package and 核心 {}({})· {} Core {} ({}) · {} 核心分工 Core Types 核心数 Cores -核心最高温度;余量是离 100°C 还差多少度,越接近 0 越可能因过热降频 Hottest core; headroom is how many degrees remain before 100°C — the closer to 0, the more likely thermal throttling 核心热力图 Core Heatmap 核心负载 Core Load 检查 Check @@ -1339,7 +1346,6 @@ Apple 智能本机模型当前不可用。 The on-device Apple Intelligence mode 通知 Notifications 通知可以正常显示。发生你打开的状况时,会像这样提醒你。 Notifications are working. You'll be alerted like this when something you turned on happens. 通过 {} 登录 Signed in with {} -速度与省电介于两者之间 In between on speed and efficiency 部分网站直连、部分经代理,每个网站走哪个出口见下方。 Some sites connect directly and some go through a proxy. See below for the exit each site uses. 部分项目需要“完全磁盘访问权限”才能扫描(Safari 缓存、废纸篓)。 Some items need Full Disk Access to scan (Safari caches, Trash). 配置方式 Configuration diff --git a/Packages/XStatsKit/Sources/Metrics/CPUSampler.swift b/Packages/XStatsKit/Sources/Metrics/CPUSampler.swift index 72276f6..bf35cca 100644 --- a/Packages/XStatsKit/Sources/Metrics/CPUSampler.swift +++ b/Packages/XStatsKit/Sources/Metrics/CPUSampler.swift @@ -10,32 +10,42 @@ public struct CPUSampler { public static func topology() -> CPUTopology { let brand = Sysctl.string("machdep.cpu.brand_string") ?? "CPU" let logical = Sysctl.int("hw.logicalcpu") ?? ProcessInfo.processInfo.activeProcessorCount - let levels = Sysctl.int("hw.nperflevels") ?? 1 + let count = Sysctl.int("hw.nperflevels") ?? 1 + let levels = (1...max(1, logical)).contains(count) ? (0.. 1 else { - return CPUTopology(brand: brand, logicalCores: logical, - clusters: [CPUCluster(id: 0, name: tr("核心"), coreIndices: Array(0.. CPUTopology { + let logical = max(1, logicalCores) + let fallback = CPUTopology(brand: brand, logicalCores: logical, + clusters: [CPUCluster(id: 0, name: tr("核心"), coreIndices: Array(0.. 1 else { return fallback } // 核心序号从最低性能档开始排列:perflevel(n-1) 在前,perflevel0 在后 var clusters: [CPUCluster] = [] var offset = 0 - for level in stride(from: levels - 1, through: 0, by: -1) { - let count = Sysctl.int("hw.perflevel\(level).logicalcpu") ?? 0 - guard count > 0 else { continue } - let raw = Sysctl.string("hw.perflevel\(level).name") ?? "" - clusters.append(CPUCluster(id: level, name: localizedClusterName(raw), coreIndices: Array(offset.. 0, count <= logical - offset else { return fallback } + let kind = CPUCluster.Kind(systemName: raw) + clusters.append(CPUCluster(id: level, name: localizedClusterName(raw, kind: kind), + coreIndices: Array(offset.. String { - switch raw.lowercased() { - case "super": tr("超级核") - case "performance": tr("性能核") - case "efficiency": tr("能效核") - default: raw.isEmpty ? tr("核心") : raw + private static func localizedClusterName(_ raw: String, kind: CPUCluster.Kind) -> String { + switch kind { + case .superCore: tr("超级核") + case .performance: tr("性能核") + case .efficiency: tr("能效核") + case .unknown: raw.isEmpty ? tr("核心") : raw } } @@ -80,6 +90,7 @@ public struct CPUSampler { let user = Double(sumUser) / Double(sumAll) let system = Double(sumSystem) / Double(sumAll) return CPULoad(total: min(1, user + system), user: user, system: system, perCore: perCore, - loadAverage: loadCount > 0 ? Array(averages.prefix(Int(loadCount))) : []) + loadAverage: loadCount > 0 ? Array(averages.prefix(Int(loadCount))) : [], + thermalState: ProcessInfo.processInfo.thermalState) } } diff --git a/Packages/XStatsKit/Sources/Metrics/Models.swift b/Packages/XStatsKit/Sources/Metrics/Models.swift index 2861350..eabe46e 100644 --- a/Packages/XStatsKit/Sources/Metrics/Models.swift +++ b/Packages/XStatsKit/Sources/Metrics/Models.swift @@ -4,9 +4,31 @@ import Localization import SMC public struct CPUCluster: Sendable, Equatable, Identifiable { + /// 保留系统报告的核心类型,界面说明不依赖本地化名称或机型猜测。 + public enum Kind: Sendable { + case superCore, performance, efficiency, unknown + + init(systemName: String) { + switch systemName.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() { + case "super": self = .superCore + case "performance": self = .performance + case "efficiency": self = .efficiency + default: self = .unknown + } + } + } + public let id: Int // perflevel 序号,0 为最高性能档 public let name: String // 超级核 / 性能核 / 能效核 public let coreIndices: [Int] // 对应 host_processor_info 的核心序号 + public let kind: Kind + + init(id: Int, name: String, coreIndices: [Int], kind: Kind = .unknown) { + self.id = id + self.name = name + self.coreIndices = coreIndices + self.kind = kind + } } public struct CPUTopology: Sendable, Equatable { @@ -21,6 +43,8 @@ public struct CPULoad: Sendable, Equatable { public var system: Double public var perCore: [Double] public var loadAverage: [Double] + /// macOS 系统热状态,与传感器温度独立;没有采样时不假定为正常。 + public var thermalState: ProcessInfo.ThermalState? = nil } public enum MemoryPressure: Int, Sendable { diff --git a/Packages/XStatsKit/Sources/XStatsUI/Design/Components.swift b/Packages/XStatsKit/Sources/XStatsUI/Design/Components.swift index 92106fb..a21078c 100644 --- a/Packages/XStatsKit/Sources/XStatsUI/Design/Components.swift +++ b/Packages/XStatsKit/Sources/XStatsUI/Design/Components.swift @@ -339,6 +339,15 @@ enum Tone { static func forTemperature(_ celsius: Double) -> Tone { celsius >= DS.Thermal.hot ? .error : celsius >= DS.Thermal.warm ? .warning : .primary } + + static func forThermalState(_ state: ProcessInfo.ThermalState) -> Tone { + switch state { + case .nominal: .success + case .fair: .warning + case .serious, .critical: .error + @unknown default: .neutral + } + } } struct StatusBadge: View { diff --git a/Packages/XStatsKit/Sources/XStatsUI/Popover/MetricPopovers.swift b/Packages/XStatsKit/Sources/XStatsUI/Popover/MetricPopovers.swift index 5bd38a3..df1d8cd 100644 --- a/Packages/XStatsKit/Sources/XStatsUI/Popover/MetricPopovers.swift +++ b/Packages/XStatsKit/Sources/XStatsUI/Popover/MetricPopovers.swift @@ -130,13 +130,43 @@ struct CPUPopover: View { } extension CPUCluster { - /// 这类核心的分工:最高档最快、最低档最省电、中间档介于两者之间;只有一类核心时没有分工可言 + enum WorkloadRole { + case heavy, parallel, light + + var title: String { + switch self { + case .heavy: tr("最快,重活优先交给它们") + case .parallel: tr("高效处理多线程任务") + case .light: tr("更省电,负责后台和轻量任务") + } + } + } + + /// S/P 芯片的第二档仍是性能核,不能仅凭档位把它描述为能效核。 + func workloadRole(in topology: CPUTopology) -> WorkloadRole? { + guard topology.clusters.count > 1 else { return nil } + switch kind { + case .superCore: return .heavy + case .performance: return id == 0 ? .heavy : .parallel + case .efficiency: return .light + case .unknown: return nil + } + } + func role(in topology: CPUTopology) -> String? { - let levels = topology.clusters.count - guard levels > 1 else { return nil } - if id == 0 { return tr("最快,重活优先交给它们") } - if id == levels - 1 { return tr("更省电,负责后台和轻量任务") } - return tr("速度与省电介于两者之间") + workloadRole(in: topology)?.title + } +} + +extension ProcessInfo.ThermalState { + var pressureLabel: String { + switch self { + case .nominal: tr("热压力:正常") + case .fair: tr("热压力:偏高") + case .serious: tr("热压力:严重") + case .critical: tr("热压力:临界") + @unknown default: tr("热压力:未知") + } } } @@ -203,6 +233,7 @@ private struct CPUHero: View { let total = cpu?.total ?? 0 let history = store.cpuTotal.elements let temperature = store.sensors?.temperature(.cpu) + let thermalState = cpu?.thermalState let fahrenheit = model.settings.useFahrenheit // 走势按真实时间定位:最新一次采样在右边缘,往左铺满所选时长 let duration = TimeInterval(model.settings.cpuChartSeconds) @@ -232,9 +263,16 @@ private struct CPUHero: View { .dsFont(.xs) .foregroundStyle(DS.Palette.textTertiary) } - .help(tr("核心最高温度;余量是离 100°C 还差多少度,越接近 0 越可能因过热降频")) + .help(tr("核心最高温度;余量以 100°C 为参考,不代表设备实际降频阈值。")) } } + HStack { + // 首次 CPU 采样前也保留状态行,避免弹窗因标签出现而改变高度。 + StatusBadge(text: thermalState?.pressureLabel ?? tr("热压力:未知"), + tone: thermalState.map(Tone.forThermalState) ?? .neutral) + .help(tr("由 macOS 报告的系统热状态,与 CPU 温度读数独立。")) + Spacer(minLength: 0) + } if isDetailPage { HStack(spacing: DS.Space.s2) { PercentAxis(height: DS.Size.chartHeight * 2) diff --git a/Packages/XStatsKit/Tests/LocalizationTests/LanguageSupportTests.swift b/Packages/XStatsKit/Tests/LocalizationTests/LanguageSupportTests.swift index 9874528..3066473 100644 --- a/Packages/XStatsKit/Tests/LocalizationTests/LanguageSupportTests.swift +++ b/Packages/XStatsKit/Tests/LocalizationTests/LanguageSupportTests.swift @@ -86,6 +86,9 @@ struct LanguageSupportTests { let catalog = try #require(Translations.catalog(for: language)) #expect(catalog.sourceKeys == Translations.shared.sourceKeys) for key in ["语言", "应用 UI 语言", "自动检测", "搜索语言", "没有匹配的语言", + "热压力:正常", "热压力:偏高", "热压力:严重", "热压力:临界", "热压力:未知", + "高效处理多线程任务", "由 macOS 报告的系统热状态,与 CPU 温度读数独立。", + "核心最高温度;余量以 100°C 为参考,不代表设备实际降频阈值。", "亮度", "对比度", "音量", "显示器 {} 台", "重新检测显示器控制", "显示器不支持此控制", "暂时无法读取,请检查 DDC/CI 与连接", "显示链路正在恢复…", "修改未确认,请重新检测", diff --git a/Packages/XStatsKit/Tests/MetricsTests/CPUTopologyTests.swift b/Packages/XStatsKit/Tests/MetricsTests/CPUTopologyTests.swift new file mode 100644 index 0000000..fca4b2d --- /dev/null +++ b/Packages/XStatsKit/Tests/MetricsTests/CPUTopologyTests.swift @@ -0,0 +1,63 @@ +// Copyright (C) 2026 ysicing +// SPDX-License-Identifier: AGPL-3.0-or-later + +import Foundation +import Testing +@testable import Metrics + +struct CPUTopologyTests { + @Test func preservesPerformanceAndEfficiencyGroups() { + let topology = CPUSampler.topology(brand: "Fixture P/E", logicalCores: 8, + levels: [("Performance", 4), ("Efficiency", 4)]) + #expect(topology.clusters.map(\.kind) == [.performance, .efficiency]) + #expect(topology.clusters.map(\.id) == [0, 1]) + #expect(topology.clusters.map(\.coreIndices) == [[4, 5, 6, 7], [0, 1, 2, 3]]) + } + + @Test func preservesSuperAndPerformanceWithoutInventingEfficiency() { + let topology = CPUSampler.topology(brand: "Fixture S/P", logicalCores: 18, + levels: [("Super", 6), ("Performance", 12)]) + #expect(topology.clusters.map(\.kind) == [.superCore, .performance]) + #expect(topology.clusters.map(\.coreIndices) == [Array(12..<18), Array(0..<12)]) + } + + @Test func preservesThreeCoreTypesAndTheirProcessorIndices() { + let topology = CPUSampler.topology(brand: "Fixture S/P/E", logicalCores: 12, + levels: [("Super", 2), ("Performance", 4), ("Efficiency", 6)]) + #expect(topology.clusters.map(\.kind) == [.superCore, .performance, .efficiency]) + #expect(topology.clusters.map(\.coreIndices) == [[10, 11], [6, 7, 8, 9], [0, 1, 2, 3, 4, 5]]) + } + + @Test(arguments: [[("Performance", 4), ("Efficiency", 0)], + [("Super", 2), ("Performance", 4)], + [("Performance", 8), ("Efficiency", 4)], + [("Performance", -1), ("Efficiency", 9)], []]) + func inconsistentCountsFallBackWithoutDroppingOrInventingCores(_ levels: [(String, Int)]) { + let topology = CPUSampler.topology(brand: "Fixture", logicalCores: 8, levels: levels) + #expect(topology.clusters.count == 1) + #expect(topology.clusters.first?.kind == .unknown) + #expect(topology.clusters.first?.coreIndices == Array(0..<8)) + } + + @Test func unfamiliarNamesAreNotClassifiedFromChipBrand() { + let topology = CPUSampler.topology(brand: "Apple M6", logicalCores: 8, + levels: [("Future Fast", 4), ("Future Small", 4)]) + #expect(topology.clusters.map(\.kind) == [.unknown, .unknown]) + #expect(topology.clusters.map(\.name) == ["Future Fast", "Future Small"]) + } + + @Test func normalizesSystemCoreTypeNames() { + let topology = CPUSampler.topology(brand: "Fixture", logicalCores: 8, + levels: [(" SUPER ", 2), ("performance", 4), ("Efficiency", 2)]) + #expect(topology.clusters.map(\.kind) == [.superCore, .performance, .efficiency]) + } + + @Test func cpuSampleIncludesSystemThermalState() async throws { + var sampler = CPUSampler() + _ = sampler.sample() + try await Task.sleep(for: .milliseconds(200)) + let sample = sampler.sample() + let load = try #require(sample) + #expect(load.thermalState != nil) + } +} diff --git a/Packages/XStatsKit/Tests/XStatsUITests/CPUPresentationTests.swift b/Packages/XStatsKit/Tests/XStatsUITests/CPUPresentationTests.swift new file mode 100644 index 0000000..160614b --- /dev/null +++ b/Packages/XStatsKit/Tests/XStatsUITests/CPUPresentationTests.swift @@ -0,0 +1,58 @@ +// Copyright (C) 2026 ysicing +// SPDX-License-Identifier: AGPL-3.0-or-later + +import Foundation +import Testing +@testable import Metrics +@testable import XStatsUI + +struct CPUPresentationTests { + @Test func superAndPerformanceGroupsHaveDistinctWorkloadRoles() { + let superCore = CPUCluster(id: 0, name: "Fixture S", coreIndices: [12, 13], kind: .superCore) + let performance = CPUCluster(id: 1, name: "Fixture P", coreIndices: [0, 1], kind: .performance) + let topology = CPUTopology(brand: "Fixture S/P", logicalCores: 4, clusters: [superCore, performance]) + #expect(superCore.workloadRole(in: topology) == .heavy) + #expect(performance.workloadRole(in: topology) == .parallel) + } + + @Test func traditionalPerformanceAndEfficiencyRolesRemainDistinct() { + let performance = CPUCluster(id: 0, name: "Fixture P", coreIndices: [2, 3], kind: .performance) + let efficiency = CPUCluster(id: 1, name: "Fixture E", coreIndices: [0, 1], kind: .efficiency) + let topology = CPUTopology(brand: "Fixture P/E", logicalCores: 4, clusters: [performance, efficiency]) + #expect(performance.workloadRole(in: topology) == .heavy) + #expect(efficiency.workloadRole(in: topology) == .light) + } + + @Test func genericGroupDoesNotInventWorkloadRole() { + let generic = CPUCluster(id: 0, name: "Fixture", coreIndices: [0, 1], kind: .unknown) + let topology = CPUTopology(brand: "Fixture", logicalCores: 2, clusters: [generic]) + #expect(generic.workloadRole(in: topology) == nil) + } + + @Test func unfamiliarHigherTierDoesNotMakePerformanceTierTheFastest() { + let future = CPUCluster(id: 0, name: "Future", coreIndices: [2, 3], kind: .unknown) + let performance = CPUCluster(id: 1, name: "P", coreIndices: [0, 1], kind: .performance) + let topology = CPUTopology(brand: "Fixture", logicalCores: 4, clusters: [future, performance]) + #expect(performance.workloadRole(in: topology) == .parallel) + #expect(future.workloadRole(in: topology) == nil) + } + + @Test(arguments: [(ProcessInfo.ThermalState.nominal, Tone.success), (.fair, .warning), + (.serious, .error), (.critical, .error)]) + func thermalPressureSeverityDoesNotDependOnTemperature(_ state: ProcessInfo.ThermalState, _ tone: Tone) { + #expect(Tone.forThermalState(state) == tone) + } + + @MainActor @Test func thermalStateChangesPropagateWithoutChangingCPUUtilization() { + let store = MetricsStore() + var snapshot = MetricsSnapshot() + snapshot.cpu = CPULoad(total: 0.5, user: 0.3, system: 0.2, perCore: [0.5], loadAverage: []) + snapshot.cpu?.thermalState = .nominal + store.apply(snapshot) + #expect(store.cpu?.thermalState == .nominal) + snapshot.cpu?.thermalState = .serious + store.apply(snapshot) + #expect(store.cpu?.total == 0.5) + #expect(store.cpu?.thermalState == .serious) + } +} diff --git a/Packages/XStatsKit/Tests/XStatsUITests/XStatsUITests.swift b/Packages/XStatsKit/Tests/XStatsUITests/XStatsUITests.swift index 92b16d7..a2554c4 100644 --- a/Packages/XStatsKit/Tests/XStatsUITests/XStatsUITests.swift +++ b/Packages/XStatsKit/Tests/XStatsUITests/XStatsUITests.swift @@ -705,13 +705,13 @@ private func isolatedDefaults() -> UserDefaults { @Suite struct CoreRoleTests { @Test func describesEachTierInPlainWords() { - let fast = CPUCluster(id: 0, name: "超级核", coreIndices: [12, 13]) - let middle = CPUCluster(id: 1, name: "性能核", coreIndices: [4, 5]) - let slow = CPUCluster(id: 2, name: "能效核", coreIndices: [0, 1]) + let fast = CPUCluster(id: 0, name: "S", coreIndices: [4, 5], kind: .superCore) + let middle = CPUCluster(id: 1, name: "P", coreIndices: [2, 3], kind: .performance) + let slow = CPUCluster(id: 2, name: "E", coreIndices: [0, 1], kind: .efficiency) let three = CPUTopology(brand: "Apple", logicalCores: 6, clusters: [fast, middle, slow]) - #expect(fast.role(in: three) == "最快,重活优先交给它们") - #expect(middle.role(in: three) == "速度与省电介于两者之间") - #expect(slow.role(in: three) == "更省电,负责后台和轻量任务") + #expect(fast.workloadRole(in: three) == .heavy) + #expect(middle.workloadRole(in: three) == .parallel) + #expect(slow.workloadRole(in: three) == .light) // 只有一类核心(Intel)时没有分工可言 let single = CPUTopology(brand: "Intel", logicalCores: 8, clusters: [CPUCluster(id: 0, name: "核心", coreIndices: Array(0..<8))]) #expect(single.clusters[0].role(in: single) == nil) diff --git a/README.en.md b/README.en.md index 5cfa756..f661373 100644 --- a/README.en.md +++ b/README.en.md @@ -42,6 +42,8 @@ brew install --cask xstats **System monitoring**: choose CPU, GPU, memory, disk, network, battery, temperature and fan readings for the menu bar; open any metric for history, the apps using it most and hardware details. +**CPU**: shows user / system / idle utilization, per-core load rings, core-group utilization and macOS thermal pressure; temperature is shown when sensors are available. This Mac shows uptime. Groups follow the core types reported by the system, including super, performance and efficiency cores, rather than assuming two or three groups. +

CPU details Memory details diff --git a/README.ja.md b/README.ja.md index e146e18..d0ca70b 100644 --- a/README.ja.md +++ b/README.ja.md @@ -42,6 +42,8 @@ brew install --cask xstats **システム監視**:CPU、GPU、メモリ、ディスク、ネットワーク、バッテリー、温度、ファンから表示する項目を選べます。項目を開くと履歴、使用量の多いアプリ、ハードウェアの詳細を確認できます。 +**CPU**:ユーザー / システム / アイドルの使用率、コアごとの負荷リング、コアグループの使用率、macOS の熱負荷状態を表示します。対応するセンサーがあれば温度も表示し、「この Mac」で稼働時間を確認できます。グループはシステムが報告するスーパー / パフォーマンス / 高効率コアに従い、2 種類や 3 種類に固定しません。 +

CPU の詳細 メモリの詳細 diff --git a/README.ko.md b/README.ko.md index cdbe15f..5776c3b 100644 --- a/README.ko.md +++ b/README.ko.md @@ -42,6 +42,8 @@ brew install --cask xstats **시스템 모니터링**: CPU, GPU, 메모리, 디스크, 네트워크, 배터리, 온도, 팬 중에서 메뉴 막대에 표시할 항목을 고릅니다. 항목을 열면 기록, 많이 사용하는 앱, 하드웨어 세부 정보를 볼 수 있습니다. +**CPU**: 사용자 / 시스템 / 유휴 사용률, 코어별 부하 링, 코어 그룹 사용률과 macOS 열 상태를 표시합니다. 사용 가능한 센서가 있으면 온도도 표시하며, ‘이 Mac’에서 가동 시간을 확인할 수 있습니다. 그룹은 시스템이 보고한 슈퍼 / 성능 / 효율 코어 유형에 따라 표시하며, 두 그룹이나 세 그룹으로 고정하지 않습니다. +

CPU 세부 정보 메모리 세부 정보 diff --git a/README.md b/README.md index b5ab6c9..7e3bd04 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,8 @@ brew install --cask xstats **系统监控**:在菜单栏选择 CPU、GPU、内存、磁盘、网络、电池、温度与风扇读数;点开任意指标查看历史趋势、占用最多的应用和硬件详情。 +**CPU**:显示用户 / 系统 / 空闲占用、逐核负载圆环、核心组占用与 macOS 热压力;有可用传感器时显示温度。“本机信息”显示运行时间。核心分组随系统报告的类型呈现,支持超级核、性能核和能效核,不固定为两组或三组。 +

CPU 详情 内存详情