Skip to content

MemoryCache: optimized clock access, optimized expiration checks #53469

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

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
64ec768
System clock abstractions to facilitate clock quantization
edevoogd May 13, 2021
74a49af
Add ISystemClockTemporalContext to TestClock
edevoogd May 13, 2021
66ae3e5
Additional references and unlocking TFM-specific capabilities for .NE…
edevoogd May 13, 2021
132ac84
Adding clock quantization primitives
edevoogd May 14, 2021
9376f4f
Add ClockQuantizer and supporting TemporalContextDriver
edevoogd May 14, 2021
aafd8b6
Adapt time-related `CacheEntry` properties and backing storage.
edevoogd May 15, 2021
556a852
Optimize expiration scan trigger checks through rewrite in terms of c…
edevoogd May 15, 2021
71fa1b0
Optimize `TryGetValue()` with interval-based expiration checks and `L…
edevoogd May 16, 2021
f8e0a12
Optimize SetEntry() with interval-based expiration checks and LazyClo…
edevoogd May 16, 2021
3833d73
Merge branch 'dotnet:main' into memorycache-clockquantization-jit-now
edevoogd May 16, 2021
b803e28
Additional `LazyClockOffsetSerialPosition` capabilities
edevoogd May 24, 2021
b3fecdf
Merge branch 'memorycache-clockquantization-jit-now' of https://githu…
edevoogd May 24, 2021
5a25899
Rationalize `CacheEntry` time-based properties & expiration checks
edevoogd May 24, 2021
7b5db3b
Optimize `MemoryCache`
edevoogd May 25, 2021
70d15a2
Add missing #nullable pragmas
edevoogd May 25, 2021
aaccb50
Merge branch 'dotnet:main' into memorycache-clockquantization-jit-now
edevoogd May 27, 2021
752b7cb
Merge branch 'dotnet:main' into memorycache-clockquantization-jit-now
edevoogd May 29, 2021
4e395ba
Merge branch 'dotnet:main' into memorycache-clockquantization-jit-now
edevoogd May 29, 2021
14dc051
Resolve conflicts with PR #53762
edevoogd Jun 21, 2021
8c8f048
Merge branch 'dotnet:main' into memorycache-clockquantization-jit-now
edevoogd Jun 21, 2021
6636433
Fix build errors
edevoogd Jun 21, 2021
d0aac6b
Build without $(NetCoreAppCurrent) target
edevoogd Jun 22, 2021
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
63 changes: 28 additions & 35 deletions src/libraries/Microsoft.Extensions.Caching.Memory/src/CacheEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ internal sealed partial class CacheEntry : ICacheEntry

private CacheEntryTokens _tokens; // might be null if user is not using the tokens or callbacks
private TimeSpan? _absoluteExpirationRelativeToNow;
private TimeSpan? _slidingExpiration;
private long? _size;
private CacheEntry _previous; // this field is not null only before the entry is added to the cache
private object _value;
Expand All @@ -35,7 +34,14 @@ internal CacheEntry(object key, MemoryCache memoryCache)
/// <summary>
/// Gets or sets an absolute expiration date for the cache entry.
/// </summary>
public DateTimeOffset? AbsoluteExpiration { get; set; }
public DateTimeOffset? AbsoluteExpiration
{
get => AbsoluteExpirationClockOffset.HasValue ? _cache.ClockQuantizer.ClockOffsetToUtcDateTimeOffset(AbsoluteExpirationClockOffset!.Value) : null;
set
{
AbsoluteExpirationClockOffset = value.HasValue ? _cache.ClockQuantizer.DateTimeOffsetToClockOffset(value!.Value) : null;
}
}

/// <summary>
/// Gets or sets an absolute expiration time, relative to now.
Expand Down Expand Up @@ -66,18 +72,25 @@ public TimeSpan? AbsoluteExpirationRelativeToNow
/// </summary>
public TimeSpan? SlidingExpiration
{
get => _slidingExpiration;
get => !SlidingExpirationClockOffsetUnits.HasValue ? null : _cache.ClockQuantizer.ClockOffsetUnitsToTimeSpan(SlidingExpirationClockOffsetUnits!.Value);
set
{
if (value <= TimeSpan.Zero)
if (value.HasValue)
{
throw new ArgumentOutOfRangeException(
nameof(SlidingExpiration),
value,
"The sliding expiration value must be positive.");
}
if (value <= TimeSpan.Zero)
{
throw new ArgumentOutOfRangeException(
nameof(SlidingExpiration),
value,
"The sliding expiration value must be positive.");
}

_slidingExpiration = value;
SlidingExpirationClockOffsetUnits = _cache.ClockQuantizer.TimeSpanToClockOffsetUnits(value!.Value);
}
else
{
SlidingExpirationClockOffsetUnits = null;
}
}
}

Expand Down Expand Up @@ -126,8 +139,6 @@ public object Value
}
}

internal DateTimeOffset LastAccessed { get; set; }

internal EvictionReason EvictionReason { get => _state.EvictionReason; private set => _state.EvictionReason = value; }

public void Dispose()
Expand Down Expand Up @@ -155,6 +166,7 @@ public void Dispose()
}
}

// This method is merely left for testing purposes; we may get rid of this by adapting the tests
[MethodImpl(MethodImplOptions.AggressiveInlining)] // added based on profiling
internal bool CheckExpired(in DateTimeOffset now)
=> _state.IsExpired
Expand All @@ -171,33 +183,14 @@ internal void SetExpired(EvictionReason reason)
_tokens?.DetachTokens();
}

// This method is merely left for testing purposes; we may get rid of this by adapting the tests
[MethodImpl(MethodImplOptions.AggressiveInlining)] // added based on profiling
private bool CheckForExpiredTime(in DateTimeOffset now)
{
if (!AbsoluteExpiration.HasValue && !_slidingExpiration.HasValue)
{
return false;
}
Internal.ClockQuantization.LazyClockOffsetSerialPosition position = default;
Internal.ClockQuantization.LazyClockOffsetSerialPosition.AssignExactClockOffsetSerialPosition(_cache.ClockQuantizer.DateTimeOffsetToClockOffset(now), ref position);

return FullCheck(now);

bool FullCheck(in DateTimeOffset offset)
{
if (AbsoluteExpiration.HasValue && AbsoluteExpiration.Value <= offset)
{
SetExpired(EvictionReason.Expired);
return true;
}

if (_slidingExpiration.HasValue
&& (offset - LastAccessed) >= _slidingExpiration)
{
SetExpired(EvictionReason.Expired);
return true;
}

return false;
}
return CheckForExpiredTime(ref position);
}

internal void AttachTokens() => _tokens?.AttachTokens(this);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#nullable enable

using System;
using System.Runtime.CompilerServices;

namespace Microsoft.Extensions.Caching.Memory
{
internal partial class CacheEntry : ICacheEntry
{
internal long? SlidingExpirationClockOffsetUnits { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; private set; }

internal Internal.ClockQuantization.LazyClockOffsetSerialPosition LastAccessedClockOffsetSerialPosition;

internal long? AbsoluteExpirationClockOffset;


[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal bool CheckExpired(ref Internal.ClockQuantization.LazyClockOffsetSerialPosition now)
=> _state.IsExpired
|| CheckForExpiredTime(ref now)
|| (_tokens != null && _tokens.CheckForExpiredTokens(this));


[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool CheckForExpiredTime(ref Internal.ClockQuantization.LazyClockOffsetSerialPosition now)
{
return CheckForExpiredTime(ref now, AbsoluteExpirationClockOffset.HasValue, SlidingExpirationClockOffsetUnits.HasValue);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal bool CheckExpired(ref Internal.ClockQuantization.LazyClockOffsetSerialPosition now, bool absoluteExpirationUndecided, bool slidingExpirationUndecided)
=> _state.IsExpired
|| CheckForExpiredTime(ref now, absoluteExpirationUndecided, slidingExpirationUndecided)
|| (_tokens != null && _tokens.CheckForExpiredTokens(this));

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool CheckForExpiredTime(ref Internal.ClockQuantization.LazyClockOffsetSerialPosition now, bool absoluteExpirationUndecided, bool slidingExpirationUndecided)
{
if (!absoluteExpirationUndecided && !slidingExpirationUndecided)
{
return false;
}

return FullCheckForExpiredTime(ref now, absoluteExpirationUndecided, slidingExpirationUndecided);
}

private bool FullCheckForExpiredTime(ref Internal.ClockQuantization.LazyClockOffsetSerialPosition now, bool absoluteExpirationUndecided, bool slidingExpirationUndecided)
{
if (!now.IsExact)
{
return IntervalBasedFullCheckForExpiredTime(ref now, absoluteExpirationUndecided, slidingExpirationUndecided);
}

return ExactFullCheckForExpiredTime(now.ClockOffset, absoluteExpirationUndecided, slidingExpirationUndecided);
}

private bool ExactFullCheckForExpiredTime(long offset, bool absoluteExpirationUndecided, bool slidingExpirationUndecided)
{
if (absoluteExpirationUndecided && offset >= AbsoluteExpirationClockOffset!.Value)
{
SetExpired(EvictionReason.Expired);
return true;
}

if (slidingExpirationUndecided && ((offset - LastAccessedClockOffsetSerialPosition.ClockOffset) >= SlidingExpirationClockOffsetUnits!.Value))
{
SetExpired(EvictionReason.Expired);
return true;
}

return false;
}

private bool IntervalBasedFullCheckForExpiredTime(ref Internal.ClockQuantization.LazyClockOffsetSerialPosition position, bool absoluteExpirationUndecided, bool slidingExpirationUndecided)
{
var quantizer = _cache.ClockQuantizer;
var referenceOffset = position.HasValue ? position.ClockOffset : quantizer.CurrentInterval!.ClockOffset;
var nextMetronomicOffset = quantizer.NextMetronomicClockOffset!.Value;

// Relatively cheap tests based on current clock interval
var absoluteExpiresAtOffset = default(long);
if (absoluteExpirationUndecided)
{
absoluteExpiresAtOffset = AbsoluteExpirationClockOffset!.Value;
if (IntervalCheckForExpiredTime(nextMetronomicOffset, referenceOffset, absoluteExpiresAtOffset, ref absoluteExpirationUndecided))
{
SetExpired(EvictionReason.Expired);
return true;
}
}

// Relatively cheap tests based on current clock interval
var slidingExpiresAtOffset = default(long);
if (slidingExpirationUndecided)
{
slidingExpiresAtOffset = LastAccessedClockOffsetSerialPosition.ClockOffset + SlidingExpirationClockOffsetUnits!.Value;
if (IntervalCheckForExpiredTime(nextMetronomicOffset, referenceOffset, slidingExpiresAtOffset, ref slidingExpirationUndecided))
{
SetExpired(EvictionReason.Expired);
return true;
}
}

if (absoluteExpirationUndecided || slidingExpirationUndecided)
{
// If still in doubt about anything, we must bite the bullet and fetch an exact timestamp through the clock (a system call, but the least expensive option)
var now = quantizer.UtcNowClockOffset;

if (absoluteExpirationUndecided && absoluteExpiresAtOffset <= now)
{
SetExpired(EvictionReason.Expired);
return true;
}

if (slidingExpirationUndecided && slidingExpiresAtOffset <= now)
{
SetExpired(EvictionReason.Expired);
return true;
}
}

// We need an exact position for unexpired entries with sliding expiration (to properly update last accessed) and ensure proper LRU ordering as we go.
if (SlidingExpirationClockOffsetUnits.HasValue)
{
quantizer.EnsureInitializedExactClockOffsetSerialPosition(ref position, advance: true);
}

return false;
}

/// <summary>
/// The workhorse logic that aims to make as many time-based decisions as possible, solely based on expiration offset, interval boundaries and expiration policy
/// </summary>
/// <param name="nextMetronomicOffset">The upper boundary of the interval</param>
/// <param name="referenceOffset">The lower boundary of the interval</param>
/// <param name="expiresAtOffset"></param>
/// <param name="expirationUndecided">Indicated if a conclusion was already reached; typically <see langword="true" /> when an item expires sometime within the interval being expected.</param>
/// <returns><see langword="true"/> if expired, <see langword="false"/> otherwise.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IntervalCheckForExpiredTime(long nextMetronomicOffset, long referenceOffset, long expiresAtOffset, ref bool expirationUndecided)
{
expirationUndecided = false;

if (referenceOffset >= expiresAtOffset)
{
// Expired (before start of interval being inspected) - capturing all non-pessimistic cases
return true;
}

// Undecided (expiration sometime within interval being inspected) - more work needed to reach conclusion under precise expiration policy
expirationUndecided = nextMetronomicOffset > expiresAtOffset;

// We conclude that the item did not expire yet (or that we don't know yet, if expiration undecided)
return false;
}
}
}

#nullable restore
Loading