Skip to content

Commit fd3fe12

Browse files
authored
perf: improve allocations in OwinEnvironment (#58917)
1 parent 6567ae5 commit fd3fe12

12 files changed

+609
-115
lines changed

AspNetCore.slnx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,7 @@
334334
<Project Path="src/Http/Metadata/src/Microsoft.AspNetCore.Metadata.csproj" />
335335
</Folder>
336336
<Folder Name="/src/Http/Owin/" Id="c6dae135-6509-c765-458b-3693a7b28e8c">
337+
<Project Path="src/Http/Owin/benchmarks/Microsoft.AspNetCore.Owin.Microbenchmarks/Microsoft.AspNetCore.Owin.Microbenchmarks.csproj" />
337338
<Project Path="src/Http/Owin/src/Microsoft.AspNetCore.Owin.csproj" />
338339
<Project Path="src/Http/Owin/test/Microsoft.AspNetCore.Owin.Tests.csproj" />
339340
</Folder>
@@ -357,6 +358,7 @@
357358
<Folder Name="/src/Http/samples/" Id="0e46e96b-2613-2f61-4250-fc4a97d94f4c">
358359
<Project Path="src/Http/samples/MinimalSample/MinimalSample.csproj" />
359360
<Project Path="src/Http/samples/MinimalSampleFSharp/MinimalSampleFSharp.fsproj" />
361+
<Project Path="src/Http/samples/MinimalSampleOwin/MinimalSampleOwin.csproj" />
360362
<Project Path="src/Http/samples/SampleApp/HttpAbstractions.SampleApp.csproj" />
361363
</Folder>
362364
<Folder Name="/src/Http/WebUtilities/" Id="1aadc95f-e3b5-447b-ddcf-108db21eb9ed">

src/Http/HttpAbstractions.slnf

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,22 +32,24 @@
3232
"src\\Http\\Http\\src\\Microsoft.AspNetCore.Http.csproj",
3333
"src\\Http\\Http\\test\\Microsoft.AspNetCore.Http.Tests.csproj",
3434
"src\\Http\\Metadata\\src\\Microsoft.AspNetCore.Metadata.csproj",
35+
"src\\Http\\Owin\\benchmarks\\Microsoft.AspNetCore.Owin.Microbenchmarks\\Microsoft.AspNetCore.Owin.Microbenchmarks.csproj",
3536
"src\\Http\\Owin\\src\\Microsoft.AspNetCore.Owin.csproj",
3637
"src\\Http\\Owin\\test\\Microsoft.AspNetCore.Owin.Tests.csproj",
3738
"src\\Http\\Routing.Abstractions\\src\\Microsoft.AspNetCore.Routing.Abstractions.csproj",
3839
"src\\Http\\Routing.Abstractions\\test\\Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests.csproj",
3940
"src\\Http\\Routing\\perf\\Microbenchmarks\\Microsoft.AspNetCore.Routing.Microbenchmarks.csproj",
4041
"src\\Http\\Routing\\src\\Microsoft.AspNetCore.Routing.csproj",
4142
"src\\Http\\Routing\\test\\FunctionalTests\\Microsoft.AspNetCore.Routing.FunctionalTests.csproj",
43+
"src\\Http\\Routing\\test\\UnitTests\\Microsoft.AspNetCore.Routing.Tests.csproj",
4244
"src\\Http\\Routing\\test\\testassets\\Benchmarks\\Benchmarks.csproj",
4345
"src\\Http\\Routing\\test\\testassets\\RoutingSandbox\\RoutingSandbox.csproj",
4446
"src\\Http\\Routing\\test\\testassets\\RoutingWebSite\\RoutingWebSite.csproj",
45-
"src\\Http\\Routing\\test\\UnitTests\\Microsoft.AspNetCore.Routing.Tests.csproj",
46-
"src\\Http\\samples\\MinimalSample\\MinimalSample.csproj",
47-
"src\\Http\\samples\\SampleApp\\HttpAbstractions.SampleApp.csproj",
4847
"src\\Http\\WebUtilities\\perf\\Microbenchmarks\\Microsoft.AspNetCore.WebUtilities.Microbenchmarks.csproj",
4948
"src\\Http\\WebUtilities\\src\\Microsoft.AspNetCore.WebUtilities.csproj",
5049
"src\\Http\\WebUtilities\\test\\Microsoft.AspNetCore.WebUtilities.Tests.csproj",
50+
"src\\Http\\samples\\MinimalSampleOwin\\MinimalSampleOwin.csproj",
51+
"src\\Http\\samples\\MinimalSample\\MinimalSample.csproj",
52+
"src\\Http\\samples\\SampleApp\\HttpAbstractions.SampleApp.csproj",
5153
"src\\Middleware\\CORS\\src\\Microsoft.AspNetCore.Cors.csproj",
5254
"src\\Middleware\\Diagnostics.Abstractions\\src\\Microsoft.AspNetCore.Diagnostics.Abstractions.csproj",
5355
"src\\Middleware\\Diagnostics\\src\\Microsoft.AspNetCore.Diagnostics.csproj",
@@ -72,4 +74,4 @@
7274
"src\\WebEncoders\\src\\Microsoft.Extensions.WebEncoders.csproj"
7375
]
7476
}
75-
}
77+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
[assembly: BenchmarkDotNet.Attributes.AspNetCoreBenchmark]
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using BenchmarkDotNet.Attributes;
5+
using Microsoft.AspNetCore.Builder;
6+
using Microsoft.AspNetCore.Http;
7+
using Microsoft.Extensions.DependencyInjection;
8+
9+
namespace Microsoft.AspNetCore.Owin.Microbenchmarks.Benchmarks;
10+
11+
[MemoryDiagnoser]
12+
public class OwinEnvironmentBenchmark
13+
{
14+
const int RequestCount = 10000;
15+
16+
RequestDelegate _noOperationRequestDelegate;
17+
RequestDelegate _accessPortsRequestDelegate;
18+
RequestDelegate _accessHeadersRequestDelegate;
19+
20+
HttpContext _defaultHttpContext;
21+
HttpContext _httpContextWithHeaders;
22+
23+
[GlobalSetup]
24+
public void GlobalSetup()
25+
{
26+
_noOperationRequestDelegate = BuildRequestDelegate();
27+
_accessPortsRequestDelegate = BuildRequestDelegate(beforeOwinInvokeAction: env =>
28+
{
29+
_ = env.TryGetValue("server.LocalPort", out var localPort);
30+
_ = env.TryGetValue("server.RemotePort", out var remotePort);
31+
});
32+
_accessHeadersRequestDelegate = BuildRequestDelegate(
33+
beforeOwinInvokeAction: env =>
34+
{
35+
_ = env.TryGetValue("owin.RequestHeaders", out var requestHeaders);
36+
},
37+
afterOwinInvokeAction: env =>
38+
{
39+
_ = env.TryGetValue("owin.ResponseHeaders", out var responseHeaders);
40+
}
41+
);
42+
43+
_defaultHttpContext = new DefaultHttpContext();
44+
45+
_httpContextWithHeaders = new DefaultHttpContext();
46+
_httpContextWithHeaders.Request.Headers["CustomRequestHeader1"] = "CustomRequestValue";
47+
_httpContextWithHeaders.Request.Headers["CustomRequestHeader2"] = "CustomRequestValue";
48+
_httpContextWithHeaders.Response.Headers["CustomResponseHeader1"] = "CustomResponseValue";
49+
_httpContextWithHeaders.Response.Headers["CustomResponseHeader2"] = "CustomResponseValue";
50+
}
51+
52+
[Benchmark]
53+
public async Task OwinRequest_NoOperation()
54+
{
55+
foreach (var i in Enumerable.Range(0, RequestCount))
56+
{
57+
await _noOperationRequestDelegate(_defaultHttpContext);
58+
}
59+
}
60+
61+
[Benchmark]
62+
public async Task OwinRequest_AccessPorts()
63+
{
64+
foreach (var i in Enumerable.Range(0, RequestCount))
65+
{
66+
await _accessPortsRequestDelegate(_defaultHttpContext);
67+
}
68+
}
69+
70+
[Benchmark]
71+
public async Task OwinRequest_AccessHeaders()
72+
{
73+
foreach (var i in Enumerable.Range(0, RequestCount))
74+
{
75+
await _accessHeadersRequestDelegate(_httpContextWithHeaders);
76+
}
77+
}
78+
79+
private static RequestDelegate BuildRequestDelegate(
80+
Action<IDictionary<string, object>> beforeOwinInvokeAction = null,
81+
Action<IDictionary<string, object>> afterOwinInvokeAction = null)
82+
{
83+
var serviceProvider = new ServiceCollection().BuildServiceProvider();
84+
var builder = new ApplicationBuilder(serviceProvider);
85+
86+
return builder.UseOwin(addToPipeline =>
87+
{
88+
addToPipeline(next =>
89+
{
90+
return async env =>
91+
{
92+
if (beforeOwinInvokeAction is not null)
93+
{
94+
beforeOwinInvokeAction(env);
95+
}
96+
97+
await next(env);
98+
99+
if (afterOwinInvokeAction is not null)
100+
{
101+
afterOwinInvokeAction(env);
102+
}
103+
};
104+
});
105+
}).Build();
106+
}
107+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
6+
</PropertyGroup>
7+
8+
<ItemGroup>
9+
<Reference Include="BenchmarkDotNet" />
10+
<Reference Include="Microsoft.AspNetCore.Http" />
11+
<Reference Include="Microsoft.AspNetCore.Owin" />
12+
<Reference Include="Microsoft.Extensions.DependencyInjection" />
13+
14+
<Compile Include="$(SharedSourceRoot)BenchmarkRunner\*.cs" />
15+
</ItemGroup>
16+
17+
</Project>

src/Http/Owin/src/DictionaryStringArrayWrapper.cs

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,16 @@ namespace Microsoft.AspNetCore.Owin;
1010

1111
internal sealed class DictionaryStringArrayWrapper : IDictionary<string, string[]>
1212
{
13+
public readonly IHeaderDictionary Inner;
14+
1315
public DictionaryStringArrayWrapper(IHeaderDictionary inner)
1416
{
1517
Inner = inner;
1618
}
1719

18-
public readonly IHeaderDictionary Inner;
19-
20-
private static KeyValuePair<string, StringValues> Convert(KeyValuePair<string, string[]> item) => new KeyValuePair<string, StringValues>(item.Key, item.Value);
20+
private static KeyValuePair<string, StringValues> Convert(KeyValuePair<string, string[]> item) => new(item.Key, item.Value);
2121

22-
private static KeyValuePair<string, string[]> Convert(KeyValuePair<string, StringValues> item) => new KeyValuePair<string, string[]>(item.Key, item.Value);
22+
private static KeyValuePair<string, string[]> Convert(KeyValuePair<string, StringValues> item) => new(item.Key, item.Value);
2323

2424
private string[] Convert(StringValues item) => item;
2525

@@ -55,9 +55,11 @@ void ICollection<KeyValuePair<string, string[]>>.CopyTo(KeyValuePair<string, str
5555
}
5656
}
5757

58-
IEnumerator IEnumerable.GetEnumerator() => Inner.Select(Convert).GetEnumerator();
58+
public ConvertingEnumerator GetEnumerator() => new ConvertingEnumerator(Inner);
59+
60+
IEnumerator<KeyValuePair<string, string[]>> IEnumerable<KeyValuePair<string, string[]>>.GetEnumerator() => new ConvertingEnumerator(Inner);
5961

60-
IEnumerator<KeyValuePair<string, string[]>> IEnumerable<KeyValuePair<string, string[]>>.GetEnumerator() => Inner.Select(Convert).GetEnumerator();
62+
IEnumerator IEnumerable.GetEnumerator() => new ConvertingEnumerator(Inner);
6163

6264
bool ICollection<KeyValuePair<string, string[]>>.Remove(KeyValuePair<string, string[]> item) => Inner.Remove(Convert(item));
6365

@@ -74,4 +76,40 @@ bool IDictionary<string, string[]>.TryGetValue(string key, out string[] value)
7476
value = default(StringValues);
7577
return false;
7678
}
79+
80+
public struct ConvertingEnumerator : IEnumerator<KeyValuePair<string, string[]>>, IEnumerator
81+
{
82+
private IEnumerator<KeyValuePair<string, StringValues>> _inner;
83+
private KeyValuePair<string, string[]> _current;
84+
85+
internal ConvertingEnumerator(IDictionary<string, StringValues> inner)
86+
{
87+
_inner = inner.GetEnumerator();
88+
_current = default;
89+
}
90+
91+
public void Dispose()
92+
{
93+
_inner?.Dispose();
94+
_inner = null;
95+
}
96+
97+
public bool MoveNext()
98+
{
99+
if (!_inner.MoveNext())
100+
{
101+
_current = default;
102+
return false;
103+
}
104+
105+
_current = Convert(_inner.Current);
106+
return true;
107+
}
108+
109+
public KeyValuePair<string, string[]> Current => _current;
110+
111+
object IEnumerator.Current => Current;
112+
113+
void IEnumerator.Reset() => throw new NotSupportedException();
114+
}
77115
}

