Skip to content

Commit 0032a5d

Browse files
Report pre-warmed app starts (#1969)
Report pre-warmed app starts by dropping the first app start spans if pre-warming paused during these steps. This approach will shorten the app start duration, but it represents the duration a user has to wait after clicking the app icon until the app is responsive. We report the app start type in the appContext, so Sentry can make changes to the UI for prewarmed app starts. Fixes GH-1897
1 parent e2a3f3e commit 0032a5d

24 files changed

+514
-122
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## Unreleased
44

5+
### Features
6+
7+
- Report pre-warmed app starts (#1969)
8+
59
### Fixes
610

711
- Too long flush duration (#2370)

Samples/iOS-Swift/iOS-Swift/AppDelegate.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
3636
// the benchmark test starts and stops a custom transaction using a UIButton, and automatic user interaction tracing stops the transaction that begins with that button press after the idle timeout elapses, stopping the profiler (only one profiler runs regardless of the number of concurrent transactions)
3737
options.enableUserInteractionTracing = !isBenchmarking
3838
options.enableAutoPerformanceTracking = !isBenchmarking
39+
options.enablePreWarmedAppStartTracking = !isBenchmarking
3940

4041
// because we run CPU for 15 seconds at full throttle, we trigger ANR issues being sent. disable such during benchmarks.
4142
options.enableAppHangTracking = !isBenchmarking

Sentry.xcodeproj/project.pbxproj

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,9 @@
296296
7B0A542E2521C62400A71716 /* SentryFrameRemoverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0A542D2521C62400A71716 /* SentryFrameRemoverTests.swift */; };
297297
7B0A5452252311CE00A71716 /* SentryBreadcrumbTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0A5451252311CE00A71716 /* SentryBreadcrumbTests.swift */; };
298298
7B0A54562523178700A71716 /* SentryScopeSwiftTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0A54552523178700A71716 /* SentryScopeSwiftTests.swift */; };
299+
7B0DC72F288698F70039995F /* NSMutableDictionary+Sentry.h in Headers */ = {isa = PBXBuildFile; fileRef = 7B0DC72D288698F70039995F /* NSMutableDictionary+Sentry.h */; };
300+
7B0DC730288698F70039995F /* NSMutableDictionary+Sentry.m in Sources */ = {isa = PBXBuildFile; fileRef = 7B0DC72E288698F70039995F /* NSMutableDictionary+Sentry.m */; };
301+
7B0DC73428869BF40039995F /* NSMutableDictionarySentryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0DC73328869BF40039995F /* NSMutableDictionarySentryTests.swift */; };
299302
7B127B0D27CF6F2300A71ED2 /* SentryANRTrackingIntegration.h in Headers */ = {isa = PBXBuildFile; fileRef = 7B127B0C27CF6F2300A71ED2 /* SentryANRTrackingIntegration.h */; };
300303
7B127B0F27CF6F4700A71ED2 /* SentryANRTrackingIntegration.m in Sources */ = {isa = PBXBuildFile; fileRef = 7B127B0E27CF6F4700A71ED2 /* SentryANRTrackingIntegration.m */; };
301304
7B14089624878F090035403D /* SentryCrashStackEntryMapper.h in Headers */ = {isa = PBXBuildFile; fileRef = 7B14089524878F090035403D /* SentryCrashStackEntryMapper.h */; };
@@ -1034,6 +1037,9 @@
10341037
7B0A542D2521C62400A71716 /* SentryFrameRemoverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryFrameRemoverTests.swift; sourceTree = "<group>"; };
10351038
7B0A5451252311CE00A71716 /* SentryBreadcrumbTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryBreadcrumbTests.swift; sourceTree = "<group>"; };
10361039
7B0A54552523178700A71716 /* SentryScopeSwiftTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryScopeSwiftTests.swift; sourceTree = "<group>"; };
1040+
7B0DC72D288698F70039995F /* NSMutableDictionary+Sentry.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "NSMutableDictionary+Sentry.h"; path = "include/NSMutableDictionary+Sentry.h"; sourceTree = "<group>"; };
1041+
7B0DC72E288698F70039995F /* NSMutableDictionary+Sentry.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSMutableDictionary+Sentry.m"; sourceTree = "<group>"; };
1042+
7B0DC73328869BF40039995F /* NSMutableDictionarySentryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSMutableDictionarySentryTests.swift; sourceTree = "<group>"; };
10371043
7B127B0C27CF6F2300A71ED2 /* SentryANRTrackingIntegration.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryANRTrackingIntegration.h; path = include/SentryANRTrackingIntegration.h; sourceTree = "<group>"; };
10381044
7B127B0E27CF6F4700A71ED2 /* SentryANRTrackingIntegration.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryANRTrackingIntegration.m; sourceTree = "<group>"; };
10391045
7B14089524878F090035403D /* SentryCrashStackEntryMapper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryCrashStackEntryMapper.h; path = include/SentryCrashStackEntryMapper.h; sourceTree = "<group>"; };
@@ -1676,6 +1682,8 @@
16761682
63BE856F1ECEC6DE00DC44F5 /* NSDate+SentryExtras.m */,
16771683
63295AF31EF3C7DB002D4490 /* NSDictionary+SentrySanitize.h */,
16781684
63295AF41EF3C7DB002D4490 /* NSDictionary+SentrySanitize.m */,
1685+
7B0DC72D288698F70039995F /* NSMutableDictionary+Sentry.h */,
1686+
7B0DC72E288698F70039995F /* NSMutableDictionary+Sentry.m */,
16791687
861265F72404EC1500C4AFDE /* NSArray+SentrySanitize.h */,
16801688
861265F82404EC1500C4AFDE /* NSArray+SentrySanitize.m */,
16811689
7B6438A826A70F24000D0F65 /* UIViewController+Sentry.h */,
@@ -2327,6 +2335,7 @@
23272335
isa = PBXGroup;
23282336
children = (
23292337
7B6438A626A70DDB000D0F65 /* UIViewControllerSentryTests.swift */,
2338+
7B0DC73328869BF40039995F /* NSMutableDictionarySentryTests.swift */,
23302339
0A6EEADC28A657970076B469 /* UIViewRecursiveDescriptionTests.swift */,
23312340
);
23322341
path = Categories;
@@ -3118,6 +3127,7 @@
31183127
D867063F27C3BC2400048851 /* SentryCoreDataTracker.h in Headers */,
31193128
7B9657252683104C00C66E25 /* NSData+Sentry.h in Headers */,
31203129
7B6C5EDA264E8D860010D138 /* SentryFramesTrackingIntegration.h in Headers */,
3130+
7B0DC72F288698F70039995F /* NSMutableDictionary+Sentry.h in Headers */,
31213131
63FE713920DA4C1100CDBAE8 /* SentryCrashMach.h in Headers */,
31223132
63EED6BE2237923600E02400 /* SentryOptions.h in Headers */,
31233133
7BD86EC5264A63F6005439DB /* SentrySysctl.h in Headers */,
@@ -3312,6 +3322,7 @@
33123322
7B3398652459C15200BD9C96 /* SentryEnvelopeRateLimit.m in Sources */,
33133323
0A2D8D9628997845008720F6 /* NSLocale+Sentry.m in Sources */,
33143324
A2475E1F25FB648B007D9080 /* fishhook.c in Sources */,
3325+
7B0DC730288698F70039995F /* NSMutableDictionary+Sentry.m in Sources */,
33153326
7BD4BD4527EB29F50071F4FF /* SentryClientReport.m in Sources */,
33163327
631E6D341EBC679C00712345 /* SentryQueueableRequestManager.m in Sources */,
33173328
7B8713B426415BAA006D6004 /* SentryAppStartTracker.m in Sources */,
@@ -3733,6 +3744,7 @@
37333744
7B965728268321CD00C66E25 /* SentryCrashScopeObserverTests.swift in Sources */,
37343745
7BD86ECB264A6DB5005439DB /* TestSysctl.swift in Sources */,
37353746
035E73CA27D57398005EEB11 /* SentryThreadHandleTests.mm in Sources */,
3747+
7B0DC73428869BF40039995F /* NSMutableDictionarySentryTests.swift in Sources */,
37363748
7B6ADFCF26A02CAE0076C206 /* SentryCrashReportTests.swift in Sources */,
37373749
D8B76B062808066D000A58C4 /* SentryScreenshotIntegrationTests.swift in Sources */,
37383750
7B8CA85726DD4E6200DD872C /* SentryNetworkTrackerIntegrationTests.swift in Sources */,
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#import "NSMutableDictionary+Sentry.h"
2+
3+
@implementation
4+
NSMutableDictionary (Sentry)
5+
6+
- (void)mergeEntriesFromDictionary:(NSDictionary *)otherDictionary
7+
{
8+
[otherDictionary enumerateKeysAndObjectsUsingBlock:^(id otherKey, id otherObj, BOOL *stop) {
9+
if ([otherObj isKindOfClass:NSDictionary.class] &&
10+
[self[otherKey] isKindOfClass:NSDictionary.class]) {
11+
NSMutableDictionary *mergedDict = ((NSDictionary *)self[otherKey]).mutableCopy;
12+
[mergedDict mergeEntriesFromDictionary:(NSDictionary *)otherObj];
13+
self[otherKey] = mergedDict;
14+
return;
15+
}
16+
17+
self[otherKey] = otherObj;
18+
}];
19+
}
20+
21+
@end

