Skip to content

Commit 620f818

Browse files
Introduct ConcurrentTLruCache
1 parent 39ebf85 commit 620f818

File tree

7 files changed

+560
-28
lines changed

7 files changed

+560
-28
lines changed

src/ImageSharp.Web/Caching/LruCache/ConcurrentTLruCache{TKey,TValue}.cs

Lines changed: 376 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Apache License, Version 2.0.
3+
4+
namespace SixLabors.ImageSharp.Web.Caching
5+
{
6+
internal enum ItemDestination
7+
{
8+
Warm,
9+
Cold,
10+
Remove
11+
}
12+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Apache License, Version 2.0.
3+
4+
namespace SixLabors.ImageSharp.Web.Caching
5+
{
6+
internal class LongTickCountLruItem<TKey, TValue>
7+
{
8+
private volatile bool wasAccessed;
9+
private volatile bool wasRemoved;
10+
11+
#pragma warning disable SA1401 // Fields should be private
12+
public readonly TKey Key;
13+
14+
public readonly TValue Value;
15+
#pragma warning restore SA1401 // Fields should be private
16+
17+
public LongTickCountLruItem(TKey k, TValue v, long tickCount)
18+
{
19+
this.Key = k;
20+
this.Value = v;
21+
this.TickCount = tickCount;
22+
}
23+
24+
public long TickCount { get; set; }
25+
26+
public bool WasAccessed
27+
{
28+
get => this.wasAccessed;
29+
set => this.wasAccessed = value;
30+
}
31+
32+
public bool WasRemoved
33+
{
34+
get => this.wasRemoved;
35+
set => this.wasRemoved = value;
36+
}
37+
}
38+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
The TLRU cache implementation was adapted from the excellent BitFaster.Caching
2+
3+
by Alex Peck (MIT license).
4+
5+
https://github.com/bitfaster/BitFaster.Caching
6+
7+
We have chosen to avoid a direct dependency at this time so that we can independantly
8+
adjust internal mechanisms when required to suit our caching requirements.
9+
10+
API compatibility wil be preserved and we may introduce the dependancy on a later date if no
11+
changes are required.
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Apache License, Version 2.0.
3+
4+
using System;
5+
using System.Diagnostics;
6+
using System.Runtime.CompilerServices;
7+
8+
namespace SixLabors.ImageSharp.Web.Caching
9+
{
10+
/// <summary>
11+
/// Time aware Least Recently Used (TLRU) is a variant of LRU which discards the least
12+
/// recently used items first, and any item that has expired.
13+
/// </summary>
14+
/// <remarks>
15+
/// This class measures time using stopwatch.
16+
/// </remarks>
17+
internal readonly struct TLruLongTicksPolicy<TKey, TValue>
18+
{
19+
private readonly long timeToLive;
20+
21+
public TLruLongTicksPolicy(TimeSpan timeToLive)
22+
{
23+
this.timeToLive = timeToLive.Ticks;
24+
}
25+
26+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
27+
public LongTickCountLruItem<TKey, TValue> CreateItem(TKey key, TValue value)
28+
{
29+
return new LongTickCountLruItem<TKey, TValue>(key, value, Stopwatch.GetTimestamp());
30+
}
31+
32+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
33+
public void Touch(LongTickCountLruItem<TKey, TValue> item)
34+
{
35+
item.WasAccessed = true;
36+
}
37+
38+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
39+
public bool ShouldDiscard(LongTickCountLruItem<TKey, TValue> item)
40+
{
41+
if (Stopwatch.GetTimestamp() - item.TickCount > this.timeToLive)
42+
{
43+
return true;
44+
}
45+
46+
return false;
47+
}
48+
49+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
50+
public ItemDestination RouteHot(LongTickCountLruItem<TKey, TValue> item)
51+
{
52+
if (this.ShouldDiscard(item))
53+
{
54+
return ItemDestination.Remove;
55+
}
56+
57+
if (item.WasAccessed)
58+
{
59+
return ItemDestination.Warm;
60+
}
61+
62+
return ItemDestination.Cold;
63+
}
64+
65+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
66+
public ItemDestination RouteWarm(LongTickCountLruItem<TKey, TValue> item)
67+
{
68+
if (this.ShouldDiscard(item))
69+
{
70+
return ItemDestination.Remove;
71+
}
72+
73+
if (item.WasAccessed)
74+
{
75+
return ItemDestination.Warm;
76+
}
77+
78+
return ItemDestination.Cold;
79+
}
80+
81+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
82+
public ItemDestination RouteCold(LongTickCountLruItem<TKey, TValue> item)
83+
{
84+
if (this.ShouldDiscard(item))
85+
{
86+
return ItemDestination.Remove;
87+
}
88+
89+
if (item.WasAccessed)
90+
{
91+
return ItemDestination.Warm;
92+
}
93+
94+
return ItemDestination.Remove;
95+
}
96+
}
97+
}

src/ImageSharp.Web/ImageSharp.Web.csproj

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
44
<AssemblyTitle>SixLabors.ImageSharp.Web</AssemblyTitle>
@@ -26,7 +26,6 @@
2626
</ItemGroup>
2727

2828
<ItemGroup>
29-
<PackageReference Include="BitFaster.Caching" Version="1.0.1" />
3029
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" />
3130
<PackageReference Include="Microsoft.SourceLink.GitHub" />
3231
<PackageReference Include="MinVer" PrivateAssets="All" />

src/ImageSharp.Web/Middleware/ImageSharpMiddleware.cs

Lines changed: 25 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
using System.Linq;
1010
using System.Text;
1111
using System.Threading.Tasks;
12-
using BitFaster.Caching.Lru;
1312
using Microsoft.AspNetCore.Http;
1413
using Microsoft.Extensions.Logging;
1514
using Microsoft.Extensions.Options;
@@ -36,20 +35,20 @@ public class ImageSharpMiddleware
3635
/// <summary>
3736
/// Used to temporarily store source metadata reads to reduce the overhead of cache lookups.
3837
/// </summary>
39-
private static readonly ConcurrentTLru<string, ImageMetadata> SourceMetadataLru
40-
= new ConcurrentTLru<string, ImageMetadata>(1024, TimeSpan.FromMinutes(5));
38+
private static readonly ConcurrentTLruCache<string, ImageMetadata> SourceMetadataLru
39+
= new ConcurrentTLruCache<string, ImageMetadata>(1024, TimeSpan.FromMinutes(5));
4140

4241
/// <summary>
4342
/// Used to temporarily store cache resolver reads to reduce the overhead of cache lookups.
4443
/// </summary>
45-
private static readonly ConcurrentTLru<string, IImageCacheResolver> CacheResolverLru
46-
= new ConcurrentTLru<string, IImageCacheResolver>(1024, TimeSpan.FromMinutes(5));
44+
private static readonly ConcurrentTLruCache<string, IImageCacheResolver> CacheResolverLru
45+
= new ConcurrentTLruCache<string, IImageCacheResolver>(1024, TimeSpan.FromMinutes(5));
4746

4847
/// <summary>
4948
/// Used to temporarily store cache metadata reads to reduce the overhead of cache lookups.
5049
/// </summary>
51-
private static readonly ConcurrentTLru<string, ImageCacheMetadata> CacheMetadataLru
52-
= new ConcurrentTLru<string, ImageCacheMetadata>(1024, TimeSpan.FromMinutes(5));
50+
private static readonly ConcurrentTLruCache<string, ImageCacheMetadata> CacheMetadataLru
51+
= new ConcurrentTLruCache<string, ImageCacheMetadata>(1024, TimeSpan.FromMinutes(5));
5352

5453
/// <summary>
5554
/// The function processing the Http request.
@@ -268,10 +267,19 @@ private async Task ProcessRequestAsync(
268267
{
269268
ImageCacheMetadata cachedImageMetadata = default;
270269
outStream = new RecyclableMemoryStream(this.options.MemoryStreamManager);
271-
using (Stream inStream = await sourceImageResolver.OpenReadAsync())
270+
IImageFormat format;
271+
272+
// 14.9.3 CacheControl Max-Age
273+
// Check to see if the source metadata has a CacheControl Max-Age value
274+
// and use it to override the default max age from our options.
275+
TimeSpan maxAge = this.options.BrowserMaxAge;
276+
if (!sourceImageMetadata.CacheControlMaxAge.Equals(TimeSpan.MinValue))
272277
{
273-
IImageFormat format;
278+
maxAge = sourceImageMetadata.CacheControlMaxAge;
279+
}
274280

281+
using (Stream inStream = await sourceImageResolver.OpenReadAsync())
282+
{
275283
// No commands? We simply copy the stream across.
276284
if (commands.Count == 0)
277285
{
@@ -295,31 +303,22 @@ private async Task ProcessRequestAsync(
295303
image.Save(outStream);
296304
format = image.Format;
297305
}
298-
299-
// 14.9.3 CacheControl Max-Age
300-
// Check to see if the source metadata has a CacheControl Max-Age value
301-
// and use it to override the default max age from our options.
302-
TimeSpan maxAge = this.options.BrowserMaxAge;
303-
if (!sourceImageMetadata.CacheControlMaxAge.Equals(TimeSpan.MinValue))
304-
{
305-
maxAge = sourceImageMetadata.CacheControlMaxAge;
306-
}
307-
308-
cachedImageMetadata = new ImageCacheMetadata(
309-
sourceImageMetadata.LastWriteTimeUtc,
310-
DateTime.UtcNow,
311-
format.DefaultMimeType,
312-
maxAge,
313-
outStream.Length);
314306
}
315307

316308
// Allow for any further optimization of the image.
317309
outStream.Position = 0;
318-
string contentType = cachedImageMetadata.ContentType;
310+
string contentType = format.DefaultMimeType;
319311
string extension = this.formatUtilities.GetExtensionFromContentType(contentType);
320312
await this.options.OnProcessedAsync.Invoke(new ImageProcessingContext(context, outStream, commands, contentType, extension));
321313
outStream.Position = 0;
322314

315+
cachedImageMetadata = new ImageCacheMetadata(
316+
sourceImageMetadata.LastWriteTimeUtc,
317+
DateTime.UtcNow,
318+
contentType,
319+
maxAge,
320+
outStream.Length);
321+
323322
// Save the image to the cache and send the response to the caller.
324323
await this.cache.SetAsync(key, outStream, cachedImageMetadata);
325324

0 commit comments

Comments
 (0)