diff --git a/GoogleDataLogger/GoogleDataLogger/Classes/GDLClock.h b/GoogleDataLogger/GoogleDataLogger/Classes/GDLClock.h deleted file mode 100644 index 9e42e5e3883..00000000000 --- a/GoogleDataLogger/GoogleDataLogger/Classes/GDLClock.h +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2018 Google - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#import - -NS_ASSUME_NONNULL_BEGIN - -/** A struct to hold data pertaining to a snapshot in time. */ -typedef struct { - /** The current time in millis. */ - int64_t timeMillis; - - /** The device uptime in millis. */ - int64_t uptimeMillis; - - /** The timezone offset in millis. */ - int64_t timezoneOffsetMillis; -} GDLLogClockSnapshot; - -/** This class manages the device clock and produces snapshots of the current time. */ -@interface GDLClock : NSObject - -// TODO(mikehaney24): - (GDLLogClockSnapshot)snapshot; - -@end - -NS_ASSUME_NONNULL_END diff --git a/GoogleDataLogger/GoogleDataLogger/Classes/GDLClock.m b/GoogleDataLogger/GoogleDataLogger/Classes/GDLClock.m index 224f14f3455..ec3d3b85040 100644 --- a/GoogleDataLogger/GoogleDataLogger/Classes/GDLClock.m +++ b/GoogleDataLogger/GoogleDataLogger/Classes/GDLClock.m @@ -16,6 +16,152 @@ #import "GDLClock.h" -@implementation GDLClock +#import + +// Using a monotonic clock is necessary because CFAbsoluteTimeGetCurrent(), NSDate, and related all +// are subject to drift. That it to say, multiple consecutive calls do not always result in a +// time that is in the future. Clocks may be adjusted by the user, NTP, or any number of external +// factors. This class attempts to determine the wall-clock time at the time of log by capturing +// the kernel start and time since boot to determine a wallclock time in UTC. +// +// Timezone offsets at the time of a snapshot are also captured in order to provide local-time +// details. Other classes in this library depend on comparing times at some time in the future to +// a time captured in the past, and this class needs to provide a mechanism to do that. +// +// TL;DR: This class attempts to accomplish two things: 1. Provide accurate event times. 2. Provide +// a monotonic clock mechanism to accurately check if some clock snapshot was before or after +// by using a shared reference point (kernel boot time). +// +// Note: Much of the mach time stuff doesn't work properly in the simulator. So this class can be +// difficult to unit test. + +/** Returns the kernel boottime property from sysctl. + * + * Inspired by https://stackoverflow.com/a/40497811 + * + * @return The KERN_BOOTTIME property from sysctl, in nanoseconds. + */ +static int64_t KernelBootTimeInNanoseconds() { + // Caching the result is not possible because clock drift would not be accounted for. + struct timeval boottime; + int mib[2] = {CTL_KERN, KERN_BOOTTIME}; + size_t size = sizeof(boottime); + int rc = sysctl(mib, 2, &boottime, &size, NULL, 0); + if (rc != 0) { + return 0; + } + return (int64_t)boottime.tv_sec * NSEC_PER_MSEC + (int64_t)boottime.tv_usec; +} + +/** Returns value of gettimeofday, in nanoseconds. + * + * Inspired by https://stackoverflow.com/a/40497811 + * + * @return The value of gettimeofday, in nanoseconds. + */ +static int64_t UptimeInNanoseconds() { + int64_t before_now; + int64_t after_now; + struct timeval now; + + before_now = KernelBootTimeInNanoseconds(); + // Addresses a race condition in which the system time has updated, but the boottime has not. + do { + gettimeofday(&now, NULL); + after_now = KernelBootTimeInNanoseconds(); + } while (after_now != before_now); + return (int64_t)now.tv_sec * NSEC_PER_MSEC + (int64_t)now.tv_usec - before_now; +} + +// TODO: Consider adding a 'trustedTime' property that can be populated by the response from a BE. +@implementation GDLClock { + /** The kernel boot time when this clock was created. */ + int64_t _kernelBootTime; + + /** The device uptime when this clock was created. */ + int64_t _uptime; +} + +- (instancetype)init { + self = [super init]; + if (self) { + _kernelBootTime = KernelBootTimeInNanoseconds(); + _uptime = UptimeInNanoseconds(); + _timeMillis = CFAbsoluteTimeGetCurrent() * NSEC_PER_USEC; + CFTimeZoneRef timeZoneRef = CFTimeZoneCopySystem(); + _timezoneOffsetSeconds = CFTimeZoneGetSecondsFromGMT(timeZoneRef, 0); + CFRelease(timeZoneRef); + } + return self; +} + ++ (GDLClock *)snapshot { + return [[GDLClock alloc] init]; +} + ++ (instancetype)clockSnapshotInTheFuture:(uint64_t)millisInTheFuture { + GDLClock *snapshot = [self snapshot]; + snapshot->_timeMillis += millisInTheFuture; + return snapshot; +} + +- (BOOL)isAfter:(GDLClock *)otherClock { + // These clocks are trivially comparable when they share a kernel boot time. + if (_kernelBootTime == otherClock->_kernelBootTime) { + return _uptime > otherClock->_uptime; + } else { + int64_t kernelBootTimeDiff = otherClock->_kernelBootTime - _kernelBootTime; + // This isn't a great solution, but essentially, if the other clock's boot time is 'later', NO + // is returned. This can be altered by changing the system time and rebooting. + return kernelBootTimeDiff < 0 ? YES : NO; + } +} + +- (NSUInteger)hash { + // These casts lose some precision, but it's probably fine. + return (NSUInteger)_kernelBootTime ^ (NSUInteger)_uptime ^ (NSUInteger)_timeMillis; +} + +- (BOOL)isEqual:(id)object { + return [self hash] == [object hash]; +} + +#pragma mark - NSSecureCoding + +/** NSKeyedCoder key for timeMillis property. */ +static NSString *const kGDLClockTimeMillisKey = @"GDLClockTimeMillis"; + +/** NSKeyedCoder key for timezoneOffsetMillis property. */ +static NSString *const kGDLClockTimezoneOffsetSeconds = @"GDLClockTimezoneOffsetSeconds"; + +/** NSKeyedCoder key for _kernelBootTime ivar. */ +static NSString *const kGDLClockKernelBootTime = @"GDLClockKernelBootTime"; + +/** NSKeyedCoder key for _uptime ivar. */ +static NSString *const kGDLClockUptime = @"GDLClockUptime"; + ++ (BOOL)supportsSecureCoding { + return YES; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder { + self = [super init]; + if (self) { + // TODO: If the kernelBootTime is more recent, we need to change the kernel boot time and + // uptimeMillis ivars + _timeMillis = [aDecoder decodeInt64ForKey:kGDLClockTimeMillisKey]; + _timezoneOffsetSeconds = [aDecoder decodeInt64ForKey:kGDLClockTimezoneOffsetSeconds]; + _kernelBootTime = [aDecoder decodeInt64ForKey:kGDLClockKernelBootTime]; + _uptime = [aDecoder decodeInt64ForKey:kGDLClockUptime]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder { + [aCoder encodeInt64:_timeMillis forKey:kGDLClockTimeMillisKey]; + [aCoder encodeInt64:_timezoneOffsetSeconds forKey:kGDLClockTimezoneOffsetSeconds]; + [aCoder encodeInt64:_kernelBootTime forKey:kGDLClockKernelBootTime]; + [aCoder encodeInt64:_uptime forKey:kGDLClockUptime]; +} @end diff --git a/GoogleDataLogger/GoogleDataLogger/Classes/GDLLogEvent.m b/GoogleDataLogger/GoogleDataLogger/Classes/GDLLogEvent.m index 5fabe92872d..5f7df90135e 100644 --- a/GoogleDataLogger/GoogleDataLogger/Classes/GDLLogEvent.m +++ b/GoogleDataLogger/GoogleDataLogger/Classes/GDLLogEvent.m @@ -45,8 +45,10 @@ - (instancetype)copy { - (NSUInteger)hash { // This loses some precision, but it's probably fine. - NSUInteger timeHash = (NSUInteger)(_clockSnapshot.timeMillis ^ _clockSnapshot.uptimeMillis); - return [_logMapID hash] ^ _logTarget ^ [_extensionBytes hash] ^ _qosTier ^ timeHash; + NSUInteger logMapIDHash = [_logMapID hash]; + NSUInteger timeHash = [_clockSnapshot hash]; + NSUInteger extensionBytesHash = [_extensionBytes hash]; + return logMapIDHash ^ _logTarget ^ extensionBytesHash ^ _qosTier ^ timeHash; } - (void)setExtension:(id)extension { @@ -73,14 +75,8 @@ - (void)setExtension:(id)extension { /** NSCoding key for qosTier property. */ static NSString *qosTierKey = @"_qosTier"; -/** NSCoding key for clockSnapshot.timeMillis property. */ -static NSString *clockSnapshotTimeMillisKey = @"_clockSnapshotTimeMillis"; - -/** NSCoding key for clockSnapshot.uptimeMillis property. */ -static NSString *clockSnapshotUpTimeMillis = @"_clockSnapshotUpTimeMillis"; - -/** NSCoding key for clockSnapshot.timezoneOffsetMillis property. */ -static NSString *clockSnapshotTimezoneOffsetMillis = @"_clockSnapshotTimezoneOffsetMillis"; +/** NSCoding key for clockSnapshot property. */ +static NSString *clockSnapshotKey = @"_clockSnapshot"; + (BOOL)supportsSecureCoding { return YES; @@ -93,10 +89,7 @@ - (id)initWithCoder:(NSCoder *)aDecoder { if (self) { _extensionBytes = [aDecoder decodeObjectOfClass:[NSData class] forKey:extensionBytesKey]; _qosTier = [aDecoder decodeIntegerForKey:qosTierKey]; - _clockSnapshot.timeMillis = [aDecoder decodeInt64ForKey:clockSnapshotTimeMillisKey]; - _clockSnapshot.uptimeMillis = [aDecoder decodeInt64ForKey:clockSnapshotUpTimeMillis]; - _clockSnapshot.timezoneOffsetMillis = - [aDecoder decodeInt64ForKey:clockSnapshotTimezoneOffsetMillis]; + _clockSnapshot = [aDecoder decodeObjectOfClass:[GDLClock class] forKey:clockSnapshotKey]; } return self; } @@ -106,9 +99,7 @@ - (void)encodeWithCoder:(NSCoder *)aCoder { [aCoder encodeInteger:_logTarget forKey:logTargetKey]; [aCoder encodeObject:_extensionBytes forKey:extensionBytesKey]; [aCoder encodeInteger:_qosTier forKey:qosTierKey]; - [aCoder encodeInt64:_clockSnapshot.timeMillis forKey:clockSnapshotTimeMillisKey]; - [aCoder encodeInt64:_clockSnapshot.uptimeMillis forKey:clockSnapshotUpTimeMillis]; - [aCoder encodeInt64:_clockSnapshot.timezoneOffsetMillis forKey:clockSnapshotTimezoneOffsetMillis]; + [aCoder encodeObject:_clockSnapshot forKey:clockSnapshotKey]; } @end diff --git a/GoogleDataLogger/GoogleDataLogger/Classes/GDLLogger.m b/GoogleDataLogger/GoogleDataLogger/Classes/GDLLogger.m index 67924fa3b4c..97c998004b9 100644 --- a/GoogleDataLogger/GoogleDataLogger/Classes/GDLLogger.m +++ b/GoogleDataLogger/GoogleDataLogger/Classes/GDLLogger.m @@ -19,6 +19,7 @@ #import "GDLAssert.h" #import "GDLLogEvent.h" +#import "GDLLogEvent_Private.h" #import "GDLLogWriter.h" @implementation GDLLogger @@ -39,16 +40,20 @@ - (instancetype)initWithLogMapID:(NSString *)logMapID } - (void)logTelemetryEvent:(GDLLogEvent *)logEvent { + // TODO: Determine if logging an event before registration is allowed. GDLAssert(logEvent, @"You can't log a nil event"); GDLLogEvent *copiedLog = [logEvent copy]; copiedLog.qosTier = GDLLogQoSTelemetry; + copiedLog.clockSnapshot = [GDLClock snapshot]; [self.logWriterInstance writeLog:copiedLog afterApplyingTransformers:_logTransformers]; } - (void)logDataEvent:(GDLLogEvent *)logEvent { + // TODO: Determine if logging an event before registration is allowed. GDLAssert(logEvent, @"You can't log a nil event"); GDLAssert(logEvent.qosTier != GDLLogQoSTelemetry, @"Use -logTelemetryEvent, please."); GDLLogEvent *copiedLog = [logEvent copy]; + copiedLog.clockSnapshot = [GDLClock snapshot]; [self.logWriterInstance writeLog:copiedLog afterApplyingTransformers:_logTransformers]; } diff --git a/GoogleDataLogger/GoogleDataLogger/Classes/Private/GDLLogEvent_Private.h b/GoogleDataLogger/GoogleDataLogger/Classes/Private/GDLLogEvent_Private.h index 88816ebfa06..117f24c556c 100644 --- a/GoogleDataLogger/GoogleDataLogger/Classes/Private/GDLLogEvent_Private.h +++ b/GoogleDataLogger/GoogleDataLogger/Classes/Private/GDLLogEvent_Private.h @@ -26,7 +26,7 @@ NS_ASSUME_NONNULL_BEGIN @property(nonatomic) NSData *extensionBytes; /** The clock snapshot at the time of logging. */ -@property(nonatomic) GDLLogClockSnapshot clockSnapshot; +@property(nonatomic) GDLClock *clockSnapshot; @end diff --git a/GoogleDataLogger/GoogleDataLogger/Classes/Public/GDLClock.h b/GoogleDataLogger/GoogleDataLogger/Classes/Public/GDLClock.h new file mode 100644 index 00000000000..569970c4e38 --- /dev/null +++ b/GoogleDataLogger/GoogleDataLogger/Classes/Public/GDLClock.h @@ -0,0 +1,51 @@ +/* + * Copyright 2018 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** This class manages the device clock and produces snapshots of the current time. */ +@interface GDLClock : NSObject + +/** The wallclock time, UTC, in milliseconds. */ +@property(nonatomic, readonly) int64_t timeMillis; + +/** The offset from UTC in seconds. */ +@property(nonatomic, readonly) int64_t timezoneOffsetSeconds; + +/** Creates a GDLClock object using the current time and offsets. + * + * @return A new GDLClock object representing the current time state. + */ ++ (instancetype)snapshot; + +/** Creates a GDLClock object representing a time in the future, relative to now. + * + * @param millisInTheFuture The millis in the future from now this clock should represent. + * @return An instance representing a future time. + */ ++ (instancetype)clockSnapshotInTheFuture:(uint64_t)millisInTheFuture; + +/** Compares one clock with another, returns YES if the caller is after the parameter. + * + * @return YES if the calling clock's time is after the given clock's time. + */ +- (BOOL)isAfter:(GDLClock *)otherClock; + +@end + +NS_ASSUME_NONNULL_END diff --git a/GoogleDataLogger/GoogleDataLogger/Classes/Public/GoogleDataLogger.h b/GoogleDataLogger/GoogleDataLogger/Classes/Public/GoogleDataLogger.h index 50b4f061a09..7a38d1b4880 100644 --- a/GoogleDataLogger/GoogleDataLogger/Classes/Public/GoogleDataLogger.h +++ b/GoogleDataLogger/GoogleDataLogger/Classes/Public/GoogleDataLogger.h @@ -14,6 +14,7 @@ * limitations under the License. */ +#import "GDLClock.h" #import "GDLLogEvent.h" #import "GDLLogPrioritizer.h" #import "GDLLogProto.h" diff --git a/GoogleDataLogger/Tests/Unit/GDLClockTest.m b/GoogleDataLogger/Tests/Unit/GDLClockTest.m index 09efa3da62a..e16e59d93e0 100644 --- a/GoogleDataLogger/Tests/Unit/GDLClockTest.m +++ b/GoogleDataLogger/Tests/Unit/GDLClockTest.m @@ -29,4 +29,37 @@ - (void)testInit { XCTAssertNotNil([[GDLClockTest alloc] init]); } +/** Tests taking a snapshot. */ +- (void)testSnapshot { + GDLClock *snapshot; + XCTAssertNoThrow(snapshot = [GDLClock snapshot]); + XCTAssertGreaterThan(snapshot.timeMillis, 0); +} + +/** Tests that the hash of two snapshots right after each other isn't equal. */ +- (void)testHash { + GDLClock *snapshot1 = [GDLClock snapshot]; + GDLClock *snapshot2 = [GDLClock snapshot]; + XCTAssertNotEqual([snapshot1 hash], [snapshot2 hash]); +} + +/** Tests that the class supports NSSecureEncoding. */ +- (void)testSupportsSecureEncoding { + XCTAssertTrue([GDLClock supportsSecureCoding]); +} + +- (void)testEncoding { + GDLClock *clock = [GDLClock snapshot]; + NSData *clockData = [NSKeyedArchiver archivedDataWithRootObject:clock]; + GDLClock *unarchivedClock = [NSKeyedUnarchiver unarchiveObjectWithData:clockData]; + XCTAssertEqual([clock hash], [unarchivedClock hash]); + XCTAssertEqualObjects(clock, unarchivedClock); +} + +- (void)testClockSnapshotInTheFuture { + GDLClock *clock1 = [GDLClock snapshot]; + GDLClock *clock2 = [GDLClock clockSnapshotInTheFuture:1]; + XCTAssertTrue([clock2 isAfter:clock1]); +} + @end diff --git a/GoogleDataLogger/Tests/Unit/GDLLogEventTest.m b/GoogleDataLogger/Tests/Unit/GDLLogEventTest.m index 4ddb11b8838..de3f11eba8b 100644 --- a/GoogleDataLogger/Tests/Unit/GDLLogEventTest.m +++ b/GoogleDataLogger/Tests/Unit/GDLLogEventTest.m @@ -35,7 +35,9 @@ - (void)testInit { /** Tests NSKeyedArchiver encoding and decoding. */ - (void)testArchiving { XCTAssertTrue([GDLLogEvent supportsSecureCoding]); - GDLLogClockSnapshot clockSnapshot = {10, 100, 1000}; + GDLClock *clockSnapshot = [GDLClock snapshot]; + int64_t timeMillis = clockSnapshot.timeMillis; + int64_t timezoneOffsetSeconds = clockSnapshot.timezoneOffsetSeconds; GDLLogEvent *logEvent = [[GDLLogEvent alloc] initWithLogMapID:@"testID" logTarget:42]; logEvent.extensionBytes = [@"someData" dataUsingEncoding:NSUTF8StringEncoding]; logEvent.qosTier = GDLLogQoSTelemetry; @@ -52,9 +54,8 @@ - (void)testArchiving { XCTAssertEqualObjects(decodedLogEvent.extensionBytes, [@"someData" dataUsingEncoding:NSUTF8StringEncoding]); XCTAssertEqual(decodedLogEvent.qosTier, GDLLogQoSTelemetry); - XCTAssertEqual(decodedLogEvent.clockSnapshot.timeMillis, 10); - XCTAssertEqual(decodedLogEvent.clockSnapshot.uptimeMillis, 100); - XCTAssertEqual(decodedLogEvent.clockSnapshot.timezoneOffsetMillis, 1000); + XCTAssertEqual(decodedLogEvent.clockSnapshot.timeMillis, timeMillis); + XCTAssertEqual(decodedLogEvent.clockSnapshot.timezoneOffsetSeconds, timezoneOffsetSeconds); } @end