Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove old DST implementation #1245

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
210 changes: 210 additions & 0 deletions include/hermes/VM/JSLib/DateCache.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

#ifndef HERMES_DATECACHE_H
#define HERMES_DATECACHE_H

#include "hermes/VM/JSLib/DateUtil.h"

namespace hermes {
namespace vm {

/// Type of time represented by an epoch.
enum class TimeType : int8_t {
Local,
Utc,
};

/// Cache local time offset (including daylight saving offset).
///
/// Standard offset is computed via localTZA() and cached. DST is cached in an
/// array of time intervals, time in the same interval has a fixed DST offset.
/// For every new time point that is not included in any existing time interval,
/// compute its DST offset, try extending an existing interval, or creating a
/// new one and storing to the cache array (replace the least recently used
/// cache if it's full).
///
/// The algorithm of the DST caching is described below:
/// 1. Initialize every interval in the DST cache array to be [t_max, -t_max],
/// which is called "empty" interval, here t_max is the maximum epoch time
/// supported by some OS time APIs. Initialize before_ and after_ to point to
/// the first two intervals in the array. And constant Delta is the maximum time
/// range that can have at most one DST transition. Each entry also has an epoch
/// value that is used to find out the least recently used entry, but we omit
/// the operations on them in this description.
/// 2. Give a UTC time point t, check if it's included in before_, if yes,
/// return the DST offset of before_. Otherwise, go to step 3.
/// 3. Search the cache array and try to find two intervals:
/// [s_before, e_before] where s_before <= t and as close as possible.
/// [s_after, e_after] where t < s_after and e_after ends as early as
/// possible. If one interval is not found, let it point to an empty interval
/// in the cache array (if no empty interval in it, reset the least recently
/// used one to empty and use it). Then assign the two intervals to before_
/// and after_ respectively.
/// 4. Check if the new before_ is empty, if yes, compute the DST of t, assign
/// it to before_, and set the interval of before_ to [t, t]. Otherwise, go
/// to step 5.
/// 5. Check if t is included in the new non-empty before_, if yes, return its
/// DST. Otherwise, go to step 6.
/// 6. If t > R_new, where R_new = before.end + Delta, compute the DST offset
/// for t and call extendOrRecomputeAfterCache(t, offset) (described later)
/// to update after_, then swap after_ and before_, return offset.
/// Otherwise, go to step 7.
/// 7. If t <= R_new < after_.start, compute the DST offset for t and call
/// extendOrRecomputeAfterCache(t, offset) to update after_.
/// 8. If before_.offset == after_.offset, merge after_ to before_ and
/// reset after_, then return the offset. Otherwise, go to step 9.
/// 9. At this step, there must been one DST transition in interval
/// (before_.end, after_.start], compute DST of t and do binary search to
/// find a time point that has the same offset as t, and extend before_ or
/// after_ to it. In the end, return the offset.
/// 10. Given a new UTC time point, repeat step 2 - 9.
///
/// Algorithm for extendOrRecomputeAfterCache(t, offset):
/// 1. If offset == after_.offset and after_.start-Delta <= t <= after_.end,
/// let after_.start = t and return.
/// 2. If after_ is not empty, scan every entry in the cache array (except
/// before_), find out the least recently used one, reset it to empty.
/// 3. Assign offset to after_ and update its interval to be [t, t].
///
/// Note that on Linux, the standard offset and DST offset is unchanged even if
/// TZ is updated, since the underlying time API in C library caches the time
/// zone. On Windows, currently we don't detect TZ changes as well. But this
/// could change if we migrate the usage of C API to Win32 API. On MacOS, the
/// time API does not cache, so we will check if the standard offset has changed
/// in computeDaylightSaving(), and reset both the standard offset cache and DST
/// cache in the next call to getLocalTimeOffset() or daylightSavingOffsetInMs()
/// (the current call will still use the old TZ). This is to ensure that they
/// are consistent w.r.t. the current TZ.
class LocalTimeOffsetCache {
public:
/// All runtime functionality should use the instance provided in
/// JSLibStorage, this is public only because we need to construct a instance
/// in the unit tests (alternatively, we have to create a singleton function
/// to be used in the tests).
LocalTimeOffsetCache() {
reset();
}

/// Reset the standard local time offset and the DST cache.
void reset() {
::tzset();
ltza_ = localTZA();
caches_.fill(DSTCacheEntry{});
before_ = caches_.data();
after_ = caches_.data() + 1;
epoch_ = 0;
needsToReset_ = false;
}

/// Compute local timezone offset (DST included).
/// \param timeMs time in milliseconds.
/// \param timeType whether \p timeMs is UTC or local time.
double getLocalTimeOffset(double timeMs, TimeType timeType);

/// \param time_ms UTC epoch in milliseconds.
/// \return Daylight saving offset at time \p time_ms.
int daylightSavingOffsetInMs(int64_t timeMs);

private:
LocalTimeOffsetCache(const LocalTimeOffsetCache &) = delete;
LocalTimeOffsetCache operator=(const LocalTimeOffsetCache &) = delete;

/// Number of cached time intervals for DST.
static constexpr unsigned kCacheSize = 32;
/// Default length of each time interval. The implementation relies on the
/// fact that no time zones have more than one daylight savings offset change
/// per 19 days. In Egypt in 2010 they decided to suspend DST during Ramadan.
/// This led to a short interval where DST is in effect from September 10 to
/// September 30.
static constexpr int64_t kDSTDeltaMs = 19 * SECONDS_PER_DAY * MS_PER_SECOND;
// The largest time that can be passed to OS date-time library functions.
static constexpr int64_t kMaxEpochTimeInMs =
std::numeric_limits<int32_t>::max() * MS_PER_SECOND;

struct DSTCacheEntry {
/// Start and end time of this DST cache interval, in UTC time.
int64_t startMs{kMaxEpochTimeInMs};
int64_t endMs{-kMaxEpochTimeInMs};
/// The DST offset in [startMs, endMs].
int dstOffsetMs{0};
/// Used for LRU.
int epoch{0};

/// \return whether this is a valid interval.
bool isEmpty() const {
return startMs > endMs;
}
};

/// Compute the DST offset at UTC time \p timeMs.
/// Note that this may update needsToReset_ if it detects a different
/// standard offset than the cached one.
int computeDaylightSaving(int64_t timeMs);

/// Increase the epoch counter and return it.
int bumpEpoch() {
++epoch_;
return epoch_;
}

/// Find the least recently used DST cache and reuse it.
/// \param skip do not scan this cache. Technically, it can be nullptr, which
/// means we don't skip any entry in the cache array. But currently we always
/// passed in an meaningful entry pointer (mostly before_ or after_).
/// \return an empty DST cache. This should never return nullptr.
DSTCacheEntry *leastRecentlyUsedExcept(const DSTCacheEntry *const skip);

/// Scan the DST caches to find a cached interval starts at or before \p
/// timeMs (as late as possible) and an interval ends after \p timeMs (as
/// early as possible). Set the pointer to before_ and after_ (it must be true
/// that before_ != after_). If none is found, reuse an empty cache.
void findBeforeAndAfterEntries(int64_t timeMs);

/// If after_->dstOffsetMs == \p dstOffsetMs and \p timeMs is included in
/// [after_->startMs - kDSTDeltaMs, after_->endMs], extend after_ to
/// [timeMs, after_->endMs].
/// Otherwise, let after_ point to the least recently used cache entry and
/// update its interval to be [timeMs, timeMs], and its DST offset to be
/// \p dstOffsetMs.
void extendOrRecomputeAfterCache(int64_t timeMs, int dstOffsetMs);

/// Integer counter used to find least recently used cache.
int epoch_;
/// Point to one entry in caches_ array, and this invariant always
/// holds: before_->startMs <= t <= before_->endMs, where t is the last seen
/// time point.
DSTCacheEntry *before_;
/// Point to one entry in the caches_ array, and must be different from
/// before_. Unlike before_, this interval does not always end after the last
/// seen time point. Instead, each time after findBeforeAndAfterEntries() is
/// called, after_ is updated to an interval that ends after the time point,
/// and each time after extendOrRecomputeAfterCache() is called, it's updated
/// to an interval that includes the time point. At any other steps, there is
/// no invariant between after_ and the last seen time point.
DSTCacheEntry *after_;
/// A list of cached intervals computed for previously seen time points.
/// In the beginning, each interval is initialized to empty. When every
/// interval is non-empty and we need a new empty one, reset the least
/// recently used one (by comparing the epoch value) to empty.
std::array<DSTCacheEntry, kCacheSize> caches_;
/// The standard local timezone offset (without DST offset).
int64_t ltza_;
/// Whether needs to reset the cache and ltza_.
/// We don't do reset in the middle of daylightSavingOffsetInMs() (essentially
/// before any call to computeDaylightSaving() that will detect TZ changes)
/// because it may cause that function never return if another thread is
/// keeping updating TZ. But that means we may return incorrect result before
/// reset(). This is consistent with previous implementation of utcTime() and
/// localTime().
bool needsToReset_;
};

} // namespace vm
} // namespace hermes

