// Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #import "FirebasePerformance/Sources/Public/FirebasePerformance/FIRTrace.h" #import "FirebasePerformance/Sources/AppActivity/FPRAppActivityTracker.h" #import "FirebasePerformance/Sources/AppActivity/FPRSessionManager.h" #import "FirebasePerformance/Sources/Common/FPRConstants.h" #import "FirebasePerformance/Sources/Common/FPRDiagnostics.h" #import "FirebasePerformance/Sources/Configurations/FPRConfigurations.h" #import "FirebasePerformance/Sources/FPRClient.h" #import "FirebasePerformance/Sources/FPRConsoleLogger.h" #import "FirebasePerformance/Sources/FPRDataUtils.h" #import "FirebasePerformance/Sources/Gauges/FPRGaugeManager.h" #import "FirebasePerformance/Sources/Timer/FIRTrace+Internal.h" #import "FirebasePerformance/Sources/Timer/FIRTrace+Private.h" @interface FIRTrace () @property(nonatomic, copy, readwrite) NSString *name; /** Custom attributes managed internally. */ @property(nonatomic) NSMutableDictionary *customAttributes; /** Serial queue to manage mutation of attributes. */ @property(nonatomic, readwrite) dispatch_queue_t customAttributesSerialQueue; @property(nonatomic, readwrite) NSDate *startTime; @property(nonatomic, readwrite) NSDate *stopTime; /** Background activity tracker to know the background state of the trace. */ @property(nonatomic) FPRTraceBackgroundActivityTracker *backgroundActivityTracker; /** Property that denotes if the trace is a stage. */ @property(nonatomic) BOOL isStage; /** Stops an active stage that is currently active. */ - (void)stopActiveStage; /** * Updates the current trace with the session id. * @param sessionDetails Updated session details of the currently active session. */ - (void)updateTraceWithSessionId:(FPRSessionDetails *)sessionDetails; @end @implementation FIRTrace - (instancetype)initWithName:(NSString *)name { NSString *validatedName = FPRReservableName(name); FIRTrace *trace = [self initTraceWithName:validatedName]; trace.internal = NO; return trace; } - (instancetype)initInternalTraceWithName:(NSString *)name { FIRTrace *trace = [self initTraceWithName:name]; trace.internal = YES; return trace; } - (instancetype)initTraceWithName:(NSString *)name { BOOL tracingEnabled = [FPRConfigurations sharedInstance].isDataCollectionEnabled; if (!tracingEnabled) { FPRLogInfo(kFPRTraceDisabled, @"Trace feature is disabled."); return nil; } BOOL sdkEnabled = [[FPRConfigurations sharedInstance] sdkEnabled]; if (!sdkEnabled) { FPRLogInfo(kFPRTraceDisabled, @"Dropping event since Performance SDK is disabled."); return nil; } FPRAssert(name != nil, @"Name cannot be nil"); FPRAssert(name.length > 0, @"Name cannot be an empty string"); if (name == nil || name.length == 0) { FPRLogError(kFPRTraceNoName, @"Failed to initialize because of a nil or zero length name."); return nil; } self = [super init]; if (self) { _name = [name copy]; _stages = [[NSMutableArray alloc] init]; _counterList = [[FPRCounterList alloc] init]; _customAttributes = [[NSMutableDictionary alloc] init]; _customAttributesSerialQueue = dispatch_queue_create("com.google.perf.customAttributes.trace", DISPATCH_QUEUE_SERIAL); _sessionIdSerialQueue = dispatch_queue_create("com.google.perf.sessionIds.trace", DISPATCH_QUEUE_SERIAL); _activeSessions = [[NSMutableArray alloc] init]; _isStage = NO; _fprClient = [FPRClient sharedInstance]; } return self; } - (instancetype)init { FPRAssert(NO, @"Not a valid initializer."); return nil; } - (void)dealloc { // Track the number of traces that have started and not stopped. if (!self.isStage && [self isTraceStarted] && ![self isTraceStopped]) { FIRTrace *activeTrace = [FPRAppActivityTracker sharedInstance].activeTrace; [activeTrace incrementMetric:kFPRAppCounterNameTraceNotStopped byInt:1]; FPRLogError(kFPRTraceStartedNotStopped, @"Trace name %@ started, not stopped", self.name); } FPRSessionManager *sessionManager = [FPRSessionManager sharedInstance]; [sessionManager.sessionNotificationCenter removeObserver:self name:kFPRSessionIdUpdatedNotification object:sessionManager]; } #pragma mark - Public instance methods - (void)start { if (![self isTraceStarted]) { if (!self.isStage) { [[FPRGaugeManager sharedInstance] collectAllGauges]; } self.startTime = [NSDate date]; self.backgroundActivityTracker = [[FPRTraceBackgroundActivityTracker alloc] init]; FPRSessionManager *sessionManager = [FPRSessionManager sharedInstance]; if (!self.isStage) { [self updateTraceWithSessionId:[sessionManager.sessionDetails copy]]; [sessionManager.sessionNotificationCenter addObserver:self selector:@selector(sessionChanged:) name:kFPRSessionIdUpdatedNotification object:sessionManager]; } } else { FPRLogError(kFPRTraceAlreadyStopped, @"Failed to start trace %@ because it has already been started and stopped.", self.name); } } - (void)startWithStartTime:(NSDate *)startTime { [self start]; if (startTime) { self.startTime = startTime; } } - (void)stop { [self stopActiveStage]; if ([self isTraceActive]) { self.stopTime = [NSDate date]; [self.fprClient logTrace:self]; if (!self.isStage) { [[FPRGaugeManager sharedInstance] collectAllGauges]; } } else { FPRLogError(kFPRTraceNotStarted, @"Failed to stop the trace %@ because it has not been started.", self.name); } FPRSessionManager *sessionManager = [FPRSessionManager sharedInstance]; [sessionManager.sessionNotificationCenter removeObserver:self name:kFPRSessionIdUpdatedNotification object:sessionManager]; } - (void)cancel { [self stopActiveStage]; if ([self isTraceActive]) { self.stopTime = [NSDate date]; } else { FPRLogError(kFPRTraceNotStarted, @"Failed to stop the trace %@ because it has not been started.", self.name); } } - (NSTimeInterval)totalTraceTimeInterval { return [self.stopTime timeIntervalSinceDate:self.startTime]; } - (NSTimeInterval)startTimeSinceEpoch { return [self.startTime timeIntervalSince1970]; } - (BOOL)isCompleteAndValid { // Check if the trace time interval is valid. if (self.totalTraceTimeInterval <= 0) { return NO; } // Check if the counter list is valid. if (![self.counterList isValid]) { return NO; } // Check if the stages are valid. __block BOOL validTrace = YES; [self.stages enumerateObjectsUsingBlock:^(FIRTrace *stage, NSUInteger idx, BOOL *stop) { validTrace = [stage isCompleteAndValid]; if (!validTrace) { *stop = YES; } }]; return validTrace; } - (FPRTraceState)backgroundTraceState { FPRTraceBackgroundActivityTracker *backgroundActivityTracker = self.backgroundActivityTracker; if (backgroundActivityTracker) { return backgroundActivityTracker.traceBackgroundState; } return FPRTraceStateUnknown; } - (NSArray *)sessions { __block NSArray *sessionInfos = nil; dispatch_sync(self.sessionIdSerialQueue, ^{ sessionInfos = [self.activeSessions copy]; }); return sessionInfos; } #pragma mark - Stage related methods - (void)startStageNamed:(NSString *)stageName startTime:(NSDate *)startTime { if ([self isTraceActive]) { [self stopActiveStage]; if (self.isInternal) { self.activeStage = [[FIRTrace alloc] initInternalTraceWithName:stageName]; [self.activeStage startWithStartTime:startTime]; } else { NSString *validatedStageName = FPRReservableName(stageName); if (validatedStageName.length > 0) { self.activeStage = [[FIRTrace alloc] initWithName:validatedStageName]; [self.activeStage startWithStartTime:startTime]; } else { FPRLogError(kFPRTraceEmptyName, @"The stage name cannot be empty."); } } self.activeStage.isStage = YES; // Do not track background activity tracker for stages. self.activeStage.backgroundActivityTracker = nil; } else { FPRLogError(kFPRTraceNotStarted, @"Failed to create stage %@ because the trace has not been started.", stageName); } } - (void)startStageNamed:(NSString *)stageName { [self startStageNamed:stageName startTime:nil]; } - (void)stopActiveStage { if (self.activeStage) { [self.activeStage cancel]; [self.stages addObject:self.activeStage]; self.activeStage = nil; } } #pragma mark - Counter related methods - (NSDictionary *)counters { return [self.counterList counters]; } - (NSUInteger)numberOfCounters { return [self.counterList numberOfCounters]; } #pragma mark - Metrics related methods - (int64_t)valueForIntMetric:(nonnull NSString *)metricName { return [self.counterList valueForIntMetric:metricName]; } - (void)setIntValue:(int64_t)value forMetric:(nonnull NSString *)metricName { if ([self isTraceActive]) { NSString *validatedMetricName = self.isInternal ? metricName : FPRReservableName(metricName); if (validatedMetricName.length > 0) { [self.counterList setIntValue:value forMetric:validatedMetricName]; [self.activeStage setIntValue:value forMetric:validatedMetricName]; } else { FPRLogError(kFPRTraceInvalidName, @"The metric name is invalid."); } } else { FPRLogError(kFPRTraceNotStarted, @"Failed to set value for metric %@ because trace %@ has not been started.", metricName, self.name); } } - (void)incrementMetric:(nonnull NSString *)metricName byInt:(int64_t)incrementValue { if ([self isTraceActive]) { NSString *validatedMetricName = self.isInternal ? metricName : FPRReservableName(metricName); if (validatedMetricName.length > 0) { [self.counterList incrementMetric:validatedMetricName byInt:incrementValue]; [self.activeStage incrementMetric:validatedMetricName byInt:incrementValue]; FPRLogDebug(kFPRClientMetricLogged, @"Incrementing metric %@ to %lld on trace %@", validatedMetricName, [self valueForIntMetric:metricName], self.name); } else { FPRLogError(kFPRTraceInvalidName, @"The metric name is invalid."); } } else { FPRLogError(kFPRTraceNotStarted, @"Failed to increment the trace metric %@ because trace %@ has not been started.", metricName, self.name); } } - (void)deleteMetric:(nonnull NSString *)metricName { if ([self isTraceActive]) { [self.counterList deleteMetric:metricName]; [self.activeStage deleteMetric:metricName]; } } #pragma mark - Custom attributes related methods - (NSDictionary *)attributes { return [self.customAttributes copy]; } - (void)setValue:(NSString *)value forAttribute:(nonnull NSString *)attribute { BOOL canAddAttribute = YES; if ([self isTraceStopped]) { FPRLogError(kFPRTraceAlreadyStopped, @"Failed to set attribute %@ because trace %@ has already stopped.", attribute, self.name); canAddAttribute = NO; } NSString *validatedName = FPRReservableAttributeName(attribute); NSString *validatedValue = FPRValidatedAttributeValue(value); if (validatedName == nil) { FPRLogError(kFPRAttributeNoName, @"Failed to initialize because of a nil or zero length attribute name."); canAddAttribute = NO; } if (validatedValue == nil) { FPRLogError(kFPRAttributeNoValue, @"Failed to initialize because of a nil or zero length attribute value."); canAddAttribute = NO; } if (self.customAttributes.allKeys.count >= kFPRMaxTraceCustomAttributesCount) { FPRLogError(kFPRMaxAttributesReached, @"Only %d attributes allowed. Already reached maximum attribute count.", kFPRMaxTraceCustomAttributesCount); canAddAttribute = NO; } if (canAddAttribute) { // Ensure concurrency during update of attributes. dispatch_sync(self.customAttributesSerialQueue, ^{ self.customAttributes[validatedName] = validatedValue; }); } FPRLogDebug(kFPRClientMetricLogged, @"Setting attribute %@ to %@ on trace %@", validatedName, validatedValue, self.name); } - (NSString *)valueForAttribute:(NSString *)attribute { // TODO(b/175053654): Should this be happening on the serial queue for thread safety? return self.customAttributes[attribute]; } - (void)removeAttribute:(NSString *)attribute { if ([self isTraceStopped]) { FPRLogError(kFPRTraceNotStarted, @"Failed to remove attribute %@ because trace %@ has already stopped.", attribute, self.name); return; } [self.customAttributes removeObjectForKey:attribute]; } #pragma mark - Utility methods - (void)sessionChanged:(NSNotification *)notification { if ([self isTraceActive]) { NSDictionary *userInfo = notification.userInfo; FPRSessionDetails *sessionDetails = [userInfo valueForKey:kFPRSessionIdNotificationKey]; if (sessionDetails) { [self updateTraceWithSessionId:sessionDetails]; } } } - (void)updateTraceWithSessionId:(FPRSessionDetails *)sessionDetails { dispatch_sync(self.sessionIdSerialQueue, ^{ [self.activeSessions addObject:sessionDetails]; }); } - (BOOL)isTraceStarted { return self.startTime != nil; } - (BOOL)isTraceStopped { return (self.startTime != nil && self.stopTime != nil); } - (BOOL)isTraceActive { return (self.startTime != nil && self.stopTime == nil); } @end