Skip to content

Commit

Permalink
Add metrics for caching (#66479)
Browse files Browse the repository at this point in the history
  • Loading branch information
maryamariyan committed Apr 8, 2022
1 parent 51afa06 commit cef4ae1
Show file tree
Hide file tree
Showing 13 changed files with 350 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

// The compiler emits a reference to the internal copy of this type in our non-NETCoreApp assembly
// so we must include a forward to be compatible with libraries compiled against non-NETCoreApp Microsoft.Extensions.Caching.Abstractions
[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Runtime.CompilerServices.IsExternalInit))]
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,14 @@ public MemoryCacheEntryOptions() { }
public long? Size { get { throw null; } set { } }
public System.TimeSpan? SlidingExpiration { get { throw null; } set { } }
}
public partial class MemoryCacheStatistics
{
public MemoryCacheStatistics() { }
public long CurrentEntryCount { get { throw null; } init { } }
public long? CurrentEstimatedSize { get { throw null; } init { } }
public long TotalHits { get { throw null; } init { } }
public long TotalMisses { get { throw null; } init { } }
}
public partial class PostEvictionCallbackRegistration
{
public PostEvictionCallbackRegistration() { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

<ItemGroup>
<Compile Include="Microsoft.Extensions.Caching.Abstractions.cs" />
<Compile Include="Microsoft.Extensions.Caching.Abstractions.net6.0.cs" Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net6.0'))" />
<Compile Include="Microsoft.Extensions.Caching.Abstractions.Typeforwards.netcoreapp.cs" Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp'"/>
<Compile Include="$(CoreLibSharedDir)System\Runtime\CompilerServices\IsExternalInit.cs" Condition="'$(TargetFrameworkIdentifier)' != '.NETCoreApp'"
Link="Common\System\Runtime\CompilerServices\IsExternalInit.cs" />
<ProjectReference Include="..\..\Microsoft.Extensions.Primitives\ref\Microsoft.Extensions.Primitives.csproj" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// ------------------------------------------------------------------------------
// Changes to this file must follow the https://aka.ms/api-review process.
// ------------------------------------------------------------------------------

namespace Microsoft.Extensions.Caching.Memory
{
public partial interface IMemoryCache : System.IDisposable
{
MemoryCacheStatistics? GetCurrentStatistics() => null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,13 @@ public interface IMemoryCache : IDisposable
/// </summary>
/// <param name="key">An object identifying the entry.</param>
void Remove(object key);

#if NET6_0_OR_GREATER
/// <summary>
/// Gets a snapshot of the cache statistics if available.
/// </summary>
/// <returns>An instance of <see cref="MemoryCacheStatistics"/> containing a snapshot of the cache statistics.</returns>
MemoryCacheStatistics? GetCurrentStatistics() => null;
#endif
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;

namespace Microsoft.Extensions.Caching.Memory
{
/// <summary>
/// Holds a snapshot of statistics for a memory cache.
/// </summary>
public class MemoryCacheStatistics
{
/// <summary>
/// Initializes an instance of MemoryCacheStatistics.
/// </summary>
public MemoryCacheStatistics() { }

/// <summary>
/// Gets the number of <see cref="ICacheEntry" /> instances currently in the memory cache.
/// </summary>
public long CurrentEntryCount { get; init; }

/// <summary>
/// Gets an estimated sum of all the <see cref="ICacheEntry.Size" /> values currently in the memory cache.
/// </summary>
/// <returns>Returns <see langword="null"/> if size isn't being tracked. The common MemoryCache implementation tracks size whenever a SizeLimit is set on the cache.</returns>
public long? CurrentEstimatedSize { get; init; }

/// <summary>
/// Gets the total number of cache misses.
/// </summary>
public long TotalMisses { get; init; }

/// <summary>
/// Gets the total number of cache hits.
/// </summary>
public long TotalHits { get; init; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

// The compiler emits a reference to the internal copy of this type in our non-NETCoreApp assembly
// so we must include a forward to be compatible with libraries compiled against non-NETCoreApp Microsoft.Extensions.Caching.Abstractions
[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Runtime.CompilerServices.IsExternalInit))]
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,9 @@ Microsoft.Extensions.Caching.Memory.IMemoryCache</PackageDescription>
<ProjectReference Include="$(LibrariesProjectRoot)Microsoft.Extensions.Primitives\src\Microsoft.Extensions.Primitives.csproj" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFrameworkIdentifier)' != '.NETCoreApp'">
<Compile Remove="Microsoft.Extensions.Caching.Abstractions.Typeforwards.netcoreapp.cs" />
<Compile Include="$(CoreLibSharedDir)System\Runtime\CompilerServices\IsExternalInit.cs" Link="Common\System\Runtime\CompilerServices\IsExternalInit.cs" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public void Compact(double percentage) { }
public void Dispose() { }
protected virtual void Dispose(bool disposing) { }
~MemoryCache() { }
public Microsoft.Extensions.Caching.Memory.MemoryCacheStatistics? GetCurrentStatistics() { throw null; }
public void Remove(object key) { }
public bool TryGetValue(object key, out object? result) { throw null; }
}
Expand All @@ -45,6 +46,7 @@ public MemoryCacheOptions() { }
Microsoft.Extensions.Caching.Memory.MemoryCacheOptions Microsoft.Extensions.Options.IOptions<Microsoft.Extensions.Caching.Memory.MemoryCacheOptions>.Value { get { throw null; } }
public long? SizeLimit { get { throw null; } set { } }
public bool TrackLinkedCacheEntries { get { throw null; } set { } }
public bool TrackStatistics { get { throw null; } set { } }
}
public partial class MemoryDistributedCacheOptions : Microsoft.Extensions.Caching.Memory.MemoryCacheOptions
{
Expand Down
115 changes: 115 additions & 0 deletions src/libraries/Microsoft.Extensions.Caching.Memory/src/MemoryCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ public class MemoryCache : IMemoryCache

private readonly MemoryCacheOptions _options;

private readonly List<WeakReference<Stats>>? _allStats;
private readonly Stats? _accumulatedStats;
private readonly ThreadLocal<Stats>? _stats;
private CoherentState _coherentState;
private bool _disposed;
private DateTimeOffset _lastExpirationScan;
Expand Down Expand Up @@ -53,6 +56,13 @@ public MemoryCache(IOptions<MemoryCacheOptions> optionsAccessor!!, ILoggerFactor
_options.Clock = new SystemClock();
}

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

_lastExpirationScan = _options.Clock.UtcNow;
TrackLinkedCacheEntries = _options.TrackLinkedCacheEntries; // we store the setting now so it's consistent for entire MemoryCache lifetime
}
Expand Down Expand Up @@ -219,6 +229,14 @@ public bool TryGetValue(object key!!, out object? result)
}

StartScanForExpiredItemsIfNeeded(utcNow);
// Hit
if (_allStats is not null)
{
if (IntPtr.Size == 4)

This comment has been minimized.

Copy link
@amiru3f

amiru3f Oct 14, 2023

Hello @maryamariyan,

I was reviewing the source code for memory caching statistics when I came across something unexpected. I couldn't quite understand why the IntPtr.Size == 4 (32-bit platform check) is necessary.

Would you be able to provide more details? Thank you in advance.

Interlocked.Increment(ref GetStats().Hits);
else
GetStats().Hits++;
}

return true;
}
Expand All @@ -232,6 +250,15 @@ public bool TryGetValue(object key!!, out object? result)
StartScanForExpiredItemsIfNeeded(utcNow);