Sources/Sentry/Public/SentryAppStartMeasurement.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ SENTRY_NO_INIT
2727
* Initializes SentryAppStartMeasurement with the given parameters.
2828
*/
2929
- (instancetype)initWithType:(SentryAppStartType)type
30+
isPreWarmed:(BOOL)isPreWarmed
3031
appStartTimestamp:(NSDate *)appStartTimestamp
3132
duration:(NSTimeInterval)duration
3233
runtimeInitTimestamp:(NSDate *)runtimeInitTimestamp
@@ -38,14 +39,17 @@ SENTRY_NO_INIT
3839
*/
3940
@property (readonly, nonatomic, assign) SentryAppStartType type;
4041

42+
@property (readonly, nonatomic, assign) BOOL isPreWarmed;
43+
4144
/**
4245
* How long the app start took. From appStartTimestamp to when the SDK creates the
4346
* AppStartMeasurement, which is done when the OS posts UIWindowDidBecomeVisibleNotification.
4447
*/
4548
@property (readonly, nonatomic, assign) NSTimeInterval duration;
4649

4750
/**
48-
* The timestamp when the app started, which is the process start timestamp.
51+
* The timestamp when the app started, which is the process start timestamp and for prewarmed app
52+
* starts the moduleInitializationTimestamp.
4953
*/
5054
@property (readonly, nonatomic, strong) NSDate *appStartTimestamp;
5155