src/Http/Owin/src/DictionaryStringValuesWrapper.cs

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,16 @@ namespace Microsoft.AspNetCore.Owin;
1111

1212
internal sealed class DictionaryStringValuesWrapper : IHeaderDictionary
1313
{
14+
public readonly IDictionary<string, string[]> Inner;
15+
1416
public DictionaryStringValuesWrapper(IDictionary<string, string[]> inner)
1517
{
1618
Inner = inner;
1719
}
1820

19-
public readonly IDictionary<string, string[]> Inner;
20-
21-
private static KeyValuePair<string, StringValues> Convert(KeyValuePair<string, string[]> item) => new KeyValuePair<string, StringValues>(item.Key, item.Value);
21+
private static KeyValuePair<string, StringValues> Convert(KeyValuePair<string, string[]> item) => new(item.Key, item.Value);
2222

23-
private static KeyValuePair<string, string[]> Convert(KeyValuePair<string, StringValues> item) => new KeyValuePair<string, string[]>(item.Key, item.Value);
23+
private static KeyValuePair<string, string[]> Convert(KeyValuePair<string, StringValues> item) => new(item.Key, item.Value);
2424

2525
private StringValues Convert(string[] item) => item;
2626

@@ -100,9 +100,11 @@ void ICollection<KeyValuePair<string, StringValues>>.CopyTo(KeyValuePair<string,
100100
}
101101
}
102102

