Commitok összehasonlítása
7 Commit-ok
6a72c7ba64
...
v1.1.1
| Szerző | SHA1 | Dátum | |
|---|---|---|---|
|
|
1767491076 | ||
|
|
cf88044582 | ||
|
|
e8a982c00e | ||
|
|
82b55c9468 | ||
|
|
14ced8d11d | ||
|
|
c9cd87f221 | ||
|
|
6f60dd7ad8 |
@@ -5,11 +5,11 @@ A native macOS system monitor application built with SwiftUI. TopManager provide
|
|||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
## 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.
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 &&
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
30
TopManager/Services/MenuBarStatus.swift
Normal file
30
TopManager/Services/MenuBarStatus.swift
Normal file
@@ -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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
52
TopManager/Services/WindowVisibility.swift
Normal file
52
TopManager/Services/WindowVisibility.swift
Normal file
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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") {
|
||||||
|
|||||||
35
TopManagerTests/MenuBarStatusTests.swift
Normal file
35
TopManagerTests/MenuBarStatusTests.swift
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user