Skip to content
Merged
Show file tree
Hide file tree
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
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ public MemoryCacheStatistics() { }
public long? CurrentEstimatedSize { get { throw null; } init { } }
public long TotalHits { get { throw null; } init { } }
public long TotalMisses { get { throw null; } init { } }
public long TotalEvictions { get { throw null; } init { } }
}
public partial class PostEvictionCallbackRegistration
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,15 @@ public MemoryCacheStatistics() { }
/// Gets the total number of cache hits.
/// </summary>
public long TotalHits { get; init; }

/// <summary>
/// Gets the total number of cache evictions.
/// </summary>
/// <remarks>
/// This count includes entries removed due to cache eviction policies such as expiration or capacity limits.
/// It does not include entries removed explicitly by user code (for example, via <c>Remove</c> or <c>Clear</c>),
/// and does not include entries that were replaced by new values.
/// </remarks>
public long TotalEvictions { get; init; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ namespace Microsoft.Extensions.Caching.Memory
public partial class MemoryCache : Microsoft.Extensions.Caching.Memory.IMemoryCache, System.IDisposable
{
public MemoryCache(Microsoft.Extensions.Options.IOptions<Microsoft.Extensions.Caching.Memory.MemoryCacheOptions> optionsAccessor) { }
public MemoryCache(Microsoft.Extensions.Options.IOptions<Microsoft.Extensions.Caching.Memory.MemoryCacheOptions> optionsAccessor, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory) { }
public MemoryCache(Microsoft.Extensions.Options.IOptions<Microsoft.Extensions.Caching.Memory.MemoryCacheOptions> optionsAccessor, Microsoft.Extensions.Logging.ILoggerFactory? loggerFactory) { }
public MemoryCache(Microsoft.Extensions.Options.IOptions<Microsoft.Extensions.Caching.Memory.MemoryCacheOptions> optionsAccessor, Microsoft.Extensions.Logging.ILoggerFactory? loggerFactory, System.Diagnostics.Metrics.IMeterFactory? meterFactory) { }
public int Count { get { throw null; } }
public System.Collections.Generic.IEnumerable<object> Keys { get { throw null; } }
public void Clear() { }
Expand All @@ -51,6 +52,7 @@ public MemoryCacheOptions() { }
public long? SizeLimit { get { throw null; } set { } }
public bool TrackLinkedCacheEntries { get { throw null; } set { } }
public bool TrackStatistics { get { throw null; } set { } }
public string Name { get { throw null; } set { } }
}
public partial class MemoryDistributedCacheOptions : Microsoft.Extensions.Caching.Memory.MemoryCacheOptions
{
Expand Down
148 changes: 139 additions & 9 deletions src/libraries/Microsoft.Extensions.Caching.Memory/src/MemoryCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Metrics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
Expand All @@ -30,6 +31,7 @@ public class MemoryCache : IMemoryCache
private readonly List<Stats>? _allStats;
private long _accumulatedHits;
private long _accumulatedMisses;
private long _accumulatedEvictions;
private readonly ThreadLocal<StatsHandler>? _stats;
private CoherentState _coherentState;
private bool _disposed;
Expand All @@ -47,20 +49,34 @@ public MemoryCache(IOptions<MemoryCacheOptions> optionsAccessor)
/// </summary>
/// <param name="optionsAccessor">The options of the cache.</param>
/// <param name="loggerFactory">The factory used to create loggers.</param>
public MemoryCache(IOptions<MemoryCacheOptions> optionsAccessor, ILoggerFactory loggerFactory)
public MemoryCache(IOptions<MemoryCacheOptions> optionsAccessor, ILoggerFactory? loggerFactory)
: this(optionsAccessor, loggerFactory, meterFactory: null) { }

/// <summary>
/// Creates a new <see cref="MemoryCache"/> instance.
/// </summary>
/// <param name="optionsAccessor">The options of the cache.</param>
/// <param name="loggerFactory">The factory used to create loggers.</param>
/// <param name="meterFactory">The factory used to create meters for metrics collection.</param>
public MemoryCache(IOptions<MemoryCacheOptions> optionsAccessor, ILoggerFactory? loggerFactory, IMeterFactory? meterFactory)
{
ArgumentNullException.ThrowIfNull(optionsAccessor);
ArgumentNullException.ThrowIfNull(loggerFactory);

_options = optionsAccessor.Value;
_logger = loggerFactory.CreateLogger<MemoryCache>();
_logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger<MemoryCache>();

_coherentState = new CoherentState();

if (_options.TrackStatistics)
{
_allStats = new List<Stats>();
_stats = new ThreadLocal<StatsHandler>(() => new StatsHandler(this));

Meter meter = meterFactory?.Create(new MeterOptions("Microsoft.Extensions.Caching.Memory.MemoryCache")
{
Tags = [new("cache.name", _options.Name)]
Comment thread
cincuranet marked this conversation as resolved.
}) ?? SharedMeter.Instance;
InitializeMetrics(meter);
}

_lastExpirationScan = UtcNow;
Expand Down Expand Up @@ -296,7 +312,10 @@ private bool PostProcessTryGetValue(CoherentState coherentState, CacheEntry? ent
else
{
// TODO: For efficiency queue this up for batch removal
coherentState.RemoveEntry(entry, _options);
if (coherentState.RemoveEntry(entry, _options) && _allStats is not null)
{
Interlocked.Increment(ref _accumulatedEvictions);
}
}
}

Expand Down Expand Up @@ -367,7 +386,8 @@ public void Clear()
TotalMisses = sumTotal.miss,
TotalHits = sumTotal.hit,
CurrentEntryCount = Count,
CurrentEstimatedSize = _options.HasSizeLimit ? Size : null
CurrentEstimatedSize = _options.HasSizeLimit ? Size : null,
TotalEvictions = Interlocked.Read(ref _accumulatedEvictions)
};
}

Expand All @@ -377,7 +397,11 @@ public void Clear()
internal void EntryExpired(CacheEntry entry)
{
// TODO: For efficiency consider processing these expirations in batches.
_coherentState.RemoveEntry(entry, _options);
if (_coherentState.RemoveEntry(entry, _options) && _allStats is not null)
{
Interlocked.Increment(ref _accumulatedEvictions);
}

StartScanForExpiredItemsIfNeeded(UtcNow);
}

Expand Down Expand Up @@ -486,7 +510,10 @@ private void ScanForExpiredItems()
{
if (entry.CheckExpired(utcNow))
{
coherentState.RemoveEntry(entry, _options);
if (coherentState.RemoveEntry(entry, _options) && _allStats is not null)
{
Interlocked.Increment(ref _accumulatedEvictions);
}
}
}
}
Expand Down Expand Up @@ -637,9 +664,18 @@ private void Compact(long removalSizeTarget, Func<CacheEntry, long> computeEntry
ExpirePriorityBucket(ref removedSize, removalSizeTarget, computeEntrySize, entriesToRemove, normalPriEntries);
ExpirePriorityBucket(ref removedSize, removalSizeTarget, computeEntrySize, entriesToRemove, highPriEntries);

int actuallyRemoved = 0;
foreach (CacheEntry entry in entriesToRemove)
{
coherentState.RemoveEntry(entry, _options);
if (coherentState.RemoveEntry(entry, _options))
{
actuallyRemoved++;
}
}

if (actuallyRemoved > 0 && _allStats is not null)
{
Interlocked.Add(ref _accumulatedEvictions, actuallyRemoved);
}

// Policy:
Expand Down Expand Up @@ -796,7 +832,7 @@ public IEnumerable<object> GetAllKeys()

internal long Size => Volatile.Read(ref _cacheSize);

internal void RemoveEntry(CacheEntry entry, MemoryCacheOptions options)
internal bool RemoveEntry(CacheEntry entry, MemoryCacheOptions options)
{
#if NET
if (entry.Key is string s ? _stringEntries.TryRemove(KeyValuePair.Create(s, entry))
Expand All @@ -811,7 +847,10 @@ internal void RemoveEntry(CacheEntry entry, MemoryCacheOptions options)
Interlocked.Add(ref _cacheSize, -entry.Size);
}
entry.InvokeEvictionCallbacks();
return true;
}

return false;
}

