From 3e0d82d561daae09e7303b6bf3c9deda31ba1c9b Mon Sep 17 00:00:00 2001 From: Levi Broderick Date: Sat, 14 Nov 2020 13:09:22 -0800 Subject: [PATCH 1/6] Improve performance of DateTime.UtcNow --- .../src/System/DateTime.Windows.cs | 123 +++++++++++++++++- 1 file changed, 117 insertions(+), 6 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/DateTime.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/DateTime.Windows.cs index ac4e1fe6be1168..545d78979a1f71 100644 --- a/src/libraries/System.Private.CoreLib/src/System/DateTime.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/DateTime.Windows.cs @@ -3,28 +3,139 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Threading; namespace System { public readonly partial struct DateTime { - internal static readonly bool s_systemSupportsLeapSeconds = SystemSupportsLeapSeconds(); + private static LeapSecondCache? s_leapSecondCache; + + // Question: Can this be [Stdcall, SuppressGCTransition]? + private static unsafe delegate* unmanaged[Stdcall] s_pfnGetSystemTimeAsFileTime = GetGetSystemTimeAsFileTimeFnPtr(); + + private static unsafe delegate* unmanaged[Stdcall] GetGetSystemTimeAsFileTimeFnPtr() + { + IntPtr kernel32Lib = NativeLibrary.Load("kernel32.dll", typeof(DateTime).Assembly, DllImportSearchPath.System32); + IntPtr pfnGetSystemTime = NativeLibrary.GetExport(kernel32Lib, "GetSystemTimeAsFileTime"); // will never fail + + if (NativeLibrary.TryGetExport(kernel32Lib, "GetSystemTimePreciseAsFileTime", out IntPtr pfnGetSystemTimePrecise)) + { + // If GetSystemTimePreciseAsFileTime exists, we'd like to use it. However, on + // misconfigured systems, it's possible for the "precise" time to be inaccurate: + // https://github.com/dotnet/runtime/issues/9014 + // If it's inaccurate, though, we expect it to be wildly inaccurate, so as a + // workaround/heuristic, we get both the "normal" and "precise" times, and as + // long as they're close, we use the precise one. This workaround can be removed + // when we better understand what's causing the drift and the issue is no longer + // a problem or can be better worked around on all targeted OSes. + + ulong filetimeStd, filetimePrecise; + ((delegate* unmanaged[Stdcall])pfnGetSystemTime)(&filetimeStd); + ((delegate* unmanaged[Stdcall])pfnGetSystemTimePrecise)(&filetimePrecise); + + if (Math.Abs((long)(filetimeStd - filetimePrecise)) <= 100 * TicksPerMillisecond) + { + pfnGetSystemTime = pfnGetSystemTimePrecise; // use the precise version + } + } + + return (delegate* unmanaged[Stdcall])pfnGetSystemTime; + } public static unsafe DateTime UtcNow { get { - if (s_systemSupportsLeapSeconds) + // The OS tick count and .NET's tick count are slightly different. The OS tick + // count is the *absolute* number of 100-ns intervals which have elapsed since + // January 1, 1601 (UTC). Due to leap second handling, the number of ticks per + // day is variable. Dec. 30, 2016 had 864,000,000,000 ticks (a standard 24-hour + // day), but Dec. 31, 2016 had 864,010,000,000 ticks due to leap second insertion. + // In .NET, *every* day is assumed to have exactly 24 hours (864,000,000,000 ticks). + // This means that per the OS, midnight Dec. 31, 2016 + 864 bn ticks = Dec. 31, 2016 23:59:60, + // but per .NET, midnight Dec. 31, 2016 + 864 bn ticks = Jan. 1, 2017 00:00:00. + // + // We can query the OS and have it deconstruct the tick count into (yyyy-mm-dd hh:mm:ss), + // constructing a new DateTime object from these components, but this is slow. + // So instead we'll rely on the fact that leap seconds only ever adjust the day + // by +1 or -1 second, and only at the very end of the day. That is, time rolls + // 23:59:58 -> 00:00:00 (negative leap second) or 23:59:59 -> 23:59:60 (positive leap + // second). Thus we assume that each day has at least 23 hr 59 min 59 sec, or + // 863,990,000,000 ticks. + // + // We take advantage of this by caching what the OS believes the tick count is at + // the beginning of the day vs. what .NET believes the tick count is at the beginning + // of the day. When the OS returns a tick count to us, if it's within 23:59:59 of + // what midnight was on the current day, then we know that there's no way for a leap + // second to have been inserted or removed, and we can short-circuit the leap second + // handling logic by performing a quick addition and returning immediately. If the + // OS-provided tick count is outside of our cached range, we'll update the cache. + // On the off-chance the API is called on the very last second (or two) of the day, + // we'll go down the slow path without updating the cache, and once another second + // elapses we'll be able to update the cache again. + + const long MinTicksPerDay = TicksPerDay - TicksPerSecond; // a day is at least 23:59:59 long + + ulong osTicks; + s_pfnGetSystemTimeAsFileTime(&osTicks); + + // If the OS doesn't support leap second handling, short-circuit everything. + + if (!s_systemSupportsLeapSeconds) { - FullSystemTime time; - GetSystemTimeWithLeapSecondsHandling(&time); - return CreateDateTimeFromSystemTime(in time); + return new DateTime((osTicks + FileTimeOffset) | KindUtc); } - return new DateTime(((ulong)(GetSystemTimeAsFileTime() + FileTimeOffset)) | KindUtc); + // If it's between 00:00:00 (inclusive) and 23:59:59 (exclusive) on the same day + // as the previous call to UtcNow, we can use the cached value. We can't remove + // the "is it before midnight?" check below since the system clock may have been + // moved backward. + + if (s_leapSecondCache is LeapSecondCache cache) + { + if (osTicks >= cache.WindowsTicksAsOfMidnight && osTicks < (cache.WindowsTicksAsOfMidnight + MinTicksPerDay)) + { + return new DateTime((osTicks - cache.WindowsTicksAsOfMidnight + cache.DateTimeTicksAsOfMidnight) | KindUtc); + } + } + + // If we reached this point, one of the following is true: + // a) the cache hasn't yet been initialized; or + // b) the day has changed since the last call to UtcNow; or + // c) the current time is 23:59:59 or 23:59:60. + // + // In cases (a) and (b), we'll update the cache. In case (c), we + // pessimistically assume we might be inside a leap second, so we + // won't update the cache. + + DateTime dateTime = FromFileTimeLeapSecondsAware((long)osTicks); + ulong ticksIntoDay = (ulong)dateTime.Ticks % (ulong)TicksPerDay; + if (ticksIntoDay < MinTicksPerDay) + { + // It's not yet 23:59:59, so update the cache. It's ok for multiple + // threads to do this concurrently as long as the write to the static + // is published *after* the cache object's fields have been populated. + + Volatile.Write(ref s_leapSecondCache, new LeapSecondCache + { + WindowsTicksAsOfMidnight = osTicks - ticksIntoDay, + DateTimeTicksAsOfMidnight = (ulong)dateTime.Ticks - ticksIntoDay + }); + } + + return dateTime; } } + private sealed class LeapSecondCache + { + internal ulong WindowsTicksAsOfMidnight; + internal ulong DateTimeTicksAsOfMidnight; + } + + internal static readonly bool s_systemSupportsLeapSeconds = SystemSupportsLeapSeconds(); + internal static unsafe bool IsValidTimeWithLeapSeconds(int year, int month, int day, int hour, int minute, int second, DateTimeKind kind) { DateTime dt = new DateTime(year, month, day); From c1c80e9cf257f7461113cb56e378a5e1a98498a4 Mon Sep 17 00:00:00 2001 From: Levi Broderick Date: Sat, 14 Nov 2020 15:20:14 -0800 Subject: [PATCH 2/6] Further perf improvements --- .../src/System/DateTime.Windows.cs | 67 +++++++++++-------- 1 file changed, 39 insertions(+), 28 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/DateTime.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/DateTime.Windows.cs index 545d78979a1f71..d414340bd57ed2 100644 --- a/src/libraries/System.Private.CoreLib/src/System/DateTime.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/DateTime.Windows.cs @@ -77,8 +77,11 @@ public static unsafe DateTime UtcNow const long MinTicksPerDay = TicksPerDay - TicksPerSecond; // a day is at least 23:59:59 long + // Caution: Pretending managed calli convention below to work around + // lack of API (see https://github.com/dotnet/runtime/issues/38134). + ulong osTicks; - s_pfnGetSystemTimeAsFileTime(&osTicks); + ((delegate*)s_pfnGetSystemTimeAsFileTime)(&osTicks); // If the OS doesn't support leap second handling, short-circuit everything. @@ -88,50 +91,58 @@ public static unsafe DateTime UtcNow } // If it's between 00:00:00 (inclusive) and 23:59:59 (exclusive) on the same day - // as the previous call to UtcNow, we can use the cached value. We can't remove - // the "is it before midnight?" check below since the system clock may have been - // moved backward. + // as the previous call to UtcNow, we can use the cached value. Use unsigned locals + // below so that negative tick counts resulting from the system clock being set + // backward result in integer underflow and cause cache misses. if (s_leapSecondCache is LeapSecondCache cache) { - if (osTicks >= cache.WindowsTicksAsOfMidnight && osTicks < (cache.WindowsTicksAsOfMidnight + MinTicksPerDay)) + ulong deltaTicksFromMidnight = osTicks - cache.WindowsTicksAsOfMidnight; + if (deltaTicksFromMidnight < MinTicksPerDay) { - return new DateTime((osTicks - cache.WindowsTicksAsOfMidnight + cache.DateTimeTicksAsOfMidnight) | KindUtc); + return new DateTime(deltaTicksFromMidnight + cache.DateTimeDateDataAsOfMidnight); // includes UTC marker } } - // If we reached this point, one of the following is true: - // a) the cache hasn't yet been initialized; or - // b) the day has changed since the last call to UtcNow; or - // c) the current time is 23:59:59 or 23:59:60. - // - // In cases (a) and (b), we'll update the cache. In case (c), we - // pessimistically assume we might be inside a leap second, so we - // won't update the cache. + // Move uncommon fallback pack into its own subroutine so that register + // allocation in get_UtcNow can stay as optimized as possible. - DateTime dateTime = FromFileTimeLeapSecondsAware((long)osTicks); - ulong ticksIntoDay = (ulong)dateTime.Ticks % (ulong)TicksPerDay; - if (ticksIntoDay < MinTicksPerDay) + return Fallback(osTicks); + static DateTime Fallback(ulong osTicks) { - // It's not yet 23:59:59, so update the cache. It's ok for multiple - // threads to do this concurrently as long as the write to the static - // is published *after* the cache object's fields have been populated. - - Volatile.Write(ref s_leapSecondCache, new LeapSecondCache + // If we reached this point, one of the following is true: + // a) the cache hasn't yet been initialized; or + // b) the day has changed since the last call to UtcNow; or + // c) the current time is 23:59:59 or 23:59:60. + // + // In cases (a) and (b), we'll update the cache. In case (c), we + // pessimistically assume we might be inside a leap second, so we + // won't update the cache. + + DateTime dateTime = FromFileTimeLeapSecondsAware((long)osTicks); + ulong ticksIntoDay = (ulong)dateTime.Ticks % (ulong)TicksPerDay; + if (ticksIntoDay < MinTicksPerDay) { - WindowsTicksAsOfMidnight = osTicks - ticksIntoDay, - DateTimeTicksAsOfMidnight = (ulong)dateTime.Ticks - ticksIntoDay - }); - } + // It's not yet 23:59:59, so update the cache. It's ok for multiple + // threads to do this concurrently as long as the write to the static + // is published *after* the cache object's fields have been populated. + + Volatile.Write(ref s_leapSecondCache, new LeapSecondCache + { + WindowsTicksAsOfMidnight = osTicks - ticksIntoDay, + DateTimeDateDataAsOfMidnight = dateTime._dateData - ticksIntoDay // includes UTC marker + }); + } - return dateTime; + return dateTime; + } } } private sealed class LeapSecondCache { internal ulong WindowsTicksAsOfMidnight; - internal ulong DateTimeTicksAsOfMidnight; + internal ulong DateTimeDateDataAsOfMidnight; } internal static readonly bool s_systemSupportsLeapSeconds = SystemSupportsLeapSeconds(); From 6589d69b1a2cbd3e59cad374162a5b64db729bea Mon Sep 17 00:00:00 2001 From: Levi Broderick Date: Mon, 16 Nov 2020 14:19:52 -0800 Subject: [PATCH 3/6] Use month-long cache instead of day-long cache --- .../src/System/DateTime.Windows.cs | 72 +++++++++++-------- 1 file changed, 41 insertions(+), 31 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/DateTime.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/DateTime.Windows.cs index d414340bd57ed2..161f868a3a3631 100644 --- a/src/libraries/System.Private.CoreLib/src/System/DateTime.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/DateTime.Windows.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Threading; @@ -65,17 +66,19 @@ public static unsafe DateTime UtcNow // 863,990,000,000 ticks. // // We take advantage of this by caching what the OS believes the tick count is at - // the beginning of the day vs. what .NET believes the tick count is at the beginning - // of the day. When the OS returns a tick count to us, if it's within 23:59:59 of - // what midnight was on the current day, then we know that there's no way for a leap - // second to have been inserted or removed, and we can short-circuit the leap second - // handling logic by performing a quick addition and returning immediately. If the - // OS-provided tick count is outside of our cached range, we'll update the cache. - // On the off-chance the API is called on the very last second (or two) of the day, - // we'll go down the slow path without updating the cache, and once another second - // elapses we'll be able to update the cache again. - - const long MinTicksPerDay = TicksPerDay - TicksPerSecond; // a day is at least 23:59:59 long + // the beginning of the month vs. what .NET believes the tick count is at the beginning + // of the month. When the OS returns a tick count to us, if it's before 23:59:59 on the + // last day of the month, then we know that there's no way for a leap second to have been + // inserted or removed, and we can short-circuit the leap second handling logic by + // performing a quick addition and returning immediately. + // + // This relies on: + // a) Leap seconds only ever taking place on the last day of the month + // (historically these are Jun. & Dec., but we support any month); + // b) Leap seconds only ever occurring at 23:59:59; + // c) At most a single leap second being added or removed per month; and + // d) Windows never inserting a historical leap second (into the past) once the + // process is up and running (future insertions are ok). // Caution: Pretending managed calli convention below to work around // lack of API (see https://github.com/dotnet/runtime/issues/38134). @@ -90,17 +93,17 @@ public static unsafe DateTime UtcNow return new DateTime((osTicks + FileTimeOffset) | KindUtc); } - // If it's between 00:00:00 (inclusive) and 23:59:59 (exclusive) on the same day - // as the previous call to UtcNow, we can use the cached value. Use unsigned locals - // below so that negative tick counts resulting from the system clock being set - // backward result in integer underflow and cause cache misses. + // If our cache has been populated and we're within its validity period, rely + // solely on the cache. We use unsigned arithmetic below so that if the system + // clock is set backward, the resulting value will integer underflow and cause + // a cache miss, forcing us down the slow path. if (s_leapSecondCache is LeapSecondCache cache) { - ulong deltaTicksFromMidnight = osTicks - cache.WindowsTicksAsOfMidnight; - if (deltaTicksFromMidnight < MinTicksPerDay) + ulong deltaTicksFromStartOfMonth = osTicks - cache.OSFileTimeTicksAtStartOfMonth; + if (deltaTicksFromStartOfMonth < cache.CacheValidityPeriodInTicks) { - return new DateTime(deltaTicksFromMidnight + cache.DateTimeDateDataAsOfMidnight); // includes UTC marker + return new DateTime(deltaTicksFromStartOfMonth + cache.DotNetDateDataAtStartOfMonth); // includes UTC marker } } @@ -112,37 +115,44 @@ static DateTime Fallback(ulong osTicks) { // If we reached this point, one of the following is true: // a) the cache hasn't yet been initialized; or - // b) the day has changed since the last call to UtcNow; or - // c) the current time is 23:59:59 or 23:59:60. + // b) the month has changed since the last call to UtcNow; or + // c) the current time is 23:59:59 or 23:59:60 on the last day of the month. // // In cases (a) and (b), we'll update the cache. In case (c), we // pessimistically assume we might be inside a leap second, so we // won't update the cache. - DateTime dateTime = FromFileTimeLeapSecondsAware((long)osTicks); - ulong ticksIntoDay = (ulong)dateTime.Ticks % (ulong)TicksPerDay; - if (ticksIntoDay < MinTicksPerDay) + DateTime utcNow = FromFileTimeLeapSecondsAware((long)osTicks); + DateTime oneSecondBeforeMonthEnd = new DateTime(utcNow.Year, utcNow.Month, DaysInMonth(utcNow.Year, utcNow.Month), 23, 59, 59, DateTimeKind.Utc); + + if (utcNow < oneSecondBeforeMonthEnd) { - // It's not yet 23:59:59, so update the cache. It's ok for multiple - // threads to do this concurrently as long as the write to the static - // is published *after* the cache object's fields have been populated. + // It's not yet 23:59:59 on the last day of the month, so update the cache. + // It's ok for multiple threads to do this concurrently as long as the write + // to the static field is published *after* the cache's instance fields have + // been populated. + + DateTime startOfMonth = new DateTime(utcNow.Year, utcNow.Month, 1, 0, 0, 0, DateTimeKind.Utc); + Debug.Assert(startOfMonth <= utcNow); Volatile.Write(ref s_leapSecondCache, new LeapSecondCache { - WindowsTicksAsOfMidnight = osTicks - ticksIntoDay, - DateTimeDateDataAsOfMidnight = dateTime._dateData - ticksIntoDay // includes UTC marker + OSFileTimeTicksAtStartOfMonth = osTicks - (ulong)(utcNow.Ticks - startOfMonth.Ticks), + DotNetDateDataAtStartOfMonth = startOfMonth._dateData, + CacheValidityPeriodInTicks = (ulong)(oneSecondBeforeMonthEnd.Ticks - startOfMonth.Ticks) }); } - return dateTime; + return utcNow; } } } private sealed class LeapSecondCache { - internal ulong WindowsTicksAsOfMidnight; - internal ulong DateTimeDateDataAsOfMidnight; + internal ulong OSFileTimeTicksAtStartOfMonth; + internal ulong DotNetDateDataAtStartOfMonth; + internal ulong CacheValidityPeriodInTicks; } internal static readonly bool s_systemSupportsLeapSeconds = SystemSupportsLeapSeconds(); From 834d783e5dc070422718e8197b4d555408da5222 Mon Sep 17 00:00:00 2001 From: Levi Broderick Date: Mon, 16 Nov 2020 14:59:56 -0800 Subject: [PATCH 4/6] Don't use calli hack --- .../System.Private.CoreLib/src/System/DateTime.Windows.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/DateTime.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/DateTime.Windows.cs index 161f868a3a3631..a3d577a55d5784 100644 --- a/src/libraries/System.Private.CoreLib/src/System/DateTime.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/DateTime.Windows.cs @@ -80,11 +80,8 @@ public static unsafe DateTime UtcNow // d) Windows never inserting a historical leap second (into the past) once the // process is up and running (future insertions are ok). - // Caution: Pretending managed calli convention below to work around - // lack of API (see https://github.com/dotnet/runtime/issues/38134). - ulong osTicks; - ((delegate*)s_pfnGetSystemTimeAsFileTime)(&osTicks); + s_pfnGetSystemTimeAsFileTime(&osTicks); // If the OS doesn't support leap second handling, short-circuit everything. From d2a9f8a1a24517e6218bfb51a874166468954b50 Mon Sep 17 00:00:00 2001 From: Levi Broderick Date: Mon, 16 Nov 2020 15:15:33 -0800 Subject: [PATCH 5/6] Remove Windows unmanaged DateTime implementation --- .../System.Private.CoreLib.csproj | 1 - .../src/System/DateTime.Windows.CoreCLR.cs | 25 ---- .../src/classlibnative/bcltype/system.cpp | 139 +----------------- .../src/classlibnative/bcltype/system.h | 8 +- src/coreclr/src/vm/ecalllist.h | 8 +- .../src/System/DateTime.Windows.cs | 75 ++-------- 6 files changed, 14 insertions(+), 242 deletions(-) delete mode 100644 src/coreclr/src/System.Private.CoreLib/src/System/DateTime.Windows.CoreCLR.cs diff --git a/src/coreclr/src/System.Private.CoreLib/System.Private.CoreLib.csproj b/src/coreclr/src/System.Private.CoreLib/System.Private.CoreLib.csproj index 1b0204588a2f8a..2a6c8528372c3a 100644 --- a/src/coreclr/src/System.Private.CoreLib/System.Private.CoreLib.csproj +++ b/src/coreclr/src/System.Private.CoreLib/System.Private.CoreLib.csproj @@ -291,7 +291,6 @@ - Common\Interop\Windows\OleAut32\Interop.VariantClear.cs diff --git a/src/coreclr/src/System.Private.CoreLib/src/System/DateTime.Windows.CoreCLR.cs b/src/coreclr/src/System.Private.CoreLib/src/System/DateTime.Windows.CoreCLR.cs deleted file mode 100644 index 1f77180f47870f..00000000000000 --- a/src/coreclr/src/System.Private.CoreLib/src/System/DateTime.Windows.CoreCLR.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Runtime.CompilerServices; - -namespace System -{ - public readonly partial struct DateTime - { - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern unsafe bool ValidateSystemTime(Interop.Kernel32.SYSTEMTIME* time, bool localTime); - - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern unsafe bool FileTimeToSystemTime(long fileTime, FullSystemTime* time); - - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern unsafe void GetSystemTimeWithLeapSecondsHandling(FullSystemTime* time); - - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern unsafe bool SystemTimeToFileTime(Interop.Kernel32.SYSTEMTIME* time, long* fileTime); - - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern unsafe long GetSystemTimeAsFileTime(); - } -} diff --git a/src/coreclr/src/classlibnative/bcltype/system.cpp b/src/coreclr/src/classlibnative/bcltype/system.cpp index 3660c14963df99..bf688e1da6ce2d 100644 --- a/src/coreclr/src/classlibnative/bcltype/system.cpp +++ b/src/coreclr/src/classlibnative/bcltype/system.cpp @@ -30,69 +30,12 @@ #include "array.h" #include "eepolicy.h" -#ifndef TARGET_UNIX -typedef void(WINAPI *pfnGetSystemTimeAsFileTime)(LPFILETIME lpSystemTimeAsFileTime); -extern pfnGetSystemTimeAsFileTime g_pfnGetSystemTimeAsFileTime; - -void WINAPI InitializeGetSystemTimeAsFileTime(LPFILETIME lpSystemTimeAsFileTime) -{ - pfnGetSystemTimeAsFileTime func = NULL; - - HMODULE hKernel32 = WszLoadLibrary(W("kernel32.dll")); - if (hKernel32 != NULL) - { - func = (pfnGetSystemTimeAsFileTime)GetProcAddress(hKernel32, "GetSystemTimePreciseAsFileTime"); - if (func != NULL) - { - // GetSystemTimePreciseAsFileTime exists and we'd like to use it. However, on - // misconfigured systems, it's possible for the "precise" time to be inaccurate: - // https://github.com/dotnet/runtime/issues/9014 - // If it's inaccurate, though, we expect it to be wildly inaccurate, so as a - // workaround/heuristic, we get both the "normal" and "precise" times, and as - // long as they're close, we use the precise one. This workaround can be removed - // when we better understand what's causing the drift and the issue is no longer - // a problem or can be better worked around on all targeted OSes. - - FILETIME systemTimeResult; - ::GetSystemTimeAsFileTime(&systemTimeResult); - - FILETIME preciseSystemTimeResult; - func(&preciseSystemTimeResult); - - LONG64 systemTimeLong100ns = (LONG64)((((ULONG64)systemTimeResult.dwHighDateTime) << 32) | (ULONG64)systemTimeResult.dwLowDateTime); - LONG64 preciseSystemTimeLong100ns = (LONG64)((((ULONG64)preciseSystemTimeResult.dwHighDateTime) << 32) | (ULONG64)preciseSystemTimeResult.dwLowDateTime); - - const INT32 THRESHOLD_100NS = 1000000; // 100ms - if (abs(preciseSystemTimeLong100ns - systemTimeLong100ns) > THRESHOLD_100NS) - { - // Too much difference. Don't use GetSystemTimePreciseAsFileTime. - func = NULL; - } - } - } - if (func == NULL) - { - func = &::GetSystemTimeAsFileTime; - } - - InterlockedCompareExchangeT(&g_pfnGetSystemTimeAsFileTime, func, &InitializeGetSystemTimeAsFileTime); - - g_pfnGetSystemTimeAsFileTime(lpSystemTimeAsFileTime); -} - -pfnGetSystemTimeAsFileTime g_pfnGetSystemTimeAsFileTime = &InitializeGetSystemTimeAsFileTime; -#endif // TARGET_UNIX - +#ifdef TARGET_UNIX FCIMPL0(INT64, SystemNative::__GetSystemTimeAsFileTime) { FCALL_CONTRACT; - INT64 timestamp; -#ifndef TARGET_UNIX - g_pfnGetSystemTimeAsFileTime((FILETIME*)×tamp); -#else GetSystemTimeAsFileTime((FILETIME*)×tamp); -#endif #if BIGENDIAN timestamp = (INT64)(((UINT64)timestamp >> 32) | ((UINT64)timestamp << 32)); @@ -101,88 +44,8 @@ FCIMPL0(INT64, SystemNative::__GetSystemTimeAsFileTime) return timestamp; } FCIMPLEND; - - -#ifndef TARGET_UNIX - -FCIMPL1(VOID, SystemNative::GetSystemTimeWithLeapSecondsHandling, FullSystemTime *time) -{ - FCALL_CONTRACT; - INT64 timestamp; - - g_pfnGetSystemTimeAsFileTime((FILETIME*)×tamp); - - if (::FileTimeToSystemTime((FILETIME*)×tamp, &(time->systemTime))) - { - // to keep the time precision - time->hundredNanoSecond = timestamp % 10000; // 10000 is the number of 100-nano seconds per Millisecond - } - else - { - ::GetSystemTime(&(time->systemTime)); - time->hundredNanoSecond = 0; - } - - if (time->systemTime.wSecond > 59) - { - // we have a leap second, force it to last second in the minute as DateTime doesn't account for leap seconds in its calculation. - // we use the maxvalue from the milliseconds and the 100-nano seconds to avoid reporting two out of order 59 seconds - time->systemTime.wSecond = 59; - time->systemTime.wMilliseconds = 999; - time->hundredNanoSecond = 9999; - } -} -FCIMPLEND; - -FCIMPL2(FC_BOOL_RET, SystemNative::FileTimeToSystemTime, INT64 fileTime, FullSystemTime *time) -{ - FCALL_CONTRACT; - if (::FileTimeToSystemTime((FILETIME*)&fileTime, (LPSYSTEMTIME) time)) - { - // to keep the time precision - time->hundredNanoSecond = fileTime % 10000; // 10000 is the number of 100-nano seconds per Millisecond - if (time->systemTime.wSecond > 59) - { - // we have a leap second, force it to last second in the minute as DateTime doesn't account for leap seconds in its calculation. - // we use the maxvalue from the milliseconds and the 100-nano seconds to avoid reporting two out of order 59 seconds - time->systemTime.wSecond = 59; - time->systemTime.wMilliseconds = 999; - time->hundredNanoSecond = 9999; - } - FC_RETURN_BOOL(TRUE); - } - FC_RETURN_BOOL(FALSE); -} -FCIMPLEND; - -FCIMPL2(FC_BOOL_RET, SystemNative::ValidateSystemTime, SYSTEMTIME *time, CLR_BOOL localTime) -{ - FCALL_CONTRACT; - - if (localTime) - { - SYSTEMTIME st; - FC_RETURN_BOOL(::TzSpecificLocalTimeToSystemTime(NULL, time, &st)); - } - else - { - FILETIME timestamp; - FC_RETURN_BOOL(::SystemTimeToFileTime(time, ×tamp)); - } -} -FCIMPLEND; - -FCIMPL2(FC_BOOL_RET, SystemNative::SystemTimeToFileTime, SYSTEMTIME *time, INT64 *pFileTime) -{ - FCALL_CONTRACT; - - BOOL ret = ::SystemTimeToFileTime(time, (LPFILETIME) pFileTime); - FC_RETURN_BOOL(ret); -} -FCIMPLEND; #endif // TARGET_UNIX - FCIMPL0(UINT32, SystemNative::GetTickCount) { FCALL_CONTRACT; diff --git a/src/coreclr/src/classlibnative/bcltype/system.h b/src/coreclr/src/classlibnative/bcltype/system.h index 5bbf73d4a1048d..1315f0bdcb512e 100644 --- a/src/coreclr/src/classlibnative/bcltype/system.h +++ b/src/coreclr/src/classlibnative/bcltype/system.h @@ -43,13 +43,9 @@ class SystemNative public: // Functions on the System.Environment class -#ifndef TARGET_UNIX - static FCDECL1(VOID, GetSystemTimeWithLeapSecondsHandling, FullSystemTime *time); - static FCDECL2(FC_BOOL_RET, ValidateSystemTime, SYSTEMTIME *time, CLR_BOOL localTime); - static FCDECL2(FC_BOOL_RET, FileTimeToSystemTime, INT64 fileTime, FullSystemTime *time); - static FCDECL2(FC_BOOL_RET, SystemTimeToFileTime, SYSTEMTIME *time, INT64 *pFileTime); -#endif // TARGET_UNIX +#ifdef TARGET_UNIX static FCDECL0(INT64, __GetSystemTimeAsFileTime); +#endif // TARGET_UNIX static FCDECL0(UINT32, GetTickCount); static FCDECL0(UINT64, GetTickCount64); diff --git a/src/coreclr/src/vm/ecalllist.h b/src/coreclr/src/vm/ecalllist.h index 87389732f8d2e0..a4e7debdf25662 100644 --- a/src/coreclr/src/vm/ecalllist.h +++ b/src/coreclr/src/vm/ecalllist.h @@ -132,13 +132,9 @@ FCFuncStart(gDiagnosticsStackTrace) FCFuncEnd() FCFuncStart(gDateTimeFuncs) -#if !defined(TARGET_UNIX) - FCFuncElement("GetSystemTimeWithLeapSecondsHandling", SystemNative::GetSystemTimeWithLeapSecondsHandling) - FCFuncElement("ValidateSystemTime", SystemNative::ValidateSystemTime) - FCFuncElement("FileTimeToSystemTime", SystemNative::FileTimeToSystemTime) - FCFuncElement("SystemTimeToFileTime", SystemNative::SystemTimeToFileTime) -#endif // TARGET_UNIX +#if defined(TARGET_UNIX) FCFuncElement("GetSystemTimeAsFileTime", SystemNative::__GetSystemTimeAsFileTime) +#endif // TARGET_UNIX FCFuncEnd() FCFuncStart(gEnvironmentFuncs) diff --git a/src/libraries/System.Private.CoreLib/src/System/DateTime.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/DateTime.Windows.cs index a3d577a55d5784..edc23a745f4b87 100644 --- a/src/libraries/System.Private.CoreLib/src/System/DateTime.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/DateTime.Windows.cs @@ -12,7 +12,6 @@ public readonly partial struct DateTime { private static LeapSecondCache? s_leapSecondCache; - // Question: Can this be [Stdcall, SuppressGCTransition]? private static unsafe delegate* unmanaged[Stdcall] s_pfnGetSystemTimeAsFileTime = GetGetSystemTimeAsFileTimeFnPtr(); private static unsafe delegate* unmanaged[Stdcall] GetGetSystemTimeAsFileTimeFnPtr() @@ -31,6 +30,9 @@ public readonly partial struct DateTime // when we better understand what's causing the drift and the issue is no longer // a problem or can be better worked around on all targeted OSes. + // TODO: Once https://github.com/dotnet/runtime/issues/38134 is resolved, + // the calls below should suppress the GC transition. + ulong filetimeStd, filetimePrecise; ((delegate* unmanaged[Stdcall])pfnGetSystemTime)(&filetimeStd); ((delegate* unmanaged[Stdcall])pfnGetSystemTimePrecise)(&filetimePrecise); @@ -80,6 +82,9 @@ public static unsafe DateTime UtcNow // d) Windows never inserting a historical leap second (into the past) once the // process is up and running (future insertions are ok). + // TODO: Once https://github.com/dotnet/runtime/issues/38134 is resolved, + // the call below should suppress the GC transition. + ulong osTicks; s_pfnGetSystemTimeAsFileTime(&osTicks); @@ -147,9 +152,9 @@ static DateTime Fallback(ulong osTicks) private sealed class LeapSecondCache { - internal ulong OSFileTimeTicksAtStartOfMonth; - internal ulong DotNetDateDataAtStartOfMonth; - internal ulong CacheValidityPeriodInTicks; + internal ulong OSFileTimeTicksAtStartOfMonth; // Windows FILETIME value + internal ulong DotNetDateDataAtStartOfMonth; // DateTime._dateData + internal ulong CacheValidityPeriodInTicks; // 100-ns intervals from StartOfMonth (inclusive) to cache expiration (exclusive) } internal static readonly bool s_systemSupportsLeapSeconds = SystemSupportsLeapSeconds(); @@ -240,34 +245,6 @@ internal FullSystemTime(long ticks) } } -#if !CORECLR - internal static readonly bool s_systemSupportsPreciseSystemTime = SystemSupportsPreciseSystemTime(); - - private static unsafe bool SystemSupportsPreciseSystemTime() - { - if (Environment.IsWindows8OrAbove) - { - // GetSystemTimePreciseAsFileTime exists and we'd like to use it. However, on - // misconfigured systems, it's possible for the "precise" time to be inaccurate: - // https://github.com/dotnet/runtime/issues/9014 - // If it's inaccurate, though, we expect it to be wildly inaccurate, so as a - // workaround/heuristic, we get both the "normal" and "precise" times, and as - // long as they're close, we use the precise one. This workaround can be removed - // when we better understand what's causing the drift and the issue is no longer - // a problem or can be better worked around on all targeted OSes. - - long systemTimeResult; - Interop.Kernel32.GetSystemTimeAsFileTime(&systemTimeResult); - - long preciseSystemTimeResult; - Interop.Kernel32.GetSystemTimePreciseAsFileTime(&preciseSystemTimeResult); - - return Math.Abs(preciseSystemTimeResult - systemTimeResult) <= 100 * TicksPerMillisecond; - } - - return false; - } - private static unsafe bool ValidateSystemTime(Interop.Kernel32.SYSTEMTIME* time, bool localTime) { if (localTime) @@ -301,43 +278,9 @@ private static unsafe bool FileTimeToSystemTime(long fileTime, FullSystemTime* t return false; } - private static unsafe void GetSystemTimeWithLeapSecondsHandling(FullSystemTime* time) - { - if (!FileTimeToSystemTime(GetSystemTimeAsFileTime(), time)) - { - Interop.Kernel32.GetSystemTime(&time->systemTime); - time->hundredNanoSecond = 0; - if (time->systemTime.Second > 59) - { - // we have a leap second, force it to last second in the minute as DateTime doesn't account for leap seconds in its calculation. - // we use the maxvalue from the milliseconds and the 100-nano seconds to avoid reporting two out of order 59 seconds - time->systemTime.Second = 59; - time->systemTime.Milliseconds = 999; - time->hundredNanoSecond = 9999; - } - } - } - private static unsafe bool SystemTimeToFileTime(Interop.Kernel32.SYSTEMTIME* time, long* fileTime) { return Interop.Kernel32.SystemTimeToFileTime(time, fileTime) != Interop.BOOL.FALSE; } - - private static unsafe long GetSystemTimeAsFileTime() - { - long timestamp; - - if (s_systemSupportsPreciseSystemTime) - { - Interop.Kernel32.GetSystemTimePreciseAsFileTime(×tamp); - } - else - { - Interop.Kernel32.GetSystemTimeAsFileTime(×tamp); - } - - return timestamp; - } -#endif } } From 35d60b87801ec3af8e2acada7ebb5c2289441f6b Mon Sep 17 00:00:00 2001 From: Levi Broderick Date: Tue, 17 Nov 2020 14:32:06 -0800 Subject: [PATCH 6/6] Small refactor based on PR feedback --- .../src/System/DateTime.Windows.cs | 102 ++++++++++-------- 1 file changed, 55 insertions(+), 47 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/DateTime.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/DateTime.Windows.cs index edc23a745f4b87..6c5266ae34f144 100644 --- a/src/libraries/System.Private.CoreLib/src/System/DateTime.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/DateTime.Windows.cs @@ -10,7 +10,7 @@ namespace System { public readonly partial struct DateTime { - private static LeapSecondCache? s_leapSecondCache; + private static LeapSecondCache s_leapSecondCache = new LeapSecondCache(); // dummy value, will be invalidated on first call private static unsafe delegate* unmanaged[Stdcall] s_pfnGetSystemTimeAsFileTime = GetGetSystemTimeAsFileTimeFnPtr(); @@ -95,58 +95,66 @@ public static unsafe DateTime UtcNow return new DateTime((osTicks + FileTimeOffset) | KindUtc); } - // If our cache has been populated and we're within its validity period, rely - // solely on the cache. We use unsigned arithmetic below so that if the system - // clock is set backward, the resulting value will integer underflow and cause - // a cache miss, forcing us down the slow path. + return GetUtcNowFromOSTicksLeapSecondsAwareCached(osTicks); + } + } - if (s_leapSecondCache is LeapSecondCache cache) - { - ulong deltaTicksFromStartOfMonth = osTicks - cache.OSFileTimeTicksAtStartOfMonth; - if (deltaTicksFromStartOfMonth < cache.CacheValidityPeriodInTicks) - { - return new DateTime(deltaTicksFromStartOfMonth + cache.DotNetDateDataAtStartOfMonth); // includes UTC marker - } - } + // This method is extracted into its own helper so that unit tests + // can call into it via reflection. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static DateTime GetUtcNowFromOSTicksLeapSecondsAwareCached(ulong osTicks) + { + Debug.Assert(s_systemSupportsLeapSeconds, "Shouldn't call this helper if OS doesn't support leap seconds."); + + // If our cache has been populated and we're within its validity period, rely + // solely on the cache. We use unsigned arithmetic below so that if the system + // clock is set backward, the resulting value will integer underflow and cause + // a cache miss, forcing us down the slow path. + + LeapSecondCache cache = s_leapSecondCache; + ulong deltaTicksFromStartOfMonth = osTicks - cache.OSFileTimeTicksAtStartOfMonth; + if (deltaTicksFromStartOfMonth < cache.CacheValidityPeriodInTicks) + { + return new DateTime(deltaTicksFromStartOfMonth + cache.DotNetDateDataAtStartOfMonth); // includes UTC marker + } + + // Move uncommon fallback pack into its own subroutine so that register + // allocation in get_UtcNow can stay as optimized as possible. - // Move uncommon fallback pack into its own subroutine so that register - // allocation in get_UtcNow can stay as optimized as possible. + return Fallback(osTicks); + static DateTime Fallback(ulong osTicks) + { + // If we reached this point, one of the following is true: + // a) the cache hasn't yet been initialized; or + // b) the month has changed since the last call to UtcNow; or + // c) the current time is 23:59:59 or 23:59:60 on the last day of the month. + // + // In cases (a) and (b), we'll update the cache. In case (c), we + // pessimistically assume we might be inside a leap second, so we + // won't update the cache. - return Fallback(osTicks); - static DateTime Fallback(ulong osTicks) + DateTime utcNow = FromFileTimeLeapSecondsAware((long)osTicks); + DateTime oneSecondBeforeMonthEnd = new DateTime(utcNow.Year, utcNow.Month, DaysInMonth(utcNow.Year, utcNow.Month), 23, 59, 59, DateTimeKind.Utc); + + if (utcNow < oneSecondBeforeMonthEnd) { - // If we reached this point, one of the following is true: - // a) the cache hasn't yet been initialized; or - // b) the month has changed since the last call to UtcNow; or - // c) the current time is 23:59:59 or 23:59:60 on the last day of the month. - // - // In cases (a) and (b), we'll update the cache. In case (c), we - // pessimistically assume we might be inside a leap second, so we - // won't update the cache. - - DateTime utcNow = FromFileTimeLeapSecondsAware((long)osTicks); - DateTime oneSecondBeforeMonthEnd = new DateTime(utcNow.Year, utcNow.Month, DaysInMonth(utcNow.Year, utcNow.Month), 23, 59, 59, DateTimeKind.Utc); - - if (utcNow < oneSecondBeforeMonthEnd) + // It's not yet 23:59:59 on the last day of the month, so update the cache. + // It's ok for multiple threads to do this concurrently as long as the write + // to the static field is published *after* the cache's instance fields have + // been populated. + + DateTime startOfMonth = new DateTime(utcNow.Year, utcNow.Month, 1, 0, 0, 0, DateTimeKind.Utc); + Debug.Assert(startOfMonth <= utcNow); + + Volatile.Write(ref s_leapSecondCache, new LeapSecondCache { - // It's not yet 23:59:59 on the last day of the month, so update the cache. - // It's ok for multiple threads to do this concurrently as long as the write - // to the static field is published *after* the cache's instance fields have - // been populated. - - DateTime startOfMonth = new DateTime(utcNow.Year, utcNow.Month, 1, 0, 0, 0, DateTimeKind.Utc); - Debug.Assert(startOfMonth <= utcNow); - - Volatile.Write(ref s_leapSecondCache, new LeapSecondCache - { - OSFileTimeTicksAtStartOfMonth = osTicks - (ulong)(utcNow.Ticks - startOfMonth.Ticks), - DotNetDateDataAtStartOfMonth = startOfMonth._dateData, - CacheValidityPeriodInTicks = (ulong)(oneSecondBeforeMonthEnd.Ticks - startOfMonth.Ticks) - }); - } - - return utcNow; + OSFileTimeTicksAtStartOfMonth = osTicks - (ulong)(utcNow.Ticks - startOfMonth.Ticks), + DotNetDateDataAtStartOfMonth = startOfMonth._dateData, + CacheValidityPeriodInTicks = (ulong)(oneSecondBeforeMonthEnd.Ticks - startOfMonth.Ticks) + }); } + + return utcNow; } }