Peter 76e5a7fab6 Add 'submodules/HockeySDK-iOS/' from commit 'c7d0c7026303253e2ac576c02655691e5d314fe2'
git-subtree-dir: submodules/HockeySDK-iOS
git-subtree-mainline: 085acd26c4432939403765234266e3c1be0f3dd9
git-subtree-split: c7d0c7026303253e2ac576c02655691e5d314fe2
2019-06-11 18:53:14 +01:00

506 lines
16 KiB
Objective-C

#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 <stdatomic.h>
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 */