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

Commit 8e85255

Browse files
committed
Performance optimizations
- Calculate age using operations on long - Compute content length of resposne on store - Format age using the new HeaderUtility - Lazily create HeaderDictionary - Use for instead of foreach to reduce allocations from enumerators
1 parent 184c0f3 commit 8e85255

File tree

5 files changed

+47
-22
lines changed

5 files changed

+47
-22
lines changed

src/Microsoft.AspNetCore.ResponseCaching/Internal/CacheEntry/CachedResponse.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ public class CachedResponse : IResponseCacheEntry
1313

1414
public int StatusCode { get; set; }
1515

16-
public IHeaderDictionary Headers { get; set; } = new HeaderDictionary();
16+
public IHeaderDictionary Headers { get; set; }
1717

1818
public Stream Body { get; set; }
1919
}

src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.Threading.Tasks;
66
using Microsoft.Extensions.Caching.Memory;
7+
using Microsoft.Net.Http.Headers;
78

89
namespace Microsoft.AspNetCore.ResponseCaching.Internal
910
{

src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingKeyProvider.cs

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -107,13 +107,18 @@ public string CreateStorageVaryByKey(ResponseCachingContext context)
107107
builder.Append(KeyDelimiter)
108108
.Append('H');
109109

110-
foreach (var header in varyByRules.Headers)
110+
for (var i = 0; i < varyByRules.Headers.Count; i++)
111111
{
112+
var header = varyByRules.Headers[i];
113+
var headerValues = context.HttpContext.Request.Headers[header];
112114
builder.Append(KeyDelimiter)
113115
.Append(header)
114-
.Append("=")
115-
// TODO: Perf - iterate the string values instead?
116-
.Append(context.HttpContext.Request.Headers[header]);
116+
.Append("=");
117+
118+
for (var j = 0; j < headerValues.Count; j++)
119+
{
120+
builder.Append(headerValues[j]);
121+
}
117122
}
118123
}
119124

@@ -131,19 +136,28 @@ public string CreateStorageVaryByKey(ResponseCachingContext context)
131136
{
132137
builder.Append(KeyDelimiter)
133138
.AppendUpperInvariant(query.Key)
134-
.Append("=")
135-
.Append(query.Value);
139+
.Append("=");
140+
141+
for (var i = 0; i < query.Value.Count; i++)
142+
{
143+
builder.Append(query.Value[i]);
144+
}
136145
}
137146
}
138147
else
139148
{
140-
foreach (var queryKey in varyByRules.QueryKeys)
149+
for (var i = 0; i < varyByRules.QueryKeys.Count; i++)
141150
{
151+
var queryKey = varyByRules.QueryKeys[i];
152+
var queryKeyValues = context.HttpContext.Request.Query[queryKey];
142153
builder.Append(KeyDelimiter)
143154
.Append(queryKey)
144-
.Append("=")
145-
// TODO: Perf - iterate the string values instead?
146-
.Append(context.HttpContext.Request.Query[queryKey]);
155+
.Append("=");
156+
157+
for (var j = 0; j < queryKeyValues.Count; j++)
158+
{
159+
builder.Append(queryKeyValues[j]);
160+
}
147161
}
148162
}
149163
}

src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
using System;
55
using System.Collections.Generic;
6-
using System.Globalization;
76
using System.Threading.Tasks;
87
using Microsoft.AspNetCore.Http;
98
using Microsoft.AspNetCore.Http.Features;
@@ -142,18 +141,15 @@ internal async Task<bool> TryServeCachedResponseAsync(ResponseCachingContext con
142141
response.Headers.Add(header);
143142
}
144143

145-
response.Headers[HeaderNames.Age] = context.CachedEntryAge.Value.TotalSeconds.ToString("F0", CultureInfo.InvariantCulture);
144+
// Note: int64 division truncates result and errors may be up to 1 second. This reduction in
145+
// accuracy of age calculation is considered appropriate since it is small compared to clock
146+
// skews and the "Age" header is an estimate of the real age of cached content.
147+
response.Headers[HeaderNames.Age] = HeaderUtilities.FormatInt64(context.CachedEntryAge.Value.Ticks / TimeSpan.TicksPerSecond);
146148

