1 |
efrain |
1 |
// Copyright 2020 Google LLC
|
|
|
2 |
//
|
|
|
3 |
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
4 |
// you may not use this file except in compliance with the License.
|
|
|
5 |
// You may obtain a copy of the License at
|
|
|
6 |
//
|
|
|
7 |
// http://www.apache.org/licenses/LICENSE-2.0
|
|
|
8 |
//
|
|
|
9 |
// Unless required by applicable law or agreed to in writing, software
|
|
|
10 |
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
11 |
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
12 |
// See the License for the specific language governing permissions and
|
|
|
13 |
// limitations under the License.
|
|
|
14 |
|
|
|
15 |
#import "FirebasePerformance/Sources/AppActivity/FPRAppActivityTracker.h"
|
|
|
16 |
|
|
|
17 |
#import <Foundation/Foundation.h>
|
|
|
18 |
#import <UIKit/UIKit.h>
|
|
|
19 |
|
|
|
20 |
#import "FirebasePerformance/Sources/AppActivity/FPRSessionManager.h"
|
|
|
21 |
#import "FirebasePerformance/Sources/Configurations/FPRConfigurations.h"
|
|
|
22 |
#import "FirebasePerformance/Sources/Gauges/CPU/FPRCPUGaugeCollector+Private.h"
|
|
|
23 |
#import "FirebasePerformance/Sources/Gauges/FPRGaugeManager.h"
|
|
|
24 |
#import "FirebasePerformance/Sources/Gauges/Memory/FPRMemoryGaugeCollector+Private.h"
|
|
|
25 |
#import "FirebasePerformance/Sources/Timer/FIRTrace+Internal.h"
|
|
|
26 |
#import "FirebasePerformance/Sources/Timer/FIRTrace+Private.h"
|
|
|
27 |
|
|
|
28 |
static NSDate *appStartTime = nil;
|
|
|
29 |
static NSDate *doubleDispatchTime = nil;
|
|
|
30 |
static NSDate *applicationDidFinishLaunchTime = nil;
|
|
|
31 |
static NSTimeInterval gAppStartMaxValidDuration = 60 * 60; // 60 minutes.
|
|
|
32 |
static FPRCPUGaugeData *gAppStartCPUGaugeData = nil;
|
|
|
33 |
static FPRMemoryGaugeData *gAppStartMemoryGaugeData = nil;
|
|
|
34 |
static BOOL isActivePrewarm = NO;
|
|
|
35 |
|
|
|
36 |
NSString *const kFPRAppStartTraceName = @"_as";
|
|
|
37 |
NSString *const kFPRAppStartStageNameTimeToUI = @"_astui";
|
|
|
38 |
NSString *const kFPRAppStartStageNameTimeToFirstDraw = @"_astfd";
|
|
|
39 |
NSString *const kFPRAppStartStageNameTimeToUserInteraction = @"_asti";
|
|
|
40 |
NSString *const kFPRAppTraceNameForegroundSession = @"_fs";
|
|
|
41 |
NSString *const kFPRAppTraceNameBackgroundSession = @"_bs";
|
|
|
42 |
NSString *const kFPRAppCounterNameTraceEventsRateLimited = @"_fstec";
|
|
|
43 |
NSString *const kFPRAppCounterNameNetworkTraceEventsRateLimited = @"_fsntc";
|
|
|
44 |
NSString *const kFPRAppCounterNameTraceNotStopped = @"_tsns";
|
|
|
45 |
NSString *const kFPRAppCounterNameActivePrewarm = @"_fsapc";
|
|
|
46 |
|
|
|
47 |
@interface FPRAppActivityTracker ()
|
|
|
48 |
|
|
|
49 |
/** The foreground session trace. Will be set only when the app is in the foreground. */
|
|
|
50 |
@property(nonatomic, readwrite) FIRTrace *foregroundSessionTrace;
|
|
|
51 |
|
|
|
52 |
/** The background session trace. Will be set only when the app is in the background. */
|
|
|
53 |
@property(nonatomic, readwrite) FIRTrace *backgroundSessionTrace;
|
|
|
54 |
|
|
|
55 |
/** Current running state of the application. */
|
|
|
56 |
@property(nonatomic, readwrite) FPRApplicationState applicationState;
|
|
|
57 |
|
|
|
58 |
/** Trace to measure the app start performance. */
|
|
|
59 |
@property(nonatomic) FIRTrace *appStartTrace;
|
|
|
60 |
|
|
|
61 |
/** Tracks if the gauge metrics are dispatched. */
|
|
|
62 |
@property(nonatomic) BOOL appStartGaugeMetricDispatched;
|
|
|
63 |
|
|
|
64 |
/** Firebase Performance Configuration object */
|
|
|
65 |
@property(nonatomic) FPRConfigurations *configurations;
|
|
|
66 |
|
|
|
67 |
/** Starts tracking app active sessions. */
|
|
|
68 |
- (void)startAppActivityTracking;
|
|
|
69 |
|
|
|
70 |
@end
|
|
|
71 |
|
|
|
72 |
@implementation FPRAppActivityTracker
|
|
|
73 |
|
|
|
74 |
+ (void)load {
|
|
|
75 |
// This is an approximation of the app start time.
|
|
|
76 |
appStartTime = [NSDate date];
|
|
|
77 |
|
|
|
78 |
// When an app is prewarmed, Apple sets env variable ActivePrewarm to 1, then the env variable is
|
|
|
79 |
// deleted after didFinishLaunching
|
|
|
80 |
isActivePrewarm = [NSProcessInfo.processInfo.environment[@"ActivePrewarm"] isEqualToString:@"1"];
|
|
|
81 |
|
|
|
82 |
gAppStartCPUGaugeData = fprCollectCPUMetric();
|
|
|
83 |
gAppStartMemoryGaugeData = fprCollectMemoryMetric();
|
|
|
84 |
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
|
85 |
selector:@selector(windowDidBecomeVisible:)
|
|
|
86 |
name:UIWindowDidBecomeVisibleNotification
|
|
|
87 |
object:nil];
|
|
|
88 |
|
|
|
89 |
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
|
90 |
selector:@selector(applicationDidFinishLaunching:)
|
|
|
91 |
name:UIApplicationDidFinishLaunchingNotification
|
|
|
92 |
object:nil];
|
|
|
93 |
}
|
|
|
94 |
|
|
|
95 |
+ (void)windowDidBecomeVisible:(NSNotification *)notification {
|
|
|
96 |
FPRAppActivityTracker *activityTracker = [self sharedInstance];
|
|
|
97 |
[activityTracker startAppActivityTracking];
|
|
|
98 |
|
|
|
99 |
[[NSNotificationCenter defaultCenter] removeObserver:self
|
|
|
100 |
name:UIWindowDidBecomeVisibleNotification
|
|
|
101 |
object:nil];
|
|
|
102 |
}
|
|
|
103 |
|
|
|
104 |
+ (void)applicationDidFinishLaunching:(NSNotification *)notification {
|
|
|
105 |
applicationDidFinishLaunchTime = [NSDate date];
|
|
|
106 |
[[NSNotificationCenter defaultCenter] removeObserver:self
|
|
|
107 |
name:UIApplicationDidFinishLaunchingNotification
|
|
|
108 |
object:nil];
|
|
|
109 |
}
|
|
|
110 |
|
|
|
111 |
+ (instancetype)sharedInstance {
|
|
|
112 |
static FPRAppActivityTracker *instance;
|
|
|
113 |
static dispatch_once_t onceToken;
|
|
|
114 |
dispatch_once(&onceToken, ^{
|
|
|
115 |
instance = [[self alloc] initAppActivityTracker];
|
|
|
116 |
});
|
|
|
117 |
return instance;
|
|
|
118 |
}
|
|
|
119 |
|
|
|
120 |
/**
|
|
|
121 |
* Custom initializer to create an app activity tracker.
|
|
|
122 |
*/
|
|
|
123 |
- (instancetype)initAppActivityTracker {
|
|
|
124 |
self = [super init];
|
|
|
125 |
_applicationState = FPRApplicationStateUnknown;
|
|
|
126 |
_appStartGaugeMetricDispatched = NO;
|
|
|
127 |
_configurations = [FPRConfigurations sharedInstance];
|
|
|
128 |
return self;
|
|
|
129 |
}
|
|
|
130 |
|
|
|
131 |
- (void)startAppActivityTracking {
|
|
|
132 |
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
|
133 |
selector:@selector(appDidBecomeActiveNotification:)
|
|
|
134 |
name:UIApplicationDidBecomeActiveNotification
|
|
|
135 |
object:[UIApplication sharedApplication]];
|
|
|
136 |
|
|
|
137 |
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
|
138 |
selector:@selector(appWillResignActiveNotification:)
|
|
|
139 |
name:UIApplicationWillResignActiveNotification
|
|
|
140 |
object:[UIApplication sharedApplication]];
|
|
|
141 |
}
|
|
|
142 |
|
|
|
143 |
- (FIRTrace *)activeTrace {
|
|
|
144 |
if (self.foregroundSessionTrace) {
|
|
|
145 |
return self.foregroundSessionTrace;
|
|
|
146 |
}
|
|
|
147 |
return self.backgroundSessionTrace;
|
|
|
148 |
}
|
|
|
149 |
|
|
|
150 |
/**
|
|
|
151 |
* Checks if the prewarming feature is available on the current device.
|
|
|
152 |
*
|
|
|
153 |
* @return true if the OS could prewarm apps on the current device
|
|
|
154 |
*/
|
|
|
155 |
- (BOOL)isPrewarmAvailable {
|
|
|
156 |
BOOL canPrewarm = NO;
|
|
|
157 |
// Guarding for double dispatch which does not work below iOS 13, and 0.1% of app start also show
|
|
|
158 |
// signs of prewarming on iOS 14 go/paste/5533761933410304
|
|
|
159 |
if (@available(iOS 13, *)) {
|
|
|
160 |
canPrewarm = YES;
|
|
|
161 |
}
|
|
|
162 |
return canPrewarm;
|
|
|
163 |
}
|
|
|
164 |
|
|
|
165 |
/**
|
|
|
166 |
RC flag for dropping all app start events
|
|
|
167 |
*/
|
|
|
168 |
- (BOOL)isAppStartEnabled {
|
|
|
169 |
return [self.configurations prewarmDetectionMode] != PrewarmDetectionModeKeepNone;
|
|
|
170 |
}
|
|
|
171 |
|
|
|
172 |
/**
|
|
|
173 |
RC flag for enabling prewarm-detection using ActivePrewarm environment variable
|
|
|
174 |
*/
|
|
|
175 |
- (BOOL)isActivePrewarmEnabled {
|
|
|
176 |
PrewarmDetectionMode mode = [self.configurations prewarmDetectionMode];
|
|
|
177 |
return (mode == PrewarmDetectionModeActivePrewarm);
|
|
|
178 |
}
|
|
|
179 |
|
|
|
180 |
/**
|
|
|
181 |
Checks if the current app start is a prewarmed app start
|
|
|
182 |
*/
|
|
|
183 |
- (BOOL)isApplicationPreWarmed {
|
|
|
184 |
if (![self isPrewarmAvailable]) {
|
|
|
185 |
return NO;
|
|
|
186 |
}
|
|
|
187 |
|
|
|
188 |
BOOL isPrewarmed = NO;
|
|
|
189 |
|
|
|
190 |
if (isActivePrewarm == YES) {
|
|
|
191 |
isPrewarmed = isPrewarmed || [self isActivePrewarmEnabled];
|
|
|
192 |
[self.activeTrace incrementMetric:kFPRAppCounterNameActivePrewarm byInt:1];
|
|
|
193 |
} else {
|
|
|
194 |
[self.activeTrace incrementMetric:kFPRAppCounterNameActivePrewarm byInt:0];
|
|
|
195 |
}
|
|
|
196 |
|
|
|
197 |
return isPrewarmed;
|
|
|
198 |
}
|
|
|
199 |
|
|
|
200 |
/**
|
|
|
201 |
* This gets called whenever the app becomes active. A new trace will be created to track the active
|
|
|
202 |
* foreground session. Any background session trace that was running in the past will be stopped.
|
|
|
203 |
*
|
|
|
204 |
* @param notification Notification received during app launch.
|
|
|
205 |
*/
|
|
|
206 |
- (void)appDidBecomeActiveNotification:(NSNotification *)notification {
|
|
|
207 |
self.applicationState = FPRApplicationStateForeground;
|
|
|
208 |
|
|
|
209 |
static dispatch_once_t onceToken;
|
|
|
210 |
dispatch_once(&onceToken, ^{
|
|
|
211 |
self.appStartTrace = [[FIRTrace alloc] initInternalTraceWithName:kFPRAppStartTraceName];
|
|
|
212 |
[self.appStartTrace startWithStartTime:appStartTime];
|
|
|
213 |
[self.appStartTrace startStageNamed:kFPRAppStartStageNameTimeToUI startTime:appStartTime];
|
|
|
214 |
|
|
|
215 |
// Start measuring time to first draw on the App start trace.
|
|
|
216 |
[self.appStartTrace startStageNamed:kFPRAppStartStageNameTimeToFirstDraw];
|
|
|
217 |
});
|
|
|
218 |
|
|
|
219 |
// If ever the app start trace had it life in background stage, do not send the trace.
|
|
|
220 |
if (self.appStartTrace.backgroundTraceState != FPRTraceStateForegroundOnly) {
|
|
|
221 |
self.appStartTrace = nil;
|
|
|
222 |
}
|
|
|
223 |
|
|
|
224 |
// Stop the active background session trace.
|
|
|
225 |
[self.backgroundSessionTrace stop];
|
|
|
226 |
self.backgroundSessionTrace = nil;
|
|
|
227 |
|
|
|
228 |
// Start foreground session trace.
|
|
|
229 |
FIRTrace *appTrace =
|
|
|
230 |
[[FIRTrace alloc] initInternalTraceWithName:kFPRAppTraceNameForegroundSession];
|
|
|
231 |
[appTrace start];
|
|
|
232 |
self.foregroundSessionTrace = appTrace;
|
|
|
233 |
|
|
|
234 |
// Start measuring time to make the app interactive on the App start trace.
|
|
|
235 |
static BOOL TTIStageStarted = NO;
|
|
|
236 |
if (!TTIStageStarted) {
|
|
|
237 |
[self.appStartTrace startStageNamed:kFPRAppStartStageNameTimeToUserInteraction];
|
|
|
238 |
TTIStageStarted = YES;
|
|
|
239 |
|
|
|
240 |
// Assumption here is that - the app becomes interactive in the next runloop cycle.
|
|
|
241 |
// It is possible that the app does more things later, but for now we are not measuring that.
|
|
|
242 |
dispatch_async(dispatch_get_main_queue(), ^{
|
|
|
243 |
NSTimeInterval startTimeSinceEpoch = [self.appStartTrace startTimeSinceEpoch];
|
|
|
244 |
NSTimeInterval currentTimeSinceEpoch = [[NSDate date] timeIntervalSince1970];
|
|
|
245 |
|
|
|
246 |
// The below check is to account for 2 scenarios.
|
|
|
247 |
// 1. The app gets started in the background and might come to foreground a lot later.
|
|
|
248 |
// 2. The app is launched, but immediately backgrounded for some reason and the actual launch
|
|
|
249 |
// happens a lot later.
|
|
|
250 |
// Dropping the app start trace in such situations where the launch time is taking more than
|
|
|
251 |
// 60 minutes. This is an approximation, but a more agreeable timelimit for app start.
|
|
|
252 |
if ((currentTimeSinceEpoch - startTimeSinceEpoch < gAppStartMaxValidDuration) &&
|
|
|
253 |
[self isAppStartEnabled] && ![self isApplicationPreWarmed]) {
|
|
|
254 |
[self.appStartTrace stop];
|
|
|
255 |
} else {
|
|
|
256 |
[self.appStartTrace cancel];
|
|
|
257 |
}
|
|
|
258 |
});
|
|
|
259 |
}
|
|
|
260 |
|
|
|
261 |
// Let the session manager to start tracking app activity changes.
|
|
|
262 |
[[FPRSessionManager sharedInstance] startTrackingAppStateChanges];
|
|
|
263 |
}
|
|
|
264 |
|
|
|
265 |
/**
|
|
|
266 |
* This gets called whenever the app resigns its active status. The currently active foreground
|
|
|
267 |
* session trace will be stopped and a background session trace will be started.
|
|
|
268 |
*
|
|
|
269 |
* @param notification Notification received during app resigning active status.
|
|
|
270 |
*/
|
|
|
271 |
- (void)appWillResignActiveNotification:(NSNotification *)notification {
|
|
|
272 |
// Dispatch the collected gauge metrics.
|
|
|
273 |
if (!self.appStartGaugeMetricDispatched) {
|
|
|
274 |
[[FPRGaugeManager sharedInstance] dispatchMetric:gAppStartCPUGaugeData];
|
|
|
275 |
[[FPRGaugeManager sharedInstance] dispatchMetric:gAppStartMemoryGaugeData];
|
|
|
276 |
self.appStartGaugeMetricDispatched = YES;
|
|
|
277 |
}
|
|
|
278 |
|
|
|
279 |
self.applicationState = FPRApplicationStateBackground;
|
|
|
280 |
|
|
|
281 |
// Stop foreground session trace.
|
|
|
282 |
[self.foregroundSessionTrace stop];
|
|
|
283 |
self.foregroundSessionTrace = nil;
|
|
|
284 |
|
|
|
285 |
// Start background session trace.
|
|
|
286 |
self.backgroundSessionTrace =
|
|
|
287 |
[[FIRTrace alloc] initInternalTraceWithName:kFPRAppTraceNameBackgroundSession];
|
|
|
288 |
[self.backgroundSessionTrace start];
|
|
|
289 |
}
|
|
|
290 |
|
|
|
291 |
- (void)dealloc {
|
|
|
292 |
[[NSNotificationCenter defaultCenter] removeObserver:self
|
|
|
293 |
name:UIApplicationDidBecomeActiveNotification
|
|
|
294 |
object:[UIApplication sharedApplication]];
|
|
|
295 |
|
|
|
296 |
[[NSNotificationCenter defaultCenter] removeObserver:self
|
|
|
297 |
name:UIApplicationWillResignActiveNotification
|
|
|
298 |
object:[UIApplication sharedApplication]];
|
|
|
299 |
}
|
|
|
300 |
|
|
|
301 |
@end
|