2 // CKKSLaunchSequence.m
5 #import "keychain/analytics/CKKSLaunchSequence.h"
6 #import <utilities/SecCoreAnalytics.h>
9 enum { CKKSMaxLaunchEvents = 100 };
11 @interface CKKSLaunchEvent : NSObject <NSCopying>
12 - (instancetype)init NS_UNAVAILABLE;
13 - (instancetype)initWithName:(NSString *)name;
14 @property (strong) NSString *name; // "human friendly" name, included in metrics
15 @property (strong) NSDate *date; // first, or last time this event happend (depends on usecase)
16 @property (assign) unsigned counter; //times this event happened, zero indexed
20 @interface CKKSLaunchSequence () {
23 @property (readwrite) bool launched;
24 @property (strong) NSString* name;
25 @property (strong) NSMutableDictionary<NSString *,CKKSLaunchEvent *>* events; // key is uniqifier, event.name is "human friendly"
26 @property (strong) NSMutableDictionary<NSString *,id>* attributes;
28 @property (strong) NSBlockOperation *launchOperation;
29 @property (strong) NSMutableDictionary<NSString *, CKKSLaunchSequence *> *dependantLaunches;
33 @implementation CKKSLaunchEvent
35 - (instancetype)initWithName:(NSString *)name {
36 if ((self = [super init]) != NULL) {
38 _date = [NSDate date];
44 - (id)copyWithZone:(NSZone *)zone
46 CKKSLaunchEvent *copy = [[[self class] alloc] init];
48 copy.name = [self.name copyWithZone:zone];
49 copy.date = [self.date copyWithZone:zone];
50 copy.counter = self.counter;
57 @implementation CKKSLaunchSequence
59 - (instancetype)initWithRocketName:(NSString *)name {
60 if ((self = [super init]) != NULL) {
62 _events = [NSMutableDictionary dictionary];
63 _events[@"started"] = [[CKKSLaunchEvent alloc] initWithName:@"started"];
64 _launchOperation = [[NSBlockOperation alloc] init];
65 _dependantLaunches = [NSMutableDictionary dictionary];
76 - (void)setFirstLaunch:(bool)firstLaunch
78 @synchronized (self) {
82 _firstLaunch = firstLaunch;
86 - (void)addDependantLaunch:(NSString *)name child:(CKKSLaunchSequence *)child
88 @synchronized (self) {
92 if (self.dependantLaunches[name]) {
95 self.dependantLaunches[name] = child;
96 [self.launchOperation addDependency:child.launchOperation];
101 - (void)addAttribute:(NSString *)key value:(id)value {
102 if (![key isKindOfClass:[NSString class]]) {
105 @synchronized(self) {
109 if (self.attributes == nil) {
110 self.attributes = [NSMutableDictionary dictionary];
112 self.attributes[key] = value;
117 - (void)addEvent:(NSString *)eventname {
118 if (![eventname isKindOfClass:[NSString class]]) {
121 @synchronized(self) {
125 if (self.events.count > CKKSMaxLaunchEvents) {
128 CKKSLaunchEvent *event = self.events[eventname];
132 event = [[CKKSLaunchEvent alloc] initWithName:eventname];
134 self.events[eventname] = event;
138 - (void)reportMetric {
140 NSMutableDictionary *metric = [NSMutableDictionary dictionary];
141 os_assert(self.launched);
143 @synchronized(self) {
144 /* don't need to lock children, at this point, they have launched and will no longer mutate */
145 [self.dependantLaunches enumerateKeysAndObjectsUsingBlock:^(NSString *_Nonnull childKey, CKKSLaunchSequence *_Nonnull child, BOOL * _Nonnull stop) {
146 os_assert(child.launched);
147 [child.events enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, CKKSLaunchEvent * _Nonnull obj, BOOL * _Nonnull stop) {
148 CKKSLaunchEvent *childEvent = [obj copy];
149 childEvent.name = [NSString stringWithFormat:@"c:%@-%@", childKey, childEvent.name];
150 self.events[[NSString stringWithFormat:@"c:%@-%@", childKey, key]] = childEvent;
152 self.attributes[[NSString stringWithFormat:@"c:%@", childKey]] = child.attributes;
155 CKKSLaunchEvent *event = [[CKKSLaunchEvent alloc] initWithName:(self.firstLaunch ? @"first-launch" : @"re-launch")];
156 self.events[event.name] = event;
158 metric[@"events"] = [self eventsRelativeTime];
159 if (self.attributes.count) {
160 metric[@"attributes"] = self.attributes;
164 [SecCoreAnalytics sendEvent:self.name event:metric];
168 @synchronized(self) {
172 self.launched = true;
175 __weak typeof(self) weakSelf = self;
177 [self.launchOperation addExecutionBlock:^{
178 [weakSelf reportMetric];
181 [[NSOperationQueue mainQueue] addOperation:self.launchOperation];
184 - (NSArray *) eventsRelativeTime
186 NSMutableArray<CKKSLaunchEvent *>* array = [NSMutableArray array];
187 [self.events enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull __unused name, CKKSLaunchEvent * _Nonnull event, BOOL * _Nonnull __unused stop) {
188 [array addObject:event];
190 [array sortUsingComparator:^NSComparisonResult(CKKSLaunchEvent * _Nonnull obj1, CKKSLaunchEvent *_Nonnull obj2) {
191 return [obj1.date compare:obj2.date];
193 NSDate *firstEvent = array[0].date;
194 NSMutableArray<NSDictionary *> *result = [NSMutableArray array];
195 [array enumerateObjectsUsingBlock:^(CKKSLaunchEvent * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
196 NSMutableDictionary *event = [NSMutableDictionary dictionary];
197 event[@"name"] = obj.name;
198 event[@"time"] = @([obj.date timeIntervalSinceDate:firstEvent]);
200 event[@"counter"] = @(obj.counter);
202 [result addObject:event];
207 - (NSArray *) eventsByTime {
208 @synchronized (self) {
209 NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
210 dateFormatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss.SSSZ";
212 NSMutableArray<NSString *>* array = [NSMutableArray array];
213 [self.events enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull name, CKKSLaunchEvent * _Nonnull event, BOOL * _Nonnull stop) {
214 NSString *str = [NSString stringWithFormat:@"%@ - %@:%u", [dateFormatter stringFromDate:event.date], event.name, event.counter];
215 [array addObject:str];
218 [array sortUsingSelector:@selector(compare:)];
220 for (NSString *attribute in self.attributes) {
221 [array addObject:[NSString stringWithFormat:@"attr: %@: %@", attribute, [self.attributes[attribute] description]]];