Skip to content

Commit 0fc2ff6

Browse files
authored
Implement a clock. (#2273)
* Implement a clock. Files to pay attention to: GDLClock.h/m * style
1 parent e1557cf commit 0fc2ff6

File tree

9 files changed

+251
-63
lines changed

9 files changed

+251
-63
lines changed

GoogleDataLogger/GoogleDataLogger/Classes/GDLClock.h

Lines changed: 0 additions & 40 deletions
This file was deleted.

GoogleDataLogger/GoogleDataLogger/Classes/GDLClock.m

Lines changed: 147 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,152 @@
1616

1717
#import "GDLClock.h"
1818

19-
@implementation GDLClock
19+
#import <sys/sysctl.h>
20+
21+
// Using a monotonic clock is necessary because CFAbsoluteTimeGetCurrent(), NSDate, and related all
22+
// are subject to drift. That it to say, multiple consecutive calls do not always result in a
23+
// time that is in the future. Clocks may be adjusted by the user, NTP, or any number of external
24+
// factors. This class attempts to determine the wall-clock time at the time of log by capturing
25+
// the kernel start and time since boot to determine a wallclock time in UTC.
26+
//
27+
// Timezone offsets at the time of a snapshot are also captured in order to provide local-time
28+
// details. Other classes in this library depend on comparing times at some time in the future to
29+
// a time captured in the past, and this class needs to provide a mechanism to do that.
30+
//
31+
// TL;DR: This class attempts to accomplish two things: 1. Provide accurate event times. 2. Provide
32+
// a monotonic clock mechanism to accurately check if some clock snapshot was before or after
33+
// by using a shared reference point (kernel boot time).
34+
//
35+
// Note: Much of the mach time stuff doesn't work properly in the simulator. So this class can be
36+
// difficult to unit test.
37+
38+
/** Returns the kernel boottime property from sysctl.
39+
*
40+
* Inspired by https://stackoverflow.com/a/40497811
41+
*
42+
* @return The KERN_BOOTTIME property from sysctl, in nanoseconds.
43+
*/
44+
static int64_t KernelBootTimeInNanoseconds() {
45+
// Caching the result is not possible because clock drift would not be accounted for.
46+
struct timeval boottime;
47+
int mib[2] = {CTL_KERN, KERN_BOOTTIME};
48+
size_t size = sizeof(boottime);
49+
int rc = sysctl(mib, 2, &boottime, &size, NULL, 0);
50+
if (rc != 0) {
51+
return 0;
52+
}
53+
return (int64_t)boottime.tv_sec * NSEC_PER_MSEC + (int64_t)boottime.tv_usec;
54+
}
55+
56+
/** Returns value of gettimeofday, in nanoseconds.
57+
*
58+
* Inspired by https://stackoverflow.com/a/40497811
59+
*
60+
* @return The value of gettimeofday, in nanoseconds.
61+
*/
62+
static int64_t UptimeInNanoseconds() {
63+
int64_t before_now;
64+
int64_t after_now;
65+
struct timeval now;
66+
67+
before_now = KernelBootTimeInNanoseconds();
68+
// Addresses a race condition in which the system time has updated, but the boottime has not.
69+
do {
70+
gettimeofday(&now, NULL);
71+
after_now = KernelBootTimeInNanoseconds();
72+
} while (after_now != before_now);
73+
return (int64_t)now.tv_sec * NSEC_PER_MSEC + (int64_t)now.tv_usec - before_now;
74+
}
75+
76+
// TODO: Consider adding a 'trustedTime' property that can be populated by the response from a BE.
77+
@implementation GDLClock {
78+
/** The kernel boot time when this clock was created. */
79+
int64_t _kernelBootTime;
80+
81+
/** The device uptime when this clock was created. */
82+
int64_t _uptime;
83+
}
84+
85+
- (instancetype)init {
86+
self = [super init];
87+
if (self) {
88+
_kernelBootTime = KernelBootTimeInNanoseconds();
89+
_uptime = UptimeInNanoseconds();
90+
_timeMillis = CFAbsoluteTimeGetCurrent() * NSEC_PER_USEC;
91+
CFTimeZoneRef timeZoneRef = CFTimeZoneCopySystem();
92+
_timezoneOffsetSeconds = CFTimeZoneGetSecondsFromGMT(timeZoneRef, 0);
93+
CFRelease(timeZoneRef);
94+
}
95+
return self;
96+
}
97+
98+
+ (GDLClock *)snapshot {
99+
return [[GDLClock alloc] init];
100+
}
101+
102+
+ (instancetype)clockSnapshotInTheFuture:(uint64_t)millisInTheFuture {
103+
GDLClock *snapshot = [self snapshot];
104+
snapshot->_timeMillis += millisInTheFuture;
105+
return snapshot;
106+
}
107+
108+
- (BOOL)isAfter:(GDLClock *)otherClock {
109+
// These clocks are trivially comparable when they share a kernel boot time.
110+
if (_kernelBootTime == otherClock->_kernelBootTime) {
111+
return _uptime > otherClock->_uptime;
112+
} else {
113+
int64_t kernelBootTimeDiff = otherClock->_kernelBootTime - _kernelBootTime;
114+
// This isn't a great solution, but essentially, if the other clock's boot time is 'later', NO
115+
// is returned. This can be altered by changing the system time and rebooting.
116+
return kernelBootTimeDiff < 0 ? YES : NO;
117+
}
118+
}
119+
120+
- (NSUInteger)hash {
121+
// These casts lose some precision, but it's probably fine.
122+
return (NSUInteger)_kernelBootTime ^ (NSUInteger)_uptime ^ (NSUInteger)_timeMillis;
123+
}
124+
125+
- (BOOL)isEqual:(id)object {
126+
return [self hash] == [object hash];
127+
}
128+
129+
#pragma mark - NSSecureCoding
130+
131+
/** NSKeyedCoder key for timeMillis property. */
132+
static NSString *const kGDLClockTimeMillisKey = @"GDLClockTimeMillis";
133+
134+
/** NSKeyedCoder key for timezoneOffsetMillis property. */
135+
static NSString *const kGDLClockTimezoneOffsetSeconds = @"GDLClockTimezoneOffsetSeconds";
136+
137+
/** NSKeyedCoder key for _kernelBootTime ivar. */
138+
static NSString *const kGDLClockKernelBootTime = @"GDLClockKernelBootTime";
139+
140+
/** NSKeyedCoder key for _uptime ivar. */
141+
static NSString *const kGDLClockUptime = @"GDLClockUptime";
142+
143+
+ (BOOL)supportsSecureCoding {
144+
return YES;
145+
}
146+
147+
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
148+
self = [super init];
149+
if (self) {
150+
// TODO: If the kernelBootTime is more recent, we need to change the kernel boot time and
151+
// uptimeMillis ivars
152+
_timeMillis = [aDecoder decodeInt64ForKey:kGDLClockTimeMillisKey];
153+
_timezoneOffsetSeconds = [aDecoder decodeInt64ForKey:kGDLClockTimezoneOffsetSeconds];
154+
_kernelBootTime = [aDecoder decodeInt64ForKey:kGDLClockKernelBootTime];
155+
_uptime = [aDecoder decodeInt64ForKey:kGDLClockUptime];
156+
}
157+
return self;
158+
}
159+
160+
- (void)encodeWithCoder:(NSCoder *)aCoder {
161+
[aCoder encodeInt64:_timeMillis forKey:kGDLClockTimeMillisKey];
162+
[aCoder encodeInt64:_timezoneOffsetSeconds forKey:kGDLClockTimezoneOffsetSeconds];
163+
[aCoder encodeInt64:_kernelBootTime forKey:kGDLClockKernelBootTime];
164+
[aCoder encodeInt64:_uptime forKey:kGDLClockUptime];
165+
}
20166

