Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve performance of DateTime.UtcNow on Windows #50263

Merged
merged 2 commits into from
Mar 27, 2021
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
tarekgh marked this conversation as resolved.
Show resolved Hide resolved
}
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;
}
}
}