From 063d6eca0f9cc62066f0dd5b808c15b2d7ca6c97 Mon Sep 17 00:00:00 2001 From: Kristian Hellang Date: Wed, 5 Oct 2016 21:19:15 +0200 Subject: [PATCH] Added custom RFC 1123 DateTimeFormatter to improve allocation profile (#716) --- .../ContentDispositionHeaderValue.cs | 2 +- .../DateTimeFormatter.cs | 100 ++++++++++++++++++ .../HeaderUtilities.cs | 7 +- .../HttpRuleParser.cs | 6 -- .../RangeConditionHeaderValue.cs | 2 +- src/Microsoft.Net.Http.Headers/project.json | 12 +-- .../HeaderUtilitiesTest.cs | 48 +++++++++ 7 files changed, 161 insertions(+), 16 deletions(-) create mode 100644 src/Microsoft.Net.Http.Headers/DateTimeFormatter.cs create mode 100644 test/Microsoft.Net.Http.Headers.Tests/HeaderUtilitiesTest.cs diff --git a/src/Microsoft.Net.Http.Headers/ContentDispositionHeaderValue.cs b/src/Microsoft.Net.Http.Headers/ContentDispositionHeaderValue.cs index 7ceb8728d4..b7a2253ac9 100644 --- a/src/Microsoft.Net.Http.Headers/ContentDispositionHeaderValue.cs +++ b/src/Microsoft.Net.Http.Headers/ContentDispositionHeaderValue.cs @@ -321,7 +321,7 @@ namespace Microsoft.Net.Http.Headers else { // Must always be quoted - var dateString = string.Format(CultureInfo.InvariantCulture, "\"{0}\"", HttpRuleParser.DateToString(date.Value)); + var dateString = HeaderUtilities.FormatDate(date.Value, quoted: true); if (dateParameter != null) { dateParameter.Value = dateString; diff --git a/src/Microsoft.Net.Http.Headers/DateTimeFormatter.cs b/src/Microsoft.Net.Http.Headers/DateTimeFormatter.cs new file mode 100644 index 0000000000..06893155bd --- /dev/null +++ b/src/Microsoft.Net.Http.Headers/DateTimeFormatter.cs @@ -0,0 +1,100 @@ +// 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.Globalization; +using System.Runtime.CompilerServices; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.Net.Http.Headers +{ + internal static class DateTimeFormatter + { + private static readonly DateTimeFormatInfo FormatInfo = CultureInfo.InvariantCulture.DateTimeFormat; + + private static readonly string[] MonthNames = FormatInfo.AbbreviatedMonthNames; + private static readonly string[] DayNames = FormatInfo.AbbreviatedDayNames; + + private static readonly int Rfc1123DateLength = "ddd, dd MMM yyyy HH:mm:ss GMT".Length; + private static readonly int QuotedRfc1123DateLength = Rfc1123DateLength + 2; + + // ASCII numbers are in the range 48 - 57. + private const int AsciiNumberOffset = 0x30; + + private const string Gmt = "GMT"; + private const char Comma = ','; + private const char Space = ' '; + private const char Colon = ':'; + private const char Quote = '"'; + + public static string ToRfc1123String(this DateTimeOffset dateTime) + { + return ToRfc1123String(dateTime, false); + } + + public static string ToRfc1123String(this DateTimeOffset dateTime, bool quoted) + { + var universal = dateTime.UtcDateTime; + + var length = quoted ? QuotedRfc1123DateLength : Rfc1123DateLength; + var target = new InplaceStringBuilder(length); + + if (quoted) + { + target.Append(Quote); + } + + target.Append(DayNames[(int)universal.DayOfWeek]); + target.Append(Comma); + target.Append(Space); + AppendNumber(ref target, universal.Day); + target.Append(Space); + target.Append(MonthNames[universal.Month - 1]); + target.Append(Space); + AppendYear(ref target, universal.Year); + target.Append(Space); + AppendTimeOfDay(ref target, universal.TimeOfDay); + target.Append(Space); + target.Append(Gmt); + + if (quoted) + { + target.Append(Quote); + } + + return target.ToString(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void AppendYear(ref InplaceStringBuilder target, int year) + { + target.Append(GetAsciiChar(year / 1000)); + target.Append(GetAsciiChar(year % 1000 / 100)); + target.Append(GetAsciiChar(year % 100 / 10)); + target.Append(GetAsciiChar(year % 10)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void AppendTimeOfDay(ref InplaceStringBuilder target, TimeSpan timeOfDay) + { + AppendNumber(ref target, timeOfDay.Hours); + target.Append(Colon); + AppendNumber(ref target, timeOfDay.Minutes); + target.Append(Colon); + AppendNumber(ref target, timeOfDay.Seconds); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void AppendNumber(ref InplaceStringBuilder target, int number) + { + target.Append(GetAsciiChar(number / 10)); + target.Append(GetAsciiChar(number % 10)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static char GetAsciiChar(int value) + { + return (char)(AsciiNumberOffset + value); + } + } +} diff --git a/src/Microsoft.Net.Http.Headers/HeaderUtilities.cs b/src/Microsoft.Net.Http.Headers/HeaderUtilities.cs index eee2ea9c7d..786b54ddb9 100644 --- a/src/Microsoft.Net.Http.Headers/HeaderUtilities.cs +++ b/src/Microsoft.Net.Http.Headers/HeaderUtilities.cs @@ -220,7 +220,12 @@ namespace Microsoft.Net.Http.Headers public static string FormatDate(DateTimeOffset dateTime) { - return HttpRuleParser.DateToString(dateTime); + return FormatDate(dateTime, false); + } + + public static string FormatDate(DateTimeOffset dateTime, bool quoted) + { + return dateTime.ToRfc1123String(quoted); } public static string RemoveQuotes(string input) diff --git a/src/Microsoft.Net.Http.Headers/HttpRuleParser.cs b/src/Microsoft.Net.Http.Headers/HttpRuleParser.cs index e30465bd4f..637835996c 100644 --- a/src/Microsoft.Net.Http.Headers/HttpRuleParser.cs +++ b/src/Microsoft.Net.Http.Headers/HttpRuleParser.cs @@ -233,12 +233,6 @@ namespace Microsoft.Net.Http.Headers return HttpParseResult.Parsed; } - internal static string DateToString(DateTimeOffset dateTime) - { - // Format according to RFC1123; 'r' uses invariant info (DateTimeFormatInfo.InvariantInfo) - return dateTime.ToUniversalTime().ToString("r", CultureInfo.InvariantCulture); - } - internal static bool TryStringToDate(string input, out DateTimeOffset result) { // Try the various date formats in the order listed above. diff --git a/src/Microsoft.Net.Http.Headers/RangeConditionHeaderValue.cs b/src/Microsoft.Net.Http.Headers/RangeConditionHeaderValue.cs index c564722d51..4960e4beab 100644 --- a/src/Microsoft.Net.Http.Headers/RangeConditionHeaderValue.cs +++ b/src/Microsoft.Net.Http.Headers/RangeConditionHeaderValue.cs @@ -53,7 +53,7 @@ namespace Microsoft.Net.Http.Headers { if (_entityTag == null) { - return HttpRuleParser.DateToString(_lastModified.Value); + return HeaderUtilities.FormatDate(_lastModified.Value); } return _entityTag.ToString(); } diff --git a/src/Microsoft.Net.Http.Headers/project.json b/src/Microsoft.Net.Http.Headers/project.json index 4f3a4ed848..703cf5151c 100644 --- a/src/Microsoft.Net.Http.Headers/project.json +++ b/src/Microsoft.Net.Http.Headers/project.json @@ -19,14 +19,12 @@ "xmlDoc": true }, "dependencies": { - "NETStandard.Library": "1.6.1-*" + "Microsoft.Extensions.Primitives": "1.1.0-*", + "NETStandard.Library": "1.6.1-*", + "System.Buffers": "4.3.0-*", + "System.Diagnostics.Contracts": "4.3.0-*" }, "frameworks": { - "netstandard1.1": { - "dependencies": { - "System.Buffers": "4.3.0-*", - "System.Diagnostics.Contracts": "4.3.0-*" - } - } + "netstandard1.1": {} } } \ No newline at end of file diff --git a/test/Microsoft.Net.Http.Headers.Tests/HeaderUtilitiesTest.cs b/test/Microsoft.Net.Http.Headers.Tests/HeaderUtilitiesTest.cs new file mode 100644 index 0000000000..ccae6e577f --- /dev/null +++ b/test/Microsoft.Net.Http.Headers.Tests/HeaderUtilitiesTest.cs @@ -0,0 +1,48 @@ +// 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 Xunit; + +namespace Microsoft.Net.Http.Headers +{ + public static class HeaderUtilitiesTest + { + private const string Rfc1123Format = "r"; + + [Theory] + [MemberData(nameof(TestValues))] + public static void ReturnsSameResultAsRfc1123String(DateTimeOffset dateTime, bool quoted) + { + var formatted = dateTime.ToString(Rfc1123Format); + var expected = quoted ? $"\"{formatted}\"" : formatted; + var actual = HeaderUtilities.FormatDate(dateTime, quoted); + + Assert.Equal(expected, actual); + } + + public static TheoryData TestValues + { + get + { + var data = new TheoryData(); + + var now = DateTimeOffset.Now; + + foreach (var quoted in new[] { true, false }) + { + for (var i = 0; i < 60; i++) + { + data.Add(now.AddSeconds(i), quoted); + data.Add(now.AddMinutes(i), quoted); + data.Add(now.AddDays(i), quoted); + data.Add(now.AddMonths(i), quoted); + data.Add(now.AddYears(i), quoted); + } + } + + return data; + } + } + } +}