Commitok összehasonlítása

8 Commit-ok

Szerző SHA1 Üzenet Dátum
hariel1985
1767491076 Keep the app version in project.yml so XcodeGen can't reset it
project.yml declared `info: path: TopManager/Info.plist` without
properties, so every `xcodegen generate` rewrote Info.plist with
XcodeGen's defaults, 1.0 (1). That silently undid the version bump in
cf88044: its message says 1.1.1 (3), but the commit actually set
Info.plist back to 1.0 (1).

The version now lives in MARKETING_VERSION / CURRENT_PROJECT_VERSION in
project.yml, and the generated Info.plist references them. Regenerating
is idempotent and the built app reports 1.1.1 (3).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 15:26:28 +02:00
hariel1985
cf88044582 Explain Memory, RAM and Compressed in the process inspector
RAM exceeding Memory is normal for any SwiftUI/AppKit app — resident size
includes system framework pages shared by every process, while the
footprint counts only the process's own modified pages (in RAM,
compressed or swapped) — but it reads like a bug without context.

The inspector now shows a short "About memory" note under the figures,
plus a line saying memory figures are unavailable when the process
belongs to another user or the system (they read as zero otherwise).

Bump version to 1.1.1 (3).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 15:22:14 +02:00
hariel1985
e8a982c00e docs: bump the release badge to v1.1.0
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 14:12:32 +02:00
hariel1985
82b55c9468 docs: point the Download link at TopManager 1.1
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 14:12:18 +02:00
hariel1985
14ced8d11d Bump version to 1.1 (2)
First version change since v1.0.0: adds the RAM and Compressed memory
columns and ships the memory-leak, pause-while-hidden and zero-value
flicker fixes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 14:00:49 +02:00
hariel1985
c9cd87f221 Show RAM and compressed memory per process
Adds two columns to the Processes table (and rows to the inspector) next
to Memory, which is the physical footprint:

- RAM: pages currently in physical memory (ri_resident_size, from the
  proc_pid_rusage call that was already being made).
- Compressed: the original size of the process's pages held by the
  compressor, via task_name_for_pid + TASK_VM_INFO. This is also the
  closest per-process swap figure: macOS pages out compressor segments,
  not processes, and those stay counted here. Activity Monitor's
  "Compressed Memory" and top's CMPRS report the same value.

Both need no privileges for the user's own processes; other users' and
system processes report 0, as the Memory column already did. Values match
top/ps within sampling noise; a full refresh of ~650 processes including
the extra call takes ~5 ms.

Memory, RAM and Compressed live in one @TableColumnBuilder property:
Table takes at most 10 columns per block, and inlining them also
exceeded the type checker's time limit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 13:53:47 +02:00
hariel1985
6f60dd7ad8 Pause view updates while no window is visible; fix zero-value flicker
The window left open all day grew the app by ~360 MB in 24 h even after
the .tabItem fix. Isolating each piece in lab builds showed the growth is
the sum of several small framework leaks on macOS 26.6 that fire on every
view update: SwiftUI Table cell updates, AppKit text-cell KVO
dependencies (NSKeyValueDependency) and an ObservationRegistrar created
inside DesignLibrary. With the window closed every counter stayed flat;
minimized or fully covered, SwiftUI kept updating and kept leaking.

- WindowVisibility tracks whether any TopManager window is on screen
  (occlusion state; the status item's host window is ignored).
- SystemMonitor records into an always-current LiveState and only
  mirrors it into the @Published properties while something is visible.
  Resuming publishes everything at once, so views catch up immediately.
  Metrics history, alerts and the menu bar keep running while paused.
- MenuBarStatus carries the pre-formatted menu-bar strings, republished
  only when the visible text changes.
- AlertCenter assigns healthScore/diagnosis only when they change.
- Monitoring now starts in applicationDidFinishLaunching instead of the
  main window's onAppear, so it also runs when the app launches with no
  window (state restoration after the window was closed).

Also fixes a visible bug: ProcessMonitor's lightweight refresh (2 of
every 3 fetches) emitted 0 for memory, CPU and threads instead of the
last known values, so ~90% of rows showed "Zero KB" most of the time.