result = null;
// Miss
if (_allStats is not null)
{
if (IntPtr.Size == 4)
Interlocked.Increment(ref GetStats().Misses);
else
GetStats().Misses++;
}

return false;
}

Expand Down Expand Up @@ -270,6 +297,27 @@ public void Clear()
}
}

/// <summary>
/// Gets a snapshot of the current statistics for the memory cache.
/// </summary>
/// <returns>Returns <see langword="null"/> if statistics are not being tracked because <see cref="MemoryCacheOptions.TrackStatistics" /> is <see langword="false"/>.</returns>
public MemoryCacheStatistics? GetCurrentStatistics()
{
if (_allStats is not null)
{
(long hit, long miss) sumTotal = Sum();
return new MemoryCacheStatistics()
{
TotalMisses = sumTotal.miss,
TotalHits = sumTotal.hit,
CurrentEntryCount = Count,
CurrentEstimatedSize = _options.SizeLimit.HasValue ? Size : null
};
}

return null;
}

internal void EntryExpired(CacheEntry entry)
{
// TODO: For efficiency consider processing these expirations in batches.
Expand All @@ -295,6 +343,72 @@ void ScheduleTask(DateTimeOffset utcNow)
}
}

private (long, long) Sum()
{
lock (_allStats!)
{
long hits = _accumulatedStats!.Hits;
long misses = _accumulatedStats.Misses;

foreach (WeakReference<Stats> wr in _allStats)
{
if (wr.TryGetTarget(out Stats? stats))
{
hits += Interlocked.Read(ref stats.Hits);
misses += Interlocked.Read(ref stats.Misses);
}
}

return (hits, misses);
}
}

private Stats GetStats() => _stats!.Value!;

internal sealed class Stats
{
private readonly MemoryCache? _memoryCache;
public long Hits;
public long Misses;

public Stats() { }

public Stats(MemoryCache memoryCache)
{
_memoryCache = memoryCache;
_memoryCache.AddToStats(this);
}

~Stats() => _memoryCache?.RemoveFromStats(this);
}

private void RemoveFromStats(Stats current)
{
lock (_allStats!)
{
for (int i = 0; i < _allStats.Count; i++)
{
if (_allStats[i].TryGetTarget(out Stats? stats) && stats == current)
{
_allStats.RemoveAt(i);
break;
}
}

_accumulatedStats!.Hits += Interlocked.Read(ref current.Hits);
_accumulatedStats.Misses += Interlocked.Read(ref current.Misses);
_allStats.TrimExcess();
}
}

private void AddToStats(Stats current)
{
lock (_allStats!)
{
_allStats.Add(new WeakReference<Stats>(current));
}
}

private static void ScanForExpiredItems(MemoryCache cache)
{
DateTimeOffset now = cache._lastExpirationScan = cache._options.Clock!.UtcNow;
Expand Down Expand Up @@ -469,6 +583,7 @@ protected virtual void Dispose(bool disposing)
{
if (disposing)
{
_stats?.Dispose();
GC.SuppressFinalize(this);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ public double CompactionPercentage
/// <remarks>Prior to .NET 7 this feature was always enabled.</remarks>
public bool TrackLinkedCacheEntries { get; set; }

/// <summary>
/// Gets or sets whether to track memory cache statistics. Disabled by default.
/// </summary>
public bool TrackStatistics { get; set; }

MemoryCacheOptions IOptions<MemoryCacheOptions>.Value
{
get { return this; }
Expand Down
Loading

0 comments on commit cef4ae1

Please sign in to comment.