Files
MacMonitor/Sources/Telemetry/Network/MMNetworkBandwidthProvider.m
gronodandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> 4a8fd0e5ef fix(telemetry): realign snapshot contract — provider IDs and dictionary field keys (fixes #30, fixes #31)
Providers emitted identifiers and field keys that decodeSnapshot never
looked up, leaving CPU/kernel/load/disk sections zeroed and memory%,
storage%, network, process RSS, and fan fields decoded as zero.

Store-side decode now reads the emitted schema; providers gain the
previously-missing fields: network isUp (IFF_UP via getifaddrs), process
isStopped/isZombie (pbi_status), fan name (F{i}ID). Memory utilization is
computed as used/total in the decoder. decodeSnapshot is internal for
@testable access; new MMSnapshotContractTests cover the emitted-key
contract with synthetic dictionaries (93 tests total, 0 failures).

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-08 15:09:19 +01:00

218 lines
9.3 KiB
Objective-C

#import "MMNetworkBandwidthProvider.h"
#import <sys/sysctl.h>
#import <net/if.h>
#import <net/if_dl.h>
#import <net/route.h>
#import <ifaddrs.h>
#import <arpa/inet.h>
#import <mach/mach_time.h>
#import <os/lock.h>
@interface MMNetworkBandwidthProvider () {
os_unfair_lock _lock;
NSDictionary<NSString *, NSDictionary<NSString *, NSNumber *> *> *_previousSnapshots;
uint64_t _previousTimestamp;
mach_timebase_info_data_t _timebase;
}
@end
@implementation MMNetworkBandwidthProvider
- (instancetype)init {
self = [super init];
if (self) {
_lock = OS_UNFAIR_LOCK_INIT;
_previousSnapshots = nil;
_previousTimestamp = 0;
mach_timebase_info(&_timebase);
}
return self;
}
- (MMTelemetryDomain)domain {
return MMTelemetryDomainNetwork;
}
- (NSString *)providerIdentifier {
return @"com.i3omb.macmonitor.telemetry.network.bandwidth";
}
- (BOOL)isAvailable {
return YES;
}
+ (NSDictionary<NSString *, id> *)calculateBandwidthMetricsWithCurrentSnapshots:(NSDictionary<NSString *, NSDictionary<NSString *, NSNumber *> *> *)current
previousSnapshots:(nullable NSDictionary<NSString *, NSDictionary<NSString *, NSNumber *> *> *)previous
timeDelta:(NSTimeInterval)timeDelta
ipAddresses:(nullable NSDictionary<NSString *, NSString *> *)ipAddresses
interfaceUpFlags:(nullable NSDictionary<NSString *, NSNumber *> *)interfaceUpFlags {
double totalDownBps = 0.0;
double totalUpBps = 0.0;
double totalDownPps = 0.0;
double totalUpPps = 0.0;
NSMutableArray<NSDictionary<NSString *, id> *> *interfaces = [NSMutableArray arrayWithCapacity:current.count];
NSArray<NSString *> *sortedNames = [current.allKeys sortedArrayUsingSelector:@selector(compare:)];
NSString *primaryInterface = @"";
double maxActivity = -1.0;
for (NSString *name in sortedNames) {
// Skip loopback for aggregate statistics
BOOL isLoopback = [name hasPrefix:@"lo"];
NSDictionary<NSString *, NSNumber *> *curr = current[name];
NSDictionary<NSString *, NSNumber *> *prev = previous ? previous[name] : nil;
uint64_t currInBytes = [curr[@"inBytes"] unsignedLongLongValue];
uint64_t currOutBytes = [curr[@"outBytes"] unsignedLongLongValue];
uint64_t currInPackets = [curr[@"inPackets"] unsignedLongLongValue];
uint64_t currOutPackets = [curr[@"outPackets"] unsignedLongLongValue];
uint64_t inErrors = [curr[@"inErrors"] unsignedLongLongValue];
uint64_t outErrors = [curr[@"outErrors"] unsignedLongLongValue];
uint64_t prevInBytes = prev ? [prev[@"inBytes"] unsignedLongLongValue] : currInBytes;
uint64_t prevOutBytes = prev ? [prev[@"outBytes"] unsignedLongLongValue] : currOutBytes;
uint64_t prevInPackets = prev ? [prev[@"inPackets"] unsignedLongLongValue] : currInPackets;
uint64_t prevOutPackets = prev ? [prev[@"outPackets"] unsignedLongLongValue] : currOutPackets;
double deltaInBytes = (currInBytes >= prevInBytes) ? (double)(currInBytes - prevInBytes) : 0.0;
double deltaOutBytes = (currOutBytes >= prevOutBytes) ? (double)(currOutBytes - prevOutBytes) : 0.0;
double deltaInPackets = (currInPackets >= prevInPackets) ? (double)(currInPackets - prevInPackets) : 0.0;
double deltaOutPackets = (currOutPackets >= prevOutPackets) ? (double)(currOutPackets - prevOutPackets) : 0.0;
double downBps = (timeDelta > 0.0001) ? (deltaInBytes / timeDelta) : 0.0;
double upBps = (timeDelta > 0.0001) ? (deltaOutBytes / timeDelta) : 0.0;
double downPps = (timeDelta > 0.0001) ? (deltaInPackets / timeDelta) : 0.0;
double upPps = (timeDelta > 0.0001) ? (deltaOutPackets / timeDelta) : 0.0;
if (!isLoopback) {
totalDownBps += downBps;
totalUpBps += upBps;
totalDownPps += downPps;
totalUpPps += upPps;
double activity = downBps + upBps;
if (activity > maxActivity) {
maxActivity = activity;
primaryInterface = name;
}
}
NSString *ip = ipAddresses[name] ?: @"";
BOOL isUp = interfaceUpFlags[name] ? [interfaceUpFlags[name] boolValue] : NO;
[interfaces addObject:@{
@"name": name,
@"isUp": @(isUp),
@"downloadBytesPerSec": @(downBps),
@"uploadBytesPerSec": @(upBps),
@"downloadPacketsPerSec": @(downPps),
@"uploadPacketsPerSec": @(upPps),
@"cumulativeInBytes": @(currInBytes),
@"cumulativeOutBytes": @(currOutBytes),
@"cumulativeInPackets": @(currInPackets),
@"cumulativeOutPackets": @(currOutPackets),
@"inErrors": @(inErrors),
@"outErrors": @(outErrors),
@"ipv4Address": ip,
@"isLoopback": @(isLoopback)
}];
}
if (primaryInterface.length == 0 && current[@"en0"]) {
primaryInterface = @"en0";
}
return @{
@"interfaces": interfaces,
@"totalDownloadBytesPerSec": @(totalDownBps),
@"totalUploadBytesPerSec": @(totalUpBps),
@"totalDownloadPacketsPerSec": @(totalDownPps),
@"totalUploadPacketsPerSec": @(totalUpPps),
@"primaryInterface": primaryInterface,
@"timeDelta": @(timeDelta)
};
}
- (nullable NSDictionary<NSString *, id> *)sampleTelemetryWithError:(NSError **)error {
uint64_t now = mach_absolute_time();
// 1. Read 64-bit interface stats via NET_RT_IFLIST2
NSMutableDictionary<NSString *, NSDictionary<NSString *, NSNumber *> *> *currentSnapshots = [NSMutableDictionary dictionary];
int mib[] = { CTL_NET, PF_ROUTE, 0, 0, NET_RT_IFLIST2, 0 };
size_t len = 0;
if (sysctl(mib, 6, NULL, &len, NULL, 0) == 0 && len > 0) {
char *buf = malloc(len);
if (buf && sysctl(mib, 6, buf, &len, NULL, 0) == 0) {
char *next = buf;
char *lim = buf + len;
while (next < lim) {
struct if_msghdr *ifm = (struct if_msghdr *)next;
next += ifm->ifm_msglen;
if (ifm->ifm_type == RTM_IFINFO2) {
struct if_msghdr2 *if2m = (struct if_msghdr2 *)ifm;
struct sockaddr_dl *sdl = (struct sockaddr_dl *)(if2m + 1);
if (sdl->sdl_nlen > 0 && sdl->sdl_nlen < 32) {
char nameBuf[33] = {0};
memcpy(nameBuf, sdl->sdl_data, sdl->sdl_nlen);
NSString *name = [NSString stringWithUTF8String:nameBuf];
currentSnapshots[name] = @{
@"inBytes": @(if2m->ifm_data.ifi_ibytes),
@"outBytes": @(if2m->ifm_data.ifi_obytes),
@"inPackets": @(if2m->ifm_data.ifi_ipackets),
@"outPackets": @(if2m->ifm_data.ifi_opackets),
@"inErrors": @(if2m->ifm_data.ifi_ierrors),
@"outErrors": @(if2m->ifm_data.ifi_oerrors)
};
}
}
}
}
if (buf) free(buf);
}
// 2. Query IPv4 addresses and interface flags via getifaddrs
NSMutableDictionary<NSString *, NSString *> *ipDict = [NSMutableDictionary dictionary];
NSMutableDictionary<NSString *, NSNumber *> *upDict = [NSMutableDictionary dictionary];
struct ifaddrs *ifap = NULL;
if (getifaddrs(&ifap) == 0) {
for (struct ifaddrs *ifa = ifap; ifa != NULL; ifa = ifa->ifa_next) {
NSString *name = [NSString stringWithUTF8String:ifa->ifa_name];
upDict[name] = @((ifa->ifa_flags & IFF_UP) != 0);
if (ifa->ifa_addr && ifa->ifa_addr->sa_family == AF_INET) {
char ipBuf[INET_ADDRSTRLEN] = {0};
struct sockaddr_in *sin = (struct sockaddr_in *)ifa->ifa_addr;
if (inet_ntop(AF_INET, &sin->sin_addr, ipBuf, sizeof(ipBuf))) {
ipDict[name] = [NSString stringWithUTF8String:ipBuf];
}
}
}
freeifaddrs(ifap);
}
os_unfair_lock_lock(&_lock);
NSDictionary<NSString *, NSDictionary<NSString *, NSNumber *> *> *prev = _previousSnapshots;
uint64_t prevTime = _previousTimestamp;
_previousSnapshots = currentSnapshots;
_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 [MMNetworkBandwidthProvider calculateBandwidthMetricsWithCurrentSnapshots:currentSnapshots
previousSnapshots:prev
timeDelta:timeDelta
ipAddresses:ipDict
interfaceUpFlags:upDict];
}
@end