-
Notifications
You must be signed in to change notification settings - Fork 84
Add Brotli compression provider #342
Changes from all commits
f1005aa
3023146
9cd9a79
d7fada4
812bdab
8ad00c3
852d22c
c275fb6
298a2cd
c1689b7
91068a5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
|
||
[assembly: BenchmarkDotNet.Attributes.AspNetCoreBenchmark] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<OutputType>Exe</OutputType> | ||
<TargetFramework>netcoreapp2.1</TargetFramework> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.ResponseCompression\Microsoft.AspNetCore.ResponseCompression.csproj" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="BenchmarkDotNet" Version="$(BenchmarkDotNetPackageVersion)" /> | ||
<PackageReference Include="Microsoft.AspNetCore.Http" Version="$(MicrosoftAspNetCoreHttpPackageVersion)" /> | ||
<PackageReference Include="Microsoft.AspNetCore.BenchmarkRunner.Sources" Version="$(MicrosoftAspNetCoreBenchmarkRunnerSourcesPackageVersion)" /> | ||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="$(MicrosoftExtensionsDependencyInjectionPackageVersion)" /> | ||
</ItemGroup> | ||
|
||
</Project> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
|
||
using System.Collections.Generic; | ||
using BenchmarkDotNet.Attributes; | ||
using Microsoft.AspNetCore.Builder; | ||
using Microsoft.AspNetCore.Http; | ||
using Microsoft.Extensions.DependencyInjection; | ||
using Microsoft.Extensions.Options; | ||
using Microsoft.Net.Http.Headers; | ||
|
||
namespace Microsoft.AspNetCore.ResponseCompression.Benchmarks | ||
{ | ||
public class ResponseCompressionProviderBenchmark | ||
{ | ||
[GlobalSetup] | ||
public void GlobalSetup() | ||
{ | ||
var services = new ServiceCollection() | ||
.AddOptions() | ||
.AddResponseCompression() | ||
.BuildServiceProvider(); | ||
|
||
var options = new ResponseCompressionOptions(); | ||
|
||
Provider = new ResponseCompressionProvider(services, Options.Create(options)); | ||
} | ||
|
||
[ParamsSource(nameof(EncodingStrings))] | ||
public string AcceptEncoding { get; set; } | ||
|
||
public static IEnumerable<string> EncodingStrings() | ||
{ | ||
return new[] | ||
{ | ||
"gzip;q=0.8, compress;q=0.6, br;q=0.4", | ||
"gzip, compress, br", | ||
"br, compress, gzip", | ||
"gzip, compress", | ||
"identity", | ||
"*" | ||
}; | ||
} | ||
|
||
public ResponseCompressionProvider Provider { get; set; } | ||
|
||
[Benchmark] | ||
public ICompressionProvider GetCompressionProvider() | ||
{ | ||
var context = new DefaultHttpContext(); | ||
|
||
context.Request.Headers[HeaderNames.AcceptEncoding] = AcceptEncoding; | ||
|
||
return Provider.GetCompressionProvider(context); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
|
||
using System; | ||
using System.IO; | ||
using System.IO.Compression; | ||
using Microsoft.Extensions.Options; | ||
|
||
namespace Microsoft.AspNetCore.ResponseCompression | ||
{ | ||
/// <summary> | ||
/// Brotli compression provider. | ||
/// </summary> | ||
public class BrotliCompressionProvider : ICompressionProvider | ||
{ | ||
/// <summary> | ||
/// Creates a new instance of <see cref="BrotliCompressionProvider"/> with options. | ||
/// </summary> | ||
/// <param name="options"></param> | ||
public BrotliCompressionProvider(IOptions<BrotliCompressionProviderOptions> options) | ||
{ | ||
if (options == null) | ||
{ | ||
throw new ArgumentNullException(nameof(options)); | ||
} | ||
|
||
Options = options.Value; | ||
} | ||
|
||
private BrotliCompressionProviderOptions Options { get; } | ||
|
||
/// <inheritdoc /> | ||
public string EncodingName => "br"; | ||
|
||
/// <inheritdoc /> | ||
public bool SupportsFlush => true; | ||
|
||
/// <inheritdoc /> | ||
public Stream CreateStream(Stream outputStream) | ||
{ | ||
#if NETCOREAPP2_1 | ||
return new BrotliStream(outputStream, Options.Level, leaveOpen: true); | ||
#elif NET461 || NETSTANDARD2_0 | ||
// Brotli is only supported in .NET Core 2.1+ | ||
throw new PlatformNotSupportedException(); | ||
#else | ||
#error Target frameworks need to be updated. | ||
#endif | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
|
||
using System.IO.Compression; | ||
using Microsoft.Extensions.Options; | ||
|
||
namespace Microsoft.AspNetCore.ResponseCompression | ||
{ | ||
/// <summary> | ||
/// Options for the <see cref="BrotliCompressionProvider"/> | ||
/// </summary> | ||
public class BrotliCompressionProviderOptions : IOptions<BrotliCompressionProviderOptions> | ||
{ | ||
/// <summary> | ||
/// What level of compression to use for the stream. The default is <see cref="CompressionLevel.Fastest"/>. | ||
/// </summary> | ||
public CompressionLevel Level { get; set; } = CompressionLevel.Fastest; | ||
|
||
/// <inheritdoc /> | ||
BrotliCompressionProviderOptions IOptions<BrotliCompressionProviderOptions>.Value => this; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -38,7 +38,17 @@ public ResponseCompressionProvider(IServiceProvider services, IOptions<ResponseC | |
if (_providers.Length == 0) | ||
{ | ||
// Use the factory so it can resolve IOptions<GzipCompressionProviderOptions> from DI. | ||
_providers = new ICompressionProvider[] { new CompressionProviderFactory(typeof(GzipCompressionProvider)) }; | ||
_providers = new ICompressionProvider[] | ||
{ | ||
#if NETCOREAPP2_1 | ||
new CompressionProviderFactory(typeof(BrotliCompressionProvider)), | ||
#elif NET461 || NETSTANDARD2_0 | ||
// Brotli is only supported in .NET Core 2.1+ | ||
#else | ||
#error Target frameworks need to be updated. | ||
#endif | ||
new CompressionProviderFactory(typeof(GzipCompressionProvider)), | ||
}; | ||
} | ||
for (var i = 0; i < _providers.Length; i++) | ||
{ | ||
|
@@ -62,42 +72,76 @@ public ResponseCompressionProvider(IServiceProvider services, IOptions<ResponseC | |
/// <inheritdoc /> | ||
public virtual ICompressionProvider GetCompressionProvider(HttpContext context) | ||
{ | ||
IList<StringWithQualityHeaderValue> unsorted; | ||
|
||
// e.g. Accept-Encoding: gzip, deflate, sdch | ||
var accept = context.Request.Headers[HeaderNames.AcceptEncoding]; | ||
if (!StringValues.IsNullOrEmpty(accept) | ||
&& StringWithQualityHeaderValue.TryParseList(accept, out unsorted) | ||
&& unsorted != null && unsorted.Count > 0) | ||
|
||
if (StringValues.IsNullOrEmpty(accept)) | ||
{ | ||
// TODO PERF: clients don't usually include quality values so this sort will not have any effect. Fast-path? | ||
var sorted = unsorted | ||
.Where(s => s.Quality.GetValueOrDefault(1) > 0) | ||
.OrderByDescending(s => s.Quality.GetValueOrDefault(1)); | ||
return null; | ||
} | ||
|
||
foreach (var encoding in sorted) | ||
if (StringWithQualityHeaderValue.TryParseList(accept, out var encodings)) | ||
{ | ||
if (encodings.Count == 0) | ||
{ | ||
// There will rarely be more than three providers, and there's only one by default | ||
foreach (var provider in _providers) | ||
return null; | ||
} | ||
|
||
var candidates = new HashSet<ProviderCandidate>(); | ||
|
||
foreach (var encoding in encodings) | ||
{ | ||
var encodingName = encoding.Value; | ||
var quality = encoding.Quality.GetValueOrDefault(1); | ||
|
||
if (quality < double.Epsilon) | ||
{ | ||
if (StringSegment.Equals(provider.EncodingName, encoding.Value, StringComparison.OrdinalIgnoreCase)) | ||
continue; | ||
} | ||
|
||
for (int i = 0; i < _providers.Length; i++) | ||
{ | ||
var provider = _providers[i]; | ||
|
||
if (StringSegment.Equals(provider.EncodingName, encodingName, StringComparison.OrdinalIgnoreCase)) | ||
{ | ||
return provider; | ||
candidates.Add(new ProviderCandidate(provider.EncodingName, quality, i, provider)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is starting to become a lot of work on every response, and you'd get the same results most of the time, no? e.g. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think it's much more work, it's just doing it the other way around by matching first and ordering after, but we can certainly try to be smarter to avoid some work 👍🏻 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Might be worth checking/considering making _providers a Dictionary? Besides that, I don't see much you can do. |
||
} | ||
} | ||
|
||
// Uncommon but valid options | ||
if (StringSegment.Equals("*", encoding.Value, StringComparison.Ordinal)) | ||
if (StringSegment.Equals("*", encodingName, StringComparison.Ordinal)) | ||
{ | ||
// Any | ||
return _providers[0]; | ||
for (int i = 0; i < _providers.Length; i++) | ||
{ | ||
var provider = _providers[i]; | ||
|
||
// Any provider is a candidate. | ||
candidates.Add(new ProviderCandidate(provider.EncodingName, quality, i, provider)); | ||
} | ||
|
||
break; | ||
} | ||
if (StringSegment.Equals("identity", encoding.Value, StringComparison.OrdinalIgnoreCase)) | ||
|
||
if (StringSegment.Equals("identity", encodingName, StringComparison.OrdinalIgnoreCase)) | ||
{ | ||
// No compression | ||
return null; | ||
// We add 'identity' to the list of "candidates" with a very low priority and no provider. | ||
// This will allow it to be ordered based on its quality (and priority) later in the method. | ||
candidates.Add(new ProviderCandidate(encodingName.Value, quality, priority: int.MaxValue, provider: null)); | ||
} | ||
} | ||
|
||
if (candidates.Count <= 1) | ||
{ | ||
return candidates.ElementAtOrDefault(0).Provider; | ||
} | ||
|
||
var accepted = candidates | ||
.OrderByDescending(x => x.Quality) | ||
.ThenBy(x => x.Priority) | ||
.First(); | ||
|
||
return accepted.Provider; | ||
} | ||
|
||
return null; | ||
|
@@ -139,5 +183,39 @@ public bool CheckRequestAcceptsCompression(HttpContext context) | |
} | ||
return !string.IsNullOrEmpty(context.Request.Headers[HeaderNames.AcceptEncoding]); | ||
} | ||
|
||
private readonly struct ProviderCandidate : IEquatable<ProviderCandidate> | ||
{ | ||
public ProviderCandidate(string encodingName, double quality, int priority, ICompressionProvider provider) | ||
{ | ||
EncodingName = encodingName; | ||
Quality = quality; | ||
Priority = priority; | ||
Provider = provider; | ||
} | ||
|
||
public string EncodingName { get; } | ||
|
||
public double Quality { get; } | ||
|
||
public int Priority { get; } | ||
|
||
public ICompressionProvider Provider { get; } | ||
|
||
public bool Equals(ProviderCandidate other) | ||
{ | ||
return string.Equals(EncodingName, other.EncodingName, StringComparison.OrdinalIgnoreCase); | ||
} | ||
|
||
public override bool Equals(object obj) | ||
{ | ||
return obj is ProviderCandidate candidate && Equals(candidate); | ||
} | ||
|
||
public override int GetHashCode() | ||
{ | ||
return StringComparer.OrdinalIgnoreCase.GetHashCode(EncodingName); | ||
} | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Relating to options, can you add a comment to ResponseCompressionOptions saying the Providers's priority is based on their ordering in the list?