Real-Time Network Bandwidth & Interface Throughput #15

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

Resolved in Milestone 3. Implemented MMNetworkBandwidthProvider.h/.m querying 64-bit NET_RT_IFLIST2 sysctl data, getifaddrs IP mapping, download/upload Bps and pps. Verified with unit tests. Merged into develop.

Resolved in Milestone 3. Implemented `MMNetworkBandwidthProvider.h/.m` querying 64-bit `NET_RT_IFLIST2` sysctl data, `getifaddrs` IP mapping, download/upload Bps and pps. Verified with unit tests. Merged into `develop`.
Author
Owner

Technical Implementation Plan & Code Solution

1. BSD 64-Bit Network Interface Architecture

Standard getifaddrs returns 32-bit byte counters which overflow quickly on gigabit connections. On macOS Sonoma, 64-bit counters (struct if_data64) must be queried using sysctl with NET_RT_IFLIST2 or cast via AF_LINK interface entries.


2. Objective-C Provider (MMNetworkProvider.m)

#import "MMNetworkProvider.h"
#import <sys/sysctl.h>
#import <net/if.h>
#import <net/if_var.h>
#import <net/if_dl.h>
#import <net/route.h>
#import <mach/mach_time.h>

@interface MMNetworkInterfaceState : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) uint64_t prevInBytes;
@property (nonatomic, assign) uint64_t prevOutBytes;
@property (nonatomic, assign) double downloadBps;
@property (nonatomic, assign) double uploadBps;
@end

@implementation MMNetworkInterfaceState
@end

@interface MMNetworkProvider () {
    NSMutableDictionary<NSString *, MMNetworkInterfaceState *> *_interfaceCache;
    uint64_t _prevTimestamp;
    mach_timebase_info_data_t _timebase;
}
@end

@implementation MMNetworkProvider

- (instancetype)init {
    self = [super init];
    if (self) {
        _interfaceCache = [NSMutableDictionary dictionary];
        mach_timebase_info(&_timebase);
        _prevTimestamp = mach_absolute_time();
    }
    return self;
}

- (NSDictionary<NSString *, id> *)sampleTelemetryWithError:(NSError **)error {
    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) return @{};

    char *buf = malloc(len);
    if (!buf) return @{};

    if (sysctl(mib, 6, buf, &len, NULL, 0) < 0) {
        free(buf);
        return @{};
    }

    uint64_t now = mach_absolute_time();
    uint64_t elapsedNs = (now - _prevTimestamp) * _timebase.numer / _timebase.denom;
    double elapsedSec = (double)elapsedNs / 1_000_000_000.0;
    if (elapsedSec <= 0.05) elapsedSec = 1.0;

    char *lim = buf + len;
    char *next = buf;

    double totalDownBps = 0;
    double totalUpBps = 0;
    NSMutableArray *interfaces = [NSMutableArray array];

    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);

            char ifNameBuf[32];
            memcpy(ifNameBuf, sdl->sdl_data, sdl->sdl_nlen);
            ifNameBuf[sdl->sdl_nlen] = '\0';
            NSString *ifName = [NSString stringWithUTF8String:ifNameBuf];

            // Filter loopback
            if ([ifName hasPrefix:@"lo"]) continue;

            uint64_t inBytes = if2m->ifm_data.ifi_ibytes;
            uint64_t outBytes = if2m->ifm_data.ifi_obytes;

            MMNetworkInterfaceState *state = _interfaceCache[ifName];
            if (!state) {
                state = [[MMNetworkInterfaceState alloc] init];
                state.name = ifName;
                state.prevInBytes = inBytes;
                state.prevOutBytes = outBytes;
                _interfaceCache[ifName] = state;
                continue;
            }

            double downRate = (inBytes >= state.prevInBytes) ? (inBytes - state.prevInBytes) / elapsedSec : 0;
            double upRate = (outBytes >= state.prevOutBytes) ? (outBytes - state.prevOutBytes) / elapsedSec : 0;

            state.prevInBytes = inBytes;
            state.prevOutBytes = outBytes;
            state.downloadBps = downRate;
            state.uploadBps = upRate;

            totalDownBps += downRate;
            totalUpBps += upRate;

            [interfaces addObject:@{
                @"name": ifName,
                @"downloadBytesPerSec": @(downRate),
                @"uploadBytesPerSec": @(upRate),
                @"totalInBytes": @(inBytes),
                @"totalOutBytes": @(outBytes)
            }];
        }
    }

    free(buf);
    _prevTimestamp = now;

    return @{
        @"totalDownloadBytesPerSec": @(totalDownBps),
        @"totalUploadBytesPerSec": @(totalUpBps),
        @"interfaces": [interfaces copy]
    };
}
@end

3. Swift UI Integration

@Observable
public final class NetworkSnapshot {
    public var downloadBytesPerSec: Double = 0.0
    public var uploadBytesPerSec: Double = 0.0

    public var downloadFormatted: String {
        ByteCountFormatter.string(fromByteCount: Int64(downloadBytesPerSec), countStyle: .binary) + "/s"
    }

    public var uploadFormatted: String {
        ByteCountFormatter.string(fromByteCount: Int64(uploadBytesPerSec), countStyle: .binary) + "/s"
    }

    public func update(from dict: [String: Any]) {
        self.downloadBytesPerSec = dict["totalDownloadBytesPerSec"] as? Double ?? 0.0
        self.uploadBytesPerSec = dict["totalUploadBytesPerSec"] as? Double ?? 0.0
    }
}

