|
16 | 16 |
|
17 | 17 | #import "GDLClock.h"
|
18 | 18 |
|
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 | +} |
20 | 166 |
|
21 | 167 | @end
|
0 commit comments