Skip to content
This repository was archived by the owner on Nov 20, 2018. It is now read-only.

Commit 4d4b7b7

Browse files
committed
Use pooled StringBuilder to reduce allocations when adding response cookies
- #561 - new `SetCookieHeaderValue.AppendToStringBuilder()` method; avoids per-call `StringBuilder` allocation - `ResponseCookies` uses `ObjectPool<StringBuilder>` that `ResponseCookiesFeature` provides - `ResponseCookiesFeature` creates an `ObjectPoolProvider` instance if none in DI - `IResponseCookiesFeature` exposes `int` properties supporting middleware overrides of the pool policy nit: Add some doc comments
1 parent 6a5a053 commit 4d4b7b7

File tree

7 files changed

+151
-30
lines changed

7 files changed

+151
-30
lines changed

src/Microsoft.AspNetCore.Http.Features/IResponseCookiesFeature.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,33 @@
33

44
namespace Microsoft.AspNetCore.Http.Features
55
{
6+
/// <summary>
7+
/// Feature containing cookies which will be returned with the response.
8+
/// </summary>
69
public interface IResponseCookiesFeature
710
{
11+
/// <summary>
12+
/// Initial capacity of instances used in the implementation of this feature, in <see cref="char"/>s.
13+
/// </summary>
14+
/// <remarks>
15+
/// For example, the initial capacity of <see cref="System.Text.StringBuilder"/> instances obtained
16+
/// from an object pool.
17+
/// </remarks>
18+
int InitialPooledInstanceCapacity { get; set; }
19+
20+
/// <summary>
21+
/// Maximum retained capacity of instances used in the implementation of this feature, in <see cref="char"/>s.
22+
/// Instances larger than this will be deleted rather than preserved in the pool.
23+
/// </summary>
24+
/// <remarks>
25+
/// For example, the maximum retained capacity of <see cref="System.Text.StringBuilder"/> instances obtained
26+
/// from an object pool.
27+
/// </remarks>
28+
int MaximumRetainedPooledInstanceCapacity { get; set; }
29+
30+
/// <summary>
31+
/// Cookies which will be returned with the response.
32+
/// </summary>
833
IResponseCookies Cookies { get; }
934
}
1035
}

src/Microsoft.AspNetCore.Http/Features/ResponseCookiesFeature.cs

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,31 +2,64 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using Microsoft.AspNetCore.Http.Internal;
5+
using Microsoft.Extensions.ObjectPool;
56

67
namespace Microsoft.AspNetCore.Http.Features.Internal
78
{
9+
/// <summary>
10+
/// Default implementation of <see cref="IResponseCookiesFeature"/>.
11+
/// </summary>
812
public class ResponseCookiesFeature : IResponseCookiesFeature
913
{
1014
private FeatureReferences<IHttpResponseFeature> _features;
15+
private FeatureReferences<IServiceProvidersFeature> _services;
1116
private IResponseCookies _cookiesCollection;
1217

18+
/// <summary>
19+
/// Initializes a new <see cref="ResponseCookiesFeature"/> instance.
20+
/// </summary>
21+
/// <param name="features">
22+
/// <see cref="IFeatureCollection"/> containing all defined features, including this
23+
/// <see cref="IResponseCookiesFeature"/> and the <see cref="IHttpResponseFeature"/>.
24+
/// </param>
1325
public ResponseCookiesFeature(IFeatureCollection features)
1426
{
1527
_features = new FeatureReferences<IHttpResponseFeature>(features);
28+
_services = new FeatureReferences<IServiceProvidersFeature>(features);
1629
}
1730

18-
private IHttpResponseFeature HttpResponseFeature =>
19-
_features.Fetch(ref _features.Cache, f => null);
31+
/// <inheritdoc />
32+
/// <value>Default is <c>100</c> <see cref="char"/>s.</value>
33+
public int InitialPooledInstanceCapacity { get; set; } = new StringBuilderPooledObjectPolicy().InitialCapacity;
2034

35+
/// <inheritdoc />
36+
/// <value>Default is <c>4048</c> <see cref="char"/>s.</value>
37+
public int MaximumRetainedPooledInstanceCapacity { get; set; } =
38+
new StringBuilderPooledObjectPolicy().MaximumRetainedCapacity;
39+
40+
private IHttpResponseFeature HttpResponseFeature => _features.Fetch(ref _features.Cache, f => null);
41+
42+
private IServiceProvidersFeature ServiceProvidersFeature => _services.Fetch(ref _services.Cache, f => null);
43+
44+
/// <inheritdoc />
2145
public IResponseCookies Cookies
2246
{
2347
get
2448
{
2549
if (_cookiesCollection == null)
2650
{
2751
var headers = HttpResponseFeature.Headers;
28-
_cookiesCollection = new ResponseCookies(headers);
52+
53+
var serviceProvider = ServiceProvidersFeature.RequestServices;
54+
var provider = (ObjectPoolProvider)serviceProvider.GetService(typeof(ObjectPoolProvider)) ??
55+
new DefaultObjectPoolProvider();
56+
var pool = provider.CreateStringBuilderPool(
57+
InitialPooledInstanceCapacity,
58+
MaximumRetainedPooledInstanceCapacity);
59+
60+
_cookiesCollection = new ResponseCookies(headers, pool);
2961
}
62+
3063
return _cookiesCollection;
3164
}
3265
}

src/Microsoft.AspNetCore.Http/ResponseCookies.cs

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5-
using System.Text.Encodings.Web;
65
using System.Collections.Generic;
6+
using System.Text;
7+
using Microsoft.Extensions.ObjectPool;
78
using Microsoft.Extensions.Primitives;
89
using Microsoft.Net.Http.Headers;
910

@@ -14,18 +15,26 @@ namespace Microsoft.AspNetCore.Http.Internal
1415
/// </summary>
1516
public class ResponseCookies : IResponseCookies
1617
{
18+
private readonly ObjectPool<StringBuilder> _builderPool;
19+
1720
/// <summary>
1821
/// Create a new wrapper
1922
/// </summary>
20-
/// <param name="headers"></param>
21-
public ResponseCookies(IHeaderDictionary headers)
23+
/// <param name="headers">The <see cref="IHeaderDictionary"/> for the response.</param>
24+
/// <param name="builderPool">The <see cref="ObjectPool{T}"/> used.</param>
25+
public ResponseCookies(IHeaderDictionary headers, ObjectPool<StringBuilder> builderPool)
2226
{
2327
if (headers == null)
2428
{
2529
throw new ArgumentNullException(nameof(headers));
2630
}
31+
if (builderPool == null)
32+
{
33+
throw new ArgumentNullException(nameof(builderPool));
34+
}
2735

2836
Headers = headers;
37+
_builderPool = builderPool;
2938
}
3039

3140
private IHeaderDictionary Headers { get; set; }
@@ -38,13 +47,25 @@ public ResponseCookies(IHeaderDictionary headers)
3847
public void Append(string key, string value)
3948
{
4049
var setCookieHeaderValue = new SetCookieHeaderValue(
41-
Uri.EscapeDataString(key),
42-
Uri.EscapeDataString(value))
50+
Uri.EscapeDataString(key),
51+
Uri.EscapeDataString(value))
4352
{
4453
Path = "/"
4554
};
4655

47-
Headers[HeaderNames.SetCookie] = StringValues.Concat(Headers[HeaderNames.SetCookie], setCookieHeaderValue.ToString());
56+
string cookieValue;
57+
var stringBuilder = _builderPool.Get();
58+
try
59+
{
60+
setCookieHeaderValue.AppendToStringBuilder(stringBuilder);
61+
cookieValue = stringBuilder.ToString();
62+
}
63+
finally
64+
{
65+
_builderPool.Return(stringBuilder);
66+
}
67+
68+
Headers[HeaderNames.SetCookie] = StringValues.Concat(Headers[HeaderNames.SetCookie], cookieValue);
4869
}
4970

5071
/// <summary>
@@ -61,8 +82,8 @@ public void Append(string key, string value, CookieOptions options)
6182
}
6283

6384
var setCookieHeaderValue = new SetCookieHeaderValue(
64-
Uri.EscapeDataString(key),
65-
Uri.EscapeDataString(value))
85+
Uri.EscapeDataString(key),
86+
Uri.EscapeDataString(value))
6687
{
6788
Domain = options.Domain,
6889
Path = options.Path,
@@ -71,7 +92,19 @@ public void Append(string key, string value, CookieOptions options)
7192
HttpOnly = options.HttpOnly,
7293
};
7394

74-
Headers[HeaderNames.SetCookie] = StringValues.Concat(Headers[HeaderNames.SetCookie], setCookieHeaderValue.ToString());
95+
string cookieValue;
96+
var stringBuilder = _builderPool.Get();
97+
try
98+
{
99+
setCookieHeaderValue.AppendToStringBuilder(stringBuilder);
100+
cookieValue = stringBuilder.ToString();
101+
}
102+
finally
103+
{
104+
_builderPool.Return(stringBuilder);
105+
}
106+
107+
Headers[HeaderNames.SetCookie] = StringValues.Concat(Headers[HeaderNames.SetCookie], cookieValue);
75108
}
76109

77110
/// <summary>
@@ -94,7 +127,7 @@ public void Delete(string key, CookieOptions options)
94127
{
95128
throw new ArgumentNullException(nameof(options));
96129
}
97-
130+
98131
var encodedKeyPlusEquals = Uri.EscapeDataString(key) + "=";
99132
bool domainHasValue = !string.IsNullOrEmpty(options.Domain);
100133
bool pathHasValue = !string.IsNullOrEmpty(options.Path);
@@ -130,7 +163,7 @@ public void Delete(string key, CookieOptions options)
130163
newValues.Add(values[i]);
131164
}
132165
}
133-
166+
134167
Headers[HeaderNames.SetCookie] = new StringValues(newValues.ToArray());
135168
}
136169

src/Microsoft.AspNetCore.Http/project.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"dependencies": {
1818
"Microsoft.AspNetCore.Http.Abstractions": "1.0.0-*",
1919
"Microsoft.AspNetCore.WebUtilities": "1.0.0-*",
20+
"Microsoft.Extensions.ObjectPool": "1.0.0-*",
2021
"Microsoft.Net.Http.Headers": "1.0.0-*"
2122
},
2223
"frameworks": {

src/Microsoft.Net.Http.Headers/SetCookieHeaderValue.cs

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -90,42 +90,54 @@ public string Value
9090
public override string ToString()
9191
{
9292
StringBuilder header = new StringBuilder();
93+
AppendToStringBuilder(header);
9394

94-
header.Append(_name);
95-
header.Append("=");
96-
header.Append(_value);
95+
return header.ToString();
96+
}
97+
98+
/// <summary>
99+
/// Append string representation of this <see cref="SetCookieHeaderValue"/> to given
100+
/// <paramref name="builder"/>.
101+
/// </summary>
102+
/// <param name="builder">
103+
/// The <see cref="StringBuilder"/> to receive the string representation of this
104+
/// <see cref="SetCookieHeaderValue"/>.
105+
/// </param>
106+
public void AppendToStringBuilder(StringBuilder builder)
107+
{
108+
builder.Append(_name);
109+
builder.Append("=");
110+
builder.Append(_value);
97111

98112
if (Expires.HasValue)
99113
{
100-
AppendSegment(header, ExpiresToken, HeaderUtilities.FormatDate(Expires.Value));
114+
AppendSegment(builder, ExpiresToken, HeaderUtilities.FormatDate(Expires.Value));
101115
}
102116

103117
if (MaxAge.HasValue)
104118
{
105-
AppendSegment(header, MaxAgeToken, HeaderUtilities.FormatInt64((long)MaxAge.Value.TotalSeconds));
119+
AppendSegment(builder, MaxAgeToken, HeaderUtilities.FormatInt64((long)MaxAge.Value.TotalSeconds));
106120
}
107121

108122
if (Domain != null)
109123
{
110-
AppendSegment(header, DomainToken, Domain);
124+
AppendSegment(builder, DomainToken, Domain);
111125
}
112126

113127
if (Path != null)
114128
{
115-
AppendSegment(header, PathToken, Path);
129+
AppendSegment(builder, PathToken, Path);
116130
}
117131

118132
if (Secure)
119133
{
120-
AppendSegment(header, SecureToken, null);
134+
AppendSegment(builder, SecureToken, null);
121135
}
122136

123137
if (HttpOnly)
124138
{
125-
AppendSegment(header, HttpOnlyToken, null);
139+
AppendSegment(builder, HttpOnlyToken, null);
126140
}
127-
128-
return header.ToString();
129141
}
130142

131143
private static void AppendSegment(StringBuilder builder, string name, string value)

test/Microsoft.AspNetCore.Http.Tests/ResponseCookiesTest.cs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,24 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4-
using Xunit;
5-
using Microsoft.Net.Http.Headers;
4+
using System.Text;
65
using Microsoft.AspNetCore.Http.Internal;
6+
using Microsoft.Extensions.ObjectPool;
7+
using Microsoft.Net.Http.Headers;
8+
using Xunit;
79

810
namespace Microsoft.AspNetCore.Http.Tests
911
{
1012
public class ResponseCookiesTest
1113
{
14+
private static readonly ObjectPool<StringBuilder> _builderPool =
15+
new DefaultObjectPoolProvider().Create<StringBuilder>(new StringBuilderPooledObjectPolicy());
16+
1217
[Fact]
1318
public void DeleteCookieShouldSetDefaultPath()
1419
{
1520
var headers = new HeaderDictionary();
16-
var cookies = new ResponseCookies(headers);
21+
var cookies = new ResponseCookies(headers, _builderPool);
1722
var testcookie = "TestCookie";
1823

1924
cookies.Delete(testcookie);
@@ -29,7 +34,7 @@ public void DeleteCookieShouldSetDefaultPath()
2934
public void NoParamsDeleteRemovesCookieCreatedByAdd()
3035
{
3136
var headers = new HeaderDictionary();
32-
var cookies = new ResponseCookies(headers);
37+
var cookies = new ResponseCookies(headers, _builderPool);
3338
var testcookie = "TestCookie";
3439

3540
cookies.Append(testcookie, testcookie);
@@ -49,7 +54,7 @@ public void NoParamsDeleteRemovesCookieCreatedByAdd()
4954
public void EscapesKeyValuesBeforeSettingCookie(string key, string value, string expected)
5055
{
5156
var headers = new HeaderDictionary();
52-
var cookies = new ResponseCookies(headers);
57+
var cookies = new ResponseCookies(headers, _builderPool);
5358

5459
cookies.Append(key, value);
5560

test/Microsoft.Net.Http.Headers.Tests/SetCookieHeaderValueTest.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.Collections.Generic;
66
using System.Linq;
7+
using System.Text;
78
using Xunit;
89

910
namespace Microsoft.Net.Http.Headers
@@ -264,6 +265,17 @@ public void SetCookieHeaderValue_ToString(SetCookieHeaderValue input, string exp
264265
Assert.Equal(expectedValue, input.ToString());
265266
}
266267

268+
[Theory]
269+
[MemberData(nameof(SetCookieHeaderDataSet))]
270+
public void SetCookieHeaderValue_AppendToStringBuilder(SetCookieHeaderValue input, string expectedValue)
271+
{
272+
var builder = new StringBuilder();
273+
274+
input.AppendToStringBuilder(builder);
275+
276+
Assert.Equal(expectedValue, builder.ToString());
277+
}
278+
267279
[Theory]
268280
[MemberData(nameof(SetCookieHeaderDataSet))]
269281
public void SetCookieHeaderValue_Parse_AcceptsValidValues(SetCookieHeaderValue cookie, string expectedValue)

0 commit comments

Comments
 (0)