4. 64-Bit Overflow Protection

Querying RTM_IFINFO2 (struct if_data64) eliminates integer rollover bugs that plague classic 32-bit BSD getifaddrs implementations.

## Technical Implementation Plan & Code Solution ### 1. BSD 64-Bit Network Interface Architecture Standard `getifaddrs` returns 32-bit byte counters which overflow quickly on gigabit connections. On macOS Sonoma, 64-bit counters (`struct if_data64`) must be queried using `sysctl` with `NET_RT_IFLIST2` or cast via `AF_LINK` interface entries. --- ### 2. Objective-C Provider (`MMNetworkProvider.m`) ```objc #import "MMNetworkProvider.h" #import <sys/sysctl.h> #import <net/if.h> #import <net/if_var.h> #import <net/if_dl.h> #import <net/route.h> #import <mach/mach_time.h> @interface MMNetworkInterfaceState : NSObject @property (nonatomic, copy) NSString *name; @property (nonatomic, assign) uint64_t prevInBytes; @property (nonatomic, assign) uint64_t prevOutBytes; @property (nonatomic, assign) double downloadBps; @property (nonatomic, assign) double uploadBps; @end @implementation MMNetworkInterfaceState @end @interface MMNetworkProvider () { NSMutableDictionary<NSString *, MMNetworkInterfaceState *> *_interfaceCache; uint64_t _prevTimestamp; mach_timebase_info_data_t _timebase; } @end @implementation MMNetworkProvider - (instancetype)init { self = [super init]; if (self) { _interfaceCache = [NSMutableDictionary dictionary]; mach_timebase_info(&_timebase); _prevTimestamp = mach_absolute_time(); } return self; } - (NSDictionary<NSString *, id> *)sampleTelemetryWithError:(NSError **)error { 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) return @{}; char *buf = malloc(len); if (!buf) return @{}; if (sysctl(mib, 6, buf, &len, NULL, 0) < 0) { free(buf); return @{}; } uint64_t now = mach_absolute_time(); uint64_t elapsedNs = (now - _prevTimestamp) * _timebase.numer / _timebase.denom; double elapsedSec = (double)elapsedNs / 1_000_000_000.0; if (elapsedSec <= 0.05) elapsedSec = 1.0; char *lim = buf + len; char *next = buf; double totalDownBps = 0; double totalUpBps = 0; NSMutableArray *interfaces = [NSMutableArray array]; 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); char ifNameBuf[32]; memcpy(ifNameBuf, sdl->sdl_data, sdl->sdl_nlen); ifNameBuf[sdl->sdl_nlen] = '\0'; NSString *ifName = [NSString stringWithUTF8String:ifNameBuf]; // Filter loopback if ([ifName hasPrefix:@"lo"]) continue; uint64_t inBytes = if2m->ifm_data.ifi_ibytes; uint64_t outBytes = if2m->ifm_data.ifi_obytes; MMNetworkInterfaceState *state = _interfaceCache[ifName]; if (!state) { state = [[MMNetworkInterfaceState alloc] init]; state.name = ifName; state.prevInBytes = inBytes; state.prevOutBytes = outBytes; _interfaceCache[ifName] = state; continue; } double downRate = (inBytes >= state.prevInBytes) ? (inBytes - state.prevInBytes) / elapsedSec : 0; double upRate = (outBytes >= state.prevOutBytes) ? (outBytes - state.prevOutBytes) / elapsedSec : 0; state.prevInBytes = inBytes; state.prevOutBytes = outBytes; state.downloadBps = downRate; state.uploadBps = upRate; totalDownBps += downRate; totalUpBps += upRate; [interfaces addObject:@{ @"name": ifName, @"downloadBytesPerSec": @(downRate), @"uploadBytesPerSec": @(upRate), @"totalInBytes": @(inBytes), @"totalOutBytes": @(outBytes) }]; } } free(buf); _prevTimestamp = now; return @{ @"totalDownloadBytesPerSec": @(totalDownBps), @"totalUploadBytesPerSec": @(totalUpBps), @"interfaces": [interfaces copy] }; } @end ``` --- ### 3. Swift UI Integration ```swift @Observable public final class NetworkSnapshot { public var downloadBytesPerSec: Double = 0.0 public var uploadBytesPerSec: Double = 0.0 public var downloadFormatted: String { ByteCountFormatter.string(fromByteCount: Int64(downloadBytesPerSec), countStyle: .binary) + "/s" } public var uploadFormatted: String { ByteCountFormatter.string(fromByteCount: Int64(uploadBytesPerSec), countStyle: .binary) + "/s" } public func update(from dict: [String: Any]) { self.downloadBytesPerSec = dict["totalDownloadBytesPerSec"] as? Double ?? 0.0 self.uploadBytesPerSec = dict["totalUploadBytesPerSec"] as? Double ?? 0.0 } } ``` --- ### 4. 64-Bit Overflow Protection Querying `RTM_IFINFO2` (`struct if_data64`) eliminates integer rollover bugs that plague classic 32-bit BSD `getifaddrs` implementations.
gronod added this to the M3: Advanced Kernel, Process & Peripheral Telemetry milestone 2026-09-08 10:42:33 +01:00
gronod added a new dependency 2026-09-08 11:37:08 +01:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Reference: gronod/MacMonitor#15