Skip to content

Commit a11d088

Browse files
Merge pull request #105 from SixLabors/js/code-quality
Various Code Quality Fixes
2 parents cd70936 + f7422f3 commit a11d088

File tree

16 files changed

+244
-40
lines changed

16 files changed

+244
-40
lines changed

Directory.Build.props

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,14 @@
1313
<!-- Default settings that are used by other settings -->
1414
<PropertyGroup>
1515
<BaseArtifactsPath>$(MSBuildThisFileDirectory)artifacts/</BaseArtifactsPath>
16-
<BaseArtifactsPathSuffix>$(ImageSharpProjectCategory)/$(MSBuildProjectName)</BaseArtifactsPathSuffix>
16+
<BaseArtifactsPathSuffix>$(SixLaborsProjectCategory)/$(MSBuildProjectName)</BaseArtifactsPathSuffix>
1717
<RepositoryUrl Condition="'$(RepositoryUrl)' == ''">https://github.com/SixLabors/ImageSharp.Web/</RepositoryUrl>
1818
</PropertyGroup>
1919

2020
<!-- Default settings that explicitly differ from the Sdk.props defaults -->
2121
<PropertyGroup>
2222
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
23+
<BaseIntermediateOutputPath>$(BaseArtifactsPath)obj/$(BaseArtifactsPathSuffix)/</BaseIntermediateOutputPath>
2324
<DebugType>portable</DebugType>
2425
<DebugType Condition="'$(codecov)' != ''">full</DebugType>
2526
<NullableContextOptions>disable</NullableContextOptions>
@@ -58,6 +59,7 @@
5859
<!-- Default settings that explicitly differ from the Sdk.targets defaults-->
5960
<PropertyGroup>
6061
<Authors>Six Labors and contributors</Authors>
62+
<BaseOutputPath>$(BaseArtifactsPath)bin/$(BaseArtifactsPathSuffix)/</BaseOutputPath>
6163
<Company>Six Labors</Company>
6264
<PackageOutputPath>$(BaseArtifactsPath)pkg/$(BaseArtifactsPathSuffix)/$(Configuration)/</PackageOutputPath>
6365
<Product>SixLabors.ImageSharp.Web</Product>

Directory.Build.targets

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
<PackageReference Update="Azure.Storage.Blobs" Version="12.4.0" />
2626
<PackageReference Update="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All"/>
2727
<PackageReference Update="MinVer" PrivateAssets="All" Version="2.3.0" />
28-
<PackageReference Update="SixLabors.ImageSharp" Version="1.0.0" />
28+
<PackageReference Update="SixLabors.ImageSharp" Version="1.0.1" />
2929
</ItemGroup>
3030

3131
<ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp3.1'">

src/Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
<!-- DynamicProxyGenAssembly2 is needed so Moq can use our internals -->
4444
<InternalsVisibleTo Include="DynamicProxyGenAssembly2" PublicKey="0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7" />
4545
<InternalsVisibleTo Include="SixLabors.ImageSharp.Web.Tests" PublicKey="$(SixLaborsPublicKey)" />
46-
<InternalsVisibleTo Include="SixLabors.ImageSharp.Web.Benchmarks" PublicKey="$(SixLaborsPublicKey)" />
46+
<InternalsVisibleTo Include="ImageSharp.Web.Benchmarks" PublicKey="$(SixLaborsPublicKey)" />
4747
</ItemGroup>
4848

4949
</Project>

src/ImageSharp.Web.Providers.Azure/Resolvers/AzureBlobStorageImageResolver.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ public async Task<ImageMetadata> GetMetaDataAsync()
3333
/// <inheritdoc/>
3434
public async Task<Stream> OpenReadAsync()
3535
{
36+
// Copy to a MemoryStream first because RetriableStreamImpl
37+
// doesn't support Position.
3638
Stream blobStream = (await this.blob.DownloadAsync()).Value.Content;
3739
var memoryStream = new ChunkedMemoryStream();
3840

src/ImageSharp.Web/Caching/CacheHash.cs

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the Apache License, Version 2.0.
33

44
using System;
5+
using System.Runtime.CompilerServices;
56
using System.Security.Cryptography;
67
using System.Text;
78
using Microsoft.Extensions.Options;
@@ -33,17 +34,34 @@ public CacheHash(IOptions<ImageSharpMiddlewareOptions> options, MemoryAllocator
3334
}
3435

3536
/// <inheritdoc/>
37+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
3638
public string Create(string value, uint length)
3739
{
38-
int len = (int)length;
3940
int byteCount = Encoding.ASCII.GetByteCount(value);
40-
using (var hashAlgorithm = SHA256.Create())
41-
using (IManagedByteBuffer buffer = this.memoryAllocator.AllocateManagedByteBuffer(byteCount))
41+
42+
// Allocating a buffer from the pool is ~27% slower than stackalloc so use
43+
// that for short strings
44+
if (byteCount < 257)
4245
{
43-
Encoding.ASCII.GetBytes(value, 0, byteCount, buffer.Array, 0);
44-
byte[] hash = hashAlgorithm.ComputeHash(buffer.Array, 0, byteCount);
45-
return $"{HexEncoder.Encode(new Span<byte>(hash).Slice(0, len / 2))}";
46+
return HashValue(value, length, stackalloc byte[byteCount]);
4647
}
48+
49+
using IManagedByteBuffer buffer = this.memoryAllocator.AllocateManagedByteBuffer(byteCount);
50+
return HashValue(value, length, buffer.Memory.Span);
51+
}
52+
53+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
54+
private static string HashValue(ReadOnlySpan<char> value, uint length, Span<byte> bufferSpan)
55+
{
56+
using var hashAlgorithm = SHA256.Create();
57+
Encoding.ASCII.GetBytes(value, bufferSpan);
58+
59+
// Hashed output maxes out at 32 bytes @ 256bit/8 so we're safe to use stackalloc.
60+
Span<byte> hash = stackalloc byte[32];
61+
hashAlgorithm.TryComputeHash(bufferSpan, hash, out int _);
62+
63+
// length maxes out at 64 since we throw if options is greater.
64+
return HexEncoder.Encode(hash.Slice(0, (int)(length / 2)));
4765
}
4866
}
4967
}

src/ImageSharp.Web/Caching/HexEncoder.cs

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) Six Labors.
1+
// Copyright (c) Six Labors.
22
// Licensed under the Apache License, Version 2.0.
33

44
using System;
@@ -23,29 +23,38 @@ internal static class HexEncoder
2323
private static readonly char[] HexLutLo = Enumerable.Range(0, 256).Select(x => HexLutBase[x % 0x10]).ToArray();
2424

2525
/// <summary>
26-
/// Converts a <see cref="T:Span{byte}"/> to a hexidecimal formatted <see cref="string"/> padded to 2 digits.
26+
/// Converts a <see cref="ReadOnlySpan{Byte}"/> to a hexidecimal formatted <see cref="string"/> padded to 2 digits.
2727
/// </summary>
2828
/// <param name="bytes">The bytes.</param>
2929
/// <returns>The <see cref="string"/>.</returns>
3030
[MethodImpl(MethodImplOptions.AggressiveInlining)]
31-
public static string Encode(Span<byte> bytes)
31+
public static unsafe string Encode(ReadOnlySpan<byte> bytes)
32+
{
33+
fixed (byte* bytesPtr = bytes)
34+
{
35+
return string.Create(bytes.Length * 2, (Ptr: (IntPtr)bytesPtr, bytes.Length), (chars, args) =>
36+
{
37+
var ros = new ReadOnlySpan<byte>((byte*)args.Ptr, args.Length);
38+
EncodeToUtf16(ros, chars);
39+
});
40+
}
41+
}
42+
43+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
44+
private static void EncodeToUtf16(ReadOnlySpan<byte> bytes, Span<char> chars)
3245
{
33-
int length = bytes.Length;
34-
char[] chars = new char[length * 2];
35-
ref char charRef = ref MemoryMarshal.GetReference<char>(chars);
3646
ref byte bytesRef = ref MemoryMarshal.GetReference(bytes);
47+
ref char charRef = ref MemoryMarshal.GetReference(chars);
3748
ref char hiRef = ref MemoryMarshal.GetReference<char>(HexLutHi);
3849
ref char lowRef = ref MemoryMarshal.GetReference<char>(HexLutLo);
3950

4051
int index = 0;
41-
for (int i = 0; i < length; i++)
52+
for (int i = 0; i < bytes.Length; i++)
4253
{
4354
byte byteIndex = Unsafe.Add(ref bytesRef, i);
4455
Unsafe.Add(ref charRef, index++) = Unsafe.Add(ref hiRef, byteIndex);
4556
Unsafe.Add(ref charRef, index++) = Unsafe.Add(ref lowRef, byteIndex);
4657
}
47-
48-
return new string(chars, 0, chars.Length);
4958
}
5059
}
51-
}
60+
}

