diff options
Diffstat (limited to 'js/src/vm/Time.cpp')
-rw-r--r-- | js/src/vm/Time.cpp | 382 |
1 files changed, 382 insertions, 0 deletions
diff --git a/js/src/vm/Time.cpp b/js/src/vm/Time.cpp new file mode 100644 index 000000000..69e2cc41d --- /dev/null +++ b/js/src/vm/Time.cpp @@ -0,0 +1,382 @@ +/* -*- 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/. */ + +/* PR time code. */ + +#include "vm/Time.h" + +#include "mozilla/DebugOnly.h" +#include "mozilla/MathAlgorithms.h" + +#ifdef SOLARIS +#define _REENTRANT 1 +#endif +#include <string.h> +#include <time.h> + +#include "jstypes.h" +#include "jsutil.h" + +#ifdef XP_WIN +#include <windef.h> +#include <winbase.h> +#include <crtdbg.h> /* for _CrtSetReportMode */ +#include <mmsystem.h> /* for timeBegin/EndPeriod */ +#include <stdlib.h> /* for _set_invalid_parameter_handler */ + +#include "prinit.h" + +#endif + +#ifdef XP_UNIX + +#ifdef _SVID_GETTOD /* Defined only on Solaris, see Solaris <sys/types.h> */ +extern int gettimeofday(struct timeval* tv); +#endif + +#include <sys/time.h> + +#endif /* XP_UNIX */ + +using mozilla::DebugOnly; + +#if defined(XP_UNIX) +int64_t +PRMJ_Now() +{ + struct timeval tv; + +#ifdef _SVID_GETTOD /* Defined only on Solaris, see Solaris <sys/types.h> */ + gettimeofday(&tv); +#else + gettimeofday(&tv, 0); +#endif /* _SVID_GETTOD */ + + return int64_t(tv.tv_sec) * PRMJ_USEC_PER_SEC + int64_t(tv.tv_usec); +} + +#else + +// Returns the number of microseconds since the Unix epoch. +static double +FileTimeToUnixMicroseconds(const FILETIME& ft) +{ + // Get the time in 100ns intervals. + int64_t t = (int64_t(ft.dwHighDateTime) << 32) | int64_t(ft.dwLowDateTime); + + // The Windows epoch is around 1600. The Unix epoch is around 1970. + // Subtract the difference. + static const int64_t TimeToEpochIn100ns = 0x19DB1DED53E8000; + t -= TimeToEpochIn100ns; + + // Divide by 10 to convert to microseconds. + return double(t) * 0.1; +} + +struct CalibrationData { + double freq; /* The performance counter frequency */ + double offset; /* The low res 'epoch' */ + double timer_offset; /* The high res 'epoch' */ + + bool calibrated; + + CRITICAL_SECTION data_lock; +}; + +static CalibrationData calibration = { 0 }; + +static void +NowCalibrate() +{ + MOZ_ASSERT(calibration.freq > 0); + + // By wrapping a timeBegin/EndPeriod pair of calls around this loop, + // the loop seems to take much less time (1 ms vs 15ms) on Vista. + timeBeginPeriod(1); + FILETIME ft, ftStart; + GetSystemTimeAsFileTime(&ftStart); + do { + GetSystemTimeAsFileTime(&ft); + } while (memcmp(&ftStart, &ft, sizeof(ft)) == 0); + timeEndPeriod(1); + + LARGE_INTEGER now; + QueryPerformanceCounter(&now); + + calibration.offset = FileTimeToUnixMicroseconds(ft); + calibration.timer_offset = double(now.QuadPart); + calibration.calibrated = true; +} + +static const unsigned DataLockSpinCount = 4096; + +static void (WINAPI* pGetSystemTimePreciseAsFileTime)(LPFILETIME) = nullptr; + +void +PRMJ_NowInit() +{ + memset(&calibration, 0, sizeof(calibration)); + + // According to the documentation, QueryPerformanceFrequency will never + // return false or return a non-zero frequency on systems that run + // Windows XP or later. Also, the frequency is fixed so we only have to + // query it once. + LARGE_INTEGER liFreq; + DebugOnly<BOOL> res = QueryPerformanceFrequency(&liFreq); + MOZ_ASSERT(res); + calibration.freq = double(liFreq.QuadPart); + MOZ_ASSERT(calibration.freq > 0.0); + + InitializeCriticalSectionAndSpinCount(&calibration.data_lock, DataLockSpinCount); + + // Windows 8 has a new API function we can use. + if (HMODULE h = GetModuleHandle("kernel32.dll")) { + pGetSystemTimePreciseAsFileTime = + (void (WINAPI*)(LPFILETIME))GetProcAddress(h, "GetSystemTimePreciseAsFileTime"); + } +} + +void +PRMJ_NowShutdown() +{ + DeleteCriticalSection(&calibration.data_lock); +} + +#define MUTEX_LOCK(m) EnterCriticalSection(m) +#define MUTEX_UNLOCK(m) LeaveCriticalSection(m) +#define MUTEX_SETSPINCOUNT(m, c) SetCriticalSectionSpinCount((m),(c)) + +// Please see bug 363258 for why the win32 timing code is so complex. +int64_t +PRMJ_Now() +{ + if (pGetSystemTimePreciseAsFileTime) { + // Windows 8 has a new API function that does all the work. + FILETIME ft; + pGetSystemTimePreciseAsFileTime(&ft); + return int64_t(FileTimeToUnixMicroseconds(ft)); + } + + bool calibrated = false; + bool needsCalibration = !calibration.calibrated; + double cachedOffset = 0.0; + while (true) { + if (needsCalibration) { + MUTEX_LOCK(&calibration.data_lock); + + // Recalibrate only if no one else did before us. + if (calibration.offset == cachedOffset) { + // Since calibration can take a while, make any other + // threads immediately wait. + MUTEX_SETSPINCOUNT(&calibration.data_lock, 0); + + NowCalibrate(); + + calibrated = true; + + // Restore spin count. + MUTEX_SETSPINCOUNT(&calibration.data_lock, DataLockSpinCount); + } + + MUTEX_UNLOCK(&calibration.data_lock); + } + + // Calculate a low resolution time. + FILETIME ft; + GetSystemTimeAsFileTime(&ft); + double lowresTime = FileTimeToUnixMicroseconds(ft); + + // Grab high resolution time. + LARGE_INTEGER now; + QueryPerformanceCounter(&now); + double highresTimerValue = double(now.QuadPart); + + MUTEX_LOCK(&calibration.data_lock); + double highresTime = calibration.offset + + PRMJ_USEC_PER_SEC * (highresTimerValue - calibration.timer_offset) / calibration.freq; + cachedOffset = calibration.offset; + MUTEX_UNLOCK(&calibration.data_lock); + + // Assume the NT kernel ticks every 15.6 ms. Unfortunately there's no + // good way to determine this (NtQueryTimerResolution is an undocumented + // API), but 15.6 ms seems to be the max possible value. Hardcoding 15.6 + // means we'll recalibrate if the highres and lowres timers diverge by + // more than 30 ms. + static const double KernelTickInMicroseconds = 15625.25; + + // Check for clock skew. + double diff = lowresTime - highresTime; + + // For some reason that I have not determined, the skew can be + // up to twice a kernel tick. This does not seem to happen by + // itself, but I have only seen it triggered by another program + // doing some kind of file I/O. The symptoms are a negative diff + // followed by an equally large positive diff. + if (mozilla::Abs(diff) <= 2 * KernelTickInMicroseconds) { + // No detectable clock skew. + return int64_t(highresTime); + } + + if (calibrated) { + // If we already calibrated once this instance, and the + // clock is still skewed, then either the processor(s) are + // wildly changing clockspeed or the system is so busy that + // we get switched out for long periods of time. In either + // case, it would be infeasible to make use of high + // resolution results for anything, so let's resort to old + // behavior for this call. It's possible that in the + // future, the user will want the high resolution timer, so + // we don't disable it entirely. + return int64_t(lowresTime); + } + + // It is possible that when we recalibrate, we will return a + // value less than what we have returned before; this is + // unavoidable. We cannot tell the different between a + // faulty QueryPerformanceCounter implementation and user + // changes to the operating system time. Since we must + // respect user changes to the operating system time, we + // cannot maintain the invariant that Date.now() never + // decreases; the old implementation has this behavior as + // well. + needsCalibration = true; + } +} +#endif + +#ifdef XP_WIN +static void +PRMJ_InvalidParameterHandler(const wchar_t* expression, + const wchar_t* function, + const wchar_t* file, + unsigned int line, + uintptr_t pReserved) +{ + /* empty */ +} +#endif + +/* Format a time value into a buffer. Same semantics as strftime() */ +size_t +PRMJ_FormatTime(char* buf, int buflen, const char* fmt, PRMJTime* prtm) +{ + size_t result = 0; +#if defined(XP_UNIX) || defined(XP_WIN) + struct tm a; + int fake_tm_year = 0; +#ifdef XP_WIN + _invalid_parameter_handler oldHandler; + int oldReportMode; +#endif + + memset(&a, 0, sizeof(struct tm)); + + a.tm_sec = prtm->tm_sec; + a.tm_min = prtm->tm_min; + a.tm_hour = prtm->tm_hour; + a.tm_mday = prtm->tm_mday; + a.tm_mon = prtm->tm_mon; + a.tm_wday = prtm->tm_wday; + + /* + * On systems where |struct tm| has members tm_gmtoff and tm_zone, we + * must fill in those values, or else strftime will return wrong results + * (e.g., bug 511726, bug 554338). + */ +#if defined(HAVE_LOCALTIME_R) && defined(HAVE_TM_ZONE_TM_GMTOFF) + { + /* + * Fill out |td| to the time represented by |prtm|, leaving the + * timezone fields zeroed out. localtime_r will then fill in the + * timezone fields for that local time according to the system's + * timezone parameters. + */ + struct tm td; + memset(&td, 0, sizeof(td)); + td.tm_sec = prtm->tm_sec; + td.tm_min = prtm->tm_min; + td.tm_hour = prtm->tm_hour; + td.tm_mday = prtm->tm_mday; + td.tm_mon = prtm->tm_mon; + td.tm_wday = prtm->tm_wday; + td.tm_year = prtm->tm_year - 1900; + td.tm_yday = prtm->tm_yday; + td.tm_isdst = prtm->tm_isdst; + time_t t = mktime(&td); + localtime_r(&t, &td); + + a.tm_gmtoff = td.tm_gmtoff; + a.tm_zone = td.tm_zone; + } +#endif + + /* + * Years before 1900 and after 9999 cause strftime() to abort on Windows. + * To avoid that we replace it with FAKE_YEAR_BASE + year % 100 and then + * replace matching substrings in the strftime() result with the real year. + * Note that FAKE_YEAR_BASE should be a multiple of 100 to make 2-digit + * year formats (%y) work correctly (since we won't find the fake year + * in that case). + * e.g. new Date(1873, 0).toLocaleFormat('%Y %y') => "1873 73" + * See bug 327869. + */ +#define FAKE_YEAR_BASE 9900 + if (prtm->tm_year < 1900 || prtm->tm_year > 9999) { + fake_tm_year = FAKE_YEAR_BASE + prtm->tm_year % 100; + a.tm_year = fake_tm_year - 1900; + } + else { + a.tm_year = prtm->tm_year - 1900; + } + a.tm_yday = prtm->tm_yday; + a.tm_isdst = prtm->tm_isdst; + + /* + * Even with the above, SunOS 4 seems to detonate if tm_zone and tm_gmtoff + * are null. This doesn't quite work, though - the timezone is off by + * tzoff + dst. (And mktime seems to return -1 for the exact dst + * changeover time.) + */ + +#ifdef XP_WIN + oldHandler = _set_invalid_parameter_handler(PRMJ_InvalidParameterHandler); + oldReportMode = _CrtSetReportMode(_CRT_ASSERT, 0); +#endif + + result = strftime(buf, buflen, fmt, &a); + +#ifdef XP_WIN + _set_invalid_parameter_handler(oldHandler); + _CrtSetReportMode(_CRT_ASSERT, oldReportMode); +#endif + + if (fake_tm_year && result) { + char real_year[16]; + char fake_year[16]; + size_t real_year_len; + size_t fake_year_len; + char* p; + + sprintf(real_year, "%d", prtm->tm_year); + real_year_len = strlen(real_year); + sprintf(fake_year, "%d", fake_tm_year); + fake_year_len = strlen(fake_year); + + /* Replace the fake year in the result with the real year. */ + for (p = buf; (p = strstr(p, fake_year)); p += real_year_len) { + size_t new_result = result + real_year_len - fake_year_len; + if ((int)new_result >= buflen) { + return 0; + } + memmove(p + real_year_len, p + fake_year_len, strlen(p + fake_year_len)); + memcpy(p, real_year, real_year_len); + result = new_result; + *(buf + result) = '\0'; + } + } +#endif + return result; +} |