Files
MacMonitor/Sources/Telemetry/Process/MMProcessTelemetryProvider.m
gronod 72ed8256c8
MacMonitor CI/CD Pipeline / Build & Test (Intel x86_64) (push) Canceled after 0s
feat(telemetry): implement live process explorer and resource attribution provider (fixes #11)
2026-09-08 12:39:23 +01:00

161 lines
5.7 KiB
Objective-C

#import "MMProcessTelemetryProvider.h"
#import <libproc.h>
#import <sys/proc_info.h>
#import <pwd.h>
#import <signal.h>
#import <mach/mach_time.h>
#import <os/lock.h>
@interface MMProcessTelemetryProvider () {
os_unfair_lock _lock;
NSMutableDictionary<NSNumber *, NSNumber *> *_previousCPUTimes;
uint64_t _previousTimestamp;
mach_timebase_info_data_t _timebase;
NSMutableDictionary<NSNumber *, NSString *> *_usernameCache;
}
@end
@implementation MMProcessTelemetryProvider
- (instancetype)init {
self = [super init];
if (self) {
_lock = OS_UNFAIR_LOCK_INIT;
_previousCPUTimes = [NSMutableDictionary dictionary];
_previousTimestamp = 0;
_usernameCache = [NSMutableDictionary dictionary];
mach_timebase_info(&_timebase);
}
return self;
}
- (MMTelemetryDomain)domain {
return MMTelemetryDomainProcess;
}
- (NSString *)providerIdentifier {
return @"com.i3omb.macmonitor.telemetry.process";
}
- (BOOL)isAvailable {
return YES;
}
+ (BOOL)terminateProcessWithPID:(pid_t)pid force:(BOOL)force {
int sig = force ? SIGKILL : SIGTERM;
return kill(pid, sig) == 0;
}
+ (NSArray<NSDictionary<NSString *, id> *> *)calculateProcessListWithRawProcesses:(NSArray<NSDictionary<NSString *, id> *> *)rawProcesses
previousCPUTimes:(nullable NSDictionary<NSNumber *, NSNumber *> *)previousCPUTimes
timeDelta:(NSTimeInterval)timeDelta {
NSMutableArray<NSDictionary<NSString *, id> *> *processed = [NSMutableArray arrayWithCapacity:rawProcesses.count];
for (NSDictionary<NSString *, id> *raw in rawProcesses) {
NSNumber *pidNum = raw[@"pid"];
uint64_t currCPUTime = [raw[@"cpuTimeNs"] unsignedLongLongValue];
double cpuPercent = 0.0;
if (previousCPUTimes && previousCPUTimes[pidNum]) {
uint64_t prevCPUTime = [previousCPUTimes[pidNum] unsignedLongLongValue];
if (currCPUTime >= prevCPUTime && timeDelta > 0.001) {
uint64_t deltaNs = currCPUTime - prevCPUTime;
double deltaSec = (double)deltaNs / 1e9;
cpuPercent = (deltaSec / timeDelta) * 100.0;
}
}
NSMutableDictionary<NSString *, id> *entry = [raw mutableCopy];
entry[@"cpuPercent"] = @(cpuPercent);
[processed addObject:entry];
}
// Sort descending by cpuPercent
[processed sortUsingComparator:^NSComparisonResult(NSDictionary<NSString *, id> *obj1, NSDictionary<NSString *, id> *obj2) {
NSNumber *c1 = obj1[@"cpuPercent"];
NSNumber *c2 = obj2[@"cpuPercent"];
return [c2 compare:c1];
}];
return processed;
}
- (nullable NSDictionary<NSString *, id> *)sampleTelemetryWithError:(NSError **)error {
uint64_t now = mach_absolute_time();
int pids[4096];
int bytes = proc_listpids(PROC_ALL_PIDS, 0, pids, sizeof(pids));
int pidCount = bytes / sizeof(int);
NSMutableArray<NSDictionary<NSString *, id> *> *rawList = [NSMutableArray arrayWithCapacity:pidCount];
NSMutableDictionary<NSNumber *, NSNumber *> *currentCPUTimes = [NSMutableDictionary dictionaryWithCapacity:pidCount];
NSUInteger totalThreads = 0;
for (int i = 0; i < pidCount; i++) {
pid_t pid = pids[i];
if (pid <= 0) continue;
struct proc_taskallinfo tai;
if (proc_pidinfo(pid, PROC_PIDTASKALLINFO, 0, &tai, sizeof(tai)) != sizeof(tai)) {
continue;
}
uint64_t cpuTimeNs = tai.ptinfo.pti_total_user + tai.ptinfo.pti_total_system;
currentCPUTimes[@(pid)] = @(cpuTimeNs);
totalThreads += tai.ptinfo.pti_threadnum;
// Resolve username
NSNumber *uidNum = @(tai.pbsd.pbi_uid);
NSString *username = _usernameCache[uidNum];
if (!username) {
struct passwd *pw = getpwuid(tai.pbsd.pbi_uid);
username = pw ? [NSString stringWithUTF8String:pw->pw_name] : [uidNum stringValue];
_usernameCache[uidNum] = username;
}
NSString *pname = [NSString stringWithUTF8String:tai.pbsd.pbi_name];
[rawList addObject:@{
@"pid": @(pid),
@"ppid": @(tai.pbsd.pbi_ppid),
@"uid": uidNum,
@"username": username,
@"name": pname,
@"cpuTimeNs": @(cpuTimeNs),
@"residentBytes": @(tai.ptinfo.pti_resident_size),
@"virtualBytes": @(tai.ptinfo.pti_virtual_size),
@"threadCount": @(tai.ptinfo.pti_threadnum),
@"runningThreads": @(tai.ptinfo.pti_numrunning)
}];
}
os_unfair_lock_lock(&_lock);
NSDictionary<NSNumber *, NSNumber *> *prev = [_previousCPUTimes copy];
uint64_t prevTime = _previousTimestamp;
_previousCPUTimes = currentCPUTimes;
_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;
}
NSArray<NSDictionary<NSString *, id> *> *processed = [MMProcessTelemetryProvider calculateProcessListWithRawProcesses:rawList
previousCPUTimes:prev
timeDelta:timeDelta];
return @{
@"processCount": @(processed.count),
@"totalThreads": @(totalThreads),
@"processes": processed,
@"timeDelta": @(timeDelta)
};
}
@end