Skip to content

Commit

Permalink
fix: cpu usage readings for profiling (#3214)
Browse files Browse the repository at this point in the history
  • Loading branch information
armcknight authored Aug 15, 2023
1 parent b066509 commit 7f14650
Show file tree
Hide file tree
Showing 8 changed files with 82 additions and 74 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## Unreleased

### Fixes

- Fix CPU usage collection for upcoming visualization in profiling flamecharts (#3214)

## 8.9.5

### Hybrid SDK support
Expand Down
6 changes: 3 additions & 3 deletions SentryTestUtils/TestSentrySystemWrapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ public class TestSentrySystemWrapper: SentrySystemWrapper {
public var memoryFootprintBytes: SentryRAMBytes?

public var cpuUsageError: NSError?
public var cpuUsagePerCore: [NSNumber]?
public var cpuUsage: NSNumber?
}

public var overrides = Override()
Expand All @@ -19,10 +19,10 @@ public class TestSentrySystemWrapper: SentrySystemWrapper {
return overrides.memoryFootprintBytes ?? super.memoryFootprintBytes(error)
}

public override func cpuUsagePerCore() throws -> [NSNumber] {
public override func cpuUsage() throws -> NSNumber {
if let errorOverride = overrides.cpuUsageError {
throw errorOverride
}
return try overrides.cpuUsagePerCore ?? super.cpuUsagePerCore()
return try overrides.cpuUsage ?? super.cpuUsage()
}
}
53 changes: 18 additions & 35 deletions Sources/Sentry/SentryMetricProfiler.mm
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
# import "SentryEvent+Private.h"
# import "SentryFormatter.h"
# import "SentryLog.h"
# import "SentryNSProcessInfoWrapper.h"
# import "SentryNSTimerFactory.h"
# import "SentrySystemWrapper.h"
# import "SentryTime.h"
Expand All @@ -28,7 +27,7 @@ @implementation SentryMetricReading
@end

NSString *const kSentryMetricProfilerSerializationKeyMemoryFootprint = @"memory_footprint";
NSString *const kSentryMetricProfilerSerializationKeyCPUUsageFormat = @"cpu_usage_%d";
NSString *const kSentryMetricProfilerSerializationKeyCPUUsage = @"cpu_usage";

NSString *const kSentryMetricProfilerSerializationUnitBytes = @"byte";
NSString *const kSentryMetricProfilerSerializationUnitPercentage = @"percent";
Expand Down Expand Up @@ -78,25 +77,14 @@ @implementation SentryMetricReading
@implementation SentryMetricProfiler {
SentryDispatchSourceWrapper *_dispatchSource;

/// arrays of readings keyed on NSNumbers representing the core number for the set of readings
NSMutableDictionary<NSNumber *, NSMutableArray<SentryMetricReading *> *> *_cpuUsage;

NSMutableArray<SentryMetricReading *> *_cpuUsage;
NSMutableArray<SentryMetricReading *> *_memoryFootprint;
}

- (instancetype)init
{
if (self = [super init]) {
_cpuUsage =
[NSMutableDictionary<NSNumber *, NSMutableArray<SentryMetricReading *> *> dictionary];
const auto processorCount
= SentryDependencyContainer.sharedInstance.processInfoWrapper.processorCount;
SENTRY_LOG_DEBUG(
@"Preparing %lu arrays for CPU core usage readings", (long unsigned)processorCount);
for (NSUInteger core = 0; core < processorCount; core++) {
_cpuUsage[@(core)] = [NSMutableArray<SentryMetricReading *> array];
}

_cpuUsage = [NSMutableArray<SentryMetricReading *> array];
_memoryFootprint = [NSMutableArray<SentryMetricReading *> array];
}
return self;
Expand All @@ -123,11 +111,10 @@ - (void)stop
and:(uint64_t)endSystemTime;
{
NSArray<SentryMetricReading *> *memoryFootprint;
NSDictionary<NSNumber *, NSArray<SentryMetricReading *> *> *cpuUsage;
NSArray<SentryMetricReading *> *cpuUsage;
@synchronized(self) {
memoryFootprint = [NSArray<SentryMetricReading *> arrayWithArray:_memoryFootprint];
cpuUsage = [NSDictionary<NSNumber *, NSArray<SentryMetricReading *> *>
dictionaryWithDictionary:_cpuUsage];
cpuUsage = [NSArray<SentryMetricReading *> arrayWithArray:_cpuUsage];
}

const auto dict = [NSMutableDictionary<NSString *, id> dictionary];
Expand All @@ -137,16 +124,11 @@ - (void)stop
kSentryMetricProfilerSerializationUnitBytes, startSystemTime, endSystemTime);
}

[cpuUsage enumerateKeysAndObjectsUsingBlock:^(NSNumber *_Nonnull core,
NSArray<SentryMetricReading *> *_Nonnull readings, BOOL *_Nonnull stop) {
if (readings.count > 0) {
dict[[NSString stringWithFormat:kSentryMetricProfilerSerializationKeyCPUUsageFormat,
core.intValue]]
= serializeValuesWithNormalizedTime(readings,
kSentryMetricProfilerSerializationUnitPercentage, startSystemTime,
endSystemTime);
}
}];
if (cpuUsage.count > 0) {
dict[kSentryMetricProfilerSerializationKeyCPUUsage]
= serializeValuesWithNormalizedTime(cpuUsage,
kSentryMetricProfilerSerializationUnitPercentage, startSystemTime, endSystemTime);
}

return dict;
}
Expand All @@ -165,7 +147,7 @@ - (void)registerSampler
attributes:dispatch_queue_attr_make_with_qos_class(
DISPATCH_QUEUE_CONCURRENT, QOS_CLASS_UTILITY, 0)
eventHandler:^{
[weakSelf recordCPUPercentagePerCore];
[weakSelf recordCPUsage];
[weakSelf recordMemoryFootprint];
}];
}
Expand All @@ -186,22 +168,23 @@ - (void)recordMemoryFootprint
}
}

