diff --git a/CHANGELOG.md b/CHANGELOG.md index 94b557982d8..36dbe419969 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## unreleased +- perf: Improve locks in SentryScope #888 + ## 6.1.0-alpha.1 - fix: Change maxAttachmentSize from MiB to bytes #891 diff --git a/Sources/Sentry/Public/SentryUser.h b/Sources/Sentry/Public/SentryUser.h index 15244e25a63..ed5503b610a 100644 --- a/Sources/Sentry/Public/SentryUser.h +++ b/Sources/Sentry/Public/SentryUser.h @@ -9,27 +9,27 @@ NS_SWIFT_NAME(User) /** * Optional: Id of the user */ -@property (nonatomic, copy) NSString *userId; +@property (atomic, copy) NSString *userId; /** * Optional: Email of the user */ -@property (nonatomic, copy) NSString *_Nullable email; +@property (atomic, copy) NSString *_Nullable email; /** * Optional: Username */ -@property (nonatomic, copy) NSString *_Nullable username; +@property (atomic, copy) NSString *_Nullable username; /** * Optional: IP Address */ -@property (nonatomic, copy) NSString *_Nullable ipAddress; +@property (atomic, copy) NSString *_Nullable ipAddress; /** * Optional: Additional data */ -@property (nonatomic, strong) NSDictionary *_Nullable data; +@property (atomic, strong) NSDictionary *_Nullable data; /** * Initializes a SentryUser with the id diff --git a/Sources/Sentry/SentryScope.m b/Sources/Sentry/SentryScope.m index 88f755eeef5..deb3f626d52 100644 --- a/Sources/Sentry/SentryScope.m +++ b/Sources/Sentry/SentryScope.m @@ -21,23 +21,23 @@ /** * Set global tags -> these will be sent with every event */ -@property (nonatomic, strong) NSMutableDictionary *_Nullable tagDictionary; +@property (atomic, strong) NSMutableDictionary *tagDictionary; /** * Set global extra -> these will be sent with every event */ -@property (nonatomic, strong) NSMutableDictionary *_Nullable extraDictionary; +@property (atomic, strong) NSMutableDictionary *extraDictionary; /** * used to add values in event context. */ -@property (nonatomic, strong) - NSMutableDictionary *> *_Nullable contextDictionary; +@property (atomic, strong) + NSMutableDictionary *> *contextDictionary; /** * Contains the breadcrumbs which will be sent with the event */ -@property (nonatomic, strong) NSMutableArray *breadcrumbArray; +@property (atomic, strong) NSMutableArray *breadcrumbArray; /** * This distribution of the application. @@ -52,7 +52,7 @@ /** * Set the fingerprint of an event to determine the grouping */ -@property (atomic, strong) NSArray *_Nullable fingerprintArray; +@property (atomic, strong) NSMutableArray *fingerprintArray; /** * SentryLevel of the event @@ -74,7 +74,12 @@ - (instancetype)initWithMaxBreadcrumbs:(NSInteger)maxBreadcrumbs if (self = [super init]) { self.listeners = [NSMutableArray new]; self.maxBreadcrumbs = maxBreadcrumbs; - [self clear]; + self.breadcrumbArray = [NSMutableArray new]; + self.tagDictionary = [NSMutableDictionary new]; + self.extraDictionary = [NSMutableDictionary new]; + self.contextDictionary = [NSMutableDictionary new]; + self.attachmentArray = [NSMutableArray new]; + self.fingerprintArray = [NSMutableArray new]; } return self; } @@ -87,17 +92,18 @@ - (instancetype)init - (instancetype)initWithScope:(SentryScope *)scope { if (self = [self init]) { - self.extraDictionary = scope.extraDictionary.mutableCopy; - self.tagDictionary = scope.tagDictionary.mutableCopy; + [_extraDictionary addEntriesFromDictionary:[scope extras]]; + [_tagDictionary addEntriesFromDictionary:[scope tags]]; + [_contextDictionary addEntriesFromDictionary:[scope context]]; + [_breadcrumbArray addObjectsFromArray:[scope breadcrumbs]]; + [_fingerprintArray addObjectsFromArray:[scope fingerprints]]; + [_attachmentArray addObjectsFromArray:[scope attachments]]; + self.maxBreadcrumbs = scope.maxBreadcrumbs; self.userObject = scope.userObject.copy; - self.contextDictionary = scope.contextDictionary.mutableCopy; - self.breadcrumbArray = scope.breadcrumbArray.mutableCopy; self.distString = scope.distString; self.environmentString = scope.environmentString; self.levelEnum = scope.levelEnum; - self.fingerprintArray = scope.fingerprintArray.mutableCopy; - self.attachmentArray = scope.attachmentArray.mutableCopy; } return self; } @@ -108,10 +114,10 @@ - (void)addBreadcrumb:(SentryBreadcrumb *)crumb { [SentryLog logWithMessage:[NSString stringWithFormat:@"Add breadcrumb: %@", crumb] andLevel:kSentryLogLevelDebug]; - @synchronized(self) { - [self.breadcrumbArray addObject:crumb]; - if ([self.breadcrumbArray count] > self.maxBreadcrumbs) { - [self.breadcrumbArray removeObjectAtIndex:0]; + @synchronized(_breadcrumbArray) { + [_breadcrumbArray addObject:crumb]; + if ([_breadcrumbArray count] > self.maxBreadcrumbs) { + [_breadcrumbArray removeObjectAtIndex:0]; } } [self notifyListeners]; @@ -119,57 +125,87 @@ - (void)addBreadcrumb:(SentryBreadcrumb *)crumb - (void)clear { - @synchronized(self) { - self.breadcrumbArray = [NSMutableArray new]; - self.userObject = nil; - self.tagDictionary = [NSMutableDictionary new]; - self.extraDictionary = [NSMutableDictionary new]; - self.contextDictionary = [NSMutableDictionary new]; - self.distString = nil; - self.environmentString = nil; - self.levelEnum = kSentryLevelNone; - self.fingerprintArray = [NSMutableArray new]; - self.attachmentArray = [NSMutableArray new]; + // As we need to synchronize the accesses of the arrays and dictionaries and we use the + // references instead of self we remove all objects instead of creating new instances. Removing + // all objects is usually O(n). This is acceptable as we don't expect a huge amount of elements + // in the arrays or dictionaries, that would slow down the performance. + @synchronized(_breadcrumbArray) { + [_breadcrumbArray removeAllObjects]; + } + @synchronized(_tagDictionary) { + [_tagDictionary removeAllObjects]; + } + @synchronized(_extraDictionary) { + [_extraDictionary removeAllObjects]; + } + @synchronized(_contextDictionary) { + [_contextDictionary removeAllObjects]; + } + @synchronized(_fingerprintArray) { + [_fingerprintArray removeAllObjects]; } + @synchronized(_attachmentArray) { + [_attachmentArray removeAllObjects]; + } + + self.userObject = nil; + self.distString = nil; + self.environmentString = nil; + self.levelEnum = kSentryLevelNone; + [self notifyListeners]; } - (void)clearBreadcrumbs { - @synchronized(self) { - [self.breadcrumbArray removeAllObjects]; + @synchronized(_breadcrumbArray) { + [_breadcrumbArray removeAllObjects]; } [self notifyListeners]; } +- (NSArray *)breadcrumbs +{ + @synchronized(_breadcrumbArray) { + return _breadcrumbArray.copy; + } +} + - (void)setContextValue:(NSDictionary *)value forKey:(NSString *)key { - @synchronized(self) { - [self.contextDictionary setValue:value forKey:key]; + @synchronized(_contextDictionary) { + [_contextDictionary setValue:value forKey:key]; } [self notifyListeners]; } - (void)removeContextForKey:(NSString *)key { - @synchronized(self) { - [self.contextDictionary removeObjectForKey:key]; + @synchronized(_contextDictionary) { + [_contextDictionary removeObjectForKey:key]; } [self notifyListeners]; } +- (NSDictionary *> *)context +{ + @synchronized(_contextDictionary) { + return _contextDictionary.copy; + } +} + - (void)setExtraValue:(id _Nullable)value forKey:(NSString *)key { - @synchronized(self) { - [self.extraDictionary setValue:value forKey:key]; + @synchronized(_extraDictionary) { + [_extraDictionary setValue:value forKey:key]; } [self notifyListeners]; } - (void)removeExtraForKey:(NSString *)key { - @synchronized(self) { - [self.extraDictionary removeObjectForKey:key]; + @synchronized(_extraDictionary) { + [_extraDictionary removeObjectForKey:key]; } [self notifyListeners]; } @@ -179,24 +215,31 @@ - (void)setExtras:(NSDictionary *_Nullable)extras if (extras == nil) { return; } - @synchronized(self) { - [self.extraDictionary addEntriesFromDictionary:extras]; + @synchronized(_extraDictionary) { + [_extraDictionary addEntriesFromDictionary:extras]; } [self notifyListeners]; } +- (NSDictionary *)extras +{ + @synchronized(_extraDictionary) { + return _extraDictionary.copy; + } +} + - (void)setTagValue:(NSString *)value forKey:(NSString *)key { - @synchronized(self) { - self.tagDictionary[key] = value; + @synchronized(_tagDictionary) { + _tagDictionary[key] = value; } [self notifyListeners]; } - (void)removeTagForKey:(NSString *)key { - @synchronized(self) { - [self.tagDictionary removeObjectForKey:key]; + @synchronized(_tagDictionary) { + [_tagDictionary removeObjectForKey:key]; } [self notifyListeners]; } @@ -206,12 +249,19 @@ - (void)setTags:(NSDictionary *_Nullable)tags if (tags == nil) { return; } - @synchronized(self) { - [self.tagDictionary addEntriesFromDictionary:tags]; + @synchronized(_tagDictionary) { + [_tagDictionary addEntriesFromDictionary:tags]; } [self notifyListeners]; } +- (NSDictionary *)tags +{ + @synchronized(_tagDictionary) { + return _tagDictionary.copy; + } +} + - (void)setUser:(SentryUser *_Nullable)user { self.userObject = user; @@ -232,17 +282,23 @@ - (void)setEnvironment:(NSString *_Nullable)environment - (void)setFingerprint:(NSArray *_Nullable)fingerprint { - @synchronized(self) { - if (fingerprint == nil) { - self.fingerprintArray = [NSArray new]; - } else { - self.fingerprintArray = fingerprint.mutableCopy; + @synchronized(_fingerprintArray) { + [_fingerprintArray removeAllObjects]; + if (fingerprint != nil) { + [_fingerprintArray addObjectsFromArray:fingerprint]; } - self.fingerprintArray = fingerprint; } + [self notifyListeners]; } +- (NSArray *)fingerprints +{ + @synchronized(_fingerprintArray) { + return _fingerprintArray.copy; + } +} + - (void)setLevel:(enum SentryLevel)level { self.levelEnum = level; @@ -251,142 +307,134 @@ - (void)setLevel:(enum SentryLevel)level - (void)addAttachment:(SentryAttachment *)attachment { - @synchronized(self.attachmentArray) { - [self.attachmentArray addObject:attachment]; + @synchronized(_attachmentArray) { + [_attachmentArray addObject:attachment]; } } - (NSArray *)attachments { - @synchronized(self.attachmentArray) { - return self.attachmentArray.copy; + @synchronized(_attachmentArray) { + return _attachmentArray.copy; } } - (NSDictionary *)serialize { - @synchronized(self) { - NSMutableDictionary *serializedData = [NSMutableDictionary new]; - [serializedData setValue:[self.tagDictionary copy] forKey:@"tags"]; - [serializedData setValue:[self.extraDictionary copy] forKey:@"extra"]; - [serializedData setValue:[self.contextDictionary copy] forKey:@"context"]; - [serializedData setValue:[self.userObject serialize] forKey:@"user"]; - [serializedData setValue:self.distString forKey:@"dist"]; - [serializedData setValue:self.environmentString forKey:@"environment"]; - [serializedData setValue:[self.fingerprintArray copy] forKey:@"fingerprint"]; - if (self.levelEnum != kSentryLevelNone) { - [serializedData setValue:SentryLevelNames[self.levelEnum] forKey:@"level"]; - } - NSArray *crumbs = [self serializeBreadcrumbs]; - if (crumbs.count > 0) { - [serializedData setValue:crumbs forKey:@"breadcrumbs"]; - } - return serializedData; + NSMutableDictionary *serializedData = [NSMutableDictionary new]; + [serializedData setValue:[self tags] forKey:@"tags"]; + [serializedData setValue:[self extras] forKey:@"extra"]; + [serializedData setValue:[self context] forKey:@"context"]; + [serializedData setValue:[self.userObject serialize] forKey:@"user"]; + [serializedData setValue:self.distString forKey:@"dist"]; + [serializedData setValue:self.environmentString forKey:@"environment"]; + [serializedData setValue:[self fingerprints] forKey:@"fingerprint"]; + + SentryLevel level = self.levelEnum; + if (level != kSentryLevelNone) { + [serializedData setValue:SentryLevelNames[level] forKey:@"level"]; + } + NSArray *crumbs = [self serializeBreadcrumbs]; + if (crumbs.count > 0) { + [serializedData setValue:crumbs forKey:@"breadcrumbs"]; } + return serializedData; } - (NSArray *)serializeBreadcrumbs { - NSMutableArray *crumbs = [NSMutableArray new]; + NSMutableArray *serializedCrumbs = [NSMutableArray new]; - for (SentryBreadcrumb *crumb in self.breadcrumbArray) { - [crumbs addObject:[crumb serialize]]; + NSArray *crumbs = [self breadcrumbs]; + for (SentryBreadcrumb *crumb in crumbs) { + [serializedCrumbs addObject:[crumb serialize]]; } - return crumbs; + return serializedCrumbs; } - (void)applyToSession:(SentrySession *)session { - @synchronized(self) { - if (nil != self.userObject) { - session.user = self.userObject.copy; - } + SentryUser *userObject = self.userObject; + if (nil != userObject) { + session.user = userObject.copy; + } - NSString *environment = self.environmentString; - if (nil != environment) { - // TODO: Make sure environment set on options is applied to the - // scope so it's available now - session.environment = environment; - } + NSString *environment = self.environmentString; + if (nil != environment) { + // TODO: Make sure environment set on options is applied to the + // scope so it's available now + session.environment = environment; } } - (SentryEvent *__nullable)applyToEvent:(SentryEvent *)event maxBreadcrumb:(NSUInteger)maxBreadcrumbs { - @synchronized(self) { - if (nil != self.tagDictionary) { - if (nil == event.tags) { - event.tags = self.tagDictionary.copy; - } else { - NSMutableDictionary *newTags = [NSMutableDictionary new]; - [newTags addEntriesFromDictionary:self.tagDictionary]; - [newTags addEntriesFromDictionary:event.tags]; - event.tags = newTags; - } - } - - if (nil != self.extraDictionary) { - if (nil == event.extra) { - event.extra = self.extraDictionary.copy; - } else { - NSMutableDictionary *newExtra = [NSMutableDictionary new]; - [newExtra addEntriesFromDictionary:self.extraDictionary]; - [newExtra addEntriesFromDictionary:event.extra]; - event.extra = newExtra; - } - } + if (nil == event.tags) { + event.tags = [self tags]; + } else { + NSMutableDictionary *newTags = [NSMutableDictionary new]; + [newTags addEntriesFromDictionary:[self tags]]; + [newTags addEntriesFromDictionary:event.tags]; + event.tags = newTags; + } - if (nil != self.userObject) { - event.user = self.userObject.copy; - } + if (nil == event.extra) { + event.extra = [self extras]; + } else { + NSMutableDictionary *newExtra = [NSMutableDictionary new]; + [newExtra addEntriesFromDictionary:[self extras]]; + [newExtra addEntriesFromDictionary:event.extra]; + event.extra = newExtra; + } - NSString *dist = self.distString; - if (nil != dist && nil == event.dist) { - // dist can also be set via options but scope takes precedence. - event.dist = dist; - } + NSArray *fingerprints = [self fingerprints]; + if (fingerprints.count > 0 && nil == event.fingerprint) { + event.fingerprint = fingerprints; + } - NSString *environment = self.environmentString; - if (nil != environment && nil == event.environment) { - // environment can also be set via options but scope takes - // precedence. - event.environment = environment; - } + if (nil == event.breadcrumbs) { + NSArray *breadcrumbs = [self breadcrumbs]; + event.breadcrumbs = [breadcrumbs + subarrayWithRange:NSMakeRange(0, MIN(maxBreadcrumbs, [breadcrumbs count]))]; + } - NSArray *fingerprint = self.fingerprintArray; - if (fingerprint.count > 0 && nil == event.fingerprint) { - event.fingerprint = fingerprint.mutableCopy; - } + if (nil == event.context) { + event.context = [self context]; + } else { + NSMutableDictionary *newContext = [NSMutableDictionary new]; + [newContext addEntriesFromDictionary:[self context]]; + [newContext addEntriesFromDictionary:event.context]; + event.context = newContext; + } - if (self.levelEnum != kSentryLevelNone) { - // We always want to set the level from the scope since this has - // benn set on purpose - event.level = self.levelEnum; - } + SentryUser *user = self.userObject.copy; + if (nil != user) { + event.user = user; + } - if (nil != self.breadcrumbArray) { - if (nil == event.breadcrumbs) { - event.breadcrumbs = [self.breadcrumbArray - subarrayWithRange:NSMakeRange( - 0, MIN(maxBreadcrumbs, [self.breadcrumbArray count]))]; - } - } + NSString *dist = self.distString; + if (nil != dist && nil == event.dist) { + // dist can also be set via options but scope takes precedence. + event.dist = dist; + } - if (nil != self.contextDictionary) { - if (nil == event.context) { - event.context = self.contextDictionary; - } else { - NSMutableDictionary *newContext = [NSMutableDictionary new]; - [newContext addEntriesFromDictionary:self.contextDictionary]; - [newContext addEntriesFromDictionary:event.context]; - event.context = newContext; - } - } + NSString *environment = self.environmentString; + if (nil != environment && nil == event.environment) { + // environment can also be set via options but scope takes + // precedence. + event.environment = environment; + } - return event; + SentryLevel level = self.levelEnum; + if (level != kSentryLevelNone) { + // We always want to set the level from the scope since this has + // benn set on purpose + event.level = level; } + + return event; } @end diff --git a/Sources/Sentry/SentryUser.m b/Sources/Sentry/SentryUser.m index 710ce2c0bc7..66f5b50bada 100644 --- a/Sources/Sentry/SentryUser.m +++ b/Sources/Sentry/SentryUser.m @@ -23,12 +23,14 @@ - (id)copyWithZone:(nullable NSZone *)zone { SentryUser *copy = [[SentryUser allocWithZone:zone] init]; - if (copy != nil) { - copy.userId = self.userId; - copy.email = self.email; - copy.username = self.username; - copy.ipAddress = self.ipAddress; - copy.data = self.data.mutableCopy; + @synchronized(self) { + if (copy != nil) { + copy.userId = self.userId; + copy.email = self.email; + copy.username = self.username; + copy.ipAddress = self.ipAddress; + copy.data = self.data.copy; + } } return copy; @@ -38,55 +40,76 @@ - (id)copyWithZone:(nullable NSZone *)zone { NSMutableDictionary *serializedData = [[NSMutableDictionary alloc] init]; - [serializedData setValue:self.userId forKey:@"id"]; - [serializedData setValue:self.email forKey:@"email"]; - [serializedData setValue:self.username forKey:@"username"]; - [serializedData setValue:self.ipAddress forKey:@"ip_address"]; - [serializedData setValue:[self.data sentry_sanitize] forKey:@"data"]; + @synchronized(self) { + [serializedData setValue:self.userId forKey:@"id"]; + [serializedData setValue:self.email forKey:@"email"]; + [serializedData setValue:self.username forKey:@"username"]; + [serializedData setValue:self.ipAddress forKey:@"ip_address"]; + [serializedData setValue:[self.data sentry_sanitize] forKey:@"data"]; + } return serializedData; } - (BOOL)isEqual:(id _Nullable)other { - if (other == self) - return YES; - if (!other || ![[other class] isEqual:[self class]]) - return NO; + @synchronized(self) { + if (other == self) + return YES; + if (!other || ![[other class] isEqual:[self class]]) + return NO; - return [self isEqualToUser:other]; + return [self isEqualToUser:other]; + } } - (BOOL)isEqualToUser:(SentryUser *)user { - if (self == user) + @synchronized(self) { + // We need to get some local copies of the properties, because they could be modified during + // the if statements + + if (self == user) + return YES; + if (user == nil) + return NO; + + NSString *otherUserId = user.userId; + if (self.userId != otherUserId && ![self.userId isEqualToString:otherUserId]) + return NO; + + NSString *otherEmail = user.email; + if (self.email != otherEmail && ![self.email isEqualToString:otherEmail]) + return NO; + + NSString *otherUsername = user.username; + if (self.username != otherUsername && ![self.username isEqualToString:otherUsername]) + return NO; + + NSString *otherIpAdress = user.ipAddress; + if (self.ipAddress != otherIpAdress && ![self.ipAddress isEqualToString:otherIpAdress]) + return NO; + + NSDictionary *otherUserData = user.data; + if (self.data != otherUserData && ![self.data isEqualToDictionary:otherUserData]) + return NO; return YES; - if (user == nil) - return NO; - if (self.userId != user.userId && ![self.userId isEqualToString:user.userId]) - return NO; - if (self.email != user.email && ![self.email isEqualToString:user.email]) - return NO; - if (self.username != user.username && ![self.username isEqualToString:user.username]) - return NO; - if (self.ipAddress != user.ipAddress && ![self.ipAddress isEqualToString:user.ipAddress]) - return NO; - if (self.data != user.data && ![self.data isEqualToDictionary:user.data]) - return NO; - return YES; + } } - (NSUInteger)hash { - NSUInteger hash = 17; + @synchronized(self) { + NSUInteger hash = 17; - hash = hash * 23 + [self.userId hash]; - hash = hash * 23 + [self.email hash]; - hash = hash * 23 + [self.username hash]; - hash = hash * 23 + [self.ipAddress hash]; - hash = hash * 23 + [self.data hash]; + hash = hash * 23 + [self.userId hash]; + hash = hash * 23 + [self.email hash]; + hash = hash * 23 + [self.username hash]; + hash = hash * 23 + [self.ipAddress hash]; + hash = hash * 23 + [self.data hash]; - return hash; + return hash; + } } @end diff --git a/Tests/SentryTests/Protocol/SentryUserTests.swift b/Tests/SentryTests/Protocol/SentryUserTests.swift index 92df6d6e82a..d9e9dced0d5 100644 --- a/Tests/SentryTests/Protocol/SentryUserTests.swift +++ b/Tests/SentryTests/Protocol/SentryUserTests.swift @@ -76,4 +76,52 @@ class SentryUserTests: XCTestCase { XCTAssertEqual(TestData.user, copiedUser) } + + // Altough we only run this test above the below specified versions, we exped the + // implementation to be thread safe + // With this test we test if modifications from multiple threads don't lead to a crash. + @available(tvOS 10.0, *) + @available(OSX 10.12, *) + @available(iOS 10.0, *) + func testModifyingFromMultipleThreads() { + let queue = DispatchQueue(label: "SentryScopeTests", qos: .userInteractive, attributes: [.concurrent, .initiallyInactive]) + let group = DispatchGroup() + + let user = TestData.user.copy() as! User + + for i in Array(0...20) { + group.enter() + queue.async { + + // The number is kept small for the CI to not take to long. + // If you really want to test this increase to 100_000 or so. + for _ in Array (0...1_000) { + + // Simulate some real world modifications of the user + + user.userId = "\(i)" + + // Trigger is equal + user.isEqual(to: TestData.user) + user.serialize() + + user.serialize() + + user.email = "john@example.com" + user.username = "\(i)" + user.ipAddress = "\(i)" + + user.data?["\(i)"] = "\(i)" + + // Trigger hash + XCTAssertNotNil([user: user]) + } + + group.leave() + } + } + + queue.activate() + group.waitWithTimeout() + } } diff --git a/Tests/SentryTests/SentryScopeSwiftTests.swift b/Tests/SentryTests/SentryScopeSwiftTests.swift index 9846151fdb3..fa19789500c 100644 --- a/Tests/SentryTests/SentryScopeSwiftTests.swift +++ b/Tests/SentryTests/SentryScopeSwiftTests.swift @@ -214,4 +214,81 @@ class SentryScopeSwiftTests: XCTestCase { XCTAssertEqual(1, attachments.count) } + + // Altough we only run this test above the below specified versions, we exped the + // implementation to be thread safe + // With this test we test if modifications from multiple threads don't lead to a crash. + @available(tvOS 10.0, *) + @available(OSX 10.12, *) + @available(iOS 10.0, *) + func testModifyingFromMultipleThreads() { + let queue = DispatchQueue(label: "SentryScopeTests", qos: .userInteractive, attributes: [.concurrent, .initiallyInactive]) + let group = DispatchGroup() + + let scope = fixture.scope + + for _ in Array(0...20) { + group.enter() + queue.async { + + // The number is kept small for the CI to not take to long. + // If you really want to test this increase to 100_000 or so. + for _ in Array(0...1_000) { + + // Simulate some real world modifications of the user + + let key = "key" + + _ = Scope(scope: scope) + + for _ in Array(0...100) { + scope.add(self.fixture.breadcrumb) + } + + scope.serialize() + scope.clearBreadcrumbs() + scope.add(self.fixture.breadcrumb) + + scope.apply(to: SentrySession(releaseName: "1.0.0")) + + scope.setFingerprint(nil) + scope.setFingerprint(["finger", "print"]) + + scope.setContext(value: ["some": "value"], key: key) + scope.removeContext(key: key) + + scope.setExtra(value: 1, key: key) + scope.removeExtra(key: key) + scope.setExtras(["value": "1", "value2": "2"]) + + scope.apply(to: TestData.event, maxBreadcrumb: 5) + + scope.setTag(value: "value", key: key) + scope.removeTag(key: key) + scope.setTags(["tag1": "hello", "tag2": "hello"]) + + scope.add(TestData.fileAttachment) + + for _ in Array(0...10) { + scope.add(self.fixture.breadcrumb) + } + scope.serialize() + + scope.setUser(self.fixture.user) + scope.setDist("dist") + scope.setEnvironment("env") + scope.setLevel(SentryLevel.debug) + + scope.apply(to: SentrySession(releaseName: "1.0.0")) + scope.apply(to: TestData.event, maxBreadcrumb: 5) + + scope.serialize() + } + + group.leave() + } + } + + queue.activate() + group.waitWithTimeout() } }