From eed3c7642ada205a4645dc3153fcb4c3c04c363c Mon Sep 17 00:00:00 2001 From: Levi Broderick Date: Fri, 26 Mar 2021 21:52:19 -0700 Subject: [PATCH] Improve performance of DateTime.UtcNow on Windows (#50263) Optimizes leap-second handling by avoiding calls to FileTimeToSystemTime when possible --- .../src/System/DateTime.Windows.cs | 194 ++++++++++++++++-- 1 file changed, 177 insertions(+), 17 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 7814a9f5fdbcf..d936053f40318 100644 --- a/src/libraries/System.Private.CoreLib/src/System/DateTime.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/DateTime.Windows.cs @@ -2,8 +2,8 @@ // 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; namespace System { @@ -15,31 +15,26 @@ public static unsafe DateTime UtcNow { get { - ulong fileTime; - s_pfnGetSystemTimeAsFileTime(&fileTime); + ulong fileTimeTmp; // mark only the temp local as address-taken + s_pfnGetSystemTimeAsFileTime(&fileTimeTmp); + ulong fileTime = fileTimeTmp; if (s_systemSupportsLeapSeconds) { - Interop.Kernel32.SYSTEMTIME time; - ulong hundredNanoSecond; + // Query the leap second cache first, which avoids expensive calls to GetFileTimeAsSystemTime. - if (Interop.Kernel32.FileTimeToSystemTime(&fileTime, &time) != Interop.BOOL.FALSE) + LeapSecondCache cacheValue = s_leapSecondCache; + ulong ticksSinceStartOfCacheValidityWindow = fileTime - cacheValue.OSFileTimeTicksAtStartOfValidityWindow; + if (ticksSinceStartOfCacheValidityWindow < LeapSecondCache.ValidityPeriodInTicks) { - // to keep the time precision - ulong tmp = fileTime; // temp. variable avoids double read from memory - hundredNanoSecond = tmp % TicksPerMillisecond; - } - else - { - Interop.Kernel32.GetSystemTime(&time); - hundredNanoSecond = 0; + return new DateTime(dateData: cacheValue.DotnetDateDataAtStartOfValidityWindow + ticksSinceStartOfCacheValidityWindow); } - return CreateDateTimeFromSystemTime(in time, hundredNanoSecond); + return UpdateLeapSecondCacheAndReturnUtcNow(); // couldn't use the cache, go down the slow path } else { - return new DateTime(fileTime + FileTimeOffset | KindUtc); + return new DateTime(dateData: fileTime + (FileTimeOffset | KindUtc)); } } } @@ -109,7 +104,6 @@ private static unsafe ulong ToFileTimeLeapSecondsAware(long ticks) return fileTime + (uint)tick; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] private static DateTime CreateDateTimeFromSystemTime(in Interop.Kernel32.SYSTEMTIME time, ulong hundredNanoSecond) { uint year = time.Year; @@ -171,5 +165,171 @@ private static DateTime CreateDateTimeFromSystemTime(in Interop.Kernel32.SYSTEMT return (delegate* unmanaged[SuppressGCTransition])pfnGetSystemTime; } + + private static unsafe DateTime UpdateLeapSecondCacheAndReturnUtcNow() + { + // From conversations with the Windows team, the OS has the ability to update leap second + // data while applications are running. Leap second data is published on WU well ahead of + // the actual event. Additionally, the OS's list of leap seconds will only ever expand + // from the end. There won't be a situation where a leap second will ever be inserted into + // the middle of the list of all known leap seconds. + // + // Normally, this would mean that we could just ask "will a leap second occur in the next + // 24 hours?" and cache this value. However, it's possible that the current machine may have + // deferred updates so long that when a leap second is added to the end of the list, it + // actually occurs in the past (compared to UtcNow). To account for this possibility, we + // limit our cache's lifetime to just a few minutes (the "validity window"). If a deferred + // OS update occurs and a past leap second is added, this limits the window in which our + // cache will return incorrect values. + + Debug.Assert(s_systemSupportsLeapSeconds); + Debug.Assert(LeapSecondCache.ValidityPeriodInTicks < TicksPerDay - TicksPerSecond, "Leap second cache validity window should be less than 23:59:59."); + + ulong fileTimeNow; + s_pfnGetSystemTimeAsFileTime(&fileTimeNow); + + // If we reached this point, our leap second cache is stale, and we need to update it. + // First, convert the FILETIME to a SYSTEMTIME. + + Interop.Kernel32.SYSTEMTIME systemTimeNow; + ulong hundredNanoSecondNow = fileTimeNow % TicksPerMillisecond; + + // We need the FILETIME and the SYSTEMTIME to reflect each other's values. + // If FileTimeToSystemTime fails, call GetSystemTime and try again until it succeeds. + if (Interop.Kernel32.FileTimeToSystemTime(&fileTimeNow, &systemTimeNow) == Interop.BOOL.FALSE) + { + return LowGranularityNonCachedFallback(); + } + + // If we're currently within a positive leap second, early-exit since our cache can't handle + // this situation. Once midnight rolls around the next call to DateTime.UtcNow should update + // the cache correctly. + + if (systemTimeNow.Second >= 60) + { + return CreateDateTimeFromSystemTime(systemTimeNow, hundredNanoSecondNow); + } + + // Our cache will be valid for some amount of time (the "validity window"). + // Check if a leap second will occur within this window. + + ulong fileTimeAtEndOfValidityPeriod = fileTimeNow + LeapSecondCache.ValidityPeriodInTicks; + Interop.Kernel32.SYSTEMTIME systemTimeAtEndOfValidityPeriod; + if (Interop.Kernel32.FileTimeToSystemTime(&fileTimeAtEndOfValidityPeriod, &systemTimeAtEndOfValidityPeriod) == Interop.BOOL.FALSE) + { + return LowGranularityNonCachedFallback(); + } + + ulong fileTimeAtStartOfValidityWindow; + ulong dotnetDateDataAtStartOfValidityWindow; + + // A leap second can only occur at the end of the day, and we can only leap by +/- 1 second + // at a time. To see if a leap second occurs within the upcoming validity window, we can + // compare the 'seconds' values at the start and the end of the window. + + if (systemTimeAtEndOfValidityPeriod.Second == systemTimeNow.Second) + { + // If we reached this block, a leap second will not occur within the validity window. + // We can cache the validity window starting at UtcNow. + + fileTimeAtStartOfValidityWindow = fileTimeNow; + dotnetDateDataAtStartOfValidityWindow = CreateDateTimeFromSystemTime(systemTimeNow, hundredNanoSecondNow)._dateData; + } + else + { + // If we reached this block, a leap second will occur within the validity window. We cannot + // allow the cache to cover this entire window, otherwise the cache will start reporting + // incorrect values once the leap second occurs. To account for this, we slide the validity + // window back a little bit. The window will have the same duration as before, but instead + // of beginning now, we'll choose the proper begin time so that it ends at 23:59:59.000. + + Interop.Kernel32.SYSTEMTIME systemTimeAtBeginningOfDay = systemTimeNow; + systemTimeAtBeginningOfDay.Hour = 0; + systemTimeAtBeginningOfDay.Minute = 0; + systemTimeAtBeginningOfDay.Second = 0; + systemTimeAtBeginningOfDay.Milliseconds = 0; + + ulong fileTimeAtBeginningOfDay; + if (Interop.Kernel32.SystemTimeToFileTime(&systemTimeAtBeginningOfDay, &fileTimeAtBeginningOfDay) == Interop.BOOL.FALSE) + { + return LowGranularityNonCachedFallback(); + } + + // StartOfValidityWindow = MidnightUtc + 23:59:59 - ValidityPeriod + fileTimeAtStartOfValidityWindow = fileTimeAtBeginningOfDay + (TicksPerDay - TicksPerSecond) - LeapSecondCache.ValidityPeriodInTicks; + if (fileTimeNow - fileTimeAtStartOfValidityWindow >= LeapSecondCache.ValidityPeriodInTicks) + { + // If we're inside this block, then we slid the validity window back so far that the current time is no + // longer within the window. This can only occur if the current time is 23:59:59.xxx and the next second is a + // positive leap second (23:59:60.xxx). For example, if the current time is 23:59:59.123, assuming a + // 5-minute validity period, we'll slide the validity window back to [23:54:59.000, 23:59:59.000). + // + // Depending on how the current process is configured, the OS may report time data in one of two ways. If + // the current process is leap-second aware (has the PROCESS_LEAP_SECOND_INFO_FLAG_ENABLE_SIXTY_SECOND flag set), + // then a SYSTEMTIME object will report leap seconds by setting the 'wSecond' field to 60. If the current + // process is not leap-second aware, the OS will compress the last two seconds of the day as follows. + // + // Actual time GetSystemTime returns + // ======================================== + // 23:59:59.000 23:59:59.000 + // 23:59:59.500 23:59:59.250 + // 23:59:60.000 23:59:59.500 + // 23:59:60.500 23:59:59.750 + // 00:00:00.000 00:00:00.000 (next day) + // + // In this scenario, we'll skip the caching logic entirely, relying solely on the OS-provided SYSTEMTIME + // struct to tell us how to interpret the time information. + + Debug.Assert(systemTimeNow.Hour == 23 && systemTimeNow.Minute == 59 && systemTimeNow.Second == 59); + return CreateDateTimeFromSystemTime(systemTimeNow, hundredNanoSecondNow); + } + + dotnetDateDataAtStartOfValidityWindow = CreateDateTimeFromSystemTime(systemTimeAtBeginningOfDay, 0)._dateData + (TicksPerDay - TicksPerSecond) - LeapSecondCache.ValidityPeriodInTicks; + } + + // Finally, update the cache and return UtcNow. + + Debug.Assert(fileTimeNow - fileTimeAtStartOfValidityWindow < LeapSecondCache.ValidityPeriodInTicks, "We should be within the validity window."); + Volatile.Write(ref s_leapSecondCache, new LeapSecondCache() + { + OSFileTimeTicksAtStartOfValidityWindow = fileTimeAtStartOfValidityWindow, + DotnetDateDataAtStartOfValidityWindow = dotnetDateDataAtStartOfValidityWindow + }); + + return new DateTime(dateData: dotnetDateDataAtStartOfValidityWindow + fileTimeNow - fileTimeAtStartOfValidityWindow); + + static DateTime LowGranularityNonCachedFallback() + { + // If we reached this point, one of the Win32 APIs FileTimeToSystemTime or SystemTimeToFileTime + // failed. This should never happen in practice, as this would imply that the Win32 API + // GetSystemTimeAsFileTime returned an invalid value to us at the start of the calling method. + // But, just to be safe, if this ever does happen, we'll bypass the caching logic entirely + // and fall back to GetSystemTime. This results in a loss of granularity (millisecond-only, + // not rdtsc-based), but at least it means we won't fail. + + Debug.Fail("Our Win32 calls should never fail."); + + Interop.Kernel32.SYSTEMTIME systemTimeNow; + Interop.Kernel32.GetSystemTime(&systemTimeNow); + return CreateDateTimeFromSystemTime(systemTimeNow, 0); + } + } + + // The leap second cache. May be accessed by multiple threads simultaneously. + // Writers must not mutate the object's fields after the reference is published. + // Readers are not required to use volatile semantics. + private static LeapSecondCache s_leapSecondCache = new LeapSecondCache(); + + private sealed class LeapSecondCache + { + // The length of the validity window. Must be less than 23:59:59. + internal const ulong ValidityPeriodInTicks = TicksPerMinute * 5; + + // The FILETIME value at the beginning of the validity window. + internal ulong OSFileTimeTicksAtStartOfValidityWindow; + + // The DateTime._dateData value at the beginning of the validity window. + internal ulong DotnetDateDataAtStartOfValidityWindow; + } } }