Real-Time Disk I/O Throughput & IOPS Telemetry #10

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

Resolved in Milestone 3. Implemented MMDiskIOProvider.h/.m sampling IOKit IOBlockStorageDriver statistics, BSD child media mapping, read/write Bps and IOPS counters. Verified with unit tests. Merged into develop.

Resolved in Milestone 3. Implemented `MMDiskIOProvider.h/.m` sampling IOKit `IOBlockStorageDriver` statistics, BSD child media mapping, read/write Bps and IOPS counters. Verified with unit tests. Merged into `develop`.
Author
Owner

Technical Implementation Plan & Code Solution

1. IOKit Disk I/O Telemetry Architecture

macOS tracks block storage read/write performance in the IORegistry under IOBlockStorageDriver instances. Each driver publishes an IOBlockStorageDriverStatistics dictionary.

Key metrics exposed per driver:

  • Bytes (Read): Cumulative bytes read from disk.
  • Bytes (Write): Cumulative bytes written to disk.
  • Operations (Read): Number of read transactions (IOPS).
  • Operations (Write): Number of write transactions (IOPS).

2. Objective-C Provider (MMDiskIOProvider.m)

#import "MMDiskIOProvider.h"
#import <IOKit/IOKitLib.h>
#import <IOKit/storage/IOBlockStorageDriver.h>
#import <mach/mach_time.h>

@interface MMDiskIOProvider () {
    uint64_t _prevReadBytes;
    uint64_t _prevWriteBytes;
    uint64_t _prevReadOps;
    uint64_t _prevWriteOps;
    uint64_t _prevTimestamp;
    mach_timebase_info_data_t _timebase;
}
@end

@implementation MMDiskIOProvider

- (instancetype)init {
    self = [super init];
    if (self) {
        mach_timebase_info(&_timebase);
        _prevTimestamp = mach_absolute_time();
        [self readCurrentTotalsReadBytes:&_prevReadBytes
                              writeBytes:&_prevWriteBytes
                                 readOps:&_prevReadOps
                                writeOps:&_prevWriteOps];
    }
    return self;
}

- (void)readCurrentTotalsReadBytes:(uint64_t *)rBytes
                        writeBytes:(uint64_t *)wBytes
                           readOps:(uint64_t *)rOps
                          writeOps:(uint64_t *)wOps {
    *rBytes = 0;
    *wBytes = 0;
    *rOps = 0;
    *wOps = 0;

    CFMutableDictionaryRef matching = IOServiceMatching(kIOBlockStorageDriverClass);
    if (!matching) return;

    io_iterator_t iterator;
    kern_return_t kr = IOServiceGetMatchingServices(kIOMainPortDefault, matching, &iterator);
    if (kr != KERN_SUCCESS) return;

    io_registry_entry_t entry;
    while ((entry = IOIteratorNext(iterator)) != 0) {
        CFMutableDictionaryRef props = NULL;
        if (IORegistryEntryCreateCFProperties(entry, &props, kCFAllocatorDefault, 0) == KERN_SUCCESS && props) {
            NSDictionary *dict = (__bridge_transfer NSDictionary *)props;
            NSDictionary *stats = dict[@"Statistics"];
            if (stats) {
                *rBytes += [stats[@"Bytes (Read)"] unsignedLongLongValue];
                *wBytes += [stats[@"Bytes (Write)"] unsignedLongLongValue];
                *rOps += [stats[@"Operations (Read)"] unsignedLongLongValue];
                *wOps += [stats[@"Operations (Write)"] unsignedLongLongValue];
            }
        }
        IOObjectRelease(entry);
    }
    IOObjectRelease(iterator);
}

- (NSDictionary<NSString *, id> *)sampleTelemetryWithError:(NSError **)error {
    uint64_t curReadBytes = 0, curWriteBytes = 0, curReadOps = 0, curWriteOps = 0;
    [self readCurrentTotalsReadBytes:&curReadBytes
                          writeBytes:&curWriteBytes
                             readOps:&curReadOps
                            writeOps:&curWriteOps];

    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) return @{};

    double readBps = (curReadBytes >= _prevReadBytes) ? (curReadBytes - _prevReadBytes) / elapsedSec : 0;
    double writeBps = (curWriteBytes >= _prevWriteBytes) ? (curWriteBytes - _prevWriteBytes) / elapsedSec : 0;
    double readIops = (curReadOps >= _prevReadOps) ? (curReadOps - _prevReadOps) / elapsedSec : 0;
    double writeIops = (curWriteOps >= _prevWriteOps) ? (curWriteOps - _prevWriteOps) / elapsedSec : 0;

    _prevReadBytes = curReadBytes;
    _prevWriteBytes = curWriteBytes;
    _prevReadOps = curReadOps;
    _prevWriteOps = curWriteOps;
    _prevTimestamp = now;

    return @{
        @"readBytesPerSec": @(readBps),
        @"writeBytesPerSec": @(writeBps),
        @"readIOPS": @(readIops),
        @"writeIOPS": @(writeIops),
        @"readMBs": @(readBps / (1024.0 * 1024.0)),
        @"writeMBs": @(writeBps / (1024.0 * 1024.0))
    };
}
@end

3. Swift UI Integration

@Observable
public final class DiskIOSnapshot {
    public var readMBs: Double = 0.0
    public var writeMBs: Double = 0.0
    public var readIOPS: Double = 0.0
    public var writeIOPS: Double = 0.0

    public func update(from dict: [String: Any]) {
        self.readMBs = dict["readMBs"] as? Double ?? 0.0
        self.writeMBs = dict["writeMBs"] as? Double ?? 0.0
        self.readIOPS = dict["readIOPS"] as? Double ?? 0.0
        self.writeIOPS = dict["writeIOPS"] as? Double ?? 0.0
    }
}