- (void)recordCPUPercentagePerCore
- (void)recordCPUsage
{
NSError *error;
const auto result =
[SentryDependencyContainer.sharedInstance.systemWrapper cpuUsagePerCore:&error];
[SentryDependencyContainer.sharedInstance.systemWrapper cpuUsageWithError:&error];

if (error) {
SENTRY_LOG_ERROR(@"Failed to read CPU usages: %@", error);
return;
}

if (result == nil) {
return;
}

@synchronized(self) {
[result enumerateObjectsUsingBlock:^(
NSNumber *_Nonnull usage, NSUInteger core, BOOL *_Nonnull stop) {
[_cpuUsage[@(core)] addObject:[self metricReadingForValue:usage]];
}];
[_cpuUsage addObject:[self metricReadingForValue:result]];
}
}

Expand Down
69 changes: 48 additions & 21 deletions Sources/Sentry/SentrySystemWrapper.mm
Original file line number Diff line number Diff line change
@@ -1,8 +1,22 @@
#import "SentrySystemWrapper.h"
#import "SentryDependencyContainer.h"
#import "SentryError.h"
#import "SentryNSProcessInfoWrapper.h"
#import <mach/mach.h>
#include <thread>

@implementation SentrySystemWrapper
@implementation SentrySystemWrapper {
float processorCount;
}

- (instancetype)init
{
if ((self = [super init])) {
processorCount
= (float)SentryDependencyContainer.sharedInstance.processInfoWrapper.processorCount;
}
return self;
}

- (SentryRAMBytes)memoryFootprintBytes:(NSError *__autoreleasing _Nullable *)error
{
Expand All @@ -28,35 +42,48 @@ - (SentryRAMBytes)memoryFootprintBytes:(NSError *__autoreleasing _Nullable *)err
return footprintBytes;
}

