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 1 commit
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
165 changes: 148 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,142 @@ 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.
//
// 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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this loops forever for some reason -- I guess we assume the OS is broken and it doesn't really matter that we didn't fail?

Copy link
Member Author

@GrabYourPitchforks GrabYourPitchforks Mar 25, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I originally had Environment.FailFast in here but figured that there might be a race condition where the OS is publishing new data at the same time we're running this method. If something's borked in the OS we'll probably loop forever. Hopefully we'll only loop until midnight (which is 2 seconds away at worst). :)

If this is a concern I can perhaps keep a counter limiting us to 10 tries before we failfast? Or maybe always fall back to new DateTime(GetSystemTime())?

}

// 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;
}
}
}