147149
// Copy the cached response body
148150
var body = context.CachedResponse.Body;
149151
if (body.Length > 0)
150152
{
151-
// Add a content-length if required
152-
if (!response.ContentLength.HasValue && StringValues.IsNullOrEmpty(response.Headers[HeaderNames.TransferEncoding]))
153-
{
154-
response.ContentLength = body.Length;
155-
}
156-
157153
try
158154
{
159155
await body.CopyToAsync(response.Body, StreamUtilities.BodySegmentSize, context.HttpContext.RequestAborted);
@@ -263,7 +259,8 @@ internal async Task FinalizeCacheHeadersAsync(ResponseCachingContext context)
263259
context.CachedResponse = new CachedResponse
264260
{
265261
Created = context.ResponseDate.Value,
266-
StatusCode = context.HttpContext.Response.StatusCode
262+
StatusCode = context.HttpContext.Response.StatusCode,
263+
Headers = new HeaderDictionary()
267264
};
268265

269266
foreach (var header in context.HttpContext.Response.Headers)
@@ -288,6 +285,13 @@ internal async Task FinalizeCacheBodyAsync(ResponseCachingContext context)
288285
var bufferStream = context.ResponseCachingStream.GetBufferStream();
289286
if (!contentLength.HasValue || contentLength == bufferStream.Length)
290287
{
288+
var response = context.HttpContext.Response;
289+
// Add a content-length if required
290+
if (!response.ContentLength.HasValue && StringValues.IsNullOrEmpty(response.Headers[HeaderNames.TransferEncoding]))
291+
{
292+
context.CachedResponse.Headers[HeaderNames.ContentLength] = HeaderUtilities.FormatInt64(bufferStream.Length);
293+
}
294+
291295
context.CachedResponse.Body = bufferStream;
292296
_logger.LogResponseCached();
293297
await _cache.SetAsync(context.StorageVaryKey ?? context.BaseKey, context.CachedResponse, context.CachedResponseValidFor);
@@ -371,8 +375,9 @@ internal static bool ContentIsNotModified(ResponseCachingContext context)
371375
&& EntityTagHeaderValue.TryParse(cachedResponseHeaders[HeaderNames.ETag], out eTag)
372376
&& EntityTagHeaderValue.TryParseList(ifNoneMatchHeader, out ifNoneMatchEtags))
373377
{
374-
foreach (var requestETag in ifNoneMatchEtags)
378+
for (var i = 0; i < ifNoneMatchEtags.Count; i++)
375379
{
380+
var requestETag = ifNoneMatchEtags[i];
376381
if (eTag.Compare(requestETag, useStrongComparison: false))
377382
{
378383
context.Logger.LogNotModifiedIfNoneMatchMatched(requestETag);

test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ await cache.SetAsync(
6161
"BaseKey",
6262
new CachedResponse()
6363
{
64+
Headers = new HeaderDictionary(),
6465
Body = new SegmentReadStream(new List<byte[]>(0), 0)
6566
},
6667
TimeSpan.Zero);
@@ -108,6 +109,7 @@ await cache.SetAsync(
108109
"BaseKeyVaryKey2",
109110
new CachedResponse()
110111
{
112+
Headers = new HeaderDictionary(),
111113
Body = new SegmentReadStream(new List<byte[]>(0), 0)
112114
},
113115
TimeSpan.Zero);
@@ -666,7 +668,10 @@ public async Task FinalizeCacheBody_Cache_IfContentLengthAbsent()
666668
await context.HttpContext.Response.WriteAsync(new string('0', 10));
667669

668670
context.ShouldCacheResponse = true;
669-
context.CachedResponse = new CachedResponse();
671+
context.CachedResponse = new CachedResponse()
672+
{
673+
Headers = new HeaderDictionary()
674+
};
670675
context.BaseKey = "BaseKey";
671676
context.CachedResponseValidFor = TimeSpan.FromSeconds(10);
672677

0 commit comments

Comments
 (0)