Skip to content

Implement a clock. #2273

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

Merged
merged 2 commits into from
Jan 16, 2019
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
40 changes: 0 additions & 40 deletions GoogleDataLogger/GoogleDataLogger/Classes/GDLClock.h

This file was deleted.

148 changes: 147 additions & 1 deletion GoogleDataLogger/GoogleDataLogger/Classes/GDLClock.m
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,152 @@

#import "GDLClock.h"

@implementation GDLClock
#import <sys/sysctl.h>

// 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
25 changes: 8 additions & 17 deletions GoogleDataLogger/GoogleDataLogger/Classes/GDLLogEvent.m
Original file line number Diff line number Diff line change
Expand Up @@ -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<GDLLogProto>)extension {
Expand All @@ -73,14 +75,8 @@ - (void)setExtension:(id<GDLLogProto>)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;
Expand All @@ -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;
}
Expand All @@ -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
5 changes: 5 additions & 0 deletions GoogleDataLogger/GoogleDataLogger/Classes/GDLLogger.m
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

#import "GDLAssert.h"
#import "GDLLogEvent.h"
#import "GDLLogEvent_Private.h"
#import "GDLLogWriter.h"

@implementation GDLLogger
Expand All @@ -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];
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
51 changes: 51 additions & 0 deletions GoogleDataLogger/GoogleDataLogger/Classes/Public/GDLClock.h
Original file line number Diff line number Diff line change
@@ -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 <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

/** This class manages the device clock and produces snapshots of the current time. */
@interface GDLClock : NSObject <NSSecureCoding>

/** 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
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
* limitations under the License.
*/

#import "GDLClock.h"
#import "GDLLogEvent.h"
#import "GDLLogPrioritizer.h"
#import "GDLLogProto.h"
Expand Down
33 changes: 33 additions & 0 deletions GoogleDataLogger/Tests/Unit/GDLClockTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading