Skip to content

Commit bb8eaad

Browse files
Use a TLRU implementation to reduce cache lookup cost.
1 parent 676889f commit bb8eaad

File tree

4 files changed

+71
-35
lines changed

4 files changed

+71
-35
lines changed

src/ImageSharp.Web/Caching/PhysicalFileSystemCache.cs

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -85,31 +85,17 @@ public PhysicalFileSystemCache(
8585
}
8686

8787
/// <inheritdoc/>
88-
public async Task<IImageCacheResolver> GetAsync(string key)
88+
public Task<IImageCacheResolver> GetAsync(string key)
8989
{
9090
string path = ToFilePath(key, this.cachedNameLength);
9191

9292
IFileInfo metaFileInfo = this.fileProvider.GetFileInfo(this.ToMetaDataFilePath(path));
9393
if (!metaFileInfo.Exists)
9494
{
95-
return null;
95+
return Task.FromResult<IImageCacheResolver>(null);
9696
}
9797

98-
ImageCacheMetadata metadata = default;
99-
using (Stream stream = metaFileInfo.CreateReadStream())
100-
{
101-
metadata = await ImageCacheMetadata.ReadAsync(stream);
102-
}
103-
104-
IFileInfo fileInfo = this.fileProvider.GetFileInfo(this.ToImageFilePath(path, metadata));
105-
106-
// Check to see if the file exists.
107-
if (!fileInfo.Exists)
108-
{
109-
return null;
110-
}
111-
112-
return new PhysicalFileSystemCacheResolver(fileInfo, metadata);
98+
return Task.FromResult<IImageCacheResolver>(new PhysicalFileSystemCacheResolver(metaFileInfo, this.formatUtilities));
11399
}
114100

115101
/// <inheritdoc/>
@@ -142,6 +128,7 @@ public async Task SetAsync(string key, Stream stream, ImageCacheMetadata metadat
142128
/// <param name="path">The root path.</param>
143129
/// <param name="metaData">The image metadata.</param>
144130
/// <returns>The <see cref="string"/>.</returns>
131+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
145132
private string ToImageFilePath(string path, in ImageCacheMetadata metaData)
146133
=> $"{path}.{this.formatUtilities.GetExtensionFromContentType(metaData.ContentType)}";
147134

@@ -150,6 +137,7 @@ private string ToImageFilePath(string path, in ImageCacheMetadata metaData)
150137
/// </summary>
151138
/// <param name="path">The root path.</param>
152139
/// <returns>The <see cref="string"/>.</returns>
140+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
153141
private string ToMetaDataFilePath(string path) => $"{path}.meta";
154142

155143
/// <summary>

src/ImageSharp.Web/Caching/PhysicalFileSystemCacheResolver.cs

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,23 +14,38 @@ namespace SixLabors.ImageSharp.Web.Resolvers
1414
public class PhysicalFileSystemCacheResolver : IImageCacheResolver
1515
{
1616
private readonly IFileInfo fileInfo;
17-
private readonly ImageCacheMetadata metadata;
17+
private readonly FormatUtilities formatUtilities;
18+
private ImageCacheMetadata metadata;
1819

1920
/// <summary>
2021
/// Initializes a new instance of the <see cref="PhysicalFileSystemCacheResolver"/> class.
2122
/// </summary>
22-
/// <param name="fileInfo">The input file info.</param>
23-
/// <param name="metadata">The image metadata associated with this file.</param>
24-
public PhysicalFileSystemCacheResolver(IFileInfo fileInfo, in ImageCacheMetadata metadata)
23+
/// <param name="metaFileInfo">The cached metadata file info.</param>
24+
/// <param name="formatUtilities">
25+
/// Contains various format helper methods based on the current configuration.
26+
/// </param>
27+
public PhysicalFileSystemCacheResolver(IFileInfo metaFileInfo, FormatUtilities formatUtilities)
2528
{
26-
this.fileInfo = fileInfo;
27-
this.metadata = metadata;
29+
this.fileInfo = metaFileInfo;
30+
this.formatUtilities = formatUtilities;
2831
}
2932

3033
/// <inheritdoc/>
31-
public Task<ImageCacheMetadata> GetMetaDataAsync() => Task.FromResult(this.metadata);
34+
public async Task<ImageCacheMetadata> GetMetaDataAsync()
35+
{
36+
using Stream stream = this.fileInfo.CreateReadStream();
37+
this.metadata = await ImageCacheMetadata.ReadAsync(stream);
38+
return this.metadata;
39+
}
3240

3341
/// <inheritdoc/>
34-
public Task<Stream> OpenReadAsync() => Task.FromResult(this.fileInfo.CreateReadStream());
42+
public Task<Stream> OpenReadAsync()
43+
{
44+
string path = Path.ChangeExtension(
45+
this.fileInfo.PhysicalPath,
46+
this.formatUtilities.GetExtensionFromContentType(this.metadata.ContentType));
47+
48+
return Task.FromResult<Stream>(File.OpenRead(path));
49+
}
3550
}
3651
}

src/ImageSharp.Web/ImageSharp.Web.csproj

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,19 @@
1616
</ItemGroup>
1717