21167
@end

GoogleDataLogger/GoogleDataLogger/Classes/GDLLogEvent.m

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,10 @@ - (instancetype)copy {
4545

4646
- (NSUInteger)hash {
4747
// This loses some precision, but it's probably fine.
48-
NSUInteger timeHash = (NSUInteger)(_clockSnapshot.timeMillis ^ _clockSnapshot.uptimeMillis);
49-
return [_logMapID hash] ^ _logTarget ^ [_extensionBytes hash] ^ _qosTier ^ timeHash;
48+
NSUInteger logMapIDHash = [_logMapID hash];
49+
NSUInteger timeHash = [_clockSnapshot hash];
50+
NSUInteger extensionBytesHash = [_extensionBytes hash];
51+
return logMapIDHash ^ _logTarget ^ extensionBytesHash ^ _qosTier ^ timeHash;
5052
}
5153

5254
- (void)setExtension:(id<GDLLogProto>)extension {
@@ -73,14 +75,8 @@ - (void)setExtension:(id<GDLLogProto>)extension {
7375
/** NSCoding key for qosTier property. */
7476
static NSString *qosTierKey = @"_qosTier";
7577

76-
/** NSCoding key for clockSnapshot.timeMillis property. */
77-
static NSString *clockSnapshotTimeMillisKey = @"_clockSnapshotTimeMillis";
78-
79-
/** NSCoding key for clockSnapshot.uptimeMillis property. */
80-
static NSString *clockSnapshotUpTimeMillis = @"_clockSnapshotUpTimeMillis";
81-
82-
/** NSCoding key for clockSnapshot.timezoneOffsetMillis property. */
83-
static NSString *clockSnapshotTimezoneOffsetMillis = @"_clockSnapshotTimezoneOffsetMillis";
78+
/** NSCoding key for clockSnapshot property. */
79+
static NSString *clockSnapshotKey = @"_clockSnapshot";
8480

8581
+ (BOOL)supportsSecureCoding {
8682
return YES;
@@ -93,10 +89,7 @@ - (id)initWithCoder:(NSCoder *)aDecoder {
9389
if (self) {
9490
_extensionBytes = [aDecoder decodeObjectOfClass:[NSData class] forKey:extensionBytesKey];
9591
_qosTier = [aDecoder decodeIntegerForKey:qosTierKey];
96-
_clockSnapshot.timeMillis = [aDecoder decodeInt64ForKey:clockSnapshotTimeMillisKey];
97-
_clockSnapshot.uptimeMillis = [aDecoder decodeInt64ForKey:clockSnapshotUpTimeMillis];
98-
_clockSnapshot.timezoneOffsetMillis =
99-
[aDecoder decodeInt64ForKey:clockSnapshotTimezoneOffsetMillis];
92+
_clockSnapshot = [aDecoder decodeObjectOfClass:[GDLClock class] forKey:clockSnapshotKey];
10093
}
10194
return self;
10295
}
@@ -106,9 +99,7 @@ - (void)encodeWithCoder:(NSCoder *)aCoder {
10699
[aCoder encodeInteger:_logTarget forKey:logTargetKey];
107100
[aCoder encodeObject:_extensionBytes forKey:extensionBytesKey];
108101
[aCoder encodeInteger:_qosTier forKey:qosTierKey];
109-
[aCoder encodeInt64:_clockSnapshot.timeMillis forKey:clockSnapshotTimeMillisKey];
110-
[aCoder encodeInt64:_clockSnapshot.uptimeMillis forKey:clockSnapshotUpTimeMillis];
111-
[aCoder encodeInt64:_clockSnapshot.timezoneOffsetMillis forKey:clockSnapshotTimezoneOffsetMillis];
102+
[aCoder encodeObject:_clockSnapshot forKey:clockSnapshotKey];
112103
}
113104

114105
@end

GoogleDataLogger/GoogleDataLogger/Classes/GDLLogger.m

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
#import "GDLAssert.h"
2121
#import "GDLLogEvent.h"
22+
#import "GDLLogEvent_Private.h"
2223
#import "GDLLogWriter.h"
2324

2425
@implementation GDLLogger
@@ -39,16 +40,20 @@ - (instancetype)initWithLogMapID:(NSString *)logMapID
3940
}
4041

4142
- (void)logTelemetryEvent:(GDLLogEvent *)logEvent {
43+
// TODO: Determine if logging an event before registration is allowed.
4244
GDLAssert(logEvent, @"You can't log a nil event");
4345
GDLLogEvent *copiedLog = [logEvent copy];
4446
copiedLog.qosTier = GDLLogQoSTelemetry;
47+
copiedLog.clockSnapshot = [GDLClock snapshot];
4548
[self.logWriterInstance writeLog:copiedLog afterApplyingTransformers:_logTransformers];
4649
}
4750

4851
- (void)logDataEvent:(GDLLogEvent *)logEvent {
52+
// TODO: Determine if logging an event before registration is allowed.
4953
GDLAssert(logEvent, @"You can't log a nil event");
5054
GDLAssert(logEvent.qosTier != GDLLogQoSTelemetry, @"Use -logTelemetryEvent, please.");
5155
GDLLogEvent *copiedLog = [logEvent copy];
56+
copiedLog.clockSnapshot = [GDLClock snapshot];
5257
[self.logWriterInstance writeLog:copiedLog afterApplyingTransformers:_logTransformers];
5358
}
5459

GoogleDataLogger/GoogleDataLogger/Classes/Private/GDLLogEvent_Private.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ NS_ASSUME_NONNULL_BEGIN
2626
@property(nonatomic) NSData *extensionBytes;
2727

2828
/** The clock snapshot at the time of logging. */
29-
@property(nonatomic) GDLLogClockSnapshot clockSnapshot;
29+
@property(nonatomic) GDLClock *clockSnapshot;
3030

3131
@end
3232

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright 2018 Google
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#import <Foundation/Foundation.h>
18+
19+
NS_ASSUME_NONNULL_BEGIN
20+
21+
/** This class manages the device clock and produces snapshots of the current time. */
22+
@interface GDLClock : NSObject <NSSecureCoding>
23+
24+
/** The wallclock time, UTC, in milliseconds. */
25+
@property(nonatomic, readonly) int64_t timeMillis;
26+
27+
/** The offset from UTC in seconds. */
28+
@property(nonatomic, readonly) int64_t timezoneOffsetSeconds;
29+
30+
/** Creates a GDLClock object using the current time and offsets.
31+
*
32+
* @return A new GDLClock object representing the current time state.
33+
*/
34+
+ (instancetype)snapshot;
35+
36+
/** Creates a GDLClock object representing a time in the future, relative to now.
37+
*
38+
* @param millisInTheFuture The millis in the future from now this clock should represent.
39+
* @return An instance representing a future time.
40+
*/
41+
+ (instancetype)clockSnapshotInTheFuture:(uint64_t)millisInTheFuture;
42+
43+
/** Compares one clock with another, returns YES if the caller is after the parameter.
44+
*
45+
* @return YES if the calling clock's time is after the given clock's time.
46+
*/
47+
- (BOOL)isAfter:(GDLClock *)otherClock;
48+
49+
@end
50+
51+
NS_ASSUME_NONNULL_END

GoogleDataLogger/GoogleDataLogger/Classes/Public/GoogleDataLogger.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17+
#import "GDLClock.h"
1718
#import "GDLLogEvent.h"
1819
#import "GDLLogPrioritizer.h"
1920
#import "GDLLogProto.h"

GoogleDataLogger/Tests/Unit/GDLClockTest.m

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,37 @@ - (void)testInit {
2929
XCTAssertNotNil([[GDLClockTest alloc] init]);
3030
}
3131

32+
/** Tests taking a snapshot. */
33+
- (void)testSnapshot {
34+
GDLClock *snapshot;
35+
XCTAssertNoThrow(snapshot = [GDLClock snapshot]);
36+
XCTAssertGreaterThan(snapshot.timeMillis, 0);
37+
}
38+
39+
/** Tests that the hash of two snapshots right after each other isn't equal. */
40+
- (void)testHash {
41+
GDLClock *snapshot1 = [GDLClock snapshot];
42+
GDLClock *snapshot2 = [GDLClock snapshot];
43+
XCTAssertNotEqual([snapshot1 hash], [snapshot2 hash]);
44+
}
45+
46+
/** Tests that the class supports NSSecureEncoding. */
47+
- (void)testSupportsSecureEncoding {
48+
XCTAssertTrue([GDLClock supportsSecureCoding]);
49+
}
50+
51+
- (void)testEncoding {
52+
GDLClock *clock = [GDLClock snapshot];
53+
NSData *clockData = [NSKeyedArchiver archivedDataWithRootObject:clock];
54+
GDLClock *unarchivedClock = [NSKeyedUnarchiver unarchiveObjectWithData:clockData];
55+
XCTAssertEqual([clock hash], [unarchivedClock hash]);
56+
XCTAssertEqualObjects(clock, unarchivedClock);
57+
}
58+
59+
- (void)testClockSnapshotInTheFuture {
60+
GDLClock *clock1 = [GDLClock snapshot];
61+
GDLClock *clock2 = [GDLClock clockSnapshotInTheFuture:1];
62+
XCTAssertTrue([clock2 isAfter:clock1]);
63+
}
64+
3265
@end

0 commit comments

Comments
 (0)