From 59ea64c52d78ecd8e281e576fab16c208f87d4ae Mon Sep 17 00:00:00 2001 From: "Gang Zhao (Hermes)" Date: Wed, 26 Jun 2024 12:30:19 -0700 Subject: [PATCH] Use LocalTimeOffsetCache for UTC/Local time conversion Reviewed By: avp Differential Revision: D52791285 fbshipit-source-id: f12831d68774fbe847cd45e4d280e2471039bde7 --- include/hermes/VM/JSLib/DateUtil.h | 8 +-- lib/VM/JSLib/Date.cpp | 75 ++++++++++++++++++---------- lib/VM/JSLib/DateUtil.cpp | 56 ++++++++++----------- unittests/VMRuntime/DateUtilTest.cpp | 23 ++++++--- 4 files changed, 95 insertions(+), 67 deletions(-) diff --git a/include/hermes/VM/JSLib/DateUtil.h b/include/hermes/VM/JSLib/DateUtil.h index 40a190abbad..a5f2125ba0a 100644 --- a/include/hermes/VM/JSLib/DateUtil.h +++ b/include/hermes/VM/JSLib/DateUtil.h @@ -24,6 +24,8 @@ namespace hermes { namespace vm { +class LocalTimeOffsetCache; + //===----------------------------------------------------------------------===// // Conversion constants @@ -122,11 +124,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 @@ -273,7 +275,7 @@ void timeTZString(double t, double tza, llvh::SmallVectorImpl &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 diff --git a/lib/VM/JSLib/Date.cpp b/lib/VM/JSLib/Date.cpp index c610e436c1c..1bcae6d9b6f 100644 --- a/lib/VM/JSLib/Date.cpp +++ b/lib/VM/JSLib/Date.cpp @@ -15,6 +15,7 @@ #include "hermes/Support/OSCompat.h" #include "hermes/VM/HermesValue.h" #include "hermes/VM/JSLib/DateUtil.h" +#include "hermes/VM/JSLib/JSLibStorage.h" #include "hermes/VM/Operations.h" #pragma GCC diagnostic push @@ -441,8 +442,10 @@ dateConstructor_RJS(void *, Runtime &runtime, NativeArgs args) { if (v->isString()) { // Call the String -> Date parsing function. - finalDate = timeClip(parseDate(StringPrimitive::createStringView( - runtime, Handle::vmcast(v)))); + finalDate = timeClip(parseDate( + StringPrimitive::createStringView( + runtime, Handle::vmcast(v)), + runtime.getJSLibStorage()->localTimeOffsetCache)); } else { auto numRes = toNumber_RJS(runtime, v); if (numRes == ExecutionStatus::EXCEPTION) { @@ -461,7 +464,8 @@ dateConstructor_RJS(void *, Runtime &runtime, NativeArgs args) { // makeTimeFromArgs interprets arguments as UTC. // We want them as local time, so pretend that they are, // and call utcTime to get the final UTC value we want to store. - finalDate = timeClip(utcTime(*cr)); + finalDate = timeClip( + utcTime(*cr, runtime.getJSLibStorage()->localTimeOffsetCache)); } self->setPrimitiveValue(finalDate); return self.getHermesValue(); @@ -469,7 +473,7 @@ dateConstructor_RJS(void *, Runtime &runtime, NativeArgs args) { llvh::SmallString<32> str{}; double t = curTime(); - double local = localTime(t); + double local = localTime(t, runtime.getJSLibStorage()->localTimeOffsetCache); dateTimeString(local, local - t, str); return runtime.ignoreAllocationFailure(StringPrimitive::create(runtime, str)); } @@ -480,9 +484,10 @@ dateParse_RJS(void *, Runtime &runtime, NativeArgs args) { if (res == ExecutionStatus::EXCEPTION) { return ExecutionStatus::EXCEPTION; } - return HermesValue::encodeUntrustedNumberValue( - parseDate(StringPrimitive::createStringView( - runtime, runtime.makeHandle(std::move(*res))))); + return HermesValue::encodeUntrustedNumberValue(parseDate( + StringPrimitive::createStringView( + runtime, runtime.makeHandle(std::move(*res))), + runtime.getJSLibStorage()->localTimeOffsetCache)); } CallResult dateUTC_RJS(void *, Runtime &runtime, NativeArgs args) { @@ -541,7 +546,8 @@ datePrototypeToStringHelper(void *ctx, Runtime &runtime, NativeArgs args) { } llvh::SmallString<32> str{}; if (!opts->isUTC) { - double local = localTime(t); + double local = + localTime(t, runtime.getJSLibStorage()->localTimeOffsetCache); opts->toStringFn(local, local - t, str); } else { opts->toStringFn(t, 0, str); @@ -647,7 +653,7 @@ datePrototypeGetterHelper(void *ctx, Runtime &runtime, NativeArgs args) { // Store the original value of t to be used in offset calculations. double utc = t; if (!opts->isUTC) { - t = localTime(t); + t = localTime(t, runtime.getJSLibStorage()->localTimeOffsetCache); } double result{std::numeric_limits::quiet_NaN()}; @@ -713,8 +719,9 @@ datePrototypeSetMilliseconds_RJS(void *ctx, Runtime &runtime, NativeArgs args) { "Date.prototype.setMilliseconds() called on non-Date object"); } double t = self->getPrimitiveValue(); + auto &localTimeOffsetCache = runtime.getJSLibStorage()->localTimeOffsetCache; if (!isUTC) { - t = localTime(t); + t = localTime(t, localTimeOffsetCache); } auto res = toNumber_RJS(runtime, args.getArgHandle(0)); if (res == ExecutionStatus::EXCEPTION) { @@ -723,7 +730,8 @@ datePrototypeSetMilliseconds_RJS(void *ctx, Runtime &runtime, NativeArgs args) { double ms = res->getNumber(); double date = makeDate( day(t), makeTime(hourFromTime(t), minFromTime(t), secFromTime(t), ms)); - double utcT = !isUTC ? timeClip(utcTime(date)) : timeClip(date); + double utcT = + !isUTC ? timeClip(utcTime(date, localTimeOffsetCache)) : timeClip(date); self->setPrimitiveValue(utcT); return HermesValue::encodeUntrustedNumberValue(utcT); } @@ -739,8 +747,9 @@ datePrototypeSetSeconds_RJS(void *ctx, Runtime &runtime, NativeArgs args) { "Date.prototype.setSeconds() called on non-Date object"); } double t = self->getPrimitiveValue(); + auto &localTimeOffsetCache = runtime.getJSLibStorage()->localTimeOffsetCache; if (!isUTC) { - t = localTime(t); + t = localTime(t, localTimeOffsetCache); } auto res = toNumber_RJS(runtime, args.getArgHandle(0)); if (res == ExecutionStatus::EXCEPTION) { @@ -760,7 +769,8 @@ datePrototypeSetSeconds_RJS(void *ctx, Runtime &runtime, NativeArgs args) { double date = makeDate(day(t), makeTime(hourFromTime(t), minFromTime(t), s, milli)); - double utcT = !isUTC ? timeClip(utcTime(date)) : timeClip(date); + double utcT = + !isUTC ? timeClip(utcTime(date, localTimeOffsetCache)) : timeClip(date); self->setPrimitiveValue(utcT); return HermesValue::encodeUntrustedNumberValue(utcT); } @@ -776,8 +786,9 @@ datePrototypeSetMinutes_RJS(void *ctx, Runtime &runtime, NativeArgs args) { "Date.prototype.setMinutes() called on non-Date object"); } double t = self->getPrimitiveValue(); + auto &localTimeOffsetCache = runtime.getJSLibStorage()->localTimeOffsetCache; if (!isUTC) { - t = localTime(t); + t = localTime(t, localTimeOffsetCache); } auto res = toNumber_RJS(runtime, args.getArgHandle(0)); if (res == ExecutionStatus::EXCEPTION) { @@ -806,7 +817,8 @@ datePrototypeSetMinutes_RJS(void *ctx, Runtime &runtime, NativeArgs args) { } double date = makeDate(day(t), makeTime(hourFromTime(t), m, s, milli)); - double utcT = !isUTC ? timeClip(utcTime(date)) : timeClip(date); + double utcT = + !isUTC ? timeClip(utcTime(date, localTimeOffsetCache)) : timeClip(date); self->setPrimitiveValue(utcT); return HermesValue::encodeUntrustedNumberValue(utcT); } @@ -822,8 +834,9 @@ datePrototypeSetHours_RJS(void *ctx, Runtime &runtime, NativeArgs args) { "Date.prototype.setHours() called on non-Date object"); } double t = self->getPrimitiveValue(); + auto &localTimeOffsetCache = runtime.getJSLibStorage()->localTimeOffsetCache; if (!isUTC) { - t = localTime(t); + t = localTime(t, localTimeOffsetCache); } auto res = toNumber_RJS(runtime, args.getArgHandle(0)); if (res == ExecutionStatus::EXCEPTION) { @@ -862,7 +875,8 @@ datePrototypeSetHours_RJS(void *ctx, Runtime &runtime, NativeArgs args) { } double date = makeDate(day(t), makeTime(h, m, s, milli)); - double utcT = !isUTC ? timeClip(utcTime(date)) : timeClip(date); + double utcT = + !isUTC ? timeClip(utcTime(date, localTimeOffsetCache)) : timeClip(date); self->setPrimitiveValue(utcT); return HermesValue::encodeUntrustedNumberValue(utcT); } @@ -877,8 +891,9 @@ datePrototypeSetDate_RJS(void *ctx, Runtime &runtime, NativeArgs args) { "Date.prototype.setDate() called on non-Date object"); } double t = self->getPrimitiveValue(); + auto &localTimeOffsetCache = runtime.getJSLibStorage()->localTimeOffsetCache; if (!isUTC) { - t = localTime(t); + t = localTime(t, localTimeOffsetCache); } auto res = toNumber_RJS(runtime, args.getArgHandle(0)); if (res == ExecutionStatus::EXCEPTION) { @@ -887,7 +902,8 @@ datePrototypeSetDate_RJS(void *ctx, Runtime &runtime, NativeArgs args) { double dt = res->getNumber(); double newDate = makeDate( makeDay(yearFromTime(t), monthFromTime(t), dt), timeWithinDay(t)); - double utcT = !isUTC ? timeClip(utcTime(newDate)) : timeClip(newDate); + double utcT = !isUTC ? timeClip(utcTime(newDate, localTimeOffsetCache)) + : timeClip(newDate); self->setPrimitiveValue(utcT); return HermesValue::encodeUntrustedNumberValue(utcT); } @@ -903,8 +919,9 @@ datePrototypeSetMonth_RJS(void *ctx, Runtime &runtime, NativeArgs args) { "Date.prototype.setMonth() called on non-Date object"); } double t = self->getPrimitiveValue(); + auto &localTimeOffsetCache = runtime.getJSLibStorage()->localTimeOffsetCache; if (!isUTC) { - t = localTime(t); + t = localTime(t, localTimeOffsetCache); } auto res = toNumber_RJS(runtime, args.getArgHandle(0)); if (res == ExecutionStatus::EXCEPTION) { @@ -922,7 +939,8 @@ datePrototypeSetMonth_RJS(void *ctx, Runtime &runtime, NativeArgs args) { dt = dateFromTime(t); } double newDate = makeDate(makeDay(yearFromTime(t), m, dt), timeWithinDay(t)); - double utcT = !isUTC ? timeClip(utcTime(newDate)) : timeClip(newDate); + double utcT = !isUTC ? timeClip(utcTime(newDate, localTimeOffsetCache)) + : timeClip(newDate); self->setPrimitiveValue(utcT); return HermesValue::encodeUntrustedNumberValue(utcT); } @@ -938,8 +956,9 @@ datePrototypeSetFullYear_RJS(void *ctx, Runtime &runtime, NativeArgs args) { "Date.prototype.setFullYear() called on non-Date object"); } double t = self->getPrimitiveValue(); + auto &localTimeOffsetCache = runtime.getJSLibStorage()->localTimeOffsetCache; if (!isUTC) { - t = localTime(t); + t = localTime(t, localTimeOffsetCache); } if (std::isnan(t)) { t = 0; @@ -970,7 +989,8 @@ datePrototypeSetFullYear_RJS(void *ctx, Runtime &runtime, NativeArgs args) { dt = dateFromTime(t); } double newDate = makeDate(makeDay(y, m, dt), timeWithinDay(t)); - double utcT = !isUTC ? timeClip(utcTime(newDate)) : timeClip(newDate); + double utcT = !isUTC ? timeClip(utcTime(newDate, localTimeOffsetCache)) + : timeClip(newDate); self->setPrimitiveValue(utcT); return HermesValue::encodeUntrustedNumberValue(utcT); } @@ -986,7 +1006,8 @@ datePrototypeSetYear_RJS(void *ctx, Runtime &runtime, NativeArgs args) { "Date.prototype.setYear() called on non-Date object"); } double t = self->getPrimitiveValue(); - t = localTime(t); + auto &localTimeOffsetCache = runtime.getJSLibStorage()->localTimeOffsetCache; + t = localTime(t, localTimeOffsetCache); if (std::isnan(t)) { t = 0; } @@ -1001,8 +1022,10 @@ datePrototypeSetYear_RJS(void *ctx, Runtime &runtime, NativeArgs args) { } double yint = std::trunc(y); double yr = 0 <= yint && yint <= 99 ? yint + 1900 : y; - double date = utcTime(makeDate( - makeDay(yr, monthFromTime(t), dateFromTime(t)), timeWithinDay(t))); + double date = utcTime( + makeDate( + makeDay(yr, monthFromTime(t), dateFromTime(t)), timeWithinDay(t)), + localTimeOffsetCache); double d = timeClip(date); self->setPrimitiveValue(d); return HermesValue::encodeUntrustedNumberValue(d); diff --git a/lib/VM/JSLib/DateUtil.cpp b/lib/VM/JSLib/DateUtil.cpp index 6f2441f146d..bf5a9b3c40f 100644 --- a/lib/VM/JSLib/DateUtil.cpp +++ b/lib/VM/JSLib/DateUtil.cpp @@ -6,6 +6,7 @@ */ #include "hermes/VM/JSLib/DateUtil.h" +#include "hermes/VM/JSLib/DateCache.h" #include "hermes/Platform/Unicode/PlatformUnicode.h" #include "hermes/Support/Compiler.h" @@ -418,12 +419,18 @@ double daylightSavingTA(double t) { //===----------------------------------------------------------------------===// // ES5.1 15.9.1.9 +/// https://tc39.es/ecma262/#sec-localtime /// Conversion from UTC to local time. -double localTime(double t) { - return t + localTZA() + daylightSavingTA(t); +double localTime(double t, LocalTimeOffsetCache &localTimeOffsetCache) { + // The spec requires that localTime() accepts only finite time value, but for + // simplicity, we do the check here instead of the caller site. + if (!std::isfinite(t)) { + return std::numeric_limits::quiet_NaN(); + } + return t + localTimeOffsetCache.getLocalTimeOffset(t, TimeType::Utc); } -/// https://tc39.es/ecma262/#sec-localtime +/// https://tc39.es/ecma262/#sec-utc-t /// Conversion from local time to UTC. /// /// There is time ambiguity when converting local time to UTC time. For example, @@ -438,26 +445,11 @@ double localTime(double t) { /// 12 March 2017 is skipped, it should be interpreted as 2:30 AM UTC-05 /// instead of 3:30 AM UTC-04. However, in this case, both have the same UTC /// epoch. -double utcTime(double t) { - double ltza = localTZA(); - // To compute the DST offset, we need to use UTC time (as required by - // daylightSavingTA()). However, getting the exact UTC time is not possible - // since that would be circular. Therefore, we approximate the UTC time by - // subtracting the standard time adjustment and then subtracting an additional - // hour to comply with the spec's requirements as noted in the doc-comment. - // - // For example, imagine a transition to DST that goes from UTC+0 to UTC+1, - // moving 00:00 to 01:00. Any time in the skipped hour gets mapped to a - // UTC time before the transition when we subtract an hour (e.g., 00:30 -> - // 23:30), which will correctly result in DST not being in effect. - // - // Similarly, during a transition from DST back to standard time, the hour - // from 00:00 to 01:00 is repeated. A local time in the repeated hour - // similarly gets mapped to a UTC time before the transition. - // - // Note that this will not work if the timezone offset has historical/future - // changes (which generates a different ltza than the one obtained here). - return t - ltza - daylightSavingTA(t - ltza - MS_PER_HOUR); +double utcTime(double t, LocalTimeOffsetCache &localTimeOffsetCache) { + if (!std::isfinite(t)) { + return std::numeric_limits::quiet_NaN(); + } + return t - localTimeOffsetCache.getLocalTimeOffset(t, TimeType::Local); } //===----------------------------------------------------------------------===// @@ -787,7 +779,9 @@ static bool scanInt(InputIter &it, const InputIter end, int32_t &x) { return !ref.getAsInteger(10, x); } -static double parseISODate(StringView u16str) { +static double parseISODate( + StringView u16str, + LocalTimeOffsetCache &localTimeOffsetCache) { constexpr double nan = std::numeric_limits::quiet_NaN(); auto it = u16str.begin(); @@ -872,7 +866,7 @@ static double parseISODate(StringView u16str) { // forms are interpreted as a UTC time and date-time forms are interpreted // as a local time. double t = makeDate(makeDay(y, m - 1, d), makeTime(h, min, s, ms)); - t = utcTime(t); + t = utcTime(t, localTimeOffsetCache); return t; } @@ -917,7 +911,9 @@ static double parseISODate(StringView u16str) { return makeDate(makeDay(y, m - 1, d), makeTime(h - tzh, min - tzm, s, ms)); } -static double parseESDate(StringView str) { +static double parseESDate( + StringView str, + LocalTimeOffsetCache &localTimeOffsetCache) { constexpr double nan = std::numeric_limits::quiet_NaN(); StringView tok = str; @@ -1081,7 +1077,7 @@ static double parseESDate(StringView str) { if (it == end) { // Default to local time zone if no time zone provided double t = makeDate(makeDay(y, m - 1, d), makeTime(h, min, s, ms)); - t = utcTime(t); + t = utcTime(t, localTimeOffsetCache); return t; } @@ -1161,13 +1157,13 @@ static double parseESDate(StringView str) { return makeDate(makeDay(y, m - 1, d), makeTime(h - tzh, min - tzm, s, ms)); } -double parseDate(StringView str) { - double result = parseISODate(str); +double parseDate(StringView str, LocalTimeOffsetCache &localTimeOffsetCache) { + double result = parseISODate(str, localTimeOffsetCache); if (!std::isnan(result)) { return result; } - return parseESDate(str); + return parseESDate(str, localTimeOffsetCache); } } // namespace vm diff --git a/unittests/VMRuntime/DateUtilTest.cpp b/unittests/VMRuntime/DateUtilTest.cpp index 70aff5db051..e97eafb19f4 100644 --- a/unittests/VMRuntime/DateUtilTest.cpp +++ b/unittests/VMRuntime/DateUtilTest.cpp @@ -7,6 +7,7 @@ #include "TestHelpers.h" +#include "hermes/VM/JSLib/DateCache.h" #include "hermes/VM/JSLib/DateUtil.h" #include @@ -243,28 +244,33 @@ TEST(DateUtilTest, LocalTimeTest) { // 2018-07-02T04:00:00+1200[America/New_York] (DST not in effect) // 2018-07-02T01:00:00+0900[Asia/Tokyo] (DST not observed) + LocalTimeOffsetCache localTimeOffsetCache; + #ifdef _WINDOWS setTimeZone("PST8PDT"); #else setTimeZone("America/Los_Angeles"); #endif - EXPECT_EQ(1530435600000, localTime(1530460800000)); - EXPECT_EQ(1530460800000, utcTime(1530435600000)); + localTimeOffsetCache.reset(); + EXPECT_EQ(1530435600000, localTime(1530460800000, localTimeOffsetCache)); + EXPECT_EQ(1530460800000, utcTime(1530435600000, localTimeOffsetCache)); #ifdef _WINDOWS setTimeZone("EST5EDT"); #else setTimeZone("America/New_York"); #endif - EXPECT_EQ(1530446400000, localTime(1530460800000)); - EXPECT_EQ(1530460800000, utcTime(1530446400000)); + localTimeOffsetCache.reset(); + EXPECT_EQ(1530446400000, localTime(1530460800000, localTimeOffsetCache)); + EXPECT_EQ(1530460800000, utcTime(1530446400000, localTimeOffsetCache)); #ifdef _WINDOWS // This test is skipped due to Windows deficiency in TZ env variable. #else setTimeZone("Pacific/Auckland"); - EXPECT_EQ(1530504000000, localTime(1530460800000)); - EXPECT_EQ(1530460800000, utcTime(1530504000000)); + localTimeOffsetCache.reset(); + EXPECT_EQ(1530504000000, localTime(1530460800000, localTimeOffsetCache)); + EXPECT_EQ(1530460800000, utcTime(1530504000000, localTimeOffsetCache)); #endif #ifdef _WINDOWS @@ -272,8 +278,9 @@ TEST(DateUtilTest, LocalTimeTest) { #else setTimeZone("Asia/Tokyo"); #endif - EXPECT_EQ(1530493200000, localTime(1530460800000)); - EXPECT_EQ(1530460800000, utcTime(1530493200000)); + localTimeOffsetCache.reset(); + EXPECT_EQ(1530493200000, localTime(1530460800000, localTimeOffsetCache)); + EXPECT_EQ(1530460800000, utcTime(1530493200000, localTimeOffsetCache)); hermes::oscompat::unset_env("TZ"); }