1818
<ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp2.1'">
19-
<PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" />
19+
<PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" />
2020
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" />
21-
<PackageReference Include="Microsoft.AspNetCore.Http.Extensions" />
22-
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" />
23-
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" />
24-
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" />
21+
<PackageReference Include="Microsoft.AspNetCore.Http.Extensions" />
22+
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" />
23+
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" />
24+
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" />
2525
<PackageReference Include="Microsoft.Extensions.FileProviders.Physical" />
2626
</ItemGroup>
2727

2828
<ItemGroup>
29+
<PackageReference Include="BitFaster.Caching" Version="1.0.1" />
2930
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" />
30-
<PackageReference Include="Microsoft.SourceLink.GitHub"/>
31+
<PackageReference Include="Microsoft.SourceLink.GitHub" />
3132
<PackageReference Include="MinVer" PrivateAssets="All" />
3233
<PackageReference Include="SixLabors.ImageSharp" />
3334
</ItemGroup>

src/ImageSharp.Web/Middleware/ImageSharpMiddleware.cs

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
using System.IO;
99
using System.Linq;
1010
using System.Text;
11-
using System.Threading;
1211
using System.Threading.Tasks;
12+
using BitFaster.Caching.Lru;
1313
using Microsoft.AspNetCore.Http;
1414
using Microsoft.Extensions.Logging;
1515
using Microsoft.Extensions.Options;
@@ -33,6 +33,24 @@ public class ImageSharpMiddleware
3333
/// </summary>
3434
private static readonly AsyncKeyLock AsyncLock = new AsyncKeyLock();
3535

36+
/// <summary>
37+
/// Used to temporarily store source metadata reads to reduce the overhead of cache lookups.
38+
/// </summary>
39+
private static readonly ConcurrentTLru<string, ImageMetadata> SourceMetadataLru
40+
= new ConcurrentTLru<string, ImageMetadata>(1024, TimeSpan.FromMinutes(5));
41+
42+
/// <summary>
43+
/// Used to temporarily store cache resolver reads to reduce the overhead of cache lookups.
44+
/// </summary>
45+
private static readonly ConcurrentTLru<string, IImageCacheResolver> CacheResolverLru
46+
= new ConcurrentTLru<string, IImageCacheResolver>(1024, TimeSpan.FromMinutes(5));
47+
48+
/// <summary>
49+
/// Used to temporarily store cache metadata reads to reduce the overhead of cache lookups.
50+
/// </summary>
51+
private static readonly ConcurrentTLru<string, ImageCacheMetadata> CacheMetadataLru
52+
= new ConcurrentTLru<string, ImageCacheMetadata>(1024, TimeSpan.FromMinutes(5));
53+
3654
/// <summary>
3755
/// The function processing the Http request.
3856
/// </summary>
@@ -304,6 +322,14 @@ private async Task ProcessRequestAsync(
304322

305323
// Save the image to the cache and send the response to the caller.
306324
await this.cache.SetAsync(key, outStream, cachedImageMetadata);
325+
326+
// Remove the resolver from the cache so we always resolve next request.
327+
CacheResolverLru.TryRemove(key);
328+
329+
// Replace cache metadata item value.
330+
CacheMetadataLru.TryRemove(key);
331+
CacheMetadataLru.GetOrAdd(key, _ => cachedImageMetadata);
332+
307333
await this.SendResponseAsync(imageContext, key, cachedImageMetadata, outStream, null);
308334
}
309335
catch (Exception ex)
@@ -351,14 +377,20 @@ private async Task<ValueTuple<bool, ImageMetadata>> IsNewOrUpdatedAsync(
351377
using (await AsyncLock.ReaderLockAsync(key))
352378
{
353379
// Check to see if the cache contains this image.
354-
sourceImageMetadata = await sourceImageResolver.GetMetaDataAsync();
355-
IImageCacheResolver cachedImageResolver = await this.cache.GetAsync(key);
380+
IImageCacheResolver cachedImageResolver
381+
= await CacheResolverLru.GetOrAddAsync(key, async k => await this.cache.GetAsync(k));
382+
356383
if (cachedImageResolver != null)
357384
{
358-
ImageCacheMetadata cachedImageMetadata = await cachedImageResolver.GetMetaDataAsync();
385+
ImageCacheMetadata cachedImageMetadata =
386+
await CacheMetadataLru.GetOrAddAsync(key, async _ => await cachedImageResolver.GetMetaDataAsync());
387+
359388
if (cachedImageMetadata != default)
360389
{
361390
// Has the cached image expired or has the source image been updated?
391+
sourceImageMetadata =
392+
await SourceMetadataLru.GetOrAddAsync(key, async _ => await sourceImageResolver.GetMetaDataAsync());
393+
362394
if (cachedImageMetadata.SourceLastWriteTimeUtc == sourceImageMetadata.LastWriteTimeUtc
363395
&& cachedImageMetadata.ContentLength > 0 // Fix for old cache without length property
364396
&& cachedImageMetadata.CacheLastWriteTimeUtc > (DateTimeOffset.UtcNow - this.options.CacheMaxAge))

0 commit comments

Comments
 (0)