mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00

git-subtree-dir: submodules/HockeySDK-iOS git-subtree-mainline: 085acd26c4432939403765234266e3c1be0f3dd9 git-subtree-split: c7d0c7026303253e2ac576c02655691e5d314fe2
506 lines
16 KiB
Objective-C
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 */
|
|
|