From 941e477616cf974269d9f8558ba8863271f75d0f Mon Sep 17 00:00:00 2001 From: Levi Broderick Date: Thu, 25 Mar 2021 14:53:33 -0700 Subject: [PATCH 1/2] Introduce a leap second cache on Windows --- .../src/System/DateTime.Windows.cs | 165 ++++++++++++++++-- 1 file changed, 148 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 7814a9f5fdbcf3..b79203bee1730a 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,142 @@ 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. + // + // We don't ever expect FileTimeToSystemTime or SystemTimeToFileTime to fail, but in theory + // they could do so if the OS publishes a leap second table update to all applications while + // this method is executing. If the time conversion routines fail, we'll re-run this method's + // logic from the beginning. + + Debug.Assert(s_systemSupportsLeapSeconds); + Debug.Assert(LeapSecondCache.ValidityPeriodInTicks < TicksPerDay - TicksPerSecond, "Leap second cache validity window should be less than 23:59:59."); + + TryAgain: + + 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. + while (Interop.Kernel32.FileTimeToSystemTime(&fileTimeNow, &systemTimeNow) == Interop.BOOL.FALSE) + { + goto TryAgain; + } + + // 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) + { + goto TryAgain; + } + + 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) + { + goto TryAgain; + } + + // StartOfValidityWindow = MidnightUtc + 23:59:59 - ValidityPeriod + fileTimeAtStartOfValidityWindow = fileTimeAtBeginningOfDay + (TicksPerDay - TicksPerSecond) - LeapSecondCache.ValidityPeriodInTicks; + dotnetDateDataAtStartOfValidityWindow = CreateDateTimeFromSystemTime(systemTimeAtBeginningOfDay, 0)._dateData + (TicksPerDay - TicksPerSecond) - LeapSecondCache.ValidityPeriodInTicks; + } + + // Fudge the check below by +TicksPerSecond. This accounts for the current time being 23:59:59, the next second being 23:59:60, + // and the "if a leap second will occur in the validity window" block above firing and shoving the entirety of the validity + // window before UtcNow. The returned DateTime will still be correct in this scenario. Updating the cache is pointless in + // such a scenario, but it only occurs in the second immediately preceding a positive leap second, so we'll accept the + // inefficiency this causes. + + Debug.Assert(fileTimeNow - fileTimeAtStartOfValidityWindow < LeapSecondCache.ValidityPeriodInTicks + TicksPerSecond, "We should be within the validity window."); + + // Finally, update the cache and return UtcNow. + + Volatile.Write(ref s_leapSecondCache, new LeapSecondCache() + { + OSFileTimeTicksAtStartOfValidityWindow = fileTimeAtStartOfValidityWindow, + DotnetDateDataAtStartOfValidityWindow = dotnetDateDataAtStartOfValidityWindow + }); + + return new DateTime(dateData: dotnetDateDataAtStartOfValidityWindow + fileTimeNow - fileTimeAtStartOfValidityWindow); + } + + // 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; + } } } From fd08554983177c30bf55a25c6bc4cb05bcf6f2be Mon Sep 17 00:00:00 2001 From: Levi Broderick Date: Fri, 26 Mar 2021 15:28:04 -0700 Subject: [PATCH 2/2] Code cleanup and use fallback instead of entering loop --- .../src/System/DateTime.Windows.cs | 67 +++++++++++++------ 1 file changed, 48 insertions(+), 19 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 b79203bee1730a..d936053f40318e 100644 --- a/src/libraries/System.Private.CoreLib/src/System/DateTime.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/DateTime.Windows.cs @@ -181,17 +181,10 @@ private static unsafe DateTime UpdateLeapSecondCacheAndReturnUtcNow() // 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. - // - // We don't ever expect FileTimeToSystemTime or SystemTimeToFileTime to fail, but in theory - // they could do so if the OS publishes a leap second table update to all applications while - // this method is executing. If the time conversion routines fail, we'll re-run this method's - // logic from the beginning. Debug.Assert(s_systemSupportsLeapSeconds); Debug.Assert(LeapSecondCache.ValidityPeriodInTicks < TicksPerDay - TicksPerSecond, "Leap second cache validity window should be less than 23:59:59."); - TryAgain: - ulong fileTimeNow; s_pfnGetSystemTimeAsFileTime(&fileTimeNow); @@ -203,9 +196,9 @@ private static unsafe DateTime UpdateLeapSecondCacheAndReturnUtcNow() // We need the FILETIME and the SYSTEMTIME to reflect each other's values. // If FileTimeToSystemTime fails, call GetSystemTime and try again until it succeeds. - while (Interop.Kernel32.FileTimeToSystemTime(&fileTimeNow, &systemTimeNow) == Interop.BOOL.FALSE) + if (Interop.Kernel32.FileTimeToSystemTime(&fileTimeNow, &systemTimeNow) == Interop.BOOL.FALSE) { - goto TryAgain; + return LowGranularityNonCachedFallback(); } // If we're currently within a positive leap second, early-exit since our cache can't handle @@ -224,7 +217,7 @@ private static unsafe DateTime UpdateLeapSecondCacheAndReturnUtcNow() Interop.Kernel32.SYSTEMTIME systemTimeAtEndOfValidityPeriod; if (Interop.Kernel32.FileTimeToSystemTime(&fileTimeAtEndOfValidityPeriod, &systemTimeAtEndOfValidityPeriod) == Interop.BOOL.FALSE) { - goto TryAgain; + return LowGranularityNonCachedFallback(); } ulong fileTimeAtStartOfValidityWindow; @@ -259,24 +252,44 @@ private static unsafe DateTime UpdateLeapSecondCacheAndReturnUtcNow() ulong fileTimeAtBeginningOfDay; if (Interop.Kernel32.SystemTimeToFileTime(&systemTimeAtBeginningOfDay, &fileTimeAtBeginningOfDay) == Interop.BOOL.FALSE) { - goto TryAgain; + 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; } - // Fudge the check below by +TicksPerSecond. This accounts for the current time being 23:59:59, the next second being 23:59:60, - // and the "if a leap second will occur in the validity window" block above firing and shoving the entirety of the validity - // window before UtcNow. The returned DateTime will still be correct in this scenario. Updating the cache is pointless in - // such a scenario, but it only occurs in the second immediately preceding a positive leap second, so we'll accept the - // inefficiency this causes. - - Debug.Assert(fileTimeNow - fileTimeAtStartOfValidityWindow < LeapSecondCache.ValidityPeriodInTicks + TicksPerSecond, "We should be within the validity window."); - // 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, @@ -284,6 +297,22 @@ private static unsafe DateTime UpdateLeapSecondCacheAndReturnUtcNow() }); 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.