Skip to content

Commit

Permalink
Improve performance of DateTime.UtcNow on Windows (#50263)
Browse files Browse the repository at this point in the history
Optimizes leap-second handling by avoiding calls to FileTimeToSystemTime when possible
  • Loading branch information
GrabYourPitchforks authored Mar 27, 2021
1 parent 67983f7 commit eed3c76
Showing 1 changed file with 177 additions and 17 deletions.
194 changes: 177 additions & 17 deletions src/libraries/System.Private.CoreLib/src/System/DateTime.Windows.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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));
}
}
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -171,5 +165,171 @@ private static DateTime CreateDateTimeFromSystemTime(in Interop.Kernel32.SYSTEMT

return (delegate* unmanaged[SuppressGCTransition]<ulong*, void>)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;
}
}
}

0 comments on commit eed3c76

Please sign in to comment.