Coverage Report

Created: 2025-08-28 06:48

/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