103-
IEnumerator IEnumerable.GetEnumerator() => Inner.Select(Convert).GetEnumerator();
103+
public ConvertingEnumerator GetEnumerator() => new ConvertingEnumerator(Inner);
104+
105+
IEnumerator<KeyValuePair<string, StringValues>> IEnumerable<KeyValuePair<string, StringValues>>.GetEnumerator() => new ConvertingEnumerator(Inner);
104106

105-
IEnumerator<KeyValuePair<string, StringValues>> IEnumerable<KeyValuePair<string, StringValues>>.GetEnumerator() => Inner.Select(Convert).GetEnumerator();
107+
IEnumerator IEnumerable.GetEnumerator() => new ConvertingEnumerator(Inner);
106108

107109
bool ICollection<KeyValuePair<string, StringValues>>.Remove(KeyValuePair<string, StringValues> item) => Inner.Remove(Convert(item));
108110

@@ -119,4 +121,40 @@ bool IDictionary<string, StringValues>.TryGetValue(string key, out StringValues
119121
value = default(StringValues);
120122
return false;
121123
}
124+
125+
public struct ConvertingEnumerator : IEnumerator<KeyValuePair<string, StringValues>>, IEnumerator
126+
{
127+
private IEnumerator<KeyValuePair<string, string[]>> _inner;
128+
private KeyValuePair<string, StringValues> _current;
129+
130+
internal ConvertingEnumerator(IDictionary<string, string[]> inner)
131+
{
132+
_inner = inner.GetEnumerator();
133+
_current = default;
134+
}
135+
136+
public void Dispose()
137+
{
138+
_inner?.Dispose();
139+
_inner = null;
140+
}
141+
142+
public bool MoveNext()
143+
{
144+
if (!_inner.MoveNext())
145+
{
146+
_current = default;
147+
return false;
148+
}
149+
150+
_current = Convert(_inner.Current);
151+
return true;
152+
}
153+
154+
public KeyValuePair<string, StringValues> Current => _current;
155+
156+
object IEnumerator.Current => Current;
157+
158+
void IEnumerator.Reset() => throw new NotSupportedException();
159+
}
122160
}

0 commit comments

Comments
 (0)