Live Process Explorer & Resource Attribution Table #11

Closed
opened 2026-09-08 10:28:45 +01:00 by gronod · 1 comment
Owner

Resolved in Milestone 3. Implemented MMProcessTelemetryProvider.h/.m sampling all running processes via proc_listpids and PROC_PIDTASKALLINFO, computing CPU attribution %, resident and virtual memory, thread count, username resolution, and process termination. Verified with unit tests. Merged into develop.

Resolved in Milestone 3. Implemented `MMProcessTelemetryProvider.h/.m` sampling all running processes via `proc_listpids` and `PROC_PIDTASKALLINFO`, computing CPU attribution %, resident and virtual memory, thread count, username resolution, and process termination. Verified with unit tests. Merged into `develop`.
Author
Owner

Technical Implementation Plan & Code Solution

1. High-Performance Process Collection Architecture

Enumerating hundreds of processes at 1–2 Hz requires minimal allocations and caching of static attributes (process name, icon, path). Dynamic attributes (CPU ticks, memory RSS, thread count) are updated continuously.

Key libproc structures used:

  • proc_listpids(PROC_ALL_PIDS, 0, ...)
  • proc_pidinfo(pid, PROC_PIDTASKINFO, ...) with struct proc_taskinfo
  • proc_pidpath(pid, ...)

2. Objective-C Provider (MMProcessCollector.m)

#import "MMProcessCollector.h"
#import <libproc.h>
#import <sys/sysctl.h>
#import <AppKit/AppKit.h>
#import <mach/mach_time.h>

@interface MMProcessInfo : NSObject
@property (nonatomic, assign) pid_t pid;
@property (nonatomic, assign) pid_t ppid;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *path;
@property (nonatomic, copy) NSString *username;
@property (nonatomic, strong) NSImage *icon;
@property (nonatomic, assign) uint64_t prevCPUTimeNs;
@property (nonatomic, assign) uint64_t prevTimestamp;
@property (nonatomic, assign) double cpuPercent;
@property (nonatomic, assign) uint64_t residentBytes;
@property (nonatomic, assign) uint32_t threadCount;
@end

@implementation MMProcessInfo
@end

@interface MMProcessCollector () {
    NSMutableDictionary<NSNumber *, MMProcessInfo *> *_processCache;
    mach_timebase_info_data_t _timebase;
    NSInteger _coreCount;
}
@end

@implementation MMProcessCollector

- (instancetype)init {
    self = [super init];
    if (self) {
        _processCache = [NSMutableDictionary dictionary];
        mach_timebase_info(&_timebase);
        _coreCount = [[NSProcessInfo processInfo] activeProcessorCount];
    }
    return self;
}

- (NSArray<NSDictionary *> *)sampleProcesses {
    int byteSize = proc_listpids(PROC_ALL_PIDS, 0, NULL, 0);
    if (byteSize <= 0) return @[];

    int pidCount = byteSize / sizeof(pid_t);
    pid_t *pids = malloc(byteSize);
    if (!pids) return @[];

    proc_listpids(PROC_ALL_PIDS, 0, pids, byteSize);

    uint64_t now = mach_absolute_time();
    NSMutableSet<NSNumber *> *activePids = [NSMutableSet setWithCapacity:pidCount];
    NSMutableArray<NSDictionary *> *results = [NSMutableArray arrayWithCapacity:pidCount];

    for (int i = 0; i < pidCount; i++) {
        pid_t pid = pids[i];
        if (pid <= 0) continue;

        NSNumber *pidKey = @(pid);
        [activePids addObject:pidKey];

        struct proc_taskinfo taskInfo;
        int ret = proc_pidinfo(pid, PROC_PIDTASKINFO, 0, &taskInfo, sizeof(taskInfo));
        if (ret != sizeof(taskInfo)) continue;

        MMProcessInfo *proc = _processCache[pidKey];
        if (!proc) {
            proc = [[MMProcessInfo alloc] init];
            proc.pid = pid;

            char pathBuffer[PROC_PIDPATHINFO_MAXSIZE];
            if (proc_pidpath(pid, pathBuffer, sizeof(pathBuffer)) > 0) {
                proc.path = [NSString stringWithUTF8String:pathBuffer];
                proc.name = [proc.path lastPathComponent];
            } else {
                char nameBuffer[256];
                proc_name(pid, nameBuffer, sizeof(nameBuffer));
                proc.name = [NSString stringWithUTF8String:nameBuffer];
                proc.path = @"";
            }
            _processCache[pidKey] = proc;
        }

        uint64_t totalCpuTime = taskInfo.pti_total_user + taskInfo.pti_total_system; // nanoseconds

        if (proc.prevTimestamp > 0 && now > proc.prevTimestamp) {
            uint64_t elapsedNs = (now - proc.prevTimestamp) * _timebase.numer / _timebase.denom;
            if (elapsedNs > 0 && totalCpuTime >= proc.prevCPUTimeNs) {
                uint64_t deltaCpu = totalCpuTime - proc.prevCPUTimeNs;
                proc.cpuPercent = ((double)deltaCpu / (double)elapsedNs) * 100.0;
            }
        }

        proc.prevCPUTimeNs = totalCpuTime;
        proc.prevTimestamp = now;
        proc.residentBytes = taskInfo.pti_resident_size;
        proc.threadCount = taskInfo.pti_threadnum;

        [results addObject:@{
            @"pid": @(proc.pid),
            @"name": proc.name ?: @"Unknown",
            @"path": proc.path ?: @"",
            @"cpuPercent": @(proc.cpuPercent),
            @"residentBytes": @(proc.residentBytes),
            @"threadCount": @(proc.threadCount)
        }];
    }

    free(pids);

    // Prune dead PIDs from cache
    NSMutableSet *cachedKeys = [NSMutableSet setWithArray:_processCache.allKeys];
    [cachedKeys minusSet:activePids];
    [_processCache removeObjectsForKeys:cachedKeys.allObjects];

    return [results copy];
}

- (BOOL)terminateProcess:(pid_t)pid force:(BOOL)force {
    int sig = force ? SIGKILL : SIGTERM;
    return (kill(pid, sig) == 0);
}
@end

3. Swift UI Process Table Integration

public struct ProcessItem: Identifiable, Sendable {
    public var id: Int { pid }
    public let pid: Int
    public let name: String
    public let path: String
    public let cpuPercent: Double
    public let residentBytes: UInt64
    public let threadCount: Int
}

4. Performance Optimizations

  • Caching static metadata (name, path) in _processCache avoids calling proc_pidpath repeatedly on every tick.
  • Dynamic dead-process pruning prevents memory bloat from short-lived child processes.
## Technical Implementation Plan & Code Solution ### 1. High-Performance Process Collection Architecture Enumerating hundreds of processes at 1–2 Hz requires minimal allocations and caching of static attributes (process name, icon, path). Dynamic attributes (CPU ticks, memory RSS, thread count) are updated continuously. Key `libproc` structures used: - `proc_listpids(PROC_ALL_PIDS, 0, ...)` - `proc_pidinfo(pid, PROC_PIDTASKINFO, ...)` with `struct proc_taskinfo` - `proc_pidpath(pid, ...)` --- ### 2. Objective-C Provider (`MMProcessCollector.m`) ```objc #import "MMProcessCollector.h" #import <libproc.h> #import <sys/sysctl.h> #import <AppKit/AppKit.h> #import <mach/mach_time.h> @interface MMProcessInfo : NSObject @property (nonatomic, assign) pid_t pid; @property (nonatomic, assign) pid_t ppid; @property (nonatomic, copy) NSString *name; @property (nonatomic, copy) NSString *path; @property (nonatomic, copy) NSString *username; @property (nonatomic, strong) NSImage *icon; @property (nonatomic, assign) uint64_t prevCPUTimeNs; @property (nonatomic, assign) uint64_t prevTimestamp; @property (nonatomic, assign) double cpuPercent; @property (nonatomic, assign) uint64_t residentBytes; @property (nonatomic, assign) uint32_t threadCount; @end @implementation MMProcessInfo @end @interface MMProcessCollector () { NSMutableDictionary<NSNumber *, MMProcessInfo *> *_processCache; mach_timebase_info_data_t _timebase; NSInteger _coreCount; } @end @implementation MMProcessCollector - (instancetype)init { self = [super init]; if (self) { _processCache = [NSMutableDictionary dictionary]; mach_timebase_info(&_timebase); _coreCount = [[NSProcessInfo processInfo] activeProcessorCount]; } return self; } - (NSArray<NSDictionary *> *)sampleProcesses { int byteSize = proc_listpids(PROC_ALL_PIDS, 0, NULL, 0); if (byteSize <= 0) return @[]; int pidCount = byteSize / sizeof(pid_t); pid_t *pids = malloc(byteSize); if (!pids) return @[]; proc_listpids(PROC_ALL_PIDS, 0, pids, byteSize); uint64_t now = mach_absolute_time(); NSMutableSet<NSNumber *> *activePids = [NSMutableSet setWithCapacity:pidCount]; NSMutableArray<NSDictionary *> *results = [NSMutableArray arrayWithCapacity:pidCount]; for (int i = 0; i < pidCount; i++) { pid_t pid = pids[i]; if (pid <= 0) continue; NSNumber *pidKey = @(pid); [activePids addObject:pidKey]; struct proc_taskinfo taskInfo; int ret = proc_pidinfo(pid, PROC_PIDTASKINFO, 0, &taskInfo, sizeof(taskInfo)); if (ret != sizeof(taskInfo)) continue; MMProcessInfo *proc = _processCache[pidKey]; if (!proc) { proc = [[MMProcessInfo alloc] init]; proc.pid = pid; char pathBuffer[PROC_PIDPATHINFO_MAXSIZE]; if (proc_pidpath(pid, pathBuffer, sizeof(pathBuffer)) > 0) { proc.path = [NSString stringWithUTF8String:pathBuffer]; proc.name = [proc.path lastPathComponent]; } else { char nameBuffer[256]; proc_name(pid, nameBuffer, sizeof(nameBuffer)); proc.name = [NSString stringWithUTF8String:nameBuffer]; proc.path = @""; } _processCache[pidKey] = proc; } uint64_t totalCpuTime = taskInfo.pti_total_user + taskInfo.pti_total_system; // nanoseconds if (proc.prevTimestamp > 0 && now > proc.prevTimestamp) { uint64_t elapsedNs = (now - proc.prevTimestamp) * _timebase.numer / _timebase.denom; if (elapsedNs > 0 && totalCpuTime >= proc.prevCPUTimeNs) { uint64_t deltaCpu = totalCpuTime - proc.prevCPUTimeNs; proc.cpuPercent = ((double)deltaCpu / (double)elapsedNs) * 100.0; } } proc.prevCPUTimeNs = totalCpuTime; proc.prevTimestamp = now; proc.residentBytes = taskInfo.pti_resident_size; proc.threadCount = taskInfo.pti_threadnum; [results addObject:@{ @"pid": @(proc.pid), @"name": proc.name ?: @"Unknown", @"path": proc.path ?: @"", @"cpuPercent": @(proc.cpuPercent), @"residentBytes": @(proc.residentBytes), @"threadCount": @(proc.threadCount) }]; } free(pids); // Prune dead PIDs from cache NSMutableSet *cachedKeys = [NSMutableSet setWithArray:_processCache.allKeys]; [cachedKeys minusSet:activePids]; [_processCache removeObjectsForKeys:cachedKeys.allObjects]; return [results copy]; } - (BOOL)terminateProcess:(pid_t)pid force:(BOOL)force { int sig = force ? SIGKILL : SIGTERM; return (kill(pid, sig) == 0); } @end ``` --- ### 3. Swift UI Process Table Integration ```swift public struct ProcessItem: Identifiable, Sendable { public var id: Int { pid } public let pid: Int public let name: String public let path: String public let cpuPercent: Double public let residentBytes: UInt64 public let threadCount: Int } ``` --- ### 4. Performance Optimizations - Caching static metadata (`name`, `path`) in `_processCache` avoids calling `proc_pidpath` repeatedly on every tick. - Dynamic dead-process pruning prevents memory bloat from short-lived child processes.
gronod added this to the M3: Advanced Kernel, Process & Peripheral Telemetry milestone 2026-09-08 10:42:25 +01:00
gronod added a new dependency 2026-09-08 11:37:06 +01:00
gronod added a new dependency 2026-09-08 11:37:07 +01:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Reference: gronod/MacMonitor#11