/src/hermes/lib/VM/JSLib/DateCache.cpp
Line | Count | Source (jump to first uncovered line) |
1 | | /* |
2 | | * Copyright (c) Meta Platforms, Inc. and affiliates. |
3 | | * |
4 | | * This source code is licensed under the MIT license found in the |
5 | | * LICENSE file in the root directory of this source tree. |
6 | | */ |
7 | | |
8 | | #include "hermes/VM/JSLib/DateCache.h" |
9 | | |
10 | | namespace hermes { |
11 | | namespace vm { |
12 | | |
13 | | double LocalTimeOffsetCache::getLocalTimeOffset( |
14 | | double timeMs, |
15 | 0 | TimeType timeType) { |
16 | 0 | if (needsToReset_) { |
17 | 0 | reset(); |
18 | 0 | } |
19 | |
|
20 | 0 | if (timeType == TimeType::Utc) { |
21 | | // Out of allowed range by the spec, return NaN. |
22 | 0 | if (timeMs < -TIME_RANGE_MS || timeMs > TIME_RANGE_MS) |
23 | 0 | return std::numeric_limits<double>::quiet_NaN(); |
24 | | |
25 | 0 | return ltza_ + daylightSavingOffsetInMs(timeMs); |
26 | 0 | } |
27 | | // To compute the DST offset, we need to use UTC time (as required by |
28 | | // daylightSavingOffsetInMs()). However, getting the exact UTC time is not |
29 | | // possible since that would be circular. Therefore, we approximate the UTC |
30 | | // time by subtracting the standard time adjustment and then subtracting an |
31 | | // additional hour to comply with the spec's requirements |
32 | | // (https://tc39.es/ecma262/#sec-utc-t). |
33 | | // |
34 | | // For example, imagine a transition to DST that goes from UTC+0 to UTC+1, |
35 | | // moving 00:00 to 01:00. Any time in the skipped hour gets mapped to a |
36 | | // UTC time before the transition when we subtract an hour (e.g., 00:30 -> |
37 | | // 23:30), which will correctly result in DST not being in effect. |
38 | | // |
39 | | // Similarly, during a transition from DST back to standard time, the hour |
40 | | // from 00:00 to 01:00 is repeated. A local time in the repeated hour |
41 | | // similarly gets mapped to a UTC time before the transition. |
42 | | // |
43 | | // Note that this will not work if the timezone offset has historical/future |
44 | | // changes (which generates a different ltza than the one obtained here). |
45 | 0 | double guessUTC = timeMs - ltza_ - MS_PER_HOUR; |
46 | 0 | if (guessUTC < -TIME_RANGE_MS || guessUTC > TIME_RANGE_MS) |
47 | 0 | return std::numeric_limits<double>::quiet_NaN(); |
48 | 0 | return ltza_ + daylightSavingOffsetInMs(guessUTC); |
49 | 0 | } |
50 | | |
51 | 0 | int LocalTimeOffsetCache::computeDaylightSaving(int64_t utcTimeMs) { |
52 | 0 | std::time_t t = utcTimeMs / MS_PER_SECOND; |
53 | 0 | std::tm tm; |
54 | | #ifdef _WINDOWS |
55 | | auto err = ::localtime_s(&tm, &t); |
56 | | if (err) { |
57 | | return 0; |
58 | | } |
59 | | // It's not officially documented that whether Windows C API caches time zone, |
60 | | // but actual testing shows it does. So for now, we don't detect TZ changes |
61 | | // and reset the cache here. Otherwise, we have to call tzset() and |
62 | | // _get_timezone(), which is thread unsafe. And this behavior is the same as |
63 | | // on Linux. |
64 | | #else |
65 | 0 | std::tm *brokenTime = ::localtime_r(&t, &tm); |
66 | 0 | if (!brokenTime) { |
67 | 0 | return 0; |
68 | 0 | } |
69 | 0 | int dstOffset = tm.tm_isdst ? MS_PER_HOUR : 0; |
70 | 0 | int ltza = tm.tm_gmtoff * MS_PER_SECOND - dstOffset; |
71 | | // If ltza changes, we need to reset the cache. |
72 | 0 | if (ltza != ltza_) { |
73 | 0 | needsToReset_ = true; |
74 | 0 | } |
75 | 0 | #endif |
76 | 0 | return tm.tm_isdst ? MS_PER_HOUR : 0; |
77 | 0 | } |
78 | | |
79 | 0 | int LocalTimeOffsetCache::daylightSavingOffsetInMs(int64_t utcTimeMs) { |
80 | 0 | if (needsToReset_) { |
81 | 0 | reset(); |
82 | 0 | } |
83 | | |
84 | | // Some OS library calls don't work right for dates that cannot be represented |
85 | | // with int32_t. ES5.1 requires to map the time to a year with same |
86 | | // leap-year-ness and same starting day for the year. But for compatibility, |
87 | | // other engines, such as V8, use the actual year if it is in the range of |
88 | | // 1970..2037, which corresponds to the time range 0..kMaxEpochTimeInMs. |
89 | 0 | if (utcTimeMs < 0 || utcTimeMs > kMaxEpochTimeInMs) { |
90 | 0 | utcTimeMs = |
91 | 0 | detail::equivalentTime(utcTimeMs / MS_PER_SECOND) * MS_PER_SECOND; |
92 | 0 | } |
93 | | |
94 | | // Reset the counter to avoid overflow. Each call of this function may |
95 | | // increase epoch_ by more than 1 (conservatively smaller than 10), so we need |
96 | | // to subtract it from max value. In practice, this won't happen frequently |
97 | | // since most time we should see cache hit. |
98 | 0 | if (LLVM_UNLIKELY( |
99 | 0 | epoch_ >= std::numeric_limits<decltype(epoch_)>::max() - 10)) { |
100 | 0 | reset(); |
101 | 0 | } |
102 | | |
103 | | // Cache hit. |
104 | 0 | if (candidate_->include(utcTimeMs)) { |
105 | 0 | candidate_->epoch = bumpEpoch(); |
106 | 0 | return candidate_->dstOffsetMs; |
107 | 0 | } |
108 | | |
109 | | // Try to find cached intervals that happen before/after utcTimeMs. |
110 | 0 | auto [before, after] = findBeforeAndAfterEntries(utcTimeMs); |
111 | | // Set candidate_ to before by default, and reassign it in case that the |
112 | | // after cache is hit or used. |
113 | 0 | candidate_ = before; |
114 | | |
115 | | // No cached interval yet, compute a new one with utcTimeMs. |
116 | 0 | if (before->isEmpty()) { |
117 | 0 | int dstOffset = computeDaylightSaving(utcTimeMs); |
118 | 0 | before->dstOffsetMs = dstOffset; |
119 | 0 | before->startMs = utcTimeMs; |
120 | 0 | before->endMs = utcTimeMs; |
121 | 0 | before->epoch = bumpEpoch(); |
122 | 0 | return dstOffset; |
123 | 0 | } |
124 | | |
125 | | // Hits in the cached interval. |
126 | 0 | if (before->include(utcTimeMs)) { |
127 | 0 | before->epoch = bumpEpoch(); |
128 | 0 | return before->dstOffsetMs; |
129 | 0 | } |
130 | | |
131 | | // If utcTimeMs is larger than before->endMs + kDSTDeltaMs, we can't safely |
132 | | // extend before, because it could have more than one DST transition in the |
133 | | // interval. Instead, try if we can extend after (or recompute it). |
134 | 0 | if ((utcTimeMs - kDSTDeltaMs) > before->endMs) { |
135 | 0 | int dstOffset = computeDaylightSaving(utcTimeMs); |
136 | 0 | extendOrRecomputeCacheEntry(after, utcTimeMs, dstOffset); |
137 | | // May help cache hit in subsequent calls (in case that the passed in time |
138 | | // values are adjacent). |
139 | 0 | candidate_ = after; |
140 | 0 | return dstOffset; |
141 | 0 | } |
142 | | |
143 | | // Now, utcTimeMs is in the range of (before->endMs, before->endMs + |
144 | | // kDSTDeltaMs]. |
145 | | |
146 | 0 | before->epoch = bumpEpoch(); |
147 | 0 | int64_t newAfterStart = before->endMs < kMaxEpochTimeInMs - kDSTDeltaMs |
148 | 0 | ? before->endMs + kDSTDeltaMs |
149 | 0 | : kMaxEpochTimeInMs; |
150 | | // If after starts too late, extend it to newAfterStart or recompute it. |
151 | 0 | if (newAfterStart < after->startMs) { |
152 | 0 | int dstOffset = computeDaylightSaving(newAfterStart); |
153 | 0 | extendOrRecomputeCacheEntry(after, newAfterStart, dstOffset); |
154 | 0 | } else { |
155 | 0 | after->epoch = bumpEpoch(); |
156 | 0 | } |
157 | | |
158 | | // Now after->startMs is in (before->endMs, before->endMs + kDSTDeltaMs]. |
159 | | |
160 | | // If before and after have the same DST offset, merge them. |
161 | 0 | if (before->dstOffsetMs == after->dstOffsetMs) { |
162 | 0 | before->endMs = after->endMs; |
163 | 0 | *after = DSTCacheEntry{}; |
164 | 0 | return before->dstOffsetMs; |
165 | 0 | } |
166 | | |
167 | | // Binary search in (before->endMs, after->startMs] for DST transition |
168 | | // point. Note that after->startMs could be smaller than before->endMs |
169 | | // + kDSTDeltaMs, but that small interval has the same DST offset, so we |
170 | | // can ignore them in the below search. |
171 | | // Though 5 iterations should be enough to cover kDSTDeltaMs, if the |
172 | | // assumption of only one transition in kDSTDeltaMs no longer holds, we may |
173 | | // not be able to search the result. We'll stop the loop after 5 iterations |
174 | | // anyway. |
175 | 0 | for (int i = 4; i >= 0; --i) { |
176 | 0 | int delta = after->startMs - before->endMs; |
177 | 0 | int64_t middle = before->endMs + delta / 2; |
178 | 0 | int middleDstOffset = computeDaylightSaving(middle); |
179 | 0 | if (before->dstOffsetMs == middleDstOffset) { |
180 | 0 | before->endMs = middle; |
181 | 0 | if (utcTimeMs <= before->endMs) { |
182 | 0 | return middleDstOffset; |
183 | 0 | } |
184 | 0 | } else { |
185 | 0 | assert(after->dstOffsetMs == middleDstOffset); |
186 | 0 | after->startMs = middle; |
187 | 0 | if (utcTimeMs >= after->startMs) { |
188 | | // May help cache hit in subsequent calls (in case that the passed in |
189 | | // time values are adjacent). |
190 | 0 | candidate_ = after; |
191 | 0 | return middleDstOffset; |
192 | 0 | } |
193 | 0 | } |
194 | 0 | } |
195 | | |
196 | | // Fallthrough path of the binary search, just compute the DST for utcTimeMs. |
197 | 0 | return computeDaylightSaving(utcTimeMs); |
198 | 0 | } |
199 | | |
200 | | LocalTimeOffsetCache::DSTCacheEntry * |
201 | 0 | LocalTimeOffsetCache::leastRecentlyUsedExcept(const DSTCacheEntry *const skip) { |
202 | 0 | DSTCacheEntry *result = nullptr; |
203 | 0 | for (auto &cache : caches_) { |
204 | 0 | if (&cache == skip) |
205 | 0 | continue; |
206 | 0 | if (!result || result->epoch > cache.epoch) |
207 | 0 | result = &cache; |
208 | 0 | } |
209 | 0 | *result = DSTCacheEntry{}; |
210 | 0 | return result; |
211 | 0 | } |
212 | | |
213 | | std::tuple< |
214 | | LocalTimeOffsetCache::DSTCacheEntry *, |
215 | | LocalTimeOffsetCache::DSTCacheEntry *> |
216 | 0 | LocalTimeOffsetCache::findBeforeAndAfterEntries(int64_t timeMs) { |
217 | 0 | LocalTimeOffsetCache::DSTCacheEntry *before = nullptr; |
218 | 0 | LocalTimeOffsetCache::DSTCacheEntry *after = nullptr; |
219 | | |
220 | | // `before` should start as late as possible, while `after` should end as |
221 | | // early as possible so that they're closest to timeMs. |
222 | 0 | for (auto &cache : caches_) { |
223 | 0 | if (cache.startMs <= timeMs) { |
224 | 0 | if (!before || before->startMs < cache.startMs) |
225 | 0 | before = &cache; |
226 | 0 | } else if (timeMs < cache.endMs) { |
227 | 0 | if (!after || after->endMs > cache.endMs) |
228 | 0 | after = &cache; |
229 | 0 | } |
230 | 0 | } |
231 | | |
232 | | // None is found, reuse an empty cache for later computation. |
233 | 0 | if (!before) { |
234 | 0 | before = leastRecentlyUsedExcept(after); |
235 | 0 | } |
236 | 0 | if (!after) { |
237 | 0 | after = leastRecentlyUsedExcept(before); |
238 | 0 | } |
239 | |
|
240 | 0 | assert( |
241 | 0 | before && after && before != after && |
242 | 0 | "`before` and `after` interval should be valid"); |
243 | 0 | assert( |
244 | 0 | before->isEmpty() || |
245 | 0 | before->startMs <= timeMs && |
246 | 0 | "the start time of `before` must start on or before timeMs"); |
247 | 0 | assert( |
248 | 0 | after->isEmpty() || |
249 | 0 | timeMs < after->startMs && |
250 | 0 | "The start time of `after` must start after timeMs"); |
251 | 0 | assert( |
252 | 0 | before->isEmpty() || after->isEmpty() || |
253 | 0 | before->endMs < after->startMs && |
254 | 0 | "`before` interval must strictly start before `after` interval"); |
255 | | |
256 | 0 | return {before, after}; |
257 | 0 | } |
258 | | |
259 | | void LocalTimeOffsetCache::extendOrRecomputeCacheEntry( |
260 | | DSTCacheEntry *&entry, |
261 | | int64_t timeMs, |
262 | 0 | int dstOffsetMs) { |
263 | | // It's safe to extend the interval if timeMs is in the checked range. |
264 | 0 | if (entry->dstOffsetMs == dstOffsetMs && |
265 | 0 | entry->startMs - kDSTDeltaMs <= timeMs && timeMs <= entry->endMs) { |
266 | 0 | entry->startMs = timeMs; |
267 | 0 | } else { |
268 | | // Recompute the after cache using timeMs. |
269 | 0 | if (!entry->isEmpty()) { |
270 | 0 | entry = leastRecentlyUsedExcept(candidate_); |
271 | 0 | } |
272 | 0 | entry->startMs = timeMs; |
273 | 0 | entry->endMs = timeMs; |
274 | 0 | entry->dstOffsetMs = dstOffsetMs; |
275 | 0 | entry->epoch = bumpEpoch(); |
276 | 0 | } |
277 | 0 | } |
278 | | |
279 | | } // namespace vm |
280 | | } // namespace hermes |