- (NSArray<NSNumber *> *)cpuUsagePerCore:(NSError **)error
- (NSNumber *)cpuUsageWithError:(NSError **)error
{
natural_t numCPUs = 0U;
processor_info_array_t cpuInfo;
mach_msg_type_number_t numCPUInfo;
const auto status = host_processor_info(
mach_host_self(), PROCESSOR_CPU_LOAD_INFO, &numCPUs, &cpuInfo, &numCPUInfo);
if (status != KERN_SUCCESS) {
mach_msg_type_number_t count;
thread_act_array_t list;

const auto taskThreadsStatus = task_threads(mach_task_self(), &list, &count);
if (taskThreadsStatus != KERN_SUCCESS) {
if (error) {
*error = NSErrorFromSentryErrorWithKernelError(
kSentryErrorKernel, @"host_processor_info reported an error.", status);
kSentryErrorKernel, @"task_threads reported an error.", taskThreadsStatus);
}
vm_deallocate(
mach_task_self(), reinterpret_cast<vm_address_t>(list), sizeof(*list) * count);
return nil;
}

NSMutableArray *result = [NSMutableArray arrayWithCapacity:numCPUs];
for (natural_t core = 0U; core < numCPUs; ++core) {
const auto indexBase = CPU_STATE_MAX * core;
const float user = cpuInfo[indexBase + CPU_STATE_USER];
const float sys = cpuInfo[indexBase + CPU_STATE_SYSTEM];
const float nice = cpuInfo[indexBase + CPU_STATE_NICE];
const float idle = cpuInfo[indexBase + CPU_STATE_IDLE];
const auto inUse = user + sys + nice;
const auto total = inUse + idle;
const auto usagePercent = inUse / total * 100.f;
[result addObject:@(usagePercent)];
auto usage = 0.f;
for (decltype(count) i = 0; i < count; i++) {
const auto thread = list[i];

mach_msg_type_number_t infoSize = THREAD_BASIC_INFO_COUNT;
thread_basic_info_data_t data;
const auto threadInfoStatus = thread_info(
thread, THREAD_BASIC_INFO, reinterpret_cast<thread_info_t>(&data), &infoSize);
if (threadInfoStatus != KERN_SUCCESS) {
if (error) {
*error = NSErrorFromSentryErrorWithKernelError(
kSentryErrorKernel, @"task_threads reported an error.", taskThreadsStatus);
}
vm_deallocate(
mach_task_self(), reinterpret_cast<vm_address_t>(list), sizeof(*list) * count);
return nil;
}

usage += data.cpu_usage / processorCount;
}

return result;
vm_deallocate(mach_task_self(), reinterpret_cast<vm_address_t>(list), sizeof(*list) * count);

NSLog(@"CPU usage: %f", usage);

return @(usage);
}

@end
2 changes: 1 addition & 1 deletion Sources/Sentry/include/SentryMetricProfiler.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
NS_ASSUME_NONNULL_BEGIN

SENTRY_EXTERN NSString *const kSentryMetricProfilerSerializationKeyMemoryFootprint;
SENTRY_EXTERN NSString *const kSentryMetricProfilerSerializationKeyCPUUsageFormat;
SENTRY_EXTERN NSString *const kSentryMetricProfilerSerializationKeyCPUUsage;

SENTRY_EXTERN NSString *const kSentryMetricProfilerSerializationUnitBytes;
SENTRY_EXTERN NSString *const kSentryMetricProfilerSerializationUnitPercentage;
Expand Down
2 changes: 1 addition & 1 deletion Sources/Sentry/include/SentrySystemWrapper.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ typedef mach_vm_size_t SentryRAMBytes;
* returned by the underlying system call, e.g. @c @[ @c <core-0-CPU-usage>, @c <core-1-CPU-usage>,
* @c ...] .
*/
- (nullable NSArray<NSNumber *> *)cpuUsagePerCore:(NSError **)error;
- (nullable NSNumber *)cpuUsageWithError:(NSError **)error;

@end

Expand Down
10 changes: 3 additions & 7 deletions Tests/SentryProfilerTests/SentryProfilerSwiftTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,7 @@ class SentryProfilerSwiftTests: XCTestCase {
SentryDependencyContainer.sharedInstance().dispatchFactory = dispatchFactory
SentryDependencyContainer.sharedInstance().timerFactory = timeoutTimerFactory

systemWrapper.overrides.cpuUsagePerCore = mockCPUUsages.map { NSNumber(value: $0) }
processInfoWrapper.overrides.processorCount = UInt(mockCPUUsages.count)
systemWrapper.overrides.cpuUsage = NSNumber(value: mockCPUUsage)
systemWrapper.overrides.memoryFootprintBytes = mockMemoryFootprint

#if !os(macOS)
Expand Down Expand Up @@ -87,7 +86,7 @@ class SentryProfilerSwiftTests: XCTestCase {

// mocking

let mockCPUUsages = [12.4, 63.5, 1.4, 4.6]
let mockCPUUsage = 66.6
let mockMemoryFootprint: SentryRAMBytes = 123_455
let mockUsageReadingsPerBatch = 2

Expand Down Expand Up @@ -597,10 +596,7 @@ private extension SentryProfilerSwiftTests {

let expectedUsageReadings = fixture.mockUsageReadingsPerBatch * metricsBatches

for (i, expectedUsage) in fixture.mockCPUUsages.enumerated() {
let key = NSString(format: kSentryMetricProfilerSerializationKeyCPUUsageFormat as NSString, i) as String
try assertMetricValue(measurements: measurements, key: key, numberOfReadings: expectedUsageReadings, expectedValue: expectedUsage, transaction: transaction)
}
try assertMetricValue(measurements: measurements, key: kSentryMetricProfilerSerializationKeyCPUUsage, numberOfReadings: expectedUsageReadings, expectedValue: fixture.mockCPUUsage, transaction: transaction)

try assertMetricValue(measurements: measurements, key: kSentryMetricProfilerSerializationKeyMemoryFootprint, numberOfReadings: expectedUsageReadings, expectedValue: fixture.mockMemoryFootprint, transaction: transaction)

Expand Down
8 changes: 2 additions & 6 deletions Tests/SentryProfilerTests/SentrySystemWrapperTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,8 @@ class SentrySystemWrapperTests: XCTestCase {

func testCPUUsageReportsData() throws {
XCTAssertNoThrow({
let cpuUsages = try self.fixture.systemWrapper.cpuUsagePerCore()
XCTAssertGreaterThan(cpuUsages.count, 0)
let range = 0.0 ... 100.0
cpuUsages.forEach {
XCTAssert(range.contains($0.doubleValue))
}
let cpuUsage = try XCTUnwrap(self.fixture.systemWrapper.cpuUsage())
XCTAssert((0.0 ... 100.0).contains(cpuUsage.doubleValue))
})
}

Expand Down

0 comments on commit 7f14650

Please sign in to comment.