Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: cpu usage readings for profiling #3214

Merged
merged 8 commits into from
Aug 15, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## Unreleased

### Fixes

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

## 8.9.4

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

Check warning on line 183 in Sources/Sentry/SentryMetricProfiler.mm

View check run for this annotation

Codecov / codecov/patch

Sources/Sentry/SentryMetricProfiler.mm#L183

Added line #L183 was not covered by tests
}

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

Check warning on line 54 in Sources/Sentry/SentrySystemWrapper.mm

View check run for this annotation

Codecov / codecov/patch

Sources/Sentry/SentrySystemWrapper.mm#L54

Added line #L54 was not covered by tests
}
vm_deallocate(
mach_task_self(), reinterpret_cast<vm_address_t>(list), sizeof(*list) * count);

Check warning on line 57 in Sources/Sentry/SentrySystemWrapper.mm

View check run for this annotation

Codecov / codecov/patch

Sources/Sentry/SentrySystemWrapper.mm#L56-L57

Added lines #L56 - L57 were not covered by tests
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);

Check warning on line 72 in Sources/Sentry/SentrySystemWrapper.mm

View check run for this annotation

Codecov / codecov/patch

Sources/Sentry/SentrySystemWrapper.mm#L71-L72

Added lines #L71 - L72 were not covered by tests
}
vm_deallocate(
mach_task_self(), reinterpret_cast<vm_address_t>(list), sizeof(*list) * count);
return nil;

Check warning on line 76 in Sources/Sentry/SentrySystemWrapper.mm

View check run for this annotation

Codecov / codecov/patch

Sources/Sentry/SentrySystemWrapper.mm#L74-L76

Added lines #L74 - L76 were not covered by tests
}

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
Loading