src/ImageSharp.Web/Caching/PhysicalFileSystemCache.cs

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
// Copyright (c) Six Labors.
22
// Licensed under the Apache License, Version 2.0.
33

4+
using System;
45
using System.IO;
6+
using System.Runtime.CompilerServices;
7+
using System.Runtime.InteropServices;
58
using System.Threading.Tasks;
69
using Microsoft.AspNetCore.Hosting;
710
using Microsoft.Extensions.FileProviders;
@@ -21,6 +24,11 @@ public class PhysicalFileSystemCache : IImageCache
2124
/// </summary>
2225
private readonly string cacheRootPath;
2326

27+
/// <summary>
28+
/// The length of the filename to use (minus the extension) when storing images in the image cache.
29+
/// </summary>
30+
private readonly int cachedNameLength;
31+
2432
/// <summary>
2533
/// The file provider abstraction.
2634
/// </summary>
@@ -62,6 +70,7 @@ public PhysicalFileSystemCache(
6270
Guard.NotNull(options, nameof(options));
6371
Guard.NotNullOrWhiteSpace(environment.WebRootPath, nameof(environment.WebRootPath));
6472

73+
// Allow configuration of the cache without having to register everything.
6574
this.cacheOptions = cacheOptions != null ? cacheOptions.Value : new PhysicalFileSystemCacheOptions();
6675
this.cacheRootPath = Path.Combine(environment.WebRootPath, this.cacheOptions.CacheFolder);
6776
if (!Directory.Exists(this.cacheRootPath))
@@ -71,13 +80,14 @@ public PhysicalFileSystemCache(
7180

7281
this.fileProvider = new PhysicalFileProvider(this.cacheRootPath);
7382
this.options = options.Value;
83+
this.cachedNameLength = (int)this.options.CachedNameLength;
7484
this.formatUtilies = formatUtilities;
7585
}
7686

7787
/// <inheritdoc/>
7888
public async Task<IImageCacheResolver> GetAsync(string key)
7989
{
80-
string path = this.ToFilePath(key);
90+
string path = ToFilePath(key, this.cachedNameLength);
8191

8292
IFileInfo metaFileInfo = this.fileProvider.GetFileInfo(this.ToMetaDataFilePath(path));
8393
if (!metaFileInfo.Exists)
@@ -105,7 +115,7 @@ public async Task<IImageCacheResolver> GetAsync(string key)
105115
/// <inheritdoc/>
106116
public async Task SetAsync(string key, Stream stream, ImageCacheMetadata metadata)
107117
{
108-
string path = Path.Combine(this.cacheRootPath, this.ToFilePath(key));
118+
string path = Path.Combine(this.cacheRootPath, ToFilePath(key, this.cachedNameLength));
109119
string imagePath = this.ToImageFilePath(path, metadata);
110120
string metaPath = this.ToMetaDataFilePath(path);
111121
string directory = Path.GetDirectoryName(path);
@@ -146,8 +156,35 @@ private string ToImageFilePath(string path, in ImageCacheMetadata metaData)
146156
/// Converts the key into a nested file path.
147157
/// </summary>
148158
/// <param name="key">The cache key.</param>
159+
/// <param name="cachedNameLength">The length of the cached file name minus the extension.</param>
149160
/// <returns>The <see cref="string"/>.</returns>
150-
private string ToFilePath(string key) // TODO: Avoid the allocation here.
151-
=> $"{string.Join("/", key.Substring(0, (int)this.options.CachedNameLength).ToCharArray())}/{key}";
161+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
162+
internal static unsafe string ToFilePath(string key, int cachedNameLength)
163+
{
164+
// Each key substring char + separator + key
165+
int length = (cachedNameLength * 2) + key.Length;
166+
fixed (char* keyPtr = key)
167+
{
168+
return string.Create(length, (Ptr: (IntPtr)keyPtr, key.Length), (chars, args) =>
169+
{
170+
const char separator = '/';
171+
var keySpan = new ReadOnlySpan<char>((char*)args.Ptr, args.Length);
172+
ref char keyRef = ref MemoryMarshal.GetReference(keySpan);
173+
ref char charRef = ref MemoryMarshal.GetReference(chars);
174+
175+
int index = 0;
176+
for (int i = 0; i < cachedNameLength; i++)
177+
{
178+
Unsafe.Add(ref charRef, index++) = Unsafe.Add(ref keyRef, i);
179+
Unsafe.Add(ref charRef, index++) = separator;
180+
}
181+
182+
for (int i = 0; i < keySpan.Length; i++)
183+
{
184+
Unsafe.Add(ref charRef, index++) = Unsafe.Add(ref keyRef, i);
185+
}
186+
});
187+
}
188+
}
152189
}
153190
}

src/ImageSharp.Web/Middleware/ImageSharpMiddleware.cs

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ private async Task ProcessRequestAsync(HttpContext context, bool processRequest,
230230
{
231231
// Has the cached image expired or has the source image been updated?
232232
if (cachedImageMetadata.SourceLastWriteTimeUtc == sourceImageMetadata.LastWriteTimeUtc
233-
&& cachedImageMetadata.CacheLastWriteTimeUtc > DateTimeOffset.Now.AddDays(-this.options.MaxCacheDays))
233+
&& cachedImageMetadata.CacheLastWriteTimeUtc > DateTimeOffset.UtcNow.AddDays(-this.options.MaxCacheDays))
234234
{
235235
// We're pulling the image from the cache.
236236
using (Stream cachedBuffer = await cachedImageResolver.OpenReadAsync())
@@ -265,7 +265,7 @@ private async Task ProcessRequestAsync(HttpContext context, bool processRequest,
265265
// No commands? We simply copy the stream across.
266266
if (commands.Count == 0)
267267
{
268-
format = Image.DetectFormat(this.options.Configuration, inStream);
268+
format = await Image.DetectFormatAsync(this.options.Configuration, inStream);
269269
await inStream.CopyToAsync(outStream);
270270
}
271271
else
@@ -316,11 +316,33 @@ private async Task ProcessRequestAsync(HttpContext context, bool processRequest,
316316
}
317317
finally
318318
{
319-
outStream?.Dispose();
319+
await this.StreamDisposeAsync(outStream);
320320
}
321321
}
322322
}
323323

324+
private ValueTask StreamDisposeAsync(Stream stream)
325+
{
326+
if (stream is null)
327+
{
328+
return default;
329+
}
330+
331+
#if NETCOREAPP2_1
332+
try
333+
{
334+
stream.Dispose();
335+
return default;
336+
}
337+
catch (Exception ex)
338+
{
339+
return new ValueTask(Task.FromException(ex));
340+
}
341+
#else
342+
return stream.DisposeAsync();
343+
#endif
344+
}
345+
324346
private async Task SendResponseAsync(ImageContext imageContext, string key, Stream stream, ImageCacheMetadata metadata)
325347
{
326348
imageContext.ComprehendRequestHeaders(metadata.CacheLastWriteTimeUtc, stream.Length);

tests/ImageSharp.Web.Benchmarks/CacheHashBaseline.cs renamed to tests/ImageSharp.Web.Benchmarks/Caching/CacheHashBaseline.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
using System.Text;
77
using SixLabors.ImageSharp.Web.Caching;
88

9-
namespace SixLabors.ImageSharp.Web.Benchmarks
9+
namespace SixLabors.ImageSharp.Web.Benchmarks.Caching
1010
{
1111
/// <summary>
1212
/// A baseline naive SHA256 hashing implementation
@@ -16,11 +16,11 @@ public class CacheHashBaseline : ICacheHash
1616
/// <inheritdoc/>
1717
public string Create(string value, uint length)
1818
{
19-
if (length.CompareTo(2) < 0 || length.CompareTo(64) > 0)
20-
{
21-
throw new ArgumentOutOfRangeException(nameof(length), $"Value must be greater than or equal to {2} and less than or equal to {64}.");
22-
}
23-
19+
// Don't use in benchmark.
20+
// if (length.CompareTo(2) < 0 || length.CompareTo(64) > 0)
21+
// {
22+
// throw new ArgumentOutOfRangeException(nameof(length), $"Value must be greater than or equal to {2} and less than or equal to {64}.");
23+
// }
2424
using (var hashAlgorithm = SHA256.Create())
2525
{
2626
// Concatenate the hash bytes into one long string.

tests/ImageSharp.Web.Benchmarks/Caching/CacheHashBenchmarks.cs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,28 @@ namespace SixLabors.ImageSharp.Web.Benchmarks.Caching
1212
public class CacheHashBenchmarks
1313
{
1414
private const string URL = "http://testwebsite.com/image-12345.jpeg?width=400";
15-
private static readonly IOptions<ImageSharpMiddlewareOptions> Options = Microsoft.Extensions.Options.Options.Create(new ImageSharpMiddlewareOptions());
16-
private static readonly CacheHash Sha256Hasher = new CacheHash(Options, Options.Value.Configuration.MemoryAllocator);
15+
private static readonly IOptions<ImageSharpMiddlewareOptions> MWOptions = Options.Create(new ImageSharpMiddlewareOptions());
16+
private static readonly CacheHash Sha256Hasher = new CacheHash(MWOptions, MWOptions.Value.Configuration.MemoryAllocator);
1717
private static readonly CacheHashBaseline NaiveSha256Hasher = new CacheHashBaseline();
1818

1919
[Benchmark(Baseline = true, Description = "Baseline Sha256Hasher")]
2020
public string HashUsingBaselineHash() => NaiveSha256Hasher.Create(URL, 12);
2121

2222
[Benchmark(Description = "Sha256Hasher")]
2323
public string HashUsingSha256() => Sha256Hasher.Create(URL, 12);
24+
25+
/*
26+
BenchmarkDotNet=v0.12.0, OS=Windows 10.0.18363
27+
Intel Core i7-8650U CPU 1.90GHz (Kaby Lake R), 1 CPU, 8 logical and 4 physical cores
28+
.NET Core SDK=3.1.401
29+
[Host] : .NET Core 3.1.7 (CoreCLR 4.700.20.36602, CoreFX 4.700.20.37001), X64 RyuJIT
30+
DefaultJob : .NET Core 3.1.7 (CoreCLR 4.700.20.36602, CoreFX 4.700.20.37001), X64 RyuJIT
31+
32+
33+
| Method | Mean | Error | StdDev | Ratio | Gen 0 | Gen 1 | Gen 2 | Allocated |
34+
|------------------------ |-----------:|--------:|--------:|------:|-------:|------:|------:|----------:|
35+
| 'Baseline Sha256Hasher' | 1,065.7 ns | 8.95 ns | 8.37 ns | 1.00 | 0.1564 | - | - | 656 B |
36+
| Sha256Hasher | 786.7 ns | 9.77 ns | 9.14 ns | 0.74 | 0.0420 | - | - | 176 B |
37+
*/
2438
}
2539
}

0 commit comments

Comments
 (0)