Compare commits

...
Author SHA1 Message Date
gronod 3e8852258a feat(memory): implement MMMemoryTelemetryProvider with Mach VM breakdown, swap, and pressure (Issue #8)
MacMonitor CI/CD Pipeline / Build & Test (Intel x86_64) (push) Canceled after 0s
2026-09-08 12:19:20 +01:00
gronod b37cfb06a9 Merge branch 'feat/5-cpu-load' into milestone/m2-primary-telemetry 2026-09-08 12:18:37 +01:00
5 changed files with 250 additions and 0 deletions
+18
View File
@@ -12,10 +12,12 @@
1B3F1C8ABF0ADFAA290FF6F6 /* MMSMCParser.m in Sources */ = {isa = PBXBuildFile; fileRef = 375AA781A2AA0A20B119C7B7 /* MMSMCParser.m */; };
3C0AC14F803C813287D8A663 /* IOKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 007F48A5A599AB4E8D216D16 /* IOKit.framework */; };
4419FE7554489B23976E2970 /* MMCPULoadProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = D68FC8100D08DB6202F46339 /* MMCPULoadProvider.m */; };
6201FD599FED061F6C736E8B /* MMMemoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 099DCF7EF03A58ADA40A94A1 /* MMMemoryTests.swift */; };
88387CDAE2AC1CC74EF89B57 /* MacMonitorApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADEA6BD38BA8D887B1DB64A9 /* MacMonitorApp.swift */; };
9BCE74C5454F81EF80492E75 /* MacMonitorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EC3EDFCD5C50CB352FCED87 /* MacMonitorTests.swift */; };
A7609CE56D49EC3419987468 /* SystemTelemetryStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B4E94FB2886C60D5548A2A /* SystemTelemetryStore.swift */; };
A9FCAEC0BF27A419DB148C4A /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E142AE61F9B87757997870DD /* ContentView.swift */; };
B9C2D6574E808B517BA34C83 /* MMMemoryTelemetryProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 991AEEAF262F3C6BE930BC77 /* MMMemoryTelemetryProvider.m */; };
CD91FC663B19DA1FEA3D49D7 /* MMAppleSMCClient.m in Sources */ = {isa = PBXBuildFile; fileRef = 5555436CD3CB8A73F5D6504F /* MMAppleSMCClient.m */; };
EAFAF5A10502D4ACC64F4720 /* MMTelemetryCoordinator.m in Sources */ = {isa = PBXBuildFile; fileRef = F9F9AD1268F906761032AF4D /* MMTelemetryCoordinator.m */; };
EC9FA3E4A119277B89E73F8D /* MMSMCTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 940D608A1AF80F75584E744E /* MMSMCTests.swift */; };
@@ -33,6 +35,8 @@
/* Begin PBXFileReference section */
007F48A5A599AB4E8D216D16 /* IOKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IOKit.framework; path = System/Library/Frameworks/IOKit.framework; sourceTree = SDKROOT; };
0130BD1E18972DF9E2955334 /* MMMemoryTelemetryProvider.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MMMemoryTelemetryProvider.h; sourceTree = "<group>"; };
099DCF7EF03A58ADA40A94A1 /* MMMemoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MMMemoryTests.swift; sourceTree = "<group>"; };
1AD9EEE571CD904BBCD1C96B /* MMSMCParser.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MMSMCParser.h; sourceTree = "<group>"; };
2345555FF9780B02A90CC7A9 /* CoreFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreFoundation.framework; path = System/Library/Frameworks/CoreFoundation.framework; sourceTree = SDKROOT; };
2EC3EDFCD5C50CB352FCED87 /* MacMonitorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacMonitorTests.swift; sourceTree = "<group>"; };
@@ -47,6 +51,7 @@
726005CB5A6C86E7D80D3CA0 /* MMAppleSMCClient.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MMAppleSMCClient.h; sourceTree = "<group>"; };
8FBB436F3D9472194D8C6EDD /* MMSMCDefines.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MMSMCDefines.h; sourceTree = "<group>"; };
940D608A1AF80F75584E744E /* MMSMCTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MMSMCTests.swift; sourceTree = "<group>"; };
991AEEAF262F3C6BE930BC77 /* MMMemoryTelemetryProvider.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MMMemoryTelemetryProvider.m; sourceTree = "<group>"; };
ADEA6BD38BA8D887B1DB64A9 /* MacMonitorApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacMonitorApp.swift; sourceTree = "<group>"; };
D68FC8100D08DB6202F46339 /* MMCPULoadProvider.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MMCPULoadProvider.m; sourceTree = "<group>"; };
E142AE61F9B87757997870DD /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
@@ -131,6 +136,7 @@
isa = PBXGroup;
children = (
78383CED205BCA772701AE48 /* CPU */,
EE2F5EFC9DC33CEDA79A131F /* Memory */,
);
path = Telemetry;
sourceTree = "<group>";
@@ -157,6 +163,7 @@
children = (
2EC3EDFCD5C50CB352FCED87 /* MacMonitorTests.swift */,
4CE92B1A8D7FDBB6499448E1 /* MMCPULoadTests.swift */,
099DCF7EF03A58ADA40A94A1 /* MMMemoryTests.swift */,
940D608A1AF80F75584E744E /* MMSMCTests.swift */,
);
path = Tests;
@@ -183,6 +190,15 @@
name = Frameworks;
sourceTree = "<group>";
};
EE2F5EFC9DC33CEDA79A131F /* Memory */ = {
isa = PBXGroup;
children = (
0130BD1E18972DF9E2955334 /* MMMemoryTelemetryProvider.h */,
991AEEAF262F3C6BE930BC77 /* MMMemoryTelemetryProvider.m */,
);
path = Memory;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@@ -259,6 +275,7 @@
buildActionMask = 2147483647;
files = (
160334CC765E091A7CBE21AA /* MMCPULoadTests.swift in Sources */,
6201FD599FED061F6C736E8B /* MMMemoryTests.swift in Sources */,
EC9FA3E4A119277B89E73F8D /* MMSMCTests.swift in Sources */,
9BCE74C5454F81EF80492E75 /* MacMonitorTests.swift in Sources */,
);
@@ -271,6 +288,7 @@
A9FCAEC0BF27A419DB148C4A /* ContentView.swift in Sources */,
CD91FC663B19DA1FEA3D49D7 /* MMAppleSMCClient.m in Sources */,
4419FE7554489B23976E2970 /* MMCPULoadProvider.m in Sources */,
B9C2D6574E808B517BA34C83 /* MMMemoryTelemetryProvider.m in Sources */,
1B3F1C8ABF0ADFAA290FF6F6 /* MMSMCParser.m in Sources */,
EAFAF5A10502D4ACC64F4720 /* MMTelemetryCoordinator.m in Sources */,
88387CDAE2AC1CC74EF89B57 /* MacMonitorApp.swift in Sources */,
@@ -13,5 +13,6 @@
// Telemetry Providers
#import "MMCPULoadProvider.h"
#import "MMMemoryTelemetryProvider.h"
#endif /* MacMonitor_Bridging_Header_h */
@@ -0,0 +1,32 @@
#ifndef MMMemoryTelemetryProvider_h
#define MMMemoryTelemetryProvider_h
#import <Foundation/Foundation.h>
#import <mach/mach.h>
#import <sys/sysctl.h>
#import "MMTelemetryProvider.h"
NS_ASSUME_NONNULL_BEGIN
/**
* MMMemoryTelemetryProvider
* Samples Mach virtual memory statistics, wired/active/inactive/compressed memory allocations,
* swap usage, and system-wide memory pressure levels.
*/
@interface MMMemoryTelemetryProvider : NSObject <MMTelemetryProvider>
@property (nonatomic, readonly) uint64_t totalPhysicalMemoryBytes;
/**
* Deterministic helper method for unit testing VM metric calculations.
*/
+ (NSDictionary<NSString *, id> *)calculateMemoryMetricsWithVMInfo:(const vm_statistics64_data_t *)vmInfo
pageSize:(vm_size_t)pageSize
totalPhysicalRAM:(uint64_t)totalRAM
swapUsage:(const struct xsw_usage * _Nullable)swap;
@end
NS_ASSUME_NONNULL_END
#endif /* MMMemoryTelemetryProvider_h */
@@ -0,0 +1,135 @@
#import "MMMemoryTelemetryProvider.h"
#import <sys/sysctl.h>
@interface MMMemoryTelemetryProvider () {
uint64_t _totalPhysicalMemoryBytes;
vm_size_t _pageSize;
}
@end
@implementation MMMemoryTelemetryProvider
- (instancetype)init {
self = [super init];
if (self) {
// Read physical memory size
uint64_t memSize = 0;
size_t size = sizeof(memSize);
sysctlbyname("hw.memsize", &memSize, &size, NULL, 0);
_totalPhysicalMemoryBytes = memSize;
// Read virtual memory page size
vm_size_t pSize = 4096;
host_page_size(mach_host_self(), &pSize);
_pageSize = pSize;
}
return self;
}
- (MMTelemetryDomain)domain {
return MMTelemetryDomainMemory;
}
- (NSString *)providerIdentifier {
return @"com.i3omb.macmonitor.telemetry.memory";
}
- (BOOL)isAvailable {
return YES;
}
- (uint64_t)totalPhysicalMemoryBytes {
return _totalPhysicalMemoryBytes;
}
+ (NSDictionary<NSString *, id> *)calculateMemoryMetricsWithVMInfo:(const vm_statistics64_data_t *)vmInfo
pageSize:(vm_size_t)pageSize
totalPhysicalRAM:(uint64_t)totalRAM
swapUsage:(const struct xsw_usage * _Nullable)swap {
if (!vmInfo) return @{};
uint64_t appMemory = (vmInfo->internal_page_count > vmInfo->purgeable_count)
? (uint64_t)(vmInfo->internal_page_count - vmInfo->purgeable_count) * pageSize
: 0;
uint64_t wired = (uint64_t)vmInfo->wire_count * pageSize;
uint64_t compressed = (uint64_t)vmInfo->compressor_page_count * pageSize;
uint64_t used = appMemory + wired + compressed;
uint64_t freeRAM = (uint64_t)vmInfo->free_count * pageSize;
uint64_t purgeable = (uint64_t)vmInfo->purgeable_count * pageSize;
uint64_t active = (uint64_t)vmInfo->active_count * pageSize;
uint64_t inactive = (uint64_t)vmInfo->inactive_count * pageSize;
// Calculate Memory Pressure percentage
uint64_t availableForUse = (uint64_t)(vmInfo->free_count + vmInfo->inactive_count) * pageSize;
double pressurePct = 0.0;
if (totalRAM > 0) {
pressurePct = (1.0 - ((double)availableForUse / (double)totalRAM)) * 100.0;
if (pressurePct < 0.0) pressurePct = 0.0;
if (pressurePct > 100.0) pressurePct = 100.0;
}
NSString *pressureStatus = @"Normal";
if (pressurePct >= 80.0) {
pressureStatus = @"Critical";
} else if (pressurePct >= 60.0) {
pressureStatus = @"Warning";
}
uint64_t swapTotal = swap ? swap->xsu_total : 0;
uint64_t swapUsed = swap ? swap->xsu_used : 0;
uint64_t swapFree = swap ? swap->xsu_avail : 0;
return @{
@"totalBytes": @(totalRAM),
@"usedBytes": @(used),
@"freeBytes": @(freeRAM),
@"appMemoryBytes": @(appMemory),
@"wiredBytes": @(wired),
@"compressedBytes": @(compressed),
@"purgeableBytes": @(purgeable),
@"activeBytes": @(active),
@"inactiveBytes": @(inactive),
@"memoryPressurePercent": @(pressurePct),
@"memoryPressureStatus": pressureStatus,
@"swapTotalBytes": @(swapTotal),
@"swapUsedBytes": @(swapUsed),
@"swapFreeBytes": @(swapFree)
};
}
- (nullable NSDictionary<NSString *, id> *)sampleTelemetryWithError:(NSError **)error {
vm_statistics64_data_t vmInfo = {0};
mach_msg_type_number_t count = HOST_VM_INFO64_COUNT;
kern_return_t kr = host_statistics64(
mach_host_self(),
HOST_VM_INFO64,
(host_info64_t)&vmInfo,
&count
);
if (kr != KERN_SUCCESS) {
if (error) {
*error = [NSError errorWithDomain:@"com.i3omb.MacMonitor.Memory"
code:kr
userInfo:@{NSLocalizedDescriptionKey: @"host_statistics64 failed to sample VM metrics"}];
}
return nil;
}
// Read swap usage
struct xsw_usage swap = {0};
size_t swapSize = sizeof(swap);
struct xsw_usage *swapPtr = NULL;
if (sysctlbyname("vm.swapusage", &swap, &swapSize, NULL, 0) == 0) {
swapPtr = &swap;
}
return [MMMemoryTelemetryProvider calculateMemoryMetricsWithVMInfo:&vmInfo
pageSize:_pageSize
totalPhysicalRAM:_totalPhysicalMemoryBytes
swapUsage:swapPtr];
}
@end
+64
View File
@@ -0,0 +1,64 @@
import XCTest
@testable import MacMonitor
final class MMMemoryTests: XCTestCase {
func testMemoryMetricCalculation() {
var vmInfo = vm_statistics64_data_t()
let pageSize: vm_size_t = 4096
let totalRAM: UInt64 = 16 * 1024 * 1024 * 1024 // 16 GB
// 1GB wired (262144 pages), 2GB app (524288 pages), 1GB compressed (262144 pages)
vmInfo.wire_count = 262_144
vmInfo.internal_page_count = 524_288
vmInfo.purgeable_count = 0
vmInfo.compressor_page_count = 262_144
vmInfo.active_count = 524_288
vmInfo.inactive_count = 1_048_576 // 4GB
vmInfo.free_count = 2_097_152 // 8GB
var swap = xsw_usage()
swap.xsu_total = 2 * 1024 * 1024 * 1024
swap.xsu_used = 512 * 1024 * 1024
swap.xsu_avail = swap.xsu_total - swap.xsu_used
withUnsafePointer(to: vmInfo) { vmPtr in
withUnsafePointer(to: swap) { swapPtr in
let metrics = MMMemoryTelemetryProvider.calculateMemoryMetrics(
withVMInfo: vmPtr,
pageSize: pageSize,
totalPhysicalRAM: totalRAM,
swapUsage: swapPtr
)
let wired = metrics["wiredBytes"] as? UInt64 ?? 0
let app = metrics["appMemoryBytes"] as? UInt64 ?? 0
let compressed = metrics["compressedBytes"] as? UInt64 ?? 0
let used = metrics["usedBytes"] as? UInt64 ?? 0
let pressureStatus = metrics["memoryPressureStatus"] as? String ?? ""
XCTAssertEqual(wired, 1024 * 1024 * 1024)
XCTAssertEqual(app, 2 * 1024 * 1024 * 1024)
XCTAssertEqual(compressed, 1024 * 1024 * 1024)
XCTAssertEqual(used, 4 * 1024 * 1024 * 1024)
XCTAssertEqual(pressureStatus, "Normal")
}
}
}
func testLiveMemoryProvider() throws {
let provider = MMMemoryTelemetryProvider()
XCTAssertTrue(provider.isAvailable)
XCTAssertEqual(provider.domain, .memory)
XCTAssertGreaterThan(provider.totalPhysicalMemoryBytes, 0)
let sample = try provider.sampleTelemetry()
let total = sample["totalBytes"] as? UInt64 ?? 0
let used = sample["usedBytes"] as? UInt64 ?? 0
let free = sample["freeBytes"] as? UInt64 ?? 0
XCTAssertGreaterThan(total, 0)
XCTAssertGreaterThan(used, 0)
XCTAssertGreaterThanOrEqual(free, 0)
}
}