#endif // HERMES_DATECACHE_H
16 changes: 9 additions & 7 deletions include/hermes/VM/JSLib/DateUtil.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
namespace hermes {
namespace vm {

class LocalTimeOffsetCache;

//===----------------------------------------------------------------------===//
// Conversion constants

Expand All @@ -41,6 +43,10 @@ constexpr double MS_PER_MINUTE = MS_PER_SECOND * SECONDS_PER_MINUTE;
constexpr double MS_PER_HOUR = MS_PER_MINUTE * MINUTES_PER_HOUR;
constexpr double MS_PER_DAY = MS_PER_HOUR * HOURS_PER_DAY;

// Time value ranges from ES2024 21.4.1.1.
constexpr int64_t TIME_RANGE_SECS = SECONDS_PER_DAY * 100000000LL;
constexpr int64_t TIME_RANGE_MS = TIME_RANGE_SECS * MS_PER_SECOND;

//===----------------------------------------------------------------------===//
// Current time

Expand Down Expand Up @@ -106,10 +112,6 @@ double localTZA();
//===----------------------------------------------------------------------===//
// ES5.1 15.9.1.8

/// Daylight saving time adjustment, in milliseconds, at time \p t.
/// \param t timestamp in milliseconds.
double daylightSavingTA(double t);

namespace detail {
// Exposed for test only
int32_t equivalentTime(int64_t epochSecs);
Expand All @@ -118,11 +120,11 @@ int32_t equivalentTime(int64_t epochSecs);
// ES5.1 15.9.1.9

/// Conversion from UTC to local time.
double localTime(double t);
double localTime(double t, LocalTimeOffsetCache &);

/// Conversion from local time to UTC.
/// Spec refers to this as the UTC() function.
double utcTime(double t);
double utcTime(double t, LocalTimeOffsetCache &);

//===----------------------------------------------------------------------===//
// ES5.1 15.9.1.10
Expand Down Expand Up @@ -269,7 +271,7 @@ void timeTZString(double t, double tza, llvh::SmallVectorImpl<char> &buf);
/// - Date.parse(x.toISOString())
/// We can extend this to support other formats as well, when the given str
/// does not conform to the 15.9.1.15 format.
double parseDate(StringView str);
double parseDate(StringView str, LocalTimeOffsetCache &);

} // namespace vm
} // namespace hermes
Expand Down
5 changes: 5 additions & 0 deletions include/hermes/VM/JSLib/JSLibStorage.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
#ifndef HERMES_VM_JSLIB_RUNTIMECOMMONSTORAGE_H
#define HERMES_VM_JSLIB_RUNTIMECOMMONSTORAGE_H

#include "hermes/VM/JSLib/DateCache.h"

#include <random>

namespace hermes {
Expand All @@ -26,6 +28,9 @@ struct JSLibStorage {
/// PRNG used by Math.random()
std::mt19937_64 randomEngine_;
bool randomEngineSeeded_ = false;

/// Time zone offset cache used in conversion between UTC and local time.
LocalTimeOffsetCache localTimeOffsetCache;
};

} // namespace vm
Expand Down
1 change: 1 addition & 0 deletions lib/VM/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ set(source_files
JSLib/RegExp.cpp
JSLib/RegExpStringIterator.cpp
JSLib/DateUtil.cpp
JSLib/DateCache.cpp
JSLib/Sorting.cpp
JSLib/Symbol.cpp
JSLib/Date.cpp JSLib/DateUtil.cpp
Expand Down