4. Edge Cases & Safety

  • All IORegistry entries retrieved via IOIteratorNext() are explicitly released with IOObjectRelease().
  • Supports multi-disk configurations on Mac Pro (internal PCIe SSD + NVMe add-in card + SATA drives).
## Technical Implementation Plan & Code Solution ### 1. IOKit Disk I/O Telemetry Architecture macOS tracks block storage read/write performance in the IORegistry under `IOBlockStorageDriver` instances. Each driver publishes an `IOBlockStorageDriverStatistics` dictionary. Key metrics exposed per driver: - `Bytes (Read)`: Cumulative bytes read from disk. - `Bytes (Write)`: Cumulative bytes written to disk. - `Operations (Read)`: Number of read transactions (IOPS). - `Operations (Write)`: Number of write transactions (IOPS). --- ### 2. Objective-C Provider (`MMDiskIOProvider.m`) ```objc #import "MMDiskIOProvider.h" #import <IOKit/IOKitLib.h> #import <IOKit/storage/IOBlockStorageDriver.h> #import <mach/mach_time.h> @interface MMDiskIOProvider () { uint64_t _prevReadBytes; uint64_t _prevWriteBytes; uint64_t _prevReadOps; uint64_t _prevWriteOps; uint64_t _prevTimestamp; mach_timebase_info_data_t _timebase; } @end @implementation MMDiskIOProvider - (instancetype)init { self = [super init]; if (self) { mach_timebase_info(&_timebase); _prevTimestamp = mach_absolute_time(); [self readCurrentTotalsReadBytes:&_prevReadBytes writeBytes:&_prevWriteBytes readOps:&_prevReadOps writeOps:&_prevWriteOps]; } return self; } - (void)readCurrentTotalsReadBytes:(uint64_t *)rBytes writeBytes:(uint64_t *)wBytes readOps:(uint64_t *)rOps writeOps:(uint64_t *)wOps { *rBytes = 0; *wBytes = 0; *rOps = 0; *wOps = 0; CFMutableDictionaryRef matching = IOServiceMatching(kIOBlockStorageDriverClass); if (!matching) return; io_iterator_t iterator; kern_return_t kr = IOServiceGetMatchingServices(kIOMainPortDefault, matching, &iterator); if (kr != KERN_SUCCESS) return; io_registry_entry_t entry; while ((entry = IOIteratorNext(iterator)) != 0) { CFMutableDictionaryRef props = NULL; if (IORegistryEntryCreateCFProperties(entry, &props, kCFAllocatorDefault, 0) == KERN_SUCCESS && props) { NSDictionary *dict = (__bridge_transfer NSDictionary *)props; NSDictionary *stats = dict[@"Statistics"]; if (stats) { *rBytes += [stats[@"Bytes (Read)"] unsignedLongLongValue]; *wBytes += [stats[@"Bytes (Write)"] unsignedLongLongValue]; *rOps += [stats[@"Operations (Read)"] unsignedLongLongValue]; *wOps += [stats[@"Operations (Write)"] unsignedLongLongValue]; } } IOObjectRelease(entry); } IOObjectRelease(iterator); } - (NSDictionary<NSString *, id> *)sampleTelemetryWithError:(NSError **)error { uint64_t curReadBytes = 0, curWriteBytes = 0, curReadOps = 0, curWriteOps = 0; [self readCurrentTotalsReadBytes:&curReadBytes writeBytes:&curWriteBytes readOps:&curReadOps writeOps:&curWriteOps]; 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) return @{}; double readBps = (curReadBytes >= _prevReadBytes) ? (curReadBytes - _prevReadBytes) / elapsedSec : 0; double writeBps = (curWriteBytes >= _prevWriteBytes) ? (curWriteBytes - _prevWriteBytes) / elapsedSec : 0; double readIops = (curReadOps >= _prevReadOps) ? (curReadOps - _prevReadOps) / elapsedSec : 0; double writeIops = (curWriteOps >= _prevWriteOps) ? (curWriteOps - _prevWriteOps) / elapsedSec : 0; _prevReadBytes = curReadBytes; _prevWriteBytes = curWriteBytes; _prevReadOps = curReadOps; _prevWriteOps = curWriteOps; _prevTimestamp = now; return @{ @"readBytesPerSec": @(readBps), @"writeBytesPerSec": @(writeBps), @"readIOPS": @(readIops), @"writeIOPS": @(writeIops), @"readMBs": @(readBps / (1024.0 * 1024.0)), @"writeMBs": @(writeBps / (1024.0 * 1024.0)) }; } @end ``` --- ### 3. Swift UI Integration ```swift @Observable public final class DiskIOSnapshot { public var readMBs: Double = 0.0 public var writeMBs: Double = 0.0 public var readIOPS: Double = 0.0 public var writeIOPS: Double = 0.0 public func update(from dict: [String: Any]) { self.readMBs = dict["readMBs"] as? Double ?? 0.0 self.writeMBs = dict["writeMBs"] as? Double ?? 0.0 self.readIOPS = dict["readIOPS"] as? Double ?? 0.0 self.writeIOPS = dict["writeIOPS"] as? Double ?? 0.0 } } ``` --- ### 4. Edge Cases & Safety - All IORegistry entries retrieved via `IOIteratorNext()` are explicitly released with `IOObjectRelease()`. - Supports multi-disk configurations on Mac Pro (internal PCIe SSD + NVMe add-in card + SATA drives).
gronod added this to the M3: Advanced Kernel, Process & Peripheral Telemetry milestone 2026-09-08 10:42:22 +01:00
gronod added a new dependency 2026-09-08 11:37:05 +01:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Reference: gronod/MacMonitor#10