diff options
Diffstat (limited to 'js/src/vm/DateTime.cpp')
-rw-r--r-- | js/src/vm/DateTime.cpp | 354 |
1 files changed, 354 insertions, 0 deletions
diff --git a/js/src/vm/DateTime.cpp b/js/src/vm/DateTime.cpp new file mode 100644 index 000000000..e35ad4285 --- /dev/null +++ b/js/src/vm/DateTime.cpp @@ -0,0 +1,354 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * vim: set ts=8 sts=4 et sw=4 tw=99: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "vm/DateTime.h" + +#include "mozilla/Atomics.h" + +#include <time.h> + +#include "jsutil.h" + +#include "js/Date.h" +#if ENABLE_INTL_API +#include "unicode/timezone.h" +#endif + +using mozilla::Atomic; +using mozilla::ReleaseAcquire; +using mozilla::UnspecifiedNaN; + +/* static */ js::DateTimeInfo +js::DateTimeInfo::instance; + +/* static */ mozilla::Atomic<bool, mozilla::ReleaseAcquire> +js::DateTimeInfo::AcquireLock::spinLock; + +static bool +ComputeLocalTime(time_t local, struct tm* ptm) +{ +#if defined(_WIN32) + return localtime_s(ptm, &local) == 0; +#elif defined(HAVE_LOCALTIME_R) + return localtime_r(&local, ptm); +#else + struct tm* otm = localtime(&local); + if (!otm) + return false; + *ptm = *otm; + return true; +#endif +} + +static bool +ComputeUTCTime(time_t t, struct tm* ptm) +{ +#if defined(_WIN32) + return gmtime_s(ptm, &t) == 0; +#elif defined(HAVE_GMTIME_R) + return gmtime_r(&t, ptm); +#else + struct tm* otm = gmtime(&t); + if (!otm) + return false; + *ptm = *otm; + return true; +#endif +} + +/* + * Compute the offset in seconds from the current UTC time to the current local + * standard time (i.e. not including any offset due to DST). + * + * Examples: + * + * Suppose we are in California, USA on January 1, 2013 at 04:00 PST (UTC-8, no + * DST in effect), corresponding to 12:00 UTC. This function would then return + * -8 * SecondsPerHour, or -28800. + * + * Or suppose we are in Berlin, Germany on July 1, 2013 at 17:00 CEST (UTC+2, + * DST in effect), corresponding to 15:00 UTC. This function would then return + * +1 * SecondsPerHour, or +3600. + */ +static int32_t +UTCToLocalStandardOffsetSeconds() +{ + using js::SecondsPerDay; + using js::SecondsPerHour; + using js::SecondsPerMinute; + +#if defined(XP_WIN) + // Windows doesn't follow POSIX: updates to the TZ environment variable are + // not reflected immediately on that platform as they are on other systems + // without this call. + _tzset(); +#endif + + // Get the current time. + time_t currentMaybeWithDST = time(nullptr); + if (currentMaybeWithDST == time_t(-1)) + return 0; + + // Break down the current time into its (locally-valued, maybe with DST) + // components. + struct tm local; + if (!ComputeLocalTime(currentMaybeWithDST, &local)) + return 0; + + // Compute a |time_t| corresponding to |local| interpreted without DST. + time_t currentNoDST; + if (local.tm_isdst == 0) { + // If |local| wasn't DST, we can use the same time. + currentNoDST = currentMaybeWithDST; + } else { + // If |local| respected DST, we need a time broken down into components + // ignoring DST. Turn off DST in the broken-down time. + local.tm_isdst = 0; + + // Compute a |time_t t| corresponding to the broken-down time with DST + // off. This has boundary-condition issues (for about the duration of + // a DST offset) near the time a location moves to a different time + // zone. But 1) errors will be transient; 2) locations rarely change + // time zone; and 3) in the absence of an API that provides the time + // zone offset directly, this may be the best we can do. + currentNoDST = mktime(&local); + if (currentNoDST == time_t(-1)) + return 0; + } + + // Break down the time corresponding to the no-DST |local| into UTC-based + // components. + struct tm utc; + if (!ComputeUTCTime(currentNoDST, &utc)) + return 0; + + // Finally, compare the seconds-based components of the local non-DST + // representation and the UTC representation to determine the actual + // difference. + int utc_secs = utc.tm_hour * SecondsPerHour + utc.tm_min * SecondsPerMinute; + int local_secs = local.tm_hour * SecondsPerHour + local.tm_min * SecondsPerMinute; + + // Same-day? Just subtract the seconds counts. + if (utc.tm_mday == local.tm_mday) + return local_secs - utc_secs; + + // If we have more UTC seconds, move local seconds into the UTC seconds' + // frame of reference and then subtract. + if (utc_secs > local_secs) + return (SecondsPerDay + local_secs) - utc_secs; + + // Otherwise we have more local seconds, so move the UTC seconds into the + // local seconds' frame of reference and then subtract. + return local_secs - (utc_secs + SecondsPerDay); +} + +void +js::DateTimeInfo::internalUpdateTimeZoneAdjustment() +{ + /* + * The difference between local standard time and UTC will never change for + * a given time zone. + */ + utcToLocalStandardOffsetSeconds = UTCToLocalStandardOffsetSeconds(); + + double newTZA = utcToLocalStandardOffsetSeconds * msPerSecond; + if (newTZA == localTZA_) + return; + + localTZA_ = newTZA; + + /* + * The initial range values are carefully chosen to result in a cache miss + * on first use given the range of possible values. Be careful to keep + * these values and the caching algorithm in sync! + */ + offsetMilliseconds = 0; + rangeStartSeconds = rangeEndSeconds = INT64_MIN; + oldOffsetMilliseconds = 0; + oldRangeStartSeconds = oldRangeEndSeconds = INT64_MIN; + + sanityCheck(); +} + +/* + * Since getDSTOffsetMilliseconds guarantees that all times seen will be + * positive, we can initialize the range at construction time with large + * negative numbers to ensure the first computation is always a cache miss and + * doesn't return a bogus offset. + */ +/* static */ void +js::DateTimeInfo::init() +{ + DateTimeInfo* dtInfo = &DateTimeInfo::instance; + + MOZ_ASSERT(dtInfo->localTZA_ == 0, + "we should be initializing only once, and the static instance " + "should have started out zeroed"); + + // Set to a totally impossible TZA so that the comparison above will fail + // and all fields will be properly initialized. + dtInfo->localTZA_ = UnspecifiedNaN<double>(); + dtInfo->internalUpdateTimeZoneAdjustment(); +} + +int64_t +js::DateTimeInfo::computeDSTOffsetMilliseconds(int64_t utcSeconds) +{ + MOZ_ASSERT(utcSeconds >= 0); + MOZ_ASSERT(utcSeconds <= MaxUnixTimeT); + +#if defined(XP_WIN) + // Windows does not follow POSIX. Updates to the TZ environment variable + // are not reflected immediately on that platform as they are on UNIX + // systems without this call. + _tzset(); +#endif + + struct tm tm; + if (!ComputeLocalTime(static_cast<time_t>(utcSeconds), &tm)) + return 0; + + int32_t dayoff = int32_t((utcSeconds + utcToLocalStandardOffsetSeconds) % SecondsPerDay); + int32_t tmoff = tm.tm_sec + (tm.tm_min * SecondsPerMinute) + (tm.tm_hour * SecondsPerHour); + + int32_t diff = tmoff - dayoff; + + if (diff < 0) + diff += SecondsPerDay; + + return diff * msPerSecond; +} + +int64_t +js::DateTimeInfo::internalGetDSTOffsetMilliseconds(int64_t utcMilliseconds) +{ + sanityCheck(); + + int64_t utcSeconds = utcMilliseconds / msPerSecond; + + if (utcSeconds > MaxUnixTimeT) { + utcSeconds = MaxUnixTimeT; + } else if (utcSeconds < 0) { + /* Go ahead a day to make localtime work (does not work with 0). */ + utcSeconds = SecondsPerDay; + } + + /* + * NB: Be aware of the initial range values when making changes to this + * code: the first call to this method, with those initial range + * values, must result in a cache miss. + */ + + if (rangeStartSeconds <= utcSeconds && utcSeconds <= rangeEndSeconds) + return offsetMilliseconds; + + if (oldRangeStartSeconds <= utcSeconds && utcSeconds <= oldRangeEndSeconds) + return oldOffsetMilliseconds; + + oldOffsetMilliseconds = offsetMilliseconds; + oldRangeStartSeconds = rangeStartSeconds; + oldRangeEndSeconds = rangeEndSeconds; + + if (rangeStartSeconds <= utcSeconds) { + int64_t newEndSeconds = Min(rangeEndSeconds + RangeExpansionAmount, MaxUnixTimeT); + if (newEndSeconds >= utcSeconds) { + int64_t endOffsetMilliseconds = computeDSTOffsetMilliseconds(newEndSeconds); + if (endOffsetMilliseconds == offsetMilliseconds) { + rangeEndSeconds = newEndSeconds; + return offsetMilliseconds; + } + + offsetMilliseconds = computeDSTOffsetMilliseconds(utcSeconds); + if (offsetMilliseconds == endOffsetMilliseconds) { + rangeStartSeconds = utcSeconds; + rangeEndSeconds = newEndSeconds; + } else { + rangeEndSeconds = utcSeconds; + } + return offsetMilliseconds; + } + + offsetMilliseconds = computeDSTOffsetMilliseconds(utcSeconds); + rangeStartSeconds = rangeEndSeconds = utcSeconds; + return offsetMilliseconds; + } + + int64_t newStartSeconds = Max<int64_t>(rangeStartSeconds - RangeExpansionAmount, 0); + if (newStartSeconds <= utcSeconds) { + int64_t startOffsetMilliseconds = computeDSTOffsetMilliseconds(newStartSeconds); + if (startOffsetMilliseconds == offsetMilliseconds) { + rangeStartSeconds = newStartSeconds; + return offsetMilliseconds; + } + + offsetMilliseconds = computeDSTOffsetMilliseconds(utcSeconds); + if (offsetMilliseconds == startOffsetMilliseconds) { + rangeStartSeconds = newStartSeconds; + rangeEndSeconds = utcSeconds; + } else { + rangeStartSeconds = utcSeconds; + } + return offsetMilliseconds; + } + + rangeStartSeconds = rangeEndSeconds = utcSeconds; + offsetMilliseconds = computeDSTOffsetMilliseconds(utcSeconds); + return offsetMilliseconds; +} + +void +js::DateTimeInfo::sanityCheck() +{ + MOZ_ASSERT(rangeStartSeconds <= rangeEndSeconds); + MOZ_ASSERT_IF(rangeStartSeconds == INT64_MIN, rangeEndSeconds == INT64_MIN); + MOZ_ASSERT_IF(rangeEndSeconds == INT64_MIN, rangeStartSeconds == INT64_MIN); + MOZ_ASSERT_IF(rangeStartSeconds != INT64_MIN, + rangeStartSeconds >= 0 && rangeEndSeconds >= 0); + MOZ_ASSERT_IF(rangeStartSeconds != INT64_MIN, + rangeStartSeconds <= MaxUnixTimeT && rangeEndSeconds <= MaxUnixTimeT); +} + +static struct IcuTimeZoneInfo +{ + Atomic<bool, ReleaseAcquire> locked; + enum { Valid = 0, NeedsUpdate } status; + + void acquire() { + while (!locked.compareExchange(false, true)) + continue; + } + + void release() { + MOZ_ASSERT(locked, "should have been acquired"); + locked = false; + } +} TZInfo; + + +JS_PUBLIC_API(void) +JS::ResetTimeZone() +{ + js::DateTimeInfo::updateTimeZoneAdjustment(); + +#if ENABLE_INTL_API && defined(ICU_TZ_HAS_RECREATE_DEFAULT) + TZInfo.acquire(); + TZInfo.status = IcuTimeZoneInfo::NeedsUpdate; + TZInfo.release(); +#endif +} + +void +js::ResyncICUDefaultTimeZone() +{ +#if ENABLE_INTL_API && defined(ICU_TZ_HAS_RECREATE_DEFAULT) + TZInfo.acquire(); + if (TZInfo.status == IcuTimeZoneInfo::NeedsUpdate) { + icu::TimeZone::recreateDefault(); + TZInfo.status = IcuTimeZoneInfo::Valid; + } + TZInfo.release(); +#endif +} |