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

Header parsing #70

Merged
merged 2 commits into from
Dec 9, 2016
Merged

Header parsing #70

merged 2 commits into from
Dec 9, 2016

Conversation

BrennanConroy
Copy link
Member

Still a bit of cleanup needed.

Saved about 791 bytes per request.
1627.6 originally
836.6 after

Benchmarks has shown an average of 3.386% increase in RPS

_requestHeaders = HttpContext.Request.GetTypedHeaders();
_parsedResponseDate = true;
DateTimeOffset date;
if (DateTimeOffset.TryParse(HttpContext.Response.Headers[HeaderNames.Date], out date))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

format/culture?

@@ -0,0 +1,23 @@
using System;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

copyright

@@ -0,0 +1,74 @@
using System;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

copyright

{
internal class CacheControlValues
{
public const string MaxAgeString = "max-age";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any chance we make these strings public in HttpAbstractions so we don't duplicated it here?

_responseDate = TypedResponseHeaders.Date;
_parsedResponseSharedMaxAge = true;
_responseSharedMaxAge = null;
foreach (var header in HttpContext.Response.Headers[HeaderNames.CacheControl])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Weren't we trying to eliminate the use of foreach to reduce allocations for enumerators?

_responseCacheControl = TypedResponseHeaders.CacheControl ?? EmptyCacheControl;
_parsedResponseExpires = true;
DateTimeOffset expires;
if (ParsingHelpers.TryStringToDate(HttpContext.Response.Headers[HeaderNames.Expires], out expires))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TryStringToDate => TryParseDate

_responseExpires = TypedResponseHeaders.Expires;
_parsedResponseMaxAge = true;
_responseMaxAge = null;
foreach (var header in HttpContext.Response.Headers[HeaderNames.CacheControl])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code looks very similar to parsing ResponseSharedMaxAge, extract into a helper method in ParsingHelpers?

var expires = context.CachedResponseHeaders.Expires;
if (responseTime >= expires)
DateTimeOffset expires;
if (context.CachedResponseHeaders[HeaderNames.Expires].Count > 0)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Combine the nested if statements.

{
// Try the various date formats in the order listed above.
// We should accept a wide verity of common formats, but only output RFC 1123 style dates.
if (DateTimeOffset.TryParseExact(input, DateFormats, DateTimeFormatInfo.InvariantInfo,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so return DateTimeOffset.TryParseExact(...)? Why the if statement? In fact you can do => DateTimeOffset.TryParseExact(...)

return false;
}

internal static bool TryParseHeaderValue(int startIndex, string header, out int value)
Copy link
Contributor

@JunTaoLuo JunTaoLuo Oct 11, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add tests for this.

while (endIndex < header.Length)
{
var c = header[endIndex];
if ((c >= '0') && (c <= '9'))
Copy link
Contributor

@JunTaoLuo JunTaoLuo Oct 11, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about spaces between equals and the first number? max-age = 60?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could have swore I tested that... will fix

context.Logger.LogNotModifiedIfNoneMatchMatched(tag);
return true;
EntityTagHeaderValue requestETag;
if (EntityTagHeaderValue.TryParse(tag, out requestETag))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

combine if statements

var lastModified = cachedResponseHeaders.LastModified ?? cachedResponseHeaders.Date;
if (lastModified <= ifUnmodifiedSince)
DateTimeOffset modified;
if (!ParsingHelpers.TryStringToDate(cachedResponseHeaders[HeaderNames.LastModified], out modified))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Combine if statements

@JunTaoLuo
Copy link
Contributor

We should probably see whether we want to do the parsing on demand each time or parse all of the cache-control values in one go, need to test the performance of the two approaches.

@@ -0,0 +1,35 @@
using Microsoft.AspNetCore.ResponseCaching.Internal;
Copy link
Contributor

@JunTaoLuo JunTaoLuo Oct 12, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No copyright = copyleft and we don't like that

@@ -0,0 +1,100 @@
// Copyright (c) .NET Foundation. All rights reserved.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file should go in the Internal folder.

internal static bool TryParseDate(string input, out DateTimeOffset result) => DateTimeOffset.TryParseExact(input, DateFormats, DateTimeFormatInfo.InvariantInfo,
DateTimeStyles.AllowWhiteSpaces | DateTimeStyles.AssumeUniversal, out result);

internal static bool TryGetHeaderValue(StringValues headers, string headerName, out int value)
Copy link
Contributor

@JunTaoLuo JunTaoLuo Oct 12, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm the method name isn't really descriptive. Maybe TryParseHeaderInt? Should also consolidate the naming so maybe the previous method should be renamed to TryParseHeaderDate

{
context.Logger.LogResponseWithoutPublicNotCacheable();
return false;
}

// Check no-store
if (context.RequestCacheControlHeaderValue.NoStore || context.ResponseCacheControlHeaderValue.NoStore)
foreach (var header in context.HttpContext.Request.Headers[HeaderNames.CacheControl])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be extracted in a helper as well. ParsingHelper.HeaderContains(string)


namespace Microsoft.AspNetCore.ResponseCaching.Internal
{
internal static class ParsingHelpers
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HttpHeaderParsingHelpers?

DateTimeOffset unmodifiedSince;
if (ParsingHelpers.TryParseDate(ifUnmodifiedSince, out unmodifiedSince))
{
if (modified <= unmodifiedSince)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Combine if

{
context.Logger.LogExpirationExpiresExceeded(responseTime, expires.Value);
return false;
if (context.ResponseTime.Value >= expires)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

combine if

}

TimeSpan? requestMaxStale = null;
if (ParsingHelpers.TryGetHeaderValue(requestCacheControlHeaders, CacheControlValues.MaxStaleString, out seconds))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually this can be simplified by having a ParsingHelpers.TryParseTimeSpan (or some consistent naming) that returns a TimeSpan.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't pass a nullable as an out parameter :(

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes you can

{
internal static class HttpHeaderParsingHelpers
{
private static readonly string[] DateFormats = new string[] {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this duplicated from HttpAbstractions?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is private in that repo. We are considering moving a few parts of this pr to abstractions so we'll see if this is still needed here.

Copy link
Contributor

@JunTaoLuo JunTaoLuo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm also taking a look at what we can move to Abstractions to eliminate duplication. Also, please review the changes in my latest commit.

{
if (cachedResponseHeaders.ETag.Compare(tag, useStrongComparison: false))
foreach (var tag in ifNoneMatchHeader)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you can use a foreach here since it's not guaranteed that each string contains only one ETag due how StringValues from HTTP headers are stored. Let's take a look at how it is parsed in Abstractions.

var lastModified = cachedResponseHeaders.LastModified ?? cachedResponseHeaders.Date;
if (lastModified <= ifUnmodifiedSince)
DateTimeOffset modified;
if (!HttpHeaderParsingHelpers.TryParseHeaderDate(cachedResponseHeaders[HeaderNames.LastModified], out modified) &&
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have this fixed in my clean up commit but just to note, both sides of the && operator are evaluated regardless of whether they are true or false. See example here: https://gist.github.com/JunTaoLuo/87f8b2e727c956333fa036f01b3275af

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Um, the first one should be TrueStatement to actually test this

Copy link
Contributor

@JunTaoLuo JunTaoLuo Nov 22, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, you're right. And in that case it works as you mentioned. Undid my changes. Must be confusing && with another operator we tested before.

@@ -365,20 +365,18 @@ internal static bool ContentIsNotModified(ResponseCachingContext context)
return true;
}

if (!StringValues.IsNullOrEmpty(cachedResponseHeaders[HeaderNames.ETag]))
EntityTagHeaderValue eTag;
if (!StringValues.IsNullOrEmpty(cachedResponseHeaders[HeaderNames.ETag])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The previous comment got collapsed. But this need to be updated to match the parsing behaviour in Abstractions.

@JunTaoLuo
Copy link
Contributor

Requires aspnet/HttpAbstractions#738

@JunTaoLuo
Copy link
Contributor

JunTaoLuo commented Dec 9, 2016

Perf results on ResponseCachingPlaintextCached:
Before: 322984.7 RPS, 1657.2 bytes/req overhead from the responsecaching middleware
After: 346747.2 RPS, 1205.3 bytes/req overhead from the responsecaching middleware

@BrennanConroy
Copy link
Member Author

🐑 🇮🇹

@JunTaoLuo JunTaoLuo merged commit e01431f into dev Dec 9, 2016
@JunTaoLuo JunTaoLuo deleted the brecon/headers branch December 9, 2016 23:53
@JunTaoLuo JunTaoLuo mentioned this pull request Dec 12, 2016
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants