Scott Violet | 787d185 | 2023-02-18 15:29:32 | [diff] [blame] | 1 | // Copyright 2023 The Chromium Authors |
| 2 | // Use of this source code is governed by a BSD-style license that can be |
| 3 | // found in the LICENSE file. |
| 4 | |
| 5 | #ifndef GIN_TIME_CLAMPER_H_ |
| 6 | #define GIN_TIME_CLAMPER_H_ |
| 7 | |
| 8 | #include <algorithm> |
| 9 | |
| 10 | #include "base/rand_util.h" |
| 11 | #include "base/time/time.h" |
| 12 | #include "gin/gin_export.h" |
| 13 | |
| 14 | namespace gin { |
| 15 | |
| 16 | // This class adds some amount of jitter to time. That is, for every |
| 17 | // `kResolutionMicros` microseconds it calculates a threshold (using a hash) |
| 18 | // that once exceeded advances to the next threshold. This is done so that |
| 19 | // time jumps slightly and does not move smoothly. |
| 20 | // |
| 21 | // NOTE: the implementation assumes it's used for servicing calls from JS, |
| 22 | // which uses the unix-epoch at time 0. |
| 23 | // TODO(skyostil): Deduplicate this with the clamper in Blink. |
| 24 | class GIN_EXPORT TimeClamper { |
| 25 | public: |
| 26 | // Public for tests. |
| 27 | static const int64_t kResolutionMicros; |
| 28 | |
| 29 | TimeClamper() : secret_(base::RandUint64()) {} |
| 30 | // This constructor should only be used in tests. |
| 31 | explicit TimeClamper(uint64_t secret) : secret_(secret) {} |
| 32 | |
| 33 | TimeClamper(const TimeClamper&) = delete; |
| 34 | TimeClamper& operator=(const TimeClamper&) = delete; |
| 35 | ~TimeClamper() = default; |
| 36 | |
| 37 | // Clamps a time to millisecond precision. The return value is in milliseconds |
| 38 | // relative to unix-epoch (which is what JS uses). |
| 39 | inline int64_t ClampToMillis(base::Time time) const { |
| 40 | // Adding jitter is non-trivial, only use it if necessary. |
| 41 | // ClampTimeResolution() adjusts the time to land on `kResolutionMicros` |
| 42 | // boundaries, and either uses the current `kResolutionMicros` boundary, or |
| 43 | // the next one. Because `kResolutionMicros` is smaller than 1ms, and this |
| 44 | // function returns millisecond accuracy, ClampTimeResolution() is only |
| 45 | // necessary when within `kResolutionMicros` of the next millisecond. |
| 46 | const int64_t now_micros = |
| 47 | (time - base::Time::UnixEpoch()).InMicroseconds(); |
| 48 | const int64_t micros = now_micros % 1000; |
| 49 | // abs() is necessary for devices with times before unix-epoch (most likely |
| 50 | // configured incorrectly). |
| 51 | if (abs(micros) + kResolutionMicros < 1000) { |
| 52 | return now_micros / 1000; |
| 53 | } |
| 54 | return ClampTimeResolution(now_micros) / 1000; |
| 55 | } |
| 56 | |
| 57 | // Clamps the time, giving microsecond precision. The return value is in |
| 58 | // milliseconds relative to unix-epoch (which is what JS uses). |
| 59 | inline double ClampToMillisHighResolution(base::Time now) const { |
| 60 | const int64_t clamped_time = |
| 61 | ClampTimeResolution((now - base::Time::UnixEpoch()).InMicroseconds()); |
| 62 | return static_cast<double>(clamped_time) / 1000.0; |
| 63 | } |
| 64 | |
| 65 | private: |
| 66 | inline int64_t ClampTimeResolution(int64_t time_micros) const { |
| 67 | if (time_micros < 0) { |
| 68 | return -ClampTimeResolutionPositiveValue(-time_micros); |
| 69 | } |
| 70 | return ClampTimeResolutionPositiveValue(time_micros); |
| 71 | } |
| 72 | |
| 73 | inline int64_t ClampTimeResolutionPositiveValue(int64_t time_micros) const { |
| 74 | DCHECK_GE(time_micros, 0u); |
| 75 | // For each clamped time interval, compute a pseudorandom transition |
| 76 | // threshold. The reported time will either be the start of that interval or |
| 77 | // the next one depending on which side of the threshold |time_seconds| is. |
| 78 | const int64_t interval = time_micros / kResolutionMicros; |
| 79 | const int64_t clamped_time_micros = interval * kResolutionMicros; |
| 80 | const int64_t tick_threshold = ThresholdFor(clamped_time_micros); |
| 81 | if (time_micros - clamped_time_micros < tick_threshold) { |
| 82 | return clamped_time_micros; |
| 83 | } |
| 84 | return clamped_time_micros + kResolutionMicros; |
| 85 | } |
| 86 | |
| 87 | inline int64_t ThresholdFor(int64_t clamped_time) const { |
| 88 | // Returns a random value between 0 and kResolutionMicros. The distribution |
| 89 | // is not necessarily equal, but for a random value it's good enough. |
| 90 | // Avoid floating-point math by rewriting: |
| 91 | // (random_value * 1.0 / UINT64_MAX) * kResolutionMicros |
| 92 | // into: |
| 93 | // random_value / (UINT64_MAX / kResolutionMicros) |
| 94 | // where we avoid integer overflow by dividing instead of multiplying. |
| 95 | const uint64_t random_value = MurmurHash3(clamped_time ^ secret_); |
| 96 | return std::min(static_cast<int64_t>(random_value / |
| 97 | (std::numeric_limits<uint64_t>::max() / |
| 98 | kResolutionMicros)), |
| 99 | kResolutionMicros); |
| 100 | } |
| 101 | |
| 102 | static inline uint64_t MurmurHash3(uint64_t value) { |
| 103 | value ^= value >> 33; |
| 104 | value *= uint64_t{0xFF51AFD7ED558CCD}; |
| 105 | value ^= value >> 33; |
| 106 | value *= uint64_t{0xC4CEB9FE1A85EC53}; |
| 107 | value ^= value >> 33; |
| 108 | return value; |
| 109 | } |
| 110 | |
| 111 | const uint64_t secret_; |
| 112 | }; |
| 113 | |
| 114 | } // namespace gin |
| 115 | |
| 116 | #endif // GIN_TIME_CLAMPER_H_ |