Compare commits

..
Author SHA1 Message Date
gronod d24fa7de35 feat(telemetry): implement kernel context switching and event counter provider (fixes #6)
MacMonitor CI/CD Pipeline / Build & Test (Intel x86_64) (push) Canceled after 0s
2026-09-08 12:32:18 +01:00
gronod 6a02d9993d Merge branch 'milestone/m2-primary-telemetry' into develop (Milestone 2 Complete)
MacMonitor CI/CD Pipeline / Build & Test (Intel x86_64) (push) Canceled after 0s
2026-09-08 12:27:05 +01:00
4 changed files with 232 additions and 0 deletions
@@ -19,5 +19,6 @@
#import "MMFanTelemetryProvider.h" #import "MMFanTelemetryProvider.h"
#import "MMComponentThermalProvider.h" #import "MMComponentThermalProvider.h"
#import "MMPowerTelemetryProvider.h" #import "MMPowerTelemetryProvider.h"
#import "MMKernelTelemetryProvider.h"
#endif /* MacMonitor_Bridging_Header_h */ #endif /* MacMonitor_Bridging_Header_h */
@@ -0,0 +1,30 @@
#ifndef MMKernelTelemetryProvider_h
#define MMKernelTelemetryProvider_h
#import <Foundation/Foundation.h>
#import "MMTelemetryProvider.h"
NS_ASSUME_NONNULL_BEGIN
/**
* MMKernelTelemetryProvider
* Samples low-level Mach kernel performance counters: context switches, system calls,
* page faults (soft, COW, zero-fill), pageins/pageouts, and computes instantaneous rates per second.
*/
@interface MMKernelTelemetryProvider : NSObject <MMTelemetryProvider>
- (instancetype)init;
/**
* Static calculator method for deterministic unit testing.
* Computes event rates per second based on previous and current cumulative counters over delta time.
*/
+ (NSDictionary<NSString *, id> *)calculateRatesWithCurrentCounters:(NSDictionary<NSString *, NSNumber *> *)current
previousCounters:(nullable NSDictionary<NSString *, NSNumber *> *)previous
timeDelta:(NSTimeInterval)timeDelta;
@end
NS_ASSUME_NONNULL_END
#endif /* MMKernelTelemetryProvider_h */
@@ -0,0 +1,131 @@
#import "MMKernelTelemetryProvider.h"
#import <mach/mach.h>
#import <mach/mach_time.h>
#import <libproc.h>
#import <os/lock.h>
@interface MMKernelTelemetryProvider () {
os_unfair_lock _lock;
NSDictionary<NSString *, NSNumber *> *_previousCounters;
uint64_t _previousTimestamp;
mach_timebase_info_data_t _timebase;
}
@end
@implementation MMKernelTelemetryProvider
- (instancetype)init {
self = [super init];
if (self) {
_lock = OS_UNFAIR_LOCK_INIT;
_previousCounters = nil;
_previousTimestamp = 0;
mach_timebase_info(&_timebase);
}
return self;
}
- (MMTelemetryDomain)domain {
return MMTelemetryDomainSystem;
}
- (NSString *)providerIdentifier {
return @"com.i3omb.macmonitor.telemetry.kernel.counters";
}
- (BOOL)isAvailable {
return YES;
}
+ (NSDictionary<NSString *, id> *)calculateRatesWithCurrentCounters:(NSDictionary<NSString *, NSNumber *> *)current
previousCounters:(nullable NSDictionary<NSString *, NSNumber *> *)previous
timeDelta:(NSTimeInterval)timeDelta {
NSMutableDictionary<NSString *, id> *result = [NSMutableDictionary dictionary];
// Copy all current cumulative counters into result
[current enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSNumber *val, BOOL *stop) {
result[[@"cumulative_" stringByAppendingString:key]] = val;
}];
NSArray<NSString *> *rateKeys = @[
@"contextSwitches", @"syscalls", @"pageFaults", @"cowFaults",
@"zeroFills", @"pageins", @"pageouts", @"decompressions", @"compressions"
];
for (NSString *key in rateKeys) {
double currentVal = [current[key] doubleValue];
double prevVal = previous ? [previous[key] doubleValue] : currentVal;
double delta = (currentVal >= prevVal) ? (currentVal - prevVal) : 0.0;
double rate = (timeDelta > 0.0001) ? (delta / timeDelta) : 0.0;
result[[key stringByAppendingString:@"Rate"]] = @(rate);
}
result[@"timeDelta"] = @(timeDelta);
return result;
}
- (nullable NSDictionary<NSString *, id> *)sampleTelemetryWithError:(NSError **)error {
uint64_t now = mach_absolute_time();
// 1. Query Mach VM Statistics for page faults, COW, zero fills, pageins/outs
vm_statistics64_data_t vmStats;
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)&vmStats, &count);
if (kr != KERN_SUCCESS) {
if (error) {
*error = [NSError errorWithDomain:@"com.i3omb.macmonitor.kernel"
code:kr
userInfo:@{NSLocalizedDescriptionKey: @"Failed to query host_statistics64"}];
}
return nil;
}
// 2. Query process table for cumulative context switches and system calls
int pids[4096];
int bytes = proc_listpids(PROC_ALL_PIDS, 0, pids, sizeof(pids));
int pidCount = bytes / sizeof(int);
uint64_t totalCSW = 0;
uint64_t totalSyscalls = 0;
for (int i = 0; i < pidCount; i++) {
if (pids[i] <= 0) continue;
struct proc_taskinfo ti;
if (proc_pidinfo(pids[i], PROC_PIDTASKINFO, 0, &ti, sizeof(ti)) == sizeof(ti)) {
totalCSW += ti.pti_csw;
totalSyscalls += (uint64_t)ti.pti_syscalls_unix + (uint64_t)ti.pti_syscalls_mach;
}
}
NSDictionary<NSString *, NSNumber *> *currentCounters = @{
@"contextSwitches": @(totalCSW),
@"syscalls": @(totalSyscalls),
@"pageFaults": @(vmStats.faults),
@"cowFaults": @(vmStats.cow_faults),
@"zeroFills": @(vmStats.zero_fill_count),
@"pageins": @(vmStats.pageins),
@"pageouts": @(vmStats.pageouts),
@"decompressions": @(vmStats.decompressions),
@"compressions": @(vmStats.compressions)
};
os_unfair_lock_lock(&_lock);
NSDictionary<NSString *, NSNumber *> *prev = _previousCounters;
uint64_t prevTime = _previousTimestamp;
_previousCounters = currentCounters;
_previousTimestamp = now;
os_unfair_lock_unlock(&_lock);
NSTimeInterval timeDelta = 1.0;
if (prevTime > 0) {
uint64_t elapsed = now - prevTime;
timeDelta = (double)elapsed * _timebase.numer / _timebase.denom / 1e9;
}
return [MMKernelTelemetryProvider calculateRatesWithCurrentCounters:currentCounters
previousCounters:prev
timeDelta:timeDelta];
}
@end
+70
View File
@@ -0,0 +1,70 @@
import XCTest
@testable import MacMonitor
final class MMKernelTelemetryTests: XCTestCase {
func testKernelRatesCalculation() {
let prev: [String: NSNumber] = [
"contextSwitches": NSNumber(value: 100_000),
"syscalls": NSNumber(value: 200_000),
"pageFaults": NSNumber(value: 50_000),
"cowFaults": NSNumber(value: 1_000),
"zeroFills": NSNumber(value: 10_000),
"pageins": NSNumber(value: 500),
"pageouts": NSNumber(value: 100),
"decompressions": NSNumber(value: 200),
"compressions": NSNumber(value: 300)
]
let curr: [String: NSNumber] = [
"contextSwitches": NSNumber(value: 102_500), // delta = 2,500
"syscalls": NSNumber(value: 210_000), // delta = 10,000
"pageFaults": NSNumber(value: 51_000), // delta = 1,000
"cowFaults": NSNumber(value: 1_050), // delta = 50
"zeroFills": NSNumber(value: 10_200), // delta = 200
"pageins": NSNumber(value: 520), // delta = 20
"pageouts": NSNumber(value: 105), // delta = 5
"decompressions": NSNumber(value: 210), // delta = 10
"compressions": NSNumber(value: 315) // delta = 15
]
let timeDelta: TimeInterval = 2.0 // 2 seconds
let rates = MMKernelTelemetryProvider.calculateRates(
withCurrentCounters: curr,
previousCounters: prev,
timeDelta: timeDelta
)
let cswRate = rates["contextSwitchesRate"] as? Double ?? 0
let syscallRate = rates["syscallsRate"] as? Double ?? 0
let faultRate = rates["pageFaultsRate"] as? Double ?? 0
let cowRate = rates["cowFaultsRate"] as? Double ?? 0
let zeroFillRate = rates["zeroFillsRate"] as? Double ?? 0
let pageinsRate = rates["pageinsRate"] as? Double ?? 0
XCTAssertEqual(cswRate, 1250.0, accuracy: 0.1)
XCTAssertEqual(syscallRate, 5000.0, accuracy: 0.1)
XCTAssertEqual(faultRate, 500.0, accuracy: 0.1)
XCTAssertEqual(cowRate, 25.0, accuracy: 0.1)
XCTAssertEqual(zeroFillRate, 100.0, accuracy: 0.1)
XCTAssertEqual(pageinsRate, 10.0, accuracy: 0.1)
let cumCSW = rates["cumulative_contextSwitches"] as? UInt64 ?? 0
XCTAssertEqual(cumCSW, 102_500)
}
func testLiveKernelProvider() throws {
let provider = MMKernelTelemetryProvider()
XCTAssertEqual(provider.domain, .system)
XCTAssertEqual(provider.providerIdentifier, "com.i3omb.macmonitor.telemetry.kernel.counters")
let sample = try provider.sampleTelemetry()
XCTAssertNotNil(sample)
XCTAssertNotNil(sample["cumulative_contextSwitches"])
XCTAssertNotNil(sample["cumulative_syscalls"])
XCTAssertNotNil(sample["cumulative_pageFaults"])
XCTAssertNotNil(sample["contextSwitchesRate"])
XCTAssertNotNil(sample["syscallsRate"])
}
}