Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Plat-8841] feature flags order #1481

Merged
merged 1 commit into from
Sep 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Bugsnag/BugsnagInternals.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
#import "BugsnagHandledState.h"
#import "BugsnagNotifier.h"

typedef NSMutableArray<BugsnagFeatureFlag *> BSGFeatureFlagStore;
@interface BSGFeatureFlagStore : NSObject <NSCopying>
@end

NS_ASSUME_NONNULL_BEGIN

Expand Down
8 changes: 4 additions & 4 deletions Bugsnag/Client/BugsnagClient.m
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ - (instancetype)initWithConfiguration:(BugsnagConfiguration *)configuration {
[_configuration setUser:[BSG_KSSystemInfo deviceAndAppHash] withEmail:_configuration.user.email andName:_configuration.user.name];
}

_featureFlagStore = [configuration.featureFlagStore mutableCopy];
_featureFlagStore = [configuration.featureFlagStore copy];

_state = [[BugsnagMetadata alloc] initWithDictionary:@{
BSGKeyClient: @{
Expand Down Expand Up @@ -762,7 +762,7 @@ - (void)notifyInternal:(BugsnagEvent *_Nonnull)event
// App hang events will already contain feature flags
if (!event.featureFlagStore.count) {
@synchronized (self.featureFlagStore) {
event.featureFlagStore = [self.featureFlagStore mutableCopy];
event.featureFlagStore = [self.featureFlagStore copy];
}
}

Expand Down Expand Up @@ -976,7 +976,7 @@ - (void)setObserver:(BSGClientObserver)observer {
};

@synchronized (self.featureFlagStore) {
for (BugsnagFeatureFlag *flag in self.featureFlagStore) {
for (BugsnagFeatureFlag *flag in self.featureFlagStore.allFlags) {
observer(BSGClientObserverAddFeatureFlag, flag);
}
}
Expand Down Expand Up @@ -1040,7 +1040,7 @@ - (void)appHangDetectedAtDate:(NSDate *)date withThreads:(NSArray<BugsnagThread
self.appHangEvent.context = self.context;

@synchronized (self.featureFlagStore) {
self.appHangEvent.featureFlagStore = [self.featureFlagStore mutableCopy];
self.appHangEvent.featureFlagStore = [self.featureFlagStore copy];
}

[self.appHangEvent symbolicateIfNeeded];
Expand Down
21 changes: 21 additions & 0 deletions Bugsnag/Helpers/BSGFeatureFlagStore.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
//

#import "BugsnagInternals.h"
#import "BSGDefines.h"

NS_ASSUME_NONNULL_BEGIN

Expand All @@ -20,4 +21,24 @@ NSArray<NSDictionary *> * BSGFeatureFlagStoreToJSON(BSGFeatureFlagStore *store);

BSGFeatureFlagStore * BSGFeatureFlagStoreFromJSON(id _Nullable json);


BSG_OBJC_DIRECT_MEMBERS
@interface BSGFeatureFlagStore ()

@property(nonatomic,nonnull,readonly) NSArray<BugsnagFeatureFlag *> * allFlags;

+ (nonnull BSGFeatureFlagStore *) fromJSON:(nonnull id)json;

- (NSUInteger) count;

- (void) addFeatureFlag:(nonnull NSString *)name withVariant:(nullable NSString *)variant;

- (void) addFeatureFlags:(nonnull NSArray<BugsnagFeatureFlag *> *)featureFlags;

- (void) clear:(nullable NSString *)name;

- (nonnull NSArray<NSDictionary *> *) toJSON;

@end

NS_ASSUME_NONNULL_END
166 changes: 141 additions & 25 deletions Bugsnag/Helpers/BSGFeatureFlagStore.m
Original file line number Diff line number Diff line change
Expand Up @@ -11,43 +11,50 @@
#import "BSGKeys.h"
#import "BugsnagFeatureFlag.h"

static void internalAddFeatureFlag(BSGFeatureFlagStore *store, BugsnagFeatureFlag *flag) {
[store removeObject:flag];
[store addObject:flag];
}

void BSGFeatureFlagStoreAddFeatureFlag(BSGFeatureFlagStore *store, NSString *name, NSString *_Nullable variant) {
internalAddFeatureFlag(store, [BugsnagFeatureFlag flagWithName:name variant:variant]);
[store addFeatureFlag:name withVariant:variant];
}

void BSGFeatureFlagStoreAddFeatureFlags(BSGFeatureFlagStore *store, NSArray<BugsnagFeatureFlag *> *featureFlags) {
for (BugsnagFeatureFlag *featureFlag in featureFlags) {
internalAddFeatureFlag(store, featureFlag);
}
[store addFeatureFlags:featureFlags];
}

void BSGFeatureFlagStoreClear(BSGFeatureFlagStore *store, NSString *_Nullable name) {
if (name) {
[store removeObject:[BugsnagFeatureFlag flagWithName:(NSString * _Nonnull)name]];
} else {
[store removeAllObjects];
}
[store clear:name];
}

NSArray<NSDictionary *> * BSGFeatureFlagStoreToJSON(BSGFeatureFlagStore *store) {
NSMutableArray<NSDictionary *> *result = [NSMutableArray array];
for (BugsnagFeatureFlag *flag in store) {
if (flag.variant) {
[result addObject:@{BSGKeyFeatureFlag: flag.name, BSGKeyVariant: (NSString * _Nonnull)flag.variant}];
} else {
[result addObject:@{BSGKeyFeatureFlag: flag.name}];
}
}
return result;
return [store toJSON];
}

BSGFeatureFlagStore * BSGFeatureFlagStoreFromJSON(id json) {
BSGFeatureFlagStore *store = [NSMutableArray array];
return [BSGFeatureFlagStore fromJSON:json];
}


/**
* Stores feature flags as a dictionary containing the flag name as a key, with the
* value being the index into an array containing the complete feature flag.
*
* Removals leave holes in the array, which gets rebuilt on clear once there are too many holes.
*
* This gives the access speed of a dictionary while keeping ordering intact.
*/
BSG_OBJC_DIRECT_MEMBERS
@interface BSGFeatureFlagStore ()
kstenerud marked this conversation as resolved.
Show resolved Hide resolved

@property(nonatomic, readwrite) NSMutableArray *flags;
@property(nonatomic, readwrite) NSMutableDictionary *indices;

@end

static const int REBUILD_AT_HOLE_COUNT = 1000;

BSG_OBJC_DIRECT_MEMBERS
@implementation BSGFeatureFlagStore
kstenerud marked this conversation as resolved.
Show resolved Hide resolved

+ (nonnull BSGFeatureFlagStore *) fromJSON:(nonnull id)json {
BSGFeatureFlagStore *store = [BSGFeatureFlagStore new];
if ([json isKindOfClass:[NSArray class]]) {
for (id item in json) {
if ([item isKindOfClass:[NSDictionary class]]) {
Expand All @@ -57,10 +64,119 @@ void BSGFeatureFlagStoreClear(BSGFeatureFlagStore *store, NSString *_Nullable na
if (![variant isKindOfClass:[NSString class]]) {
variant = nil;
}
[store addObject:[BugsnagFeatureFlag flagWithName:featureFlag variant:variant]];
[store addFeatureFlag:featureFlag withVariant:variant];
}
}
}
}
return store;
}

- (nonnull instancetype) init {
if ((self = [super init]) != nil) {
_flags = [NSMutableArray new];
_indices = [NSMutableDictionary new];
}
return self;
}

static inline int getIndexFromDict(NSDictionary *dict, NSString *name) {
NSNumber *boxedIndex = dict[name];
if (boxedIndex == nil) {
return -1;
}
return boxedIndex.intValue;
}

- (NSUInteger) count {
nickdowell marked this conversation as resolved.
Show resolved Hide resolved
return self.indices.count;
}

- (nonnull NSArray<BugsnagFeatureFlag *> *) allFlags {
NSMutableArray<BugsnagFeatureFlag *> *flags = [NSMutableArray arrayWithCapacity:self.indices.count];
for (BugsnagFeatureFlag *flag in self.flags) {
if ([flag isKindOfClass:[BugsnagFeatureFlag class]]) {
[flags addObject:flag];
}
}
return flags;
}

- (void)rebuildIfTooManyHoles {
int holeCount = (int)self.flags.count - (int)self.indices.count;
if (holeCount < REBUILD_AT_HOLE_COUNT) {
return;
}

NSMutableArray *newFlags = [NSMutableArray arrayWithCapacity:self.indices.count];
NSMutableDictionary *newIndices = [NSMutableDictionary new];
for (BugsnagFeatureFlag *flag in self.flags) {
if ([flag isKindOfClass:[BugsnagFeatureFlag class]]) {
[newFlags addObject:flag];
}
}

for (NSUInteger i = 0; i < newFlags.count; i++) {
BugsnagFeatureFlag *flag = newFlags[i];
newIndices[flag.name] = @(i);
}
self.flags = newFlags;
self.indices = newIndices;
}

- (void) addFeatureFlag:(nonnull NSString *)name withVariant:(nullable NSString *)variant {
BugsnagFeatureFlag *flag = [BugsnagFeatureFlag flagWithName:name variant:variant];

int index = getIndexFromDict(self.indices, name);
if (index >= 0) {
self.flags[(unsigned)index] = flag;
} else {
index = (int)self.flags.count;
[self.flags addObject:flag];
self.indices[name] = @(index);
}
}

- (void) addFeatureFlags:(nonnull NSArray<BugsnagFeatureFlag *> *)featureFlags {
for (BugsnagFeatureFlag *flag in featureFlags) {
[self addFeatureFlag:flag.name withVariant:flag.variant];
}
}

- (void) clear:(nullable NSString *)name {
if (name != nil) {
int index = getIndexFromDict(self.indices, name);
if (index >= 0) {
self.flags[(unsigned)index] = [NSNull null];
[self.indices removeObjectForKey:(id)name];
[self rebuildIfTooManyHoles];
}
} else {
[self.indices removeAllObjects];
[self.flags removeAllObjects];
}
}

- (nonnull NSArray<NSDictionary *> *) toJSON {
NSMutableArray<NSDictionary *> *result = [NSMutableArray array];

for (BugsnagFeatureFlag *flag in self.flags) {
if ([flag isKindOfClass:[BugsnagFeatureFlag class]]) {
if (flag.variant) {
[result addObject:@{BSGKeyFeatureFlag:flag.name, BSGKeyVariant:(NSString *_Nonnull)flag.variant}];
} else {
[result addObject:@{BSGKeyFeatureFlag:flag.name}];
}
}
}
return result;
}

- (id)copyWithZone:(NSZone *)zone {
BSGFeatureFlagStore *store = [[BSGFeatureFlagStore allocWithZone:zone] init];
store.flags = [self.flags mutableCopy];
store.indices = [self.indices mutableCopy];
return store;
}

@end
2 changes: 1 addition & 1 deletion Bugsnag/Payload/BugsnagEvent.m
Original file line number Diff line number Diff line change
Expand Up @@ -760,7 +760,7 @@ - (void)setUnhandled:(BOOL)unhandled {
// MARK: - <BugsnagFeatureFlagStore>

- (NSArray<BugsnagFeatureFlag *> *)featureFlags {
return [self.featureFlagStore copy];
return self.featureFlagStore.allFlags;
}

- (void)addFeatureFlagWithName:(NSString *)name variant:(nullable NSString *)variant {
Expand Down
70 changes: 67 additions & 3 deletions Tests/BugsnagTests/BSGFeatureFlagStoreTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,12 @@ - (void)test {
XCTAssertEqualObjects(BSGFeatureFlagStoreToJSON(store),
(@[
@{@"featureFlag": @"featureC", @"variant": @"checked"},
@{@"featureFlag": @"featureA"},
@{@"featureFlag": @"featureB"},
@{@"featureFlag": @"featureA"}
]));

XCTAssertEqualObjects(BSGFeatureFlagStoreFromJSON(BSGFeatureFlagStoreToJSON(store)),
store);
XCTAssertEqualObjects(BSGFeatureFlagStoreToJSON(BSGFeatureFlagStoreFromJSON(BSGFeatureFlagStoreToJSON(store))),
BSGFeatureFlagStoreToJSON(store));

BSGFeatureFlagStoreClear(store, @"featureB");
XCTAssertEqualObjects(BSGFeatureFlagStoreToJSON(store),
Expand All @@ -62,4 +62,68 @@ - (void)test {
XCTAssertEqualObjects(BSGFeatureFlagStoreToJSON(store), @[]);
}

- (void)testAddRemoveMany {
// Tests that rebuildIfTooManyHoles works as expected

BSGFeatureFlagStore *store = [[BSGFeatureFlagStore alloc] init];

BSGFeatureFlagStoreAddFeatureFlag(store, @"blah", @"testing");
for (int j = 0; j < 10; j++) {
for (int i = 0; i < 1000; i++) {
NSString *name = [NSString stringWithFormat:@"%d-%d", j, i];
BSGFeatureFlagStoreAddFeatureFlag(store, name, nil);
if (i < 999) {
BSGFeatureFlagStoreClear(store, name);
}
}
}

XCTAssertEqualObjects(BSGFeatureFlagStoreToJSON(store),
(@[
@{@"featureFlag": @"blah", @"variant": @"testing"},
@{@"featureFlag": @"0-999"},
@{@"featureFlag": @"1-999"},
@{@"featureFlag": @"2-999"},
@{@"featureFlag": @"3-999"},
@{@"featureFlag": @"4-999"},
@{@"featureFlag": @"5-999"},
@{@"featureFlag": @"6-999"},
@{@"featureFlag": @"7-999"},
@{@"featureFlag": @"8-999"},
@{@"featureFlag": @"9-999"},
]));
}

- (void)testAddFeatureFlagPerformance {
BSGFeatureFlagStore *store = [[BSGFeatureFlagStore alloc] init];

__auto_type block = ^{
for (int i = 0; i < 1000; i++) {
NSString *name = [NSString stringWithFormat:@"%d", i];
BSGFeatureFlagStoreAddFeatureFlag(store, name, nil);
}
};

block();

[self measureBlock:block];
}

- (void)testDictionaryPerformance {
// For comparision to show the best performance possible

NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];

__auto_type block = ^{
for (int i = 0; i < 1000; i++) {
NSString *name = [NSString stringWithFormat:@"%d", i];
[dictionary setObject:[NSNull null] forKey:name];
}
};

block();

[self measureBlock:block];
}

@end