Verified in an isolated lab instance: minimized or covered, all leak
counters stay flat while the menu-bar title keeps updating; a partially
visible window updates every tick. Growth while the window is actually
visible (~190 KB/min) is unchanged — those leaks are in Apple's
frameworks. 58/58 tests pass, including a regression test that fails on
the old code (472 of 487 own processes reported 0 memory).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 13:53:10 +02:00
hariel1985
6a72c7ba64 Ignore the root build/ directory
The existing */build/* pattern only matches build/ inside a subdirectory,
so the xcodebuild -derivedDataPath output at the repo root kept showing
up as untracked.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 10:25:42 +02:00
17 fájl változott, egészen pontosan 426 új sor hozzáadva és 101 régi sor törölve

1
.gitignore vendored
Fájl megtekintése

@@ -1,6 +1,7 @@
# Xcode # Xcode
.DS_Store .DS_Store
*/build/* */build/*
/build/
*.pbxuser *.pbxuser
!default.pbxuser !default.pbxuser
*.mode1v3 *.mode1v3

Fájl megtekintése

@@ -5,11 +5,11 @@ A native macOS system monitor application built with SwiftUI. TopManager provide
![macOS](https://img.shields.io/badge/macOS-13.0+-blue) ![macOS](https://img.shields.io/badge/macOS-13.0+-blue)
![Swift](https://img.shields.io/badge/Swift-5.9-orange) ![Swift](https://img.shields.io/badge/Swift-5.9-orange)
![License](https://img.shields.io/badge/License-GPL--3.0-green) ![License](https://img.shields.io/badge/License-GPL--3.0-green)
![Release](https://img.shields.io/badge/release-v1.0.0-brightgreen) ![Release](https://img.shields.io/badge/release-v1.1.0-brightgreen)
## Download ## Download
**[⬇️ Download TopManager 1.0 (.dmg)](https://github.com/hariel1985/TopManager/releases/latest)** **[⬇️ Download TopManager 1.1 (.dmg)](https://github.com/hariel1985/TopManager/releases/latest)**
Signed with a Developer ID and **notarized by Apple** — it opens without Gatekeeper Signed with a Developer ID and **notarized by Apple** — it opens without Gatekeeper
warnings. Open the DMG and drag TopManager to your Applications folder. warnings. Open the DMG and drag TopManager to your Applications folder.

Fájl megtekintése

@@ -10,6 +10,7 @@
00EC5A90B7B8C6C5015D90D1 /* PerformanceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F80A817DCDA254349DEA6F06 /* PerformanceView.swift */; }; 00EC5A90B7B8C6C5015D90D1 /* PerformanceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F80A817DCDA254349DEA6F06 /* PerformanceView.swift */; };
0AA7D351AEB05078FA9B4B93 /* ProcessView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B6F97E6EF65AA17A469F037 /* ProcessView.swift */; }; 0AA7D351AEB05078FA9B4B93 /* ProcessView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B6F97E6EF65AA17A469F037 /* ProcessView.swift */; };
136DAD1FE90FD682DF666667 /* TopManager_icon.png in Resources */ = {isa = PBXBuildFile; fileRef = 8E4A6E8E2305CEDC119AC6BC /* TopManager_icon.png */; }; 136DAD1FE90FD682DF666667 /* TopManager_icon.png in Resources */ = {isa = PBXBuildFile; fileRef = 8E4A6E8E2305CEDC119AC6BC /* TopManager_icon.png */; };
14D77C268F9F701E612E4FD3 /* WindowVisibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A1CB7CF2D7EA7A20E561192 /* WindowVisibility.swift */; };
1F212CE158B7D14B29077B18 /* CPUInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44281079654C7E1C9E16806C /* CPUInfo.swift */; }; 1F212CE158B7D14B29077B18 /* CPUInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44281079654C7E1C9E16806C /* CPUInfo.swift */; };
1F25A84B560709AE71C27D23 /* GPUInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = F481045B4FC6017F50849FF1 /* GPUInfo.swift */; }; 1F25A84B560709AE71C27D23 /* GPUInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = F481045B4FC6017F50849FF1 /* GPUInfo.swift */; };
24F63686869A69C8B0FCD7FA /* MetricsStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E786723FC46A64EFF5D3CE4 /* MetricsStoreTests.swift */; }; 24F63686869A69C8B0FCD7FA /* MetricsStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E786723FC46A64EFF5D3CE4 /* MetricsStoreTests.swift */; };
@@ -35,6 +36,7 @@
9F3A8E658859AD9C325E52A8 /* TrendChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AA15E30C4B694CB36DC0893 /* TrendChartView.swift */; }; 9F3A8E658859AD9C325E52A8 /* TrendChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AA15E30C4B694CB36DC0893 /* TrendChartView.swift */; };
A0649565A902BA48DB10F592 /* CPUMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19ED582C412C63A0A2E2103E /* CPUMonitor.swift */; }; A0649565A902BA48DB10F592 /* CPUMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19ED582C412C63A0A2E2103E /* CPUMonitor.swift */; };
A7EC9FB55D5088D669491C11 /* AppsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 813543165B39EBD213EB3BC7 /* AppsView.swift */; }; A7EC9FB55D5088D669491C11 /* AppsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 813543165B39EBD213EB3BC7 /* AppsView.swift */; };
AE29356C1D41F33A3E94B6B7 /* MenuBarStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B37A9AE9F6215C0C132B8D9 /* MenuBarStatus.swift */; };
B0165358B483B0ECC28DB4DE /* PowerInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C512B13DC0C1024CF39A5E /* PowerInfo.swift */; }; B0165358B483B0ECC28DB4DE /* PowerInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C512B13DC0C1024CF39A5E /* PowerInfo.swift */; };
B62B18DF32A8734D5908B16B /* DiskInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 352CA1BE2A3B43643A9DBD72 /* DiskInfo.swift */; }; B62B18DF32A8734D5908B16B /* DiskInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 352CA1BE2A3B43643A9DBD72 /* DiskInfo.swift */; };
B89695749D32F394DB459C15 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AACBF14FFC5EC68C9E438040 /* SettingsView.swift */; }; B89695749D32F394DB459C15 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AACBF14FFC5EC68C9E438040 /* SettingsView.swift */; };
@@ -43,6 +45,7 @@
CB7F422FBF9F6D152FC4F41B /* NetworkChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBFADE83965AA863B775A910 /* NetworkChartView.swift */; }; CB7F422FBF9F6D152FC4F41B /* NetworkChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBFADE83965AA863B775A910 /* NetworkChartView.swift */; };
D6FC8D1EACCB4DED5DF39372 /* TopManagerApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A404EBD588B7ACD36079A95D /* TopManagerApp.swift */; }; D6FC8D1EACCB4DED5DF39372 /* TopManagerApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A404EBD588B7ACD36079A95D /* TopManagerApp.swift */; };
D9E882888E23C26B748E22CF /* PowerMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95F7CCD4F64BE7478DFAA17F /* PowerMonitor.swift */; }; D9E882888E23C26B748E22CF /* PowerMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95F7CCD4F64BE7478DFAA17F /* PowerMonitor.swift */; };
DA03FF0D25DB8B9D3B09BC82 /* MenuBarStatusTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E44036C6DB5C055C179A3E2 /* MenuBarStatusTests.swift */; };
DB3603F30B6F559BE66E4349 /* SystemMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 279B972C08A878053821285D /* SystemMonitor.swift */; }; DB3603F30B6F559BE66E4349 /* SystemMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 279B972C08A878053821285D /* SystemMonitor.swift */; };
DE50ECC69FCE60B10364802C /* ByteFormatters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78C9B469F6A45F3CC21A7594 /* ByteFormatters.swift */; }; DE50ECC69FCE60B10364802C /* ByteFormatters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78C9B469F6A45F3CC21A7594 /* ByteFormatters.swift */; };
E379C349DC9617D6C505B74B /* GPUMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE3BB62AE0DC43777AD00658 /* GPUMonitor.swift */; }; E379C349DC9617D6C505B74B /* GPUMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE3BB62AE0DC43777AD00658 /* GPUMonitor.swift */; };
@@ -87,6 +90,7 @@
44281079654C7E1C9E16806C /* CPUInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CPUInfo.swift; sourceTree = "<group>"; }; 44281079654C7E1C9E16806C /* CPUInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CPUInfo.swift; sourceTree = "<group>"; };
4CF69D4888FDBB3669619A1A /* ProcessInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessInfo.swift; sourceTree = "<group>"; }; 4CF69D4888FDBB3669619A1A /* ProcessInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessInfo.swift; sourceTree = "<group>"; };
51C512B13DC0C1024CF39A5E /* PowerInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PowerInfo.swift; sourceTree = "<group>"; }; 51C512B13DC0C1024CF39A5E /* PowerInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PowerInfo.swift; sourceTree = "<group>"; };
5B37A9AE9F6215C0C132B8D9 /* MenuBarStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarStatus.swift; sourceTree = "<group>"; };
5E1D2EA8E1AD11B42FF225EB /* TopManager.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = TopManager.entitlements; sourceTree = "<group>"; }; 5E1D2EA8E1AD11B42FF225EB /* TopManager.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = TopManager.entitlements; sourceTree = "<group>"; };
70CCC4ED4F2BDC35ABD4F4F6 /* BatteryMathTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryMathTests.swift; sourceTree = "<group>"; }; 70CCC4ED4F2BDC35ABD4F4F6 /* BatteryMathTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryMathTests.swift; sourceTree = "<group>"; };
7326AE4B2D75476BDCFE3FFB /* MemoryLineChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryLineChart.swift; sourceTree = "<group>"; }; 7326AE4B2D75476BDCFE3FFB /* MemoryLineChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryLineChart.swift; sourceTree = "<group>"; };
@@ -94,6 +98,8 @@
7B6F97E6EF65AA17A469F037 /* ProcessView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessView.swift; sourceTree = "<group>"; }; 7B6F97E6EF65AA17A469F037 /* ProcessView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessView.swift; sourceTree = "<group>"; };
813543165B39EBD213EB3BC7 /* AppsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppsView.swift; sourceTree = "<group>"; }; 813543165B39EBD213EB3BC7 /* AppsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppsView.swift; sourceTree = "<group>"; };
892247B0F17363DFC146EDF8 /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = "<group>"; }; 892247B0F17363DFC146EDF8 /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = "<group>"; };
8A1CB7CF2D7EA7A20E561192 /* WindowVisibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowVisibility.swift; sourceTree = "<group>"; };
8E44036C6DB5C055C179A3E2 /* MenuBarStatusTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarStatusTests.swift; sourceTree = "<group>"; };
8E4A6E8E2305CEDC119AC6BC /* TopManager_icon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = TopManager_icon.png; sourceTree = "<group>"; }; 8E4A6E8E2305CEDC119AC6BC /* TopManager_icon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = TopManager_icon.png; sourceTree = "<group>"; };
8F402CE13EF67DE550786B79 /* MemoryDonutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryDonutView.swift; sourceTree = "<group>"; }; 8F402CE13EF67DE550786B79 /* MemoryDonutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryDonutView.swift; sourceTree = "<group>"; };
94D5A13CC5B9CF1B90CC2214 /* CPUChartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CPUChartView.swift; sourceTree = "<group>"; }; 94D5A13CC5B9CF1B90CC2214 /* CPUChartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CPUChartView.swift; sourceTree = "<group>"; };
@@ -126,11 +132,13 @@
162FC58F6C44EC1DD7022A86 /* DiskMonitor.swift */, 162FC58F6C44EC1DD7022A86 /* DiskMonitor.swift */,
BE3BB62AE0DC43777AD00658 /* GPUMonitor.swift */, BE3BB62AE0DC43777AD00658 /* GPUMonitor.swift */,
27FDD19B8DBB7D955A5ECB5B /* MemoryMonitor.swift */, 27FDD19B8DBB7D955A5ECB5B /* MemoryMonitor.swift */,
5B37A9AE9F6215C0C132B8D9 /* MenuBarStatus.swift */,
05CC33DC4E93F6ED2E52F4E1 /* MetricsStore.swift */, 05CC33DC4E93F6ED2E52F4E1 /* MetricsStore.swift */,
347E47F825E496F40FD8ED11 /* NetworkMonitor.swift */, 347E47F825E496F40FD8ED11 /* NetworkMonitor.swift */,
95F7CCD4F64BE7478DFAA17F /* PowerMonitor.swift */, 95F7CCD4F64BE7478DFAA17F /* PowerMonitor.swift */,
295232B4C746511757DCF8FE /* ProcessMonitor.swift */, 295232B4C746511757DCF8FE /* ProcessMonitor.swift */,
279B972C08A878053821285D /* SystemMonitor.swift */, 279B972C08A878053821285D /* SystemMonitor.swift */,
8A1CB7CF2D7EA7A20E561192 /* WindowVisibility.swift */,
); );
path = Services; path = Services;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -251,6 +259,7 @@
70CCC4ED4F2BDC35ABD4F4F6 /* BatteryMathTests.swift */, 70CCC4ED4F2BDC35ABD4F4F6 /* BatteryMathTests.swift */,
301352EB5B362C732CE1A246 /* FormattingTests.swift */, 301352EB5B362C732CE1A246 /* FormattingTests.swift */,
E539A84E57291B69D4EE0314 /* HealthAlertTests.swift */, E539A84E57291B69D4EE0314 /* HealthAlertTests.swift */,
8E44036C6DB5C055C179A3E2 /* MenuBarStatusTests.swift */,
0E786723FC46A64EFF5D3CE4 /* MetricsStoreTests.swift */, 0E786723FC46A64EFF5D3CE4 /* MetricsStoreTests.swift */,
332290DABD5326C87258723C /* ProcessDataTests.swift */, 332290DABD5326C87258723C /* ProcessDataTests.swift */,
01C19ABF49697A97F0C40378 /* SettingsTests.swift */, 01C19ABF49697A97F0C40378 /* SettingsTests.swift */,
@@ -374,6 +383,7 @@
2DE14B65EA1D4E9B4D9DA5A6 /* BatteryMathTests.swift in Sources */, 2DE14B65EA1D4E9B4D9DA5A6 /* BatteryMathTests.swift in Sources */,
49D85FF611927294159C89D1 /* FormattingTests.swift in Sources */, 49D85FF611927294159C89D1 /* FormattingTests.swift in Sources */,
489E29F78A4093D3548673E6 /* HealthAlertTests.swift in Sources */, 489E29F78A4093D3548673E6 /* HealthAlertTests.swift in Sources */,
DA03FF0D25DB8B9D3B09BC82 /* MenuBarStatusTests.swift in Sources */,
24F63686869A69C8B0FCD7FA /* MetricsStoreTests.swift in Sources */, 24F63686869A69C8B0FCD7FA /* MetricsStoreTests.swift in Sources */,
2B74C177F49B4358D9642712 /* ProcessDataTests.swift in Sources */, 2B74C177F49B4358D9642712 /* ProcessDataTests.swift in Sources */,
8789C5F4A1AF4B1E8E434D71 /* SettingsTests.swift in Sources */, 8789C5F4A1AF4B1E8E434D71 /* SettingsTests.swift in Sources */,
@@ -404,6 +414,7 @@
7A1144E43159D96564C13164 /* MemoryInfo.swift in Sources */, 7A1144E43159D96564C13164 /* MemoryInfo.swift in Sources */,
266B888ADE2FDDBCEC8D28A5 /* MemoryLineChart.swift in Sources */, 266B888ADE2FDDBCEC8D28A5 /* MemoryLineChart.swift in Sources */,
F5722D2B18EF7ABF43847EE4 /* MemoryMonitor.swift in Sources */, F5722D2B18EF7ABF43847EE4 /* MemoryMonitor.swift in Sources */,
AE29356C1D41F33A3E94B6B7 /* MenuBarStatus.swift in Sources */,
35852165B2DA76AB7F073723 /* MenuBarView.swift in Sources */, 35852165B2DA76AB7F073723 /* MenuBarView.swift in Sources */,
BCBAC37351B9A3878CC3F01B /* MetricsStore.swift in Sources */, BCBAC37351B9A3878CC3F01B /* MetricsStore.swift in Sources */,
CB7F422FBF9F6D152FC4F41B /* NetworkChartView.swift in Sources */, CB7F422FBF9F6D152FC4F41B /* NetworkChartView.swift in Sources */,
@@ -421,6 +432,7 @@
DB3603F30B6F559BE66E4349 /* SystemMonitor.swift in Sources */, DB3603F30B6F559BE66E4349 /* SystemMonitor.swift in Sources */,
D6FC8D1EACCB4DED5DF39372 /* TopManagerApp.swift in Sources */, D6FC8D1EACCB4DED5DF39372 /* TopManagerApp.swift in Sources */,
9F3A8E658859AD9C325E52A8 /* TrendChartView.swift in Sources */, 9F3A8E658859AD9C325E52A8 /* TrendChartView.swift in Sources */,
14D77C268F9F701E612E4FD3 /* WindowVisibility.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -441,12 +453,14 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = TopManager/TopManager.entitlements; CODE_SIGN_ENTITLEMENTS = TopManager/TopManager.entitlements;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 3;
GENERATE_INFOPLIST_FILE = NO; GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = TopManager/Info.plist; INFOPLIST_FILE = TopManager/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MARKETING_VERSION = 1.1.1;
PRODUCT_BUNDLE_IDENTIFIER = com.topmanager.app; PRODUCT_BUNDLE_IDENTIFIER = com.topmanager.app;
PRODUCT_NAME = TopManager; PRODUCT_NAME = TopManager;
SDKROOT = macosx; SDKROOT = macosx;
@@ -544,12 +558,14 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = TopManager/TopManager.entitlements; CODE_SIGN_ENTITLEMENTS = TopManager/TopManager.entitlements;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 3;
GENERATE_INFOPLIST_FILE = NO; GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = TopManager/Info.plist; INFOPLIST_FILE = TopManager/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MARKETING_VERSION = 1.1.1;
PRODUCT_BUNDLE_IDENTIFIER = com.topmanager.app; PRODUCT_BUNDLE_IDENTIFIER = com.topmanager.app;
PRODUCT_NAME = TopManager; PRODUCT_NAME = TopManager;
SDKROOT = macosx; SDKROOT = macosx;

Fájl megtekintése

@@ -15,8 +15,8 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>1.0</string> <string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1</string> <string>$(CURRENT_PROJECT_VERSION)</string>
</dict> </dict>
</plist> </plist>

Fájl megtekintése

@@ -8,7 +8,9 @@ struct ProcessItem: Identifiable, Hashable {
let user: String let user: String
let cpuUsage: Double // Per-core: 100% = 1 core fully utilized let cpuUsage: Double // Per-core: 100% = 1 core fully utilized
let cpuUsageTotal: Double // Normalized: 100% = all cores fully utilized let cpuUsageTotal: Double // Normalized: 100% = all cores fully utilized
let memoryUsage: Int64 let memoryUsage: Int64 // physical footprint (Activity Monitor's "Memory")
let residentMemory: Int64 // pages currently in physical RAM, shared ones included
let compressedMemory: Int64 // original size of pages in the compressor — in RAM or swapped out
let threadCount: Int32 let threadCount: Int32
let state: ProcessState let state: ProcessState
let icon: NSImage? let icon: NSImage?
@@ -42,7 +44,9 @@ struct ProcessItem: Identifiable, Hashable {
diskReadBytes: UInt64 = 0, diskReadBytes: UInt64 = 0,
diskWriteBytes: UInt64 = 0, diskWriteBytes: UInt64 = 0,
energyImpact: Double = 0, energyImpact: Double = 0,
executablePath: String? = nil executablePath: String? = nil,
residentMemory: Int64 = 0,
compressedMemory: Int64 = 0
) { ) {
self.id = pid self.id = pid
self.pid = pid self.pid = pid
@@ -51,6 +55,8 @@ struct ProcessItem: Identifiable, Hashable {
self.cpuUsage = cpuUsage self.cpuUsage = cpuUsage
self.cpuUsageTotal = cpuUsageTotal self.cpuUsageTotal = cpuUsageTotal
self.memoryUsage = memoryUsage self.memoryUsage = memoryUsage
self.residentMemory = residentMemory
self.compressedMemory = compressedMemory
self.threadCount = threadCount self.threadCount = threadCount
self.state = state self.state = state
self.icon = icon self.icon = icon
@@ -73,6 +79,8 @@ struct ProcessItem: Identifiable, Hashable {
lhs.cpuUsage == rhs.cpuUsage && lhs.cpuUsage == rhs.cpuUsage &&
lhs.cpuUsageTotal == rhs.cpuUsageTotal && lhs.cpuUsageTotal == rhs.cpuUsageTotal &&
lhs.memoryUsage == rhs.memoryUsage && lhs.memoryUsage == rhs.memoryUsage &&
lhs.residentMemory == rhs.residentMemory &&
lhs.compressedMemory == rhs.compressedMemory &&
lhs.threadCount == rhs.threadCount && lhs.threadCount == rhs.threadCount &&
lhs.state == rhs.state && lhs.state == rhs.state &&
lhs.diskReadRate == rhs.diskReadRate && lhs.diskReadRate == rhs.diskReadRate &&

Fájl megtekintése

@@ -48,8 +48,12 @@ final class AlertCenter: ObservableObject {
thermal: thermal, thermal: thermal,
diskUsedFraction: diskFraction diskUsedFraction: diskFraction
) )
healthScore = HealthModel.score(input) // Assign only on change: @Published fires on every set, and re-assigning
diagnosis = HealthModel.diagnosis(input) // equal values each tick re-rendered every health view on every refresh.
let score = HealthModel.score(input)
if score != healthScore { healthScore = score }
let findings = HealthModel.diagnosis(input)
if findings != diagnosis { diagnosis = findings }
// CPU (sustained) // CPU (sustained)
let cpuEval = AlertEvaluator.sustained(breached: cpuUsage >= thresholds.cpuPercent, let cpuEval = AlertEvaluator.sustained(breached: cpuUsage >= thresholds.cpuPercent,

Fájl megtekintése

@@ -0,0 +1,30 @@
import Foundation
/// The few always-live strings the menu-bar title shows.
///
/// Kept apart from `SystemMonitor` because the monitor stops publishing while no
/// TopManager window is on screen (see `SystemMonitor.setUIVisible`), and the
/// title must keep ticking regardless. Values are pre-formatted and only
/// re-published when the visible text actually changes: `@Published` fires on
/// every assignment, and each firing re-measures the status item.
@MainActor
final class MenuBarStatus: ObservableObject {
static let shared = MenuBarStatus()
@Published private(set) var cpuText: String?
@Published private(set) var memoryText: String?
@Published private(set) var downloadText: String?
init() {}
func update(cpu: CPUInfo?, memory: MemoryInfo?, network: NetworkInfo?) {
let cpu = cpu.map { String(format: "%.0f%%", $0.globalUsage) }
if cpu != cpuText { cpuText = cpu }
let memory = memory.map { String(format: "%.0f%%", $0.usagePercentage) }
if memory != memoryText { memoryText = memory }
let download = network.map { formatBytesPerSecond($0.totalDownloadRate) }
if download != downloadText { downloadText = download }
}
}

Fájl megtekintése

@@ -6,6 +6,7 @@ final class ProcessMonitor {
private var previousCPUTimes: [pid_t: (user: UInt64, system: UInt64, timestamp: Date)] = [:] private var previousCPUTimes: [pid_t: (user: UInt64, system: UInt64, timestamp: Date)] = [:]
private var previousDiskIO: [pid_t: (read: UInt64, write: UInt64, idle: UInt64, timestamp: Date)] = [:] private var previousDiskIO: [pid_t: (read: UInt64, write: UInt64, idle: UInt64, timestamp: Date)] = [:]
private var lastKnownCPU: [pid_t: Double] = [:] // Cache last known CPU usage private var lastKnownCPU: [pid_t: Double] = [:] // Cache last known CPU usage
private var lastDetailed: [pid_t: DetailedSample] = [:] // Reused on lightweight refreshes
private let iconCache = NSCache<NSNumber, NSImage>() private let iconCache = NSCache<NSNumber, NSImage>()
private var noIconPids: Set<pid_t> = [] // Cache for PIDs with no icon private var noIconPids: Set<pid_t> = [] // Cache for PIDs with no icon
private var nameCache: [pid_t: String] = [:] private var nameCache: [pid_t: String] = [:]
@@ -56,6 +57,7 @@ final class ProcessMonitor {
previousDiskIO.removeValue(forKey: pid) previousDiskIO.removeValue(forKey: pid)
noIconPids.remove(pid) noIconPids.remove(pid)
lastKnownCPU.removeValue(forKey: pid) lastKnownCPU.removeValue(forKey: pid)
lastDetailed.removeValue(forKey: pid)
} }
return processes return processes
@@ -81,14 +83,7 @@ final class ProcessMonitor {
let lastCPU = lastKnownCPU[pid] ?? 0 let lastCPU = lastKnownCPU[pid] ?? 0
let needsDetailedInfo = fullRefresh || lastCPU > 0.1 let needsDetailedInfo = fullRefresh || lastCPU > 0.1
let memoryUsage: Int64 let detail: DetailedSample
let threadCount: Int32
let cpuUsage: Double
var diskReadRate: Double = 0
var diskWriteRate: Double = 0
var diskReadBytes: UInt64 = 0
var diskWriteBytes: UInt64 = 0
var energyImpact: Double = 0
if needsDetailedInfo { if needsDetailedInfo {
var taskInfo = proc_taskinfo() var taskInfo = proc_taskinfo()
@@ -98,9 +93,7 @@ final class ProcessMonitor {
if hasTaskInfo { if hasTaskInfo {
let rusageData = fetchRusageData(pid: pid) let rusageData = fetchRusageData(pid: pid)
memoryUsage = rusageData.memory let cpu = calculateCPUUsage(
threadCount = taskInfo.pti_threadnum
cpuUsage = calculateCPUUsage(
pid: pid, pid: pid,
userTime: rusageData.userTime, userTime: rusageData.userTime,
systemTime: rusageData.systemTime systemTime: rusageData.systemTime
@@ -111,23 +104,33 @@ final class ProcessMonitor {
diskWrite: rusageData.diskWrite, diskWrite: rusageData.diskWrite,
idleWakeups: rusageData.idleWakeups idleWakeups: rusageData.idleWakeups
) )
diskReadRate = extra.readRate detail = DetailedSample(
diskWriteRate = extra.writeRate memory: rusageData.memory,
diskReadBytes = rusageData.diskRead resident: rusageData.resident,
diskWriteBytes = rusageData.diskWrite compressed: fetchCompressedMemory(pid: pid) ?? 0,
energyImpact = EnergyModel.impact(cpuPercent: cpuUsage, idleWakeupsPerSec: extra.idleWakeupsPerSec) threads: taskInfo.pti_threadnum,
cpu: cpu,
diskReadRate: extra.readRate,
diskWriteRate: extra.writeRate,
diskReadBytes: rusageData.diskRead,
diskWriteBytes: rusageData.diskWrite,
energy: EnergyModel.impact(cpuPercent: cpu, idleWakeupsPerSec: extra.idleWakeupsPerSec)
)
lastDetailed[pid] = detail
} else { } else {
memoryUsage = 0 detail = .zero
threadCount = 0
cpuUsage = 0
} }
} else { } else {
// Lightweight refresh - reuse last known values // Lightweight refresh - reuse last known values. This used to emit
memoryUsage = 0 // zeros, so ~90% of rows showed "Zero KB" memory (and 0 threads/CPU)
threadCount = 0 // on two of every three updates.
cpuUsage = 0 detail = lastDetailed[pid] ?? .zero
} }
let memoryUsage = detail.memory
let threadCount = detail.threads
let cpuUsage = detail.cpu
let state = determineProcessState(status: bsdInfo.pbi_status, cpuUsage: cpuUsage) let state = determineProcessState(status: bsdInfo.pbi_status, cpuUsage: cpuUsage)
let icon = fetchIcon(pid: pid) let icon = fetchIcon(pid: pid)
@@ -147,12 +150,14 @@ final class ProcessMonitor {
icon: icon, icon: icon,
parentPid: parentPid, parentPid: parentPid,
startTime: startTime, startTime: startTime,
diskReadRate: diskReadRate, diskReadRate: detail.diskReadRate,
diskWriteRate: diskWriteRate, diskWriteRate: detail.diskWriteRate,
diskReadBytes: diskReadBytes, diskReadBytes: detail.diskReadBytes,
diskWriteBytes: diskWriteBytes, diskWriteBytes: detail.diskWriteBytes,
energyImpact: energyImpact, energyImpact: detail.energy,
executablePath: pathCache[pid] executablePath: pathCache[pid],
residentMemory: detail.resident,
compressedMemory: detail.compressed
) )
} }
@@ -230,8 +235,27 @@ final class ProcessMonitor {
return name return name
} }
/// The expensive per-process readings (rusage + task info + derived rates).
private struct DetailedSample {
var memory: Int64
var resident: Int64
var compressed: Int64
var threads: Int32
var cpu: Double
var diskReadRate: Double
var diskWriteRate: Double
var diskReadBytes: UInt64
var diskWriteBytes: UInt64
var energy: Double
static let zero = DetailedSample(memory: 0, resident: 0, compressed: 0, threads: 0, cpu: 0,
diskReadRate: 0, diskWriteRate: 0,
diskReadBytes: 0, diskWriteBytes: 0, energy: 0)
}
private struct RusageData { private struct RusageData {
var memory: Int64 = 0 var memory: Int64 = 0
var resident: Int64 = 0
var userTime: UInt64 = 0 var userTime: UInt64 = 0
var systemTime: UInt64 = 0 var systemTime: UInt64 = 0
var diskRead: UInt64 = 0 var diskRead: UInt64 = 0
@@ -250,6 +274,7 @@ final class ProcessMonitor {
if result == 0 { if result == 0 {
return RusageData( return RusageData(
memory: Int64(rusage.ri_phys_footprint), memory: Int64(rusage.ri_phys_footprint),
resident: Int64(rusage.ri_resident_size),
userTime: rusage.ri_user_time, userTime: rusage.ri_user_time,
systemTime: rusage.ri_system_time, systemTime: rusage.ri_system_time,
diskRead: rusage.ri_diskio_bytesread, diskRead: rusage.ri_diskio_bytesread,
@@ -264,6 +289,7 @@ final class ProcessMonitor {
if proc_pidinfo(pid, PROC_PIDTASKINFO, 0, &taskInfo, taskInfoSize) == taskInfoSize { if proc_pidinfo(pid, PROC_PIDTASKINFO, 0, &taskInfo, taskInfoSize) == taskInfoSize {
return RusageData( return RusageData(
memory: Int64(taskInfo.pti_resident_size), memory: Int64(taskInfo.pti_resident_size),
resident: Int64(taskInfo.pti_resident_size),
userTime: taskInfo.pti_total_user, userTime: taskInfo.pti_total_user,
systemTime: taskInfo.pti_total_system systemTime: taskInfo.pti_total_system
) )
@@ -475,6 +501,29 @@ func fetchProcessArguments(pid: pid_t) -> [String] {
return ProcArgs.parse(Array(buffer.prefix(size)))?.args ?? [] return ProcArgs.parse(Array(buffer.prefix(size)))?.args ?? []
} }
/// Memory of a process currently held by the compressor, at its original
/// (uncompressed) size. macOS does not track swap per process: when compressor
/// segments are paged out to disk they stay counted here, so this is the
/// closest per-process "swapped" figure that exists — Activity Monitor's
/// "Compressed Memory" and `top`'s CMPRS show the same value.
///
/// Needs only a task *name* port, which the kernel hands out for processes of
/// the same user; returns `nil` for other users' and system processes.
func fetchCompressedMemory(pid: pid_t) -> Int64? {
var port: mach_port_name_t = 0
guard task_name_for_pid(mach_task_self_, pid, &port) == KERN_SUCCESS else { return nil }
defer { mach_port_deallocate(mach_task_self_, port) }
var info = task_vm_info_data_t()
var count = mach_msg_type_number_t(MemoryLayout<task_vm_info_data_t>.stride / MemoryLayout<natural_t>.stride)
let result = withUnsafeMutablePointer(to: &info) {
$0.withMemoryRebound(to: integer_t.self, capacity: Int(count)) {
task_info(port, task_flavor_t(TASK_VM_INFO), $0, &count)
}
}
return result == KERN_SUCCESS ? Int64(info.compressed) : nil
}
/// Counts open file descriptors for a process (files, sockets, pipes, …). /// Counts open file descriptors for a process (files, sockets, pipes, …).
func fetchOpenFileCount(pid: pid_t) -> Int? { func fetchOpenFileCount(pid: pid_t) -> Int? {
let bufferSize = proc_pidinfo(pid, PROC_PIDLISTFDS, 0, nil, 0) let bufferSize = proc_pidinfo(pid, PROC_PIDLISTFDS, 0, nil, 0)

Fájl megtekintése

@@ -6,7 +6,8 @@ final class SystemMonitor: ObservableObject {
private let backgroundQueue = DispatchQueue(label: "com.topmanager.monitor", qos: .userInitiated) private let backgroundQueue = DispatchQueue(label: "com.topmanager.monitor", qos: .userInitiated)
// Published properties (must be updated on main thread) // Published properties (must be updated on main thread). They only follow
// `live` while a TopManager window is on screen — see `setUIVisible`.
@MainActor @Published var cpuInfo: CPUInfo? @MainActor @Published var cpuInfo: CPUInfo?
@MainActor @Published var memoryInfo: MemoryInfo? @MainActor @Published var memoryInfo: MemoryInfo?
@MainActor @Published var processes: [ProcessItem] = [] @MainActor @Published var processes: [ProcessItem] = []
@@ -22,6 +23,24 @@ final class SystemMonitor: ObservableObject {
@MainActor @Published var memoryHistory: [MemoryHistoryPoint] = [] @MainActor @Published var memoryHistory: [MemoryHistoryPoint] = []
@MainActor @Published var networkHistory: [NetworkHistoryPoint] = [] @MainActor @Published var networkHistory: [NetworkHistoryPoint] = []
/// Always-current readings and chart histories. Recording, alerts and the
/// menu-bar title read from here, so they keep running while views are paused.
private struct LiveState {
var cpuInfo: CPUInfo?
var memoryInfo: MemoryInfo?
var processes: [ProcessItem] = []
var diskInfo: DiskInfo?
var networkInfo: NetworkInfo?
var gpuInfo: GPUInfo?
var powerInfo: PowerInfo?
var cpuHistory: [CPUHistoryPoint] = []
var coreHistories: [Int: [CoreHistoryPoint]] = [:]
var memoryHistory: [MemoryHistoryPoint] = []
var networkHistory: [NetworkHistoryPoint] = []
}
@MainActor private var live = LiveState()
@MainActor private var isUIVisible = true
// Sub-monitors // Sub-monitors
private let cpuMonitor = CPUMonitor() private let cpuMonitor = CPUMonitor()
private let memoryMonitor = MemoryMonitor() private let memoryMonitor = MemoryMonitor()
@@ -47,6 +66,8 @@ final class SystemMonitor: ObservableObject {
private init() {} private init() {}
@MainActor func startMonitoring() { @MainActor func startMonitoring() {
guard timer == nil else { return }
// Immediate initial fetch // Immediate initial fetch
backgroundQueue.async { [weak self] in backgroundQueue.async { [weak self] in
guard let self = self else { return } guard let self = self else { return }
@@ -134,95 +155,103 @@ final class SystemMonitor: ObservableObject {
processes: [ProcessItem]?, disk: DiskInfo?, gpu: GPUInfo?, processes: [ProcessItem]?, disk: DiskInfo?, gpu: GPUInfo?,
power: PowerInfo?) { power: PowerInfo?) {
if let info = cpu { if let info = cpu {
cpuInfo = info live.cpuInfo = info
let historyPoint = CPUHistoryPoint( live.cpuHistory.append(CPUHistoryPoint(
timestamp: info.timestamp, timestamp: info.timestamp,
usage: info.globalUsage, usage: info.globalUsage,
userUsage: info.userUsage, userUsage: info.userUsage,
systemUsage: info.systemUsage systemUsage: info.systemUsage
) ))
cpuHistory.append(historyPoint) if live.cpuHistory.count > historyLimit {
if cpuHistory.count > historyLimit { live.cpuHistory.removeFirst()
cpuHistory.removeFirst()
} }
for core in info.coreUsages { for core in info.coreUsages {
let corePoint = CoreHistoryPoint(timestamp: info.timestamp, usage: core.usage) live.coreHistories[core.id, default: []].append(
if coreHistories[core.id] == nil { CoreHistoryPoint(timestamp: info.timestamp, usage: core.usage))
coreHistories[core.id] = [] if live.coreHistories[core.id, default: []].count > historyLimit {
} live.coreHistories[core.id]?.removeFirst()
coreHistories[core.id]?.append(corePoint)
if coreHistories[core.id]?.count ?? 0 > historyLimit {
coreHistories[core.id]?.removeFirst()
} }
} }
} }
if let info = memory { if let info = memory {
memoryInfo = info live.memoryInfo = info
let historyPoint = MemoryHistoryPoint( live.memoryHistory.append(MemoryHistoryPoint(
timestamp: info.timestamp, timestamp: info.timestamp,
usedMemory: info.usedMemory, usedMemory: info.usedMemory,
totalMemory: info.totalMemory totalMemory: info.totalMemory
) ))
memoryHistory.append(historyPoint) if live.memoryHistory.count > historyLimit {
if memoryHistory.count > historyLimit { live.memoryHistory.removeFirst()
memoryHistory.removeFirst()
} }
} }
if let info = network { if let info = network {
networkInfo = info live.networkInfo = info
let historyPoint = NetworkHistoryPoint( live.networkHistory.append(NetworkHistoryPoint(
timestamp: info.timestamp, timestamp: info.timestamp,
downloadRate: info.totalDownloadRate, downloadRate: info.totalDownloadRate,
uploadRate: info.totalUploadRate uploadRate: info.totalUploadRate
) ))
networkHistory.append(historyPoint) if live.networkHistory.count > historyLimit {
if networkHistory.count > historyLimit { live.networkHistory.removeFirst()
networkHistory.removeFirst()
} }
} }
if let procs = processes { if let procs = processes { live.processes = procs }
self.processes = procs if let info = disk { live.diskInfo = info }
} if let info = gpu { live.gpuInfo = info }
if let info = power { live.powerInfo = info }
if let info = disk { if isUIVisible { publishLive() }
diskInfo = info MenuBarStatus.shared.update(cpu: live.cpuInfo, memory: live.memoryInfo, network: live.networkInfo)
}
if let info = gpu {
gpuInfo = info
}
if let info = power {
powerInfo = info
}
// Persist a system-wide sample so history survives restarts and long ranges. // Persist a system-wide sample so history survives restarts and long ranges.
if let c = cpuInfo, let m = memoryInfo { if let c = live.cpuInfo, let m = live.memoryInfo {
MetricsStore.shared.record(MetricsSample( MetricsStore.shared.record(MetricsSample(
t: c.timestamp, t: c.timestamp,
cpu: c.globalUsage, cpu: c.globalUsage,
memUsed: m.usedMemory, memUsed: m.usedMemory,
memTotal: m.totalMemory, memTotal: m.totalMemory,
netDown: networkInfo?.totalDownloadRate ?? 0, netDown: live.networkInfo?.totalDownloadRate ?? 0,
netUp: networkInfo?.totalUploadRate ?? 0 netUp: live.networkInfo?.totalUploadRate ?? 0
)) ))
} }
// Proactive health + alert evaluation (runs every cycle). // Proactive health + alert evaluation (runs every cycle).
AlertCenter.shared.evaluate( AlertCenter.shared.evaluate(
cpuUsage: cpuInfo?.globalUsage ?? 0, cpuUsage: live.cpuInfo?.globalUsage ?? 0,
memory: memoryInfo, memory: live.memoryInfo,
disk: diskInfo, disk: live.diskInfo,
thermal: currentThermalLevel(), thermal: currentThermalLevel(),
topProcess: self.processes.max { $0.cpuUsage < $1.cpuUsage }, topProcess: live.processes.max { $0.cpuUsage < $1.cpuUsage },
power: powerInfo power: live.powerInfo
) )
} }
/// Pause (`false`) or resume (`true`) publishing to views. Resuming catches
/// the views up at once with everything recorded while they were hidden.
@MainActor func setUIVisible(_ visible: Bool) {
guard visible != isUIVisible else { return }
isUIVisible = visible
if visible { publishLive() }
}
@MainActor private func publishLive() {
cpuInfo = live.cpuInfo
memoryInfo = live.memoryInfo
processes = live.processes
diskInfo = live.diskInfo
networkInfo = live.networkInfo
gpuInfo = live.gpuInfo
powerInfo = live.powerInfo
cpuHistory = live.cpuHistory
coreHistories = live.coreHistories
memoryHistory = live.memoryHistory
networkHistory = live.networkHistory
}
private func currentThermalLevel() -> ThermalLevel { private func currentThermalLevel() -> ThermalLevel {
switch ProcessInfo.processInfo.thermalState { switch ProcessInfo.processInfo.thermalState {
case .nominal: return .nominal case .nominal: return .nominal

Fájl megtekintése

@@ -0,0 +1,52 @@
import AppKit
/// Reports whether any TopManager window is actually on screen: the main window,
/// the menu-bar popover or Settings — as opposed to closed, minimized, fully
/// covered, or on another Space.
///
/// Why this exists: on macOS 26 several framework paths leak a little on every
/// view update (SwiftUI `Table` cell updates, AppKit text-cell KVO dependencies,
/// an Observation registrar created inside DesignLibrary). SwiftUI keeps updating
/// a minimized or covered window, so leaving the main window open all day grew
/// the app by several hundred MB. `SystemMonitor` stops publishing while this
/// reports `false`, which confines that growth to the time someone is looking.
@MainActor
final class WindowVisibility {
static let shared = WindowVisibility()
private(set) var isAnyWindowVisible = true
private var onChange: ((Bool) -> Void)?
private var tokens: [NSObjectProtocol] = []
private init() {}
func start(onChange: @escaping (Bool) -> Void) {
guard tokens.isEmpty else { return }
self.onChange = onChange
let names = [NSWindow.didChangeOcclusionStateNotification, NSWindow.willCloseNotification]
for name in names {
tokens.append(NotificationCenter.default.addObserver(forName: name, object: nil, queue: .main) { [weak self] _ in
// willClose arrives while the window is still on screen, so
// re-check on a later turn, once it is actually gone.
Task { @MainActor in self?.recompute() }
})
}
recompute()
}
private func recompute() {
let visible = NSApp.windows.contains(where: Self.countsAsVisible)
guard visible != isAnyWindowVisible else { return }
isAnyWindowVisible = visible
onChange?(visible)
}
/// The status item's host window is permanently on screen; counting it would
/// keep the UI publishing forever.
private static func countsAsVisible(_ window: NSWindow) -> Bool {
window.isVisible
&& window.occlusionState.contains(.visible)
&& !String(describing: type(of: window)).contains("StatusBarWindow")
}
}

Fájl megtekintése

@@ -7,6 +7,16 @@ import UserNotifications
final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDelegate { final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDelegate {
func applicationDidFinishLaunching(_ notification: Notification) { func applicationDidFinishLaunching(_ notification: Notification) {
UNUserNotificationCenter.current().delegate = self UNUserNotificationCenter.current().delegate = self
// Start here rather than in the main window's onAppear: the app can launch
// with no window at all (state restoration after the window was closed),
// and the menu bar, alerts and history must run regardless.
Task { @MainActor in
SystemMonitor.shared.startMonitoring()
AppSettings.shared.applyAll()
AlertCenter.shared.requestNotificationAuthorization()
WindowVisibility.shared.start { SystemMonitor.shared.setUIVisible($0) }
}
} }
func userNotificationCenter(_ center: UNUserNotificationCenter, func userNotificationCenter(_ center: UNUserNotificationCenter,
@@ -33,11 +43,6 @@ struct TopManagerApp: App {
.environmentObject(monitor) .environmentObject(monitor)
.environmentObject(AlertCenter.shared) .environmentObject(AlertCenter.shared)
.environmentObject(settings) .environmentObject(settings)
.onAppear {
monitor.startMonitoring()
settings.applyAll()
AlertCenter.shared.requestNotificationAuthorization()
}
} }
.windowStyle(.automatic) .windowStyle(.automatic)
.defaultSize(width: 1000, height: 700) .defaultSize(width: 1000, height: 700)
@@ -62,7 +67,7 @@ struct TopManagerApp: App {
.environmentObject(AlertCenter.shared) .environmentObject(AlertCenter.shared)
.environmentObject(settings) .environmentObject(settings)
} label: { } label: {
MenuBarLabel(monitor: monitor, settings: settings, alerts: AlertCenter.shared) MenuBarLabel(status: MenuBarStatus.shared, settings: settings, alerts: AlertCenter.shared)
} }
.menuBarExtraStyle(.window) .menuBarExtraStyle(.window)
} }

Fájl megtekintése

@@ -1,21 +1,23 @@
import SwiftUI import SwiftUI
/// The compact menu-bar title. Renders whichever metric the user picked. /// The compact menu-bar title. Renders whichever metric the user picked.
/// Reads `MenuBarStatus`, not `SystemMonitor`, so it keeps updating while the
/// monitor's view publishing is paused.
struct MenuBarLabel: View { struct MenuBarLabel: View {
@ObservedObject var monitor: SystemMonitor @ObservedObject var status: MenuBarStatus
@ObservedObject var settings: AppSettings @ObservedObject var settings: AppSettings
@ObservedObject var alerts: AlertCenter @ObservedObject var alerts: AlertCenter
var body: some View { var body: some View {
switch settings.menuBarMetric { switch settings.menuBarMetric {
case .cpu: case .cpu:
textOrIcon(monitor.cpuInfo.map { String(format: "%.0f%%", $0.globalUsage) }, icon: "cpu") textOrIcon(status.cpuText, icon: "cpu")
case .memory: case .memory:
textOrIcon(monitor.memoryInfo.map { String(format: "%.0f%%", $0.usagePercentage) }, icon: "memorychip") textOrIcon(status.memoryText, icon: "memorychip")
case .health: case .health:
Text("♥ \(alerts.healthScore)").monospacedDigit() Text("♥ \(alerts.healthScore)").monospacedDigit()
case .download: case .download:
textOrIcon(monitor.networkInfo.map { formatBytesPerSecond($0.totalDownloadRate) }, icon: "arrow.down") textOrIcon(status.downloadText, icon: "arrow.down")
} }
} }

Fájl megtekintése

@@ -28,6 +28,7 @@ struct ProcessInspectorSheet: View {
ScrollView { ScrollView {
VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 16) {
liveStats liveStats
memoryExplanation
pathSection pathSection
detailsSection detailsSection
} }
@@ -76,6 +77,8 @@ struct ProcessInspectorSheet: View {
DetailRow(label: "CPU (total)", value: String(format: "%.2f%%", process.cpuUsageTotal)) DetailRow(label: "CPU (total)", value: String(format: "%.2f%%", process.cpuUsageTotal))
DetailRow(label: "Energy", value: String(format: "%.1f", process.energyImpact)) DetailRow(label: "Energy", value: String(format: "%.1f", process.energyImpact))
DetailRow(label: "Memory", value: formatBytes(process.memoryUsage)) DetailRow(label: "Memory", value: formatBytes(process.memoryUsage))
DetailRow(label: "RAM", value: formatBytes(process.residentMemory))
DetailRow(label: "Compressed", value: process.compressedMemory > 0 ? formatBytes(process.compressedMemory) : "—")
DetailRow(label: "Threads", value: "\(process.threadCount)") DetailRow(label: "Threads", value: "\(process.threadCount)")
DetailRow(label: "Open files", value: openFiles.map(String.init) ?? "…") DetailRow(label: "Open files", value: openFiles.map(String.init) ?? "…")
DetailRow(label: "Disk read/s", value: process.diskReadRate > 0 ? formatBytesPerSecond(process.diskReadRate) : "—") DetailRow(label: "Disk read/s", value: process.diskReadRate > 0 ? formatBytesPerSecond(process.diskReadRate) : "—")
@@ -88,6 +91,25 @@ struct ProcessInspectorSheet: View {
} }
} }
/// Plain-language guide to the three memory figures; without it, RAM being
/// higher than Memory (normal for any SwiftUI app) looks like a bug.
private var memoryExplanation: some View {
VStack(alignment: .leading, spacing: 6) {
Text("About memory").font(.caption).foregroundColor(.secondary)
Group {
if process.memoryUsage == 0 && process.residentMemory == 0 {
Text("Memory figures are unavailable for processes owned by another user or the system.")
.foregroundColor(.secondary)
}
Text("**Memory** — what this process is responsible for: its own modified pages, whether in RAM, compressed or swapped out. This is its real memory cost.")
Text("**RAM** — pages physically in RAM right now, including system framework code shared by every app. It can be higher than Memory, because shared pages take up RAM only once.")
Text("**Compressed** — the part of Memory that macOS has compressed to save RAM, including pages swapped out to disk, shown at their original size.")
}
.font(.caption)
.fixedSize(horizontal: false, vertical: true)
}
}
@ViewBuilder private var pathSection: some View { @ViewBuilder private var pathSection: some View {
if let path = process.executablePath { if let path = process.executablePath {
VStack(alignment: .leading, spacing: 4) { VStack(alignment: .leading, spacing: 4) {

Fájl megtekintése

@@ -1,7 +1,7 @@
import SwiftUI import SwiftUI
enum ProcessSortColumn: String { enum ProcessSortColumn: String {
case name, pid, cpu, cpuTotal, memory, threads, user, state, energy, disk case name, pid, cpu, cpuTotal, memory, resident, compressed, threads, user, state, energy, disk
} }
struct ProcessView: View { struct ProcessView: View {
@@ -60,6 +60,10 @@ struct ProcessView: View {
comparison = lhs.cpuUsageTotal < rhs.cpuUsageTotal ? .orderedAscending : (lhs.cpuUsageTotal > rhs.cpuUsageTotal ? .orderedDescending : .orderedSame) comparison = lhs.cpuUsageTotal < rhs.cpuUsageTotal ? .orderedAscending : (lhs.cpuUsageTotal > rhs.cpuUsageTotal ? .orderedDescending : .orderedSame)
case .memory: case .memory:
comparison = lhs.memoryUsage < rhs.memoryUsage ? .orderedAscending : (lhs.memoryUsage > rhs.memoryUsage ? .orderedDescending : .orderedSame) comparison = lhs.memoryUsage < rhs.memoryUsage ? .orderedAscending : (lhs.memoryUsage > rhs.memoryUsage ? .orderedDescending : .orderedSame)
case .resident:
comparison = lhs.residentMemory < rhs.residentMemory ? .orderedAscending : (lhs.residentMemory > rhs.residentMemory ? .orderedDescending : .orderedSame)
case .compressed:
comparison = lhs.compressedMemory < rhs.compressedMemory ? .orderedAscending : (lhs.compressedMemory > rhs.compressedMemory ? .orderedDescending : .orderedSame)
case .threads: case .threads:
comparison = lhs.threadCount < rhs.threadCount ? .orderedAscending : (lhs.threadCount > rhs.threadCount ? .orderedDescending : .orderedSame) comparison = lhs.threadCount < rhs.threadCount ? .orderedAscending : (lhs.threadCount > rhs.threadCount ? .orderedDescending : .orderedSame)
case .user: case .user:
@@ -87,6 +91,34 @@ struct ProcessView: View {
return monitor.processes.first { $0.pid == pid } return monitor.processes.first { $0.pid == pid }
} }
/// Memory, RAM and Compressed as a single builder element: Table accepts at
/// most 10 columns per block, and inlining them also exceeds the type
/// checker's time limit for the table expression.
@TableColumnBuilder<ProcessItem, KeyPathComparator<ProcessItem>>
private var memoryColumns: some TableColumnContent<ProcessItem, KeyPathComparator<ProcessItem>> {
TableColumn("Memory", value: \.memoryUsage) { process in
Text(formatBytes(process.memoryUsage))
.monospacedDigit()
}
.width(80)
TableColumn("RAM", value: \.residentMemory) { process in
Text(formatBytes(process.residentMemory))
.monospacedDigit()
}
.width(80)
TableColumn("Compressed", value: \.compressedMemory) { process in
if process.compressedMemory > 0 {
Text(formatBytes(process.compressedMemory))
.monospacedDigit()
} else {
Text("—").foregroundColor(.secondary)
}
}
.width(85)
}
var body: some View { var body: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
// Summary bar - separate view to avoid re-rendering table // Summary bar - separate view to avoid re-rendering table
@@ -140,11 +172,7 @@ struct ProcessView: View {
} }
.width(60) .width(60)
TableColumn("Memory", value: \.memoryUsage) { process in memoryColumns
Text(formatBytes(process.memoryUsage))
.monospacedDigit()
}
.width(80)
TableColumn("Disk I/O", value: \.diskTotalRate) { process in TableColumn("Disk I/O", value: \.diskTotalRate) { process in
if process.diskTotalRate > 0 { if process.diskTotalRate > 0 {
@@ -246,6 +274,10 @@ struct ProcessView: View {
sortColumn = .energy sortColumn = .energy
} else if keyPathString.contains("diskTotalRate") { } else if keyPathString.contains("diskTotalRate") {
sortColumn = .disk sortColumn = .disk
} else if keyPathString.contains("residentMemory") {
sortColumn = .resident
} else if keyPathString.contains("compressedMemory") {
sortColumn = .compressed
} else if keyPathString.contains("memoryUsage") { } else if keyPathString.contains("memoryUsage") {
sortColumn = .memory sortColumn = .memory
} else if keyPathString.contains("name") { } else if keyPathString.contains("name") {

Fájl megtekintése

@@ -0,0 +1,35 @@
import XCTest
@testable import TopManager
final class MenuBarStatusTests: XCTestCase {
private func cpu(_ usage: Double) -> CPUInfo {
CPUInfo(globalUsage: usage, userUsage: usage, systemUsage: 0, idleUsage: 100 - usage, coreUsages: [])
}
@MainActor
func testFormatsVisibleText() {
let status = MenuBarStatus()
status.update(cpu: cpu(41.6), memory: nil, network: nil)
XCTAssertEqual(status.cpuText, "42%")
XCTAssertNil(status.memoryText)
XCTAssertNil(status.downloadText)
}
/// Every publish re-measures the status item, so a refresh that renders the
/// same text must not publish at all.
@MainActor
func testRepublishesOnlyWhenVisibleTextChanges() {
let status = MenuBarStatus()
var publishes = 0
let subscription = status.objectWillChange.sink { publishes += 1 }
defer { subscription.cancel() }
status.update(cpu: cpu(41.6), memory: nil, network: nil)
XCTAssertEqual(publishes, 1)
status.update(cpu: cpu(42.4), memory: nil, network: nil) // still "42%"
XCTAssertEqual(publishes, 1)
status.update(cpu: cpu(57), memory: nil, network: nil)
XCTAssertEqual(publishes, 2)
}
}

Fájl megtekintése

@@ -92,4 +92,37 @@ final class ProcessDataTests: XCTestCase {
XCTAssertNotEqual(make(energy: 5), make(energy: 9)) XCTAssertNotEqual(make(energy: 5), make(energy: 9))
XCTAssertEqual(make(energy: 5), make(energy: 5)) XCTAssertEqual(make(energy: 5), make(energy: 5))
} }
// MARK: - Live sampling
/// Fetches 1–3 are full refreshes; 4 and 5 are lightweight ones, which used
/// to report 0 memory for every idle process (~90% of rows). Only processes
/// spawned between fetches may legitimately have no reading yet.
func testLightweightRefreshKeepsLastKnownMemory() {
let monitor = ProcessMonitor()
let me = NSUserName()
for fetch in 1...6 {
let own = monitor.fetchProcesses().filter { $0.user == me }
XCTAssertFalse(own.isEmpty)
let zero = own.filter { $0.memoryUsage == 0 }.count
XCTAssertLessThan(zero, max(3, own.count / 10),
"fetch \(fetch): \(zero) of \(own.count) own processes reported 0 memory")
let zeroRAM = own.filter { $0.residentMemory == 0 }.count
XCTAssertLessThan(zeroRAM, max(3, own.count / 10),
"fetch \(fetch): \(zeroRAM) of \(own.count) own processes reported 0 RAM")
}
}
func testOwnProcessReportsResidentMemory() {
let item = ProcessMonitor().fetchProcesses().first { $0.pid == getpid() }
XCTAssertNotNil(item)
XCTAssertGreaterThan(item?.residentMemory ?? 0, 0)
XCTAssertGreaterThan(item?.memoryUsage ?? 0, 0)
}
/// A task name port is granted for our own processes only; launchd runs as root.
func testCompressedMemoryReadableForOwnProcessesOnly() {
XCTAssertNotNil(fetchCompressedMemory(pid: getpid()))
XCTAssertNil(fetchCompressedMemory(pid: 1))
}
} }

Fájl megtekintése

@@ -31,8 +31,15 @@ targets:
GENERATE_INFOPLIST_FILE: NO GENERATE_INFOPLIST_FILE: NO
SWIFT_EMIT_LOC_STRINGS: YES SWIFT_EMIT_LOC_STRINGS: YES
ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
# The app version lives here. `xcodegen generate` rewrites Info.plist from
# the `info` block below, so a version typed into Info.plist gets reset.
MARKETING_VERSION: "1.1.1"
CURRENT_PROJECT_VERSION: "3"
info: info:
path: TopManager/Info.plist path: TopManager/Info.plist
properties:
CFBundleShortVersionString: $(MARKETING_VERSION)
CFBundleVersion: $(CURRENT_PROJECT_VERSION)
TopManagerTests: TopManagerTests:
type: bundle.unit-test type: bundle.unit-test