diff --git a/CHANGELOG.md b/CHANGELOG.md index 217d2db7524..125652d9368 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Features + +- Gather profiling timeseries metrics for CPU usage and memory footprint, and thermal and memory pressure events (#2493) + ## 8.0.0-beta.4 This version adds a dependency on Swift. diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 1bb1b09f0e5..89e271cfcd2 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -601,6 +601,8 @@ 8419C0C428C1889D001C8259 /* SentryProfilerSwiftTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8419C0C328C1889D001C8259 /* SentryProfilerSwiftTests.swift */; }; 8453421228BE855D00C22EEC /* SentrySampleDecision.m in Sources */ = {isa = PBXBuildFile; fileRef = 8453421128BE855D00C22EEC /* SentrySampleDecision.m */; }; 8453421628BE8A9500C22EEC /* SentrySpanStatus.m in Sources */ = {isa = PBXBuildFile; fileRef = 8453421528BE8A9500C22EEC /* SentrySpanStatus.m */; }; + 8454CF8C293EAF9A006AC140 /* SentryMetricProfiler.h in Headers */ = {isa = PBXBuildFile; fileRef = 8454CF8A293EAF9A006AC140 /* SentryMetricProfiler.h */; }; + 8454CF8D293EAF9A006AC140 /* SentryMetricProfiler.mm in Sources */ = {isa = PBXBuildFile; fileRef = 8454CF8B293EAF9A006AC140 /* SentryMetricProfiler.mm */; }; 84A888FD28D9B11700C51DFD /* SentryProfiler+Test.h in Headers */ = {isa = PBXBuildFile; fileRef = 84A888FC28D9B11700C51DFD /* SentryProfiler+Test.h */; }; 84A8891C28DBD28900C51DFD /* SentryDevice.h in Headers */ = {isa = PBXBuildFile; fileRef = 84A8891A28DBD28900C51DFD /* SentryDevice.h */; }; 84A8891D28DBD28900C51DFD /* SentryDevice.mm in Sources */ = {isa = PBXBuildFile; fileRef = 84A8891B28DBD28900C51DFD /* SentryDevice.mm */; }; @@ -1412,6 +1414,8 @@ 844DA81F28246DE300E6B62E /* scripts */ = {isa = PBXFileReference; lastKnownFileType = folder; path = scripts; sourceTree = ""; }; 8453421128BE855D00C22EEC /* SentrySampleDecision.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentrySampleDecision.m; sourceTree = ""; }; 8453421528BE8A9500C22EEC /* SentrySpanStatus.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentrySpanStatus.m; sourceTree = ""; }; + 8454CF8A293EAF9A006AC140 /* SentryMetricProfiler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryMetricProfiler.h; path = Sources/Sentry/SentryMetricProfiler.h; sourceTree = SOURCE_ROOT; }; + 8454CF8B293EAF9A006AC140 /* SentryMetricProfiler.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = SentryMetricProfiler.mm; path = Sources/Sentry/SentryMetricProfiler.mm; sourceTree = SOURCE_ROOT; }; 84A888FC28D9B11700C51DFD /* SentryProfiler+Test.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "SentryProfiler+Test.h"; path = "Sources/Sentry/include/SentryProfiler+Test.h"; sourceTree = SOURCE_ROOT; }; 84A8891A28DBD28900C51DFD /* SentryDevice.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryDevice.h; path = include/SentryDevice.h; sourceTree = ""; }; 84A8891B28DBD28900C51DFD /* SentryDevice.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = SentryDevice.mm; sourceTree = ""; }; @@ -2803,6 +2807,8 @@ 03F84D1B27DD414C008FE43F /* SentryMachLogging.hpp */, 03F84D2C27DD4191008FE43F /* SentryMachLogging.cpp */, 03F84D1127DD414C008FE43F /* SentryProfiler.h */, + 8454CF8A293EAF9A006AC140 /* SentryMetricProfiler.h */, + 8454CF8B293EAF9A006AC140 /* SentryMetricProfiler.mm */, 84A888FC28D9B11700C51DFD /* SentryProfiler+Test.h */, 03F84D2B27DD4191008FE43F /* SentryProfiler.mm */, 03BCC38D27E2A377003232C7 /* SentryProfilingConditionals.h */, @@ -3098,6 +3104,7 @@ 63FE711F20DA4C1000CDBAE8 /* SentryCrashObjC.h in Headers */, 7BC3936825B1AB3E004F03D3 /* SentryLevelMapper.h in Headers */, 8E4E7C6E25DAAAFE006AB9E2 /* SentrySpan.h in Headers */, + 8454CF8C293EAF9A006AC140 /* SentryMetricProfiler.h in Headers */, D8ACE3CE2762187D00F5A213 /* SentryNSDataTracker.h in Headers */, 03F84D2427DD414C008FE43F /* SentryCompiler.h in Headers */, 631E6D331EBC679C00712345 /* SentryQueueableRequestManager.h in Headers */, @@ -3563,6 +3570,7 @@ 7BD729982463E93500EA3610 /* SentryDateUtil.m in Sources */, 639FCF9D1EBC7F9500778193 /* SentryThread.m in Sources */, 8E8C57A225EEFC07001CEEFA /* SentryTracesSampler.m in Sources */, + 8454CF8D293EAF9A006AC140 /* SentryMetricProfiler.mm in Sources */, 63FE714120DA4C1100CDBAE8 /* SentryCrashDate.c in Sources */, 63FE70DB20DA4C1000CDBAE8 /* SentryCrashMonitor_System.m in Sources */, 7BA61CBB247BC5D800C130A8 /* SentryCrashDefaultBinaryImageProvider.m in Sources */, diff --git a/Sources/Sentry/SentryMetricProfiler.h b/Sources/Sentry/SentryMetricProfiler.h new file mode 100644 index 00000000000..2f538d2443e --- /dev/null +++ b/Sources/Sentry/SentryMetricProfiler.h @@ -0,0 +1,24 @@ +#import + +@class SentryNSNotificationCenterWrapper; + +NS_ASSUME_NONNULL_BEGIN + +/** + * A profiler that gathers various time-series and event-based metrics on the app process, such as + * CPU and memory usage timeseries and thermal and memory pressure warning notifications. + */ +@interface SentryMetricProfiler : NSObject + +- (instancetype)initWithNotificationCenterWrapper: + (SentryNSNotificationCenterWrapper *)notificationCenterWrapper + profileStartTime:(uint64_t)profileStartTime; +- (void)start; +- (void)stop; + +/** @return All data gathered during the profiling run. */ +- (NSData *)serialize; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/SentryMetricProfiler.mm b/Sources/Sentry/SentryMetricProfiler.mm new file mode 100644 index 00000000000..17d83264092 --- /dev/null +++ b/Sources/Sentry/SentryMetricProfiler.mm @@ -0,0 +1,156 @@ +#import "SentryMetricProfiler.h" +#import "SentryMachLogging.hpp" +#import "SentryNSNotificationCenterWrapper.h" +#import "SentryTime.h" +#include + +const NSTimeInterval kSentryMetricProfilerInterval = 0.1; // 10 Hz + +@implementation SentryMetricProfiler { + NSTimer *_timer; + SentryNSNotificationCenterWrapper *_notificationCenter; + dispatch_source_t _memoryWarningSource; + dispatch_queue_t _memoryWarningQueue; + NSMutableArray *> *_cpuTimeSeries; + NSMutableArray *> *_memoryFootprintTimeSeries; + NSMutableArray *> *_thermalStateChanges; + NSMutableArray *> *_memoryPressureStateChanges; + uint64_t _profileStartTime; +} + +- (instancetype)initWithNotificationCenterWrapper: + (SentryNSNotificationCenterWrapper *)notificationCenterWrapper + profileStartTime:(uint64_t)profileStartTime +{ + if (self = [super init]) { + _cpuTimeSeries = [NSMutableArray *> array]; + _memoryFootprintTimeSeries = [NSMutableArray *> array]; + _thermalStateChanges = [NSMutableArray *> array]; + _memoryPressureStateChanges = + [NSMutableArray *> array]; + _notificationCenter = notificationCenterWrapper; + _profileStartTime = profileStartTime; + } + return self; +} + +- (void)dealloc +{ + [self stop]; +} + +#pragma mark - Public + +- (void)start +{ + [self registerTimeSeriesHandler]; + [self registerMemoryPressureWarningHandler]; +} + +- (void)stop +{ + [_timer invalidate]; + dispatch_source_cancel(_memoryWarningSource); + [_notificationCenter removeObserver:self name:NSProcessInfoThermalStateDidChangeNotification]; +} + +- (NSData *)serialize +{ + // TODO: implement + return [[NSData alloc] init]; +} + +#pragma mark - Private + +- (void)registerTimeSeriesHandler +{ + _timer = [NSTimer scheduledTimerWithTimeInterval:kSentryMetricProfilerInterval + repeats:YES + block:^(NSTimer *_Nonnull timer) { + [self recordCPUPercentage]; + [self recordMemoryFootprint]; + }]; +} + +/** + * This is a more fine-grained API, providing normal/warn/critical levels of memory usage, versus + * using `UIApplicationDidReceiveMemoryWarningNotification` which does not provide any additional + * information ("This notification does not contain a userInfo dictionary." from + * https://developer.apple.com/documentation/uikit/uiapplication/1622920-didreceivememorywarningnotificat). + */ +- (void)registerMemoryPressureWarningHandler +{ + const auto queueAttributes + = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_UTILITY, 0); + _memoryWarningQueue = dispatch_queue_create("io.sentry.queue.memory-warnings", queueAttributes); + _memoryWarningSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_MEMORYPRESSURE, 0, + DISPATCH_MEMORYPRESSURE_NORMAL | DISPATCH_MEMORYPRESSURE_WARN + | DISPATCH_MEMORYPRESSURE_CRITICAL, + _memoryWarningQueue); + dispatch_source_set_event_handler(_memoryWarningSource, ^{ + [self recordMemoryPressureState:dispatch_source_get_data(self->_memoryWarningSource)]; + }); + dispatch_resume(_memoryWarningSource); +} + +- (void)registerStateChangeNotifications +{ + // According to Apple docs: "To receive NSProcessInfoThermalStateDidChangeNotification, you must + // access the thermalState prior to registering for the notification." (from + // https://developer.apple.com/documentation/foundation/nsprocessinfothermalstatedidchangenotification/) + [self recordThermalState]; + + // According to Apple docs: "This notification is posted on the global dispatch queue." + [_notificationCenter addObserver:self + selector:@selector(handleThermalStateChangeNotification:) + name:NSProcessInfoThermalStateDidChangeNotification]; +} + +- (void)handleThermalStateChangeNotification:(NSNotification *)note +{ + [self recordThermalState]; +} + +- (void)recordThermalState +{ + [_thermalStateChanges + addObject:[self metricEntryForValue:@(NSProcessInfo.processInfo.thermalState)]]; +} + +- (void)recordMemoryPressureState:(uintptr_t)memoryPressureState +{ + [_memoryPressureStateChanges addObject:[self metricEntryForValue:@(memoryPressureState)]]; +} + +- (void)recordMemoryFootprint +{ + task_vm_info_data_t info; + mach_msg_type_number_t count = TASK_VM_INFO_COUNT; + if (SENTRY_PROF_LOG_KERN_RETURN( + task_info(mach_task_self(), TASK_VM_INFO, (task_info_t)&info, &count)) + == KERN_SUCCESS) { + mach_vm_size_t footprintBytes; + if (count >= TASK_VM_INFO_REV1_COUNT) { + footprintBytes = info.phys_footprint; + } else { + footprintBytes = info.resident_size; + } + + [_memoryFootprintTimeSeries addObject:[self metricEntryForValue:@(footprintBytes)]]; + } +} + +- (void)recordCPUPercentage +{ + // TODO: implement +} + +- (NSDictionary *)metricEntryForValue:(NSNumber *)value +{ + return @{ + @"value" : value, + @"elapsed_since_start_ns" : @(getDurationNs(_profileStartTime, getAbsoluteTime())) + }; +} + +@end