Sources/Sentry/Public/SentryOptions.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,20 @@ NS_SWIFT_NAME(Options)
240240
*/
241241
@property (nonatomic, assign) NSTimeInterval idleTimeout;
242242

243+
/**
244+
* This feature is EXPERIMENTAL.
245+
*
246+
* Report pre-warmed app starts by dropping the first app start spans if pre-warming paused during
247+
* these steps. This approach will shorten the app start duration, but it represents the duration a
248+
* user has to wait after clicking the app icon until the app is responsive.
249+
*
250+
* You can filter for different app start types in Discover with app_start_type:cold.prewarmed,
251+
* app_start_type:warm.prewarmed, app_start_type:cold, and app_start_type:warm.
252+
*
253+
* Default value is <code>NO</code>
254+
*/
255+
@property (nonatomic, assign) BOOL enablePreWarmedAppStartTracking;
256+
243257
#endif
244258

245259
/**

Sources/Sentry/SentryAppStartMeasurement.m

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ - (instancetype)initWithType:(SentryAppStartType)type
1111
didFinishLaunchingTimestamp:(NSDate *)didFinishLaunchingTimestamp
1212
{
1313
return [self initWithType:type
14+
isPreWarmed:NO
1415
appStartTimestamp:appStartTimestamp
1516
duration:duration
1617
runtimeInitTimestamp:runtimeInitTimestamp
@@ -19,6 +20,7 @@ - (instancetype)initWithType:(SentryAppStartType)type
1920
}
2021

2122
- (instancetype)initWithType:(SentryAppStartType)type
23+
isPreWarmed:(BOOL)isPreWarmed
2224
appStartTimestamp:(NSDate *)appStartTimestamp
2325
duration:(NSTimeInterval)duration
2426
runtimeInitTimestamp:(NSDate *)runtimeInitTimestamp
@@ -27,6 +29,7 @@ - (instancetype)initWithType:(SentryAppStartType)type
2729
{
2830
if (self = [super init]) {
2931
_type = type;
32+
_isPreWarmed = isPreWarmed;
3033
_appStartTimestamp = appStartTimestamp;
3134
_duration = duration;
3235
_runtimeInitTimestamp = runtimeInitTimestamp;

Sources/Sentry/SentryAppStartTracker.m

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
@property (nonatomic, strong) SentrySysctl *sysctl;
3535
@property (nonatomic, assign) BOOL wasInBackground;
3636
@property (nonatomic, strong) NSDate *didFinishLaunchingTimestamp;
37+
@property (nonatomic, assign) BOOL enablePreWarmedAppStartTracking;
3738

3839
@end
3940

@@ -55,6 +56,7 @@ - (instancetype)initWithCurrentDateProvider:(id<SentryCurrentDateProvider>)curre
5556
dispatchQueueWrapper:(SentryDispatchQueueWrapper *)dispatchQueueWrapper
5657
appStateManager:(SentryAppStateManager *)appStateManager
5758
sysctl:(SentrySysctl *)sysctl
59+
enablePreWarmedAppStartTracking:(BOOL)enablePreWarmedAppStartTracking
5860
{
5961
if (self = [super init]) {
6062
self.currentDate = currentDateProvider;
@@ -64,6 +66,7 @@ - (instancetype)initWithCurrentDateProvider:(id<SentryCurrentDateProvider>)curre
6466
self.previousAppState = [self.appStateManager loadPreviousAppState];
6567
self.wasInBackground = NO;
6668
self.didFinishLaunchingTimestamp = [currentDateProvider date];
69+
self.enablePreWarmedAppStartTracking = enablePreWarmedAppStartTracking;
6770
}
6871
return self;
6972
}
@@ -120,12 +123,17 @@ - (void)buildAppStartMeasurement
120123
void (^block)(void) = ^(void) {
121124
[self stop];
122125

123-
// Don't (yet) report pre warmed app starts.
124-
// Check if prewarm is available. Just to be safe to not drop app start data on earlier OS
125-
// verions.
126+
BOOL isPreWarmed = NO;
126127
if ([self isActivePrewarmAvailable] && isActivePrewarm) {
127-
SENTRY_LOG_INFO(@"The app was prewarmed. Not measuring app start.");
128-
return;
128+
SENTRY_LOG_INFO(@"The app was prewarmed.");
129+
130+
if (self.enablePreWarmedAppStartTracking) {
131+
isPreWarmed = YES;
132+
} else {
133+
SENTRY_LOG_INFO(
134+
@"EnablePreWarmedAppStartTracking disabled. Not measuring app start.");
135+
return;
136+
}
129137
}
130138

131139
SentryAppStartType appStartType = [self getStartType];
@@ -145,16 +153,29 @@ - (void)buildAppStartMeasurement
145153
// According to a talk at WWDC about optimizing app launch
146154
// (https://devstreaming-cdn.apple.com/videos/wwdc/2019/423lzf3qsjedrzivc7/423/423_optimizing_app_launch.pdf?dl=1
147155
// slide 17) no process exists for cold and warm launches. Since iOS 15, though, the system
148-
// might decide to pre-warm your app before the user tries to open it. The process start
149-
// time returned valid values when testing with real devices if the app start is not
150-
// prewarmed. See:
156+
// might decide to pre-warm your app before the user tries to open it.
157+
// Prewarming can stop at any of the app launch steps. Our findings show that most of
158+
// the prewarmed app starts don't call the main method. Therefore we subtract the
159+
// time before the module initialization / main method to calculate the app start
160+
// duration. If the app start stopped during a later launch step, we drop it below with
161+
// checking the SENTRY_APP_START_MAX_DURATION. With this approach, we will
162+
// lose some warm app starts, but we accept this tradeoff. Useful resources:
151163
// https://developer.apple.com/documentation/uikit/app_and_environment/responding_to_the_launch_of_your_app/about_the_app_launch_sequence#3894431
152164
// https://developer.apple.com/documentation/metrickit/mxapplaunchmetric,
153165
// https://twitter.com/steipete/status/1466013492180312068,
154166
// https://github.com/MobileNativeFoundation/discussions/discussions/146
155-
156-
NSTimeInterval appStartDuration =
157-
[[self.currentDate date] timeIntervalSinceDate:self.sysctl.processStartTimestamp];
167+
// https://eisel.me/startup
168+
NSTimeInterval appStartDuration = 0.0;
169+
NSDate *appStartTimestamp;
170+
if (isPreWarmed) {
171+
appStartDuration = [[self.currentDate date]
172+
timeIntervalSinceDate:self.sysctl.moduleInitializationTimestamp];
173+
appStartTimestamp = self.sysctl.moduleInitializationTimestamp;
174+
} else {
175+
appStartDuration =
176+
[[self.currentDate date] timeIntervalSinceDate:self.sysctl.processStartTimestamp];
177+
appStartTimestamp = self.sysctl.processStartTimestamp;
178+
}
158179

159180
// Safety check to not report app starts that are completely off.
160181
if (appStartDuration >= SENTRY_APP_START_MAX_DURATION) {
@@ -177,7 +198,8 @@ - (void)buildAppStartMeasurement
177198

178199
SentryAppStartMeasurement *appStartMeasurement = [[SentryAppStartMeasurement alloc]
179200
initWithType:appStartType
180-
appStartTimestamp:self.sysctl.processStartTimestamp
201+
isPreWarmed:isPreWarmed
202+
appStartTimestamp:appStartTimestamp
181203
duration:appStartDuration
182204
runtimeInitTimestamp:runtimeInit
183205
moduleInitializationTimestamp:self.sysctl.moduleInitializationTimestamp

Sources/Sentry/SentryAppStartTrackingIntegration.m

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,11 @@ - (BOOL)installWithOptions:(SentryOptions *)options
3737
[SentryDependencyContainer sharedInstance].appStateManager;
3838

3939
self.tracker = [[SentryAppStartTracker alloc]
40-
initWithCurrentDateProvider:currentDateProvider
41-
dispatchQueueWrapper:[[SentryDispatchQueueWrapper alloc] init]
42-
appStateManager:appStateManager
43-
sysctl:sysctl];
40+
initWithCurrentDateProvider:currentDateProvider
41+
dispatchQueueWrapper:[[SentryDispatchQueueWrapper alloc] init]
42+
appStateManager:appStateManager
43+
sysctl:sysctl
44+
enablePreWarmedAppStartTracking:options.enablePreWarmedAppStartTracking];
4445
[self.tracker start];
4546

4647
return YES;

Sources/Sentry/SentryClient.m

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -654,6 +654,12 @@ - (void)setSdk:(SentryEvent *)event
654654
if (self.options.stitchAsyncCode) {
655655
[integrations addObject:@"StitchAsyncCode"];
656656
}
657+
658+
#if SENTRY_HAS_UIKIT
659+
if (self.options.enablePreWarmedAppStartTracking) {
660+
[integrations addObject:@"PreWarmedAppStartTracking"];
661+
}
662+
#endif
657663
}
658664

659665
event.sdk = @{

0 commit comments

Comments
 (0)