#import "HockeySDKFeatureConfig.h" #if HOCKEYSDK_FEATURE_METRICS #import "HockeySDKPrivate.h" #import "BITHockeyManager.h" #import "BITChannelPrivate.h" #import "BITHockeyHelper.h" #import "BITHockeyHelper+Application.h" #import "BITTelemetryContext.h" #import "BITTelemetryData.h" #import "BITEnvelope.h" #import "BITData.h" #import "BITDevice.h" #import "BITPersistencePrivate.h" #import "BITSender.h" #import static char *const BITDataItemsOperationsQueue = "net.hockeyapp.senderQueue"; char *BITTelemetryEventBuffer; NSString *const BITChannelBlockedNotification = @"BITChannelBlockedNotification"; static NSInteger const BITDefaultMaxBatchSize = 50; static NSInteger const BITDefaultBatchInterval = 15; static NSInteger const BITSchemaVersion = 2; static NSInteger const BITDebugMaxBatchSize = 5; static NSInteger const BITDebugBatchInterval = 3; typedef _Atomic(char*) atomic_charptr; NS_ASSUME_NONNULL_BEGIN @interface BITChannel () @property (nonatomic, weak, nullable) id appDidEnterBackgroundObserver; @end @implementation BITChannel @synthesize persistence = _persistence; @synthesize channelBlocked = _channelBlocked; #pragma mark - Initialisation - (instancetype)init { if ((self = [super init])) { bit_resetEventBuffer(&BITTelemetryEventBuffer); _dataItemCount = 0; if (bit_isDebuggerAttached()) { _maxBatchSize = BITDebugMaxBatchSize; _batchInterval = BITDebugBatchInterval; } else { _maxBatchSize = BITDefaultMaxBatchSize; _batchInterval = BITDefaultBatchInterval; } dispatch_queue_t serialQueue = dispatch_queue_create(BITDataItemsOperationsQueue, DISPATCH_QUEUE_SERIAL); _dataItemsOperations = serialQueue; [self registerObservers]; } return self; } - (instancetype)initWithTelemetryContext:(BITTelemetryContext *)telemetryContext persistence:(BITPersistence *)persistence { if ((self = [self init])) { _telemetryContext = telemetryContext; _persistence = persistence; } return self; } - (void)dealloc { [self unregisterObservers]; [self invalidateTimer]; } #pragma mark - Observers - (void) registerObservers { NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; __weak typeof(self) weakSelf = self; if (nil == self.appDidEnterBackgroundObserver) { void (^notificationBlock)(NSNotification *note) = ^(NSNotification __unused *note) { typeof(self) strongSelf = weakSelf; if ([strongSelf timerIsRunning]) { /** * From the documentation for applicationDidEnterBackground: * It's likely any background tasks you start in applicationDidEnterBackground: will not run until after that method exits, * you should request additional background execution time before starting those tasks. In other words, * first call beginBackgroundTaskWithExpirationHandler: and then run the task on a dispatch queue or secondary thread. */ UIApplication *application = [UIApplication sharedApplication]; [strongSelf persistDataItemQueueWithBackgroundTask: application]; } }; self.appDidEnterBackgroundObserver = [center addObserverForName:UIApplicationDidEnterBackgroundNotification object:nil queue:NSOperationQueue.mainQueue usingBlock:notificationBlock]; } } - (void) unregisterObservers { NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; id appDidEnterBackgroundObserver = self.appDidEnterBackgroundObserver; if (appDidEnterBackgroundObserver) { [center removeObserver:appDidEnterBackgroundObserver]; self.appDidEnterBackgroundObserver = nil; } } #pragma mark - Queue management - (BOOL)isQueueBusy { if (!self.channelBlocked) { BOOL persistenceBusy = ![self.persistence isFreeSpaceAvailable]; if (persistenceBusy) { self.channelBlocked = YES; [self sendBlockingChannelNotification]; } } return self.channelBlocked; } - (void)persistDataItemQueue:(char **)eventBuffer { [self invalidateTimer]; // Make sure string (which points to BITTelemetryEventBuffer) is not changed. char *previousBuffer = NULL; char *newEmptyString = strdup(""); do { previousBuffer = *eventBuffer; // This swaps pointers and makes sure eventBuffer now has the balue of newEmptyString. if (atomic_compare_exchange_strong((atomic_charptr *)eventBuffer, &previousBuffer, newEmptyString)) { @synchronized(self) { self.dataItemCount = 0; } break; } } while(true); // Nothing to persist, freeing memory and existing. if (!previousBuffer || strlen(previousBuffer) == 0) { free(previousBuffer); return; } // Persist the data NSData *bundle = [NSData dataWithBytes:previousBuffer length:strlen(previousBuffer)]; [self.persistence persistBundle:bundle]; free(previousBuffer); // Reset both, the async-signal-safe and item counter. [self resetQueue]; } - (void)persistDataItemQueueWithBackgroundTask:(UIApplication *)application { __weak typeof(self) weakSelf = self; dispatch_async(self.dataItemsOperations, ^{ typeof(self) strongSelf = weakSelf; [strongSelf persistDataItemQueue:&BITTelemetryEventBuffer]; }); [self createBackgroundTaskWhileDataIsSending:application withWaitingGroup:nil]; } - (void)createBackgroundTaskWhileDataIsSending:(UIApplication *)application withWaitingGroup:(nullable dispatch_group_t)group { if (application == nil) { return; } // Queues needs for waiting consistently. NSArray *queues = @[ self.dataItemsOperations, // For enqueue self.persistence.persistenceQueue, // For persist dispatch_get_main_queue() // For notification ]; // Tracking for sender activity. // BITPersistenceSuccessNotification - start sending // BITSenderFinishSendingDataNotification - finish sending __block dispatch_group_t senderGroup = dispatch_group_create(); __block NSInteger senderCounter = 0; __block id persistenceSuccessObserver = [[NSNotificationCenter defaultCenter] addObserverForName:BITPersistenceSuccessNotification object:nil queue:nil usingBlock:^(__unused NSNotification *notification) { dispatch_group_enter(senderGroup); senderCounter++; if (persistenceSuccessObserver) { [[NSNotificationCenter defaultCenter] removeObserver:persistenceSuccessObserver]; persistenceSuccessObserver = nil; } }]; __block id senderFinishSendingDataObserver = [[NSNotificationCenter defaultCenter] addObserverForName:BITSenderFinishSendingDataNotification object:nil queue:nil usingBlock:^(__unused NSNotification *notification) { if (senderCounter > 0) { dispatch_group_leave(senderGroup); senderCounter--; } if (senderFinishSendingDataObserver) { [[NSNotificationCenter defaultCenter] removeObserver:senderFinishSendingDataObserver]; senderFinishSendingDataObserver = nil; } }]; BITHockeyLogVerbose(@"BITChannel: Start background task"); __block UIBackgroundTaskIdentifier backgroundTask = [application beginBackgroundTaskWithExpirationHandler:^{ BITHockeyLogVerbose(@"BITChannel: Background task is expired"); [application endBackgroundTask:backgroundTask]; backgroundTask = UIBackgroundTaskInvalid; }]; __block NSUInteger i = 0; __block __weak void (^weakWaitBlock)(void); void (^waitBlock)(void); weakWaitBlock = waitBlock = ^{ if (i < queues.count) { dispatch_queue_t queue = [queues objectAtIndex:i++]; BITHockeyLogVerbose(@"BITChannel: Waiting queue: %@", [[NSString alloc] initWithUTF8String:dispatch_queue_get_label(queue)]); dispatch_async(queue, weakWaitBlock); } else { BITHockeyLogVerbose(@"BITChannel: Waiting sender"); dispatch_group_notify(senderGroup, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ if (backgroundTask != UIBackgroundTaskInvalid) { BITHockeyLogVerbose(@"BITChannel: Cancel background task"); [application endBackgroundTask:backgroundTask]; backgroundTask = UIBackgroundTaskInvalid; } }); } }; if (group != nil) { BITHockeyLogVerbose(@"BITChannel: Waiting group"); dispatch_group_notify((dispatch_group_t)group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), waitBlock); } else { waitBlock(); } } // Resets the event buffer and count of events in the queue. - (void)resetQueue { @synchronized (self) { bit_resetEventBuffer(&BITTelemetryEventBuffer); self.dataItemCount = 0; } } #pragma mark - Adding to queue - (void)enqueueTelemetryItem:(BITTelemetryData *)item { [self enqueueTelemetryItem:item completionHandler:nil]; } - (void)enqueueTelemetryItem:(BITTelemetryData *)item completionHandler:(nullable void (^)(void))completionHandler { if (!item) { // Item is nil: Do not enqueue item and abort operation. BITHockeyLogWarning(@"WARNING: TelemetryItem was nil."); if(completionHandler) { completionHandler(); } return; } // First assigning self to weakSelf and then assigning this to strongSelf in the block is not very intuitive, this // blog post explains it very well: https://dhoerl.wordpress.com/2013/04/23/i-finally-figured-out-weakself-and-strongself/ __weak typeof(self) weakSelf = self; dispatch_async(self.dataItemsOperations, ^{ typeof(self) strongSelf = weakSelf; if (strongSelf.isQueueBusy) { // Case 1: Channel is in blocked state: Trigger sender, start timer to check after again after a while and abort operation. BITHockeyLogDebug(@"INFO: The channel is saturated. %@ was dropped.", item.debugDescription); if (![strongSelf timerIsRunning]) { [strongSelf startTimer]; } if(completionHandler) { completionHandler(); } return; } // Should be outside of @synchronized block! BOOL applicationIsInBackground = ([BITHockeyHelper applicationState] == BITApplicationStateBackground); // Enqueue item. @synchronized(self) { NSDictionary *dict = [strongSelf dictionaryForTelemetryData:item]; [strongSelf appendDictionaryToEventBuffer:dict]; // If the app is running in the background. if (strongSelf.dataItemCount >= strongSelf.maxBatchSize || applicationIsInBackground) { // Case 2: Max batch count has been reached or the app is running in the background, so write queue to disk and delete all items. [strongSelf persistDataItemQueue:&BITTelemetryEventBuffer]; } else if (strongSelf.dataItemCount > 0) { // Case 3: It is the first item, let's start the timer. if (![strongSelf timerIsRunning]) { [strongSelf startTimer]; } } if(completionHandler) { completionHandler(); } } }); } #pragma mark - Envelope telemerty items - (NSDictionary *)dictionaryForTelemetryData:(BITTelemetryData *) telemetryData { BITEnvelope *envelope = [self envelopeForTelemetryData:telemetryData]; NSDictionary *dict = [envelope serializeToDictionary]; return dict; } - (BITEnvelope *)envelopeForTelemetryData:(BITTelemetryData *)telemetryData { telemetryData.version = @(BITSchemaVersion); BITData *data = [BITData new]; data.baseData = telemetryData; data.baseType = telemetryData.dataTypeName; BITEnvelope *envelope = [BITEnvelope new]; envelope.time = bit_utcDateString([NSDate date]); envelope.iKey = self.telemetryContext.appIdentifier; envelope.tags = self.telemetryContext.contextDictionary; envelope.data = data; envelope.name = telemetryData.envelopeTypeName; return envelope; } #pragma mark - Serialization Helper - (NSString *)serializeDictionaryToJSONString:(NSDictionary *)dictionary { NSError *error; NSData *data = [NSJSONSerialization dataWithJSONObject:dictionary options:(NSJSONWritingOptions)0 error:&error]; if (!data) { BITHockeyLogError(@"ERROR: JSONSerialization error: %@", error.localizedDescription); return @"{}"; } else { return (NSString *)[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; } } #pragma mark JSON Stream - (void)appendDictionaryToEventBuffer:(NSDictionary *)dictionary { if (dictionary) { NSString *string = [self serializeDictionaryToJSONString:dictionary]; // Since we can't persist every event right away, we write it to a simple C string. // This can then be written to disk by a signal handler in case of a crash. @synchronized (self) { bit_appendStringToEventBuffer(string, &BITTelemetryEventBuffer); self.dataItemCount += 1; } BITHockeyLogVerbose(@"VERBOSE: Appended data to buffer:\n%@", string); } } void bit_appendStringToEventBuffer(NSString *string, char **eventBuffer) { if (eventBuffer == NULL) { return; } if (!string) { return; } if (*eventBuffer == NULL || strlen(*eventBuffer) == 0) { bit_resetEventBuffer(eventBuffer); } if (string.length == 0) { return; } do { char *newBuffer = NULL; char *previousBuffer = *eventBuffer; // Concatenate old string with new JSON string and add a comma. asprintf(&newBuffer, "%s%.*s\n", previousBuffer, (int)MIN(string.length, (NSUInteger)INT_MAX), string.UTF8String); // Compare newBuffer and previousBuffer. If they point to the same address, we are safe to use them. if (atomic_compare_exchange_strong((atomic_charptr *)eventBuffer, &previousBuffer, newBuffer)) { // Free the intermediate pointer. free(previousBuffer); return; } else { // newBuffer has been changed by another thread. free(newBuffer); } } while (true); } void bit_resetEventBuffer(char **eventBuffer) { if (!eventBuffer) { return; } char *prevString = NULL; char *newEmptyString = strdup(""); do { prevString = *eventBuffer; // Compare pointers to strings to make sure we are still threadsafe! if (atomic_compare_exchange_strong((atomic_charptr *)eventBuffer, &prevString, newEmptyString)) { free(prevString); return; } } while(true); } #pragma mark - Batching - (NSUInteger)maxBatchSize { if(_maxBatchSize <= 0){ return BITDefaultMaxBatchSize; } return _maxBatchSize; } - (void)invalidateTimer { @synchronized(self) { if (self.timerSource != nil) { dispatch_source_cancel((dispatch_source_t)self.timerSource); self.timerSource = nil; } } } -(BOOL)timerIsRunning { @synchronized(self) { return self.timerSource != nil; } } - (void)startTimer { @synchronized(self) { // Reset timer, if it is already running. [self invalidateTimer]; dispatch_source_t timerSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.dataItemsOperations); dispatch_source_set_timer(timerSource, dispatch_walltime(NULL, NSEC_PER_SEC * self.batchInterval), 1ull * NSEC_PER_SEC, 1ull * NSEC_PER_SEC); __weak typeof(self) weakSelf = self; dispatch_source_set_event_handler(timerSource, ^{ typeof(self) strongSelf = weakSelf; if (strongSelf) { if (strongSelf.dataItemCount > 0) { [strongSelf persistDataItemQueue:&BITTelemetryEventBuffer]; } else { strongSelf.channelBlocked = NO; } [strongSelf invalidateTimer]; } }); dispatch_resume(timerSource); self.timerSource = timerSource; } } /** * Send a BITHockeyBlockingChannelNotification to the main thread to notify observers that channel can't enqueue new items. * This is typically used to trigger sending. */ - (void)sendBlockingChannelNotification { dispatch_async(dispatch_get_main_queue(), ^{ BITHockeyLogDebug(@"Sending notification: %@", BITChannelBlockedNotification); [[NSNotificationCenter defaultCenter] postNotificationName:BITChannelBlockedNotification object:nil userInfo:nil]; }); } @end NS_ASSUME_NONNULL_END #endif /* HOCKEYSDK_FEATURE_METRICS */