#if !NET
Expand All @@ -838,5 +877,96 @@ int IEqualityComparer.GetHashCode(object obj)
}
#endif
}

private void InitializeMetrics(Meter meter)
{
// Use a weak reference for `this` to avoid keeping it alive indefinitely
// due to it being captured in the instrument's lambda and hence kept alive by the Meter.
WeakReference<MemoryCache> weakThis = new(this);
KeyValuePair<string, object?> cacheNameTag = new("cache.name", _options.Name);

meter.CreateObservableCounter("cache.requests",
() =>
{
if (!weakThis.TryGetTarget(out MemoryCache? cache) || cache._disposed)
{
return [];
}

MemoryCacheStatistics? stats = cache.GetCurrentStatistics();
return stats is null
? []
: new Measurement<long>[]
{
new(stats.TotalHits, cacheNameTag, new("cache.request.type", "hit")),
new(stats.TotalMisses, cacheNameTag, new("cache.request.type", "miss")),
};
},
unit: "{requests}",
description: "Total cache requests.");

meter.CreateObservableCounter<long>("cache.evictions",
() =>
{
if (!weakThis.TryGetTarget(out MemoryCache? cache) || cache._disposed)
{
return [];
}

MemoryCacheStatistics? stats = cache.GetCurrentStatistics();
return stats is null
? []
: [new Measurement<long>(stats.TotalEvictions, cacheNameTag)];
},
unit: "{evictions}",
description: "Total cache evictions.");

meter.CreateObservableUpDownCounter<long>("cache.entries",
() =>
{
if (!weakThis.TryGetTarget(out MemoryCache? cache) || cache._disposed)
{
return [];
}

MemoryCacheStatistics? stats = cache.GetCurrentStatistics();
return stats is null
? []
: [new Measurement<long>(stats.CurrentEntryCount, cacheNameTag)];
},
unit: "{entries}",
description: "Current number of cache entries.");

meter.CreateObservableGauge<long>("cache.estimated_size",
() =>
{
if (!weakThis.TryGetTarget(out MemoryCache? cache) || cache._disposed)
{
return [];
}

MemoryCacheStatistics? stats = cache.GetCurrentStatistics();
return stats?.CurrentEstimatedSize is long size
? [new Measurement<long>(size, cacheNameTag)]
: [];
},
unit: "By",
description: "Estimated size of the cache.");
}

Comment thread
cincuranet marked this conversation as resolved.
private sealed class SharedMeter : Meter
{
public static Meter Instance { get; } = new SharedMeter();

private SharedMeter()
: base("Microsoft.Extensions.Caching.Memory.MemoryCache")
{
}

protected override void Dispose(bool disposing)
{
// NOP to prevent disposing the global instance.
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@ public double CompactionPercentage
/// </value>
public bool TrackStatistics { get; set; }

/// <summary>
/// Gets or sets the name of this cache instance.
/// </summary>
public string Name { get; set; } = "Default";

MemoryCacheOptions IOptions<MemoryCacheOptions>.Value
{
get { return this; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
<ProjectReference Include="$(LibrariesProjectRoot)Microsoft.Extensions.Primitives\src\Microsoft.Extensions.Primitives.csproj" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' != '$(NetCoreAppCurrent)'">
<ProjectReference Include="$(LibrariesProjectRoot)System.Diagnostics.DiagnosticSource\src\System.Diagnostics.DiagnosticSource.csproj" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFrameworkIdentifier)' == '.NETFramework'">
<PackageReference Include="System.ValueTuple" Version="$(SystemValueTupleVersion)" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,36 @@ public void GetCurrentStatistics_DIMReturnsNull()
}
#endif

[Fact]
public void GetCurrentStatistics_ExplicitRemove_DoesNotTrackEviction()
{
var cache = new MemoryCache(new MemoryCacheOptions { TrackStatistics = true });

cache.Set("key", "value");
cache.Remove("key");

MemoryCacheStatistics? stats = cache.GetCurrentStatistics();
Assert.NotNull(stats);
Assert.Equal(0, stats.TotalEvictions);
}

[Fact]
public void GetCurrentStatistics_Compact_TracksTotalEvictions()
{
var cache = new MemoryCache(new MemoryCacheOptions { TrackStatistics = true });

for (int i = 0; i < 10; i++)
{
cache.Set($"key{i}", $"value{i}");
}

cache.Compact(1.0);

MemoryCacheStatistics? stats = cache.GetCurrentStatistics();
Assert.NotNull(stats);
Assert.Equal(10, stats.TotalEvictions);
}

private class FakeMemoryCache : IMemoryCache
{
public ICacheEntry CreateEntry(object key) => throw new NotImplementedException();
Expand Down
Loading
Loading