/* -*- 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" #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 #include <sys/time.h> #endif /* XP_UNIX */ using mozilla::DebugOnly; #if defined(XP_UNIX) int64_t PRMJ_Now() { struct timeval tv; gettimeofday(&tv, 0); 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; }