Prototypeing a fast header dictionary

Conflicts:
	src/Microsoft.AspNet.Server.Kestrel/project.json
This commit is contained in:
Louis DeJardin 2015-07-08 14:45:14 -07:00 committed by Stephen Halter
parent 83b2c95385
commit 963f086eb0
8 changed files with 742 additions and 1 deletions

View File

@ -26,6 +26,7 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{8A3D00B8-1CCF-4BE6-A060-11104CE2D9CE}"
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "LargeResponseApp", "samples\LargeResponseApp\LargeResponseApp.xproj", "{B35D4D31-E74C-4646-8A11-7A7A40F0021E}"
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Server.Kestrel.GeneratedCode", "src\Microsoft.AspNet.Server.Kestrel.GeneratedCode\Microsoft.AspNet.Server.Kestrel.GeneratedCode.xproj", "{BD2D4D29-1BD9-40D0-BB31-337D5416B63C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -53,6 +54,10 @@ Global
{B35D4D31-E74C-4646-8A11-7A7A40F0021E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B35D4D31-E74C-4646-8A11-7A7A40F0021E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B35D4D31-E74C-4646-8A11-7A7A40F0021E}.Release|Any CPU.Build.0 = Release|Any CPU
{BD2D4D29-1BD9-40D0-BB31-337D5416B63C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BD2D4D29-1BD9-40D0-BB31-337D5416B63C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BD2D4D29-1BD9-40D0-BB31-337D5416B63C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BD2D4D29-1BD9-40D0-BB31-337D5416B63C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -63,5 +68,6 @@ Global
{2C3CB3DC-EEBF-4F52-9E1C-4F2F972E76C3} = {8A3D00B8-1CCF-4BE6-A060-11104CE2D9CE}
{30B7617E-58EF-4382-B3EA-5B2E718CF1A6} = {2D5D5227-4DBD-499A-96B1-76A36B03B750}
{B35D4D31-E74C-4646-8A11-7A7A40F0021E} = {8A3D00B8-1CCF-4BE6-A060-11104CE2D9CE}
{BD2D4D29-1BD9-40D0-BB31-337D5416B63C} = {2D5D5227-4DBD-499A-96B1-76A36B03B750}
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,305 @@
using Microsoft.Framework.Runtime.Roslyn;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Microsoft.AspNet.Server.Kestrel.GeneratedCode
{
// This project can output the Class library as a NuGet Package.
// To enable this option, right-click on the project and select the Properties menu item. In the Build tab select "Produce outputs on build".
public class KnownHeaders : ICompileModule
{
string Each<T>(IEnumerable<T> values, Func<T, string> formatter)
{
return values.Select(formatter).Aggregate((a, b) => a + b + "\r\n");
}
class KnownHeader
{
public string Name { get; set; }
public int Index { get; set; }
public string Identifier => Name.Replace("-", "");
public string TestBit() => $"((_bits & ({1L << Index}L)) != 0)";
public string SetBit() => $"_bits |= {1L << Index}L";
public string ClearBit() => $"_bits &= ~{(1L << Index)}L";
}
public virtual void BeforeCompile(BeforeCompileContext context)
{
Console.WriteLine("I like pie");
var commonHeaders = new[]
{
"Cache-Control",
"Connection",
"Date",
"Keep-Alive",
"Pragma",
"Trailer",
"Transfer-Encoding",
"Upgrade",
"Via",
"Warning",
"Allow",
"Content-Length",
"Content-Type",
"Content-Encoding",
"Content-Language",
"Content-Location",
"Content-MD5",
"Content-Range",
"Expires",
"Last-Modified"
};
var requestHeaders = commonHeaders.Concat(new[]
{
"Accept",
"Accept-Charset",
"Accept-Encoding",
"Accept-Language",
"Authorization",
"Cookie",
"Expect",
"From",
"Host",
"If-Match",
"If-Modified-Since",
"If-None-Match",
"If-Range",
"If-Unmodified-Since",
"Max-Forwards",
"Proxy-Authorization",
"Referer",
"Range",
"TE",
"Translage",
"User-Agent",
}).Select((header, index) => new KnownHeader
{
Name = header,
Index = index
});
var responseHeaders = commonHeaders.Concat(new[]
{
"Accept-Ranges",
"Age",
"ETag",
"Location",
"Proxy-Autheticate",
"Retry-After",
"Server",
"Set-Cookie",
"Vary",
"WWW-Authenticate",
}).Select((header, index) => new KnownHeader
{
Name = header,
Index = index
});
var loops = new[]
{
new
{
Headers = requestHeaders,
HeadersByLength = requestHeaders.GroupBy(x => x.Name.Length),
ClassName = "FrameRequestHeaders"
},
new
{
Headers = responseHeaders,
HeadersByLength = responseHeaders.GroupBy(x => x.Name.Length),
ClassName = "FrameResponseHeaders"
}
};
var syntaxTree = Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree.ParseText($@"
using System;
using System.Collections.Generic;
namespace Microsoft.AspNet.Server.Kestrel.Http
{{{Each(loops, loop => $@"
public partial class {loop.ClassName}
{{
long _bits = 0;
{Each(loop.Headers, header => "string[] _" + header.Identifier + ";")}
protected override int GetCountFast()
{{
var count = Unknown.Count;
{Each(loop.Headers, header => $@"
if ({header.TestBit()})
{{
++count;
}}
")}
return count;
}}
protected override string[] GetValueFast(string key)
{{
switch(key.Length)
{{{Each(loop.HeadersByLength, byLength => $@"
case {byLength.Key}:
{{{Each(byLength, header => $@"
if (0 == StringComparer.OrdinalIgnoreCase.Compare(key, ""{header.Name}""))
{{
if ({header.TestBit()})
{{
return _{header.Identifier};
}}
else
{{
throw new System.Collections.Generic.KeyNotFoundException();
}}
}}
")}}}
break;
")}}}
return Unknown[key];
}}
protected override bool TryGetValueFast(string key, out string[] value)
{{
switch(key.Length)
{{{Each(loop.HeadersByLength, byLength => $@"
case {byLength.Key}:
{{{Each(byLength, header => $@"
if (0 == StringComparer.OrdinalIgnoreCase.Compare(key, ""{header.Name}""))
{{
if ({header.TestBit()})
{{
value = _{header.Identifier};
return true;
}}
else
{{
value = null;
return false;
}}
}}
")}}}
break;
")}}}
return Unknown.TryGetValue(key, out value);
}}
protected override void SetValueFast(string key, string[] value)
{{
switch(key.Length)
{{{Each(loop.HeadersByLength, byLength => $@"
case {byLength.Key}:
{{{Each(byLength, header => $@"
if (0 == StringComparer.OrdinalIgnoreCase.Compare(key, ""{header.Name}""))
{{
{header.SetBit()};
_{header.Identifier} = value;
return;
}}
")}}}
break;
")}}}
Unknown[key] = value;
}}
protected override void AddValueFast(string key, string[] value)
{{
switch(key.Length)
{{{Each(loop.HeadersByLength, byLength => $@"
case {byLength.Key}:
{{{Each(byLength, header => $@"
if (0 == StringComparer.OrdinalIgnoreCase.Compare(key, ""{header.Name}""))
{{
if ({header.TestBit()})
{{
throw new ArgumentException(""An item with the same key has already been added."");
}}
{header.SetBit()};
_{header.Identifier} = value;
return;
}}
")}}}
break;
")}}}
Unknown.Add(key, value);
}}
protected override bool RemoveFast(string key)
{{
switch(key.Length)
{{{Each(loop.HeadersByLength, byLength => $@"
case {byLength.Key}:
{{{Each(byLength, header => $@"
if (0 == StringComparer.OrdinalIgnoreCase.Compare(key, ""{header.Name}""))
{{
if ({header.TestBit()})
{{
{header.ClearBit()};
return true;
}}
else
{{
return false;
}}
}}
")}}}
break;
")}}}
return Unknown.Remove(key);
}}
protected override void ClearFast()
{{
_bits = 0;
Unknown.Clear();
}}
protected override void CopyToFast(KeyValuePair<string, string[]>[] array, int arrayIndex)
{{
if (arrayIndex < 0)
{{
throw new ArgumentException();
}}
{Each(loop.Headers, header => $@"
if ({header.TestBit()})
{{
if (arrayIndex == array.Length)
{{
throw new ArgumentException();
}}
array[arrayIndex] = new KeyValuePair<string, string[]>(""{header.Name}"", _{header.Identifier});
++arrayIndex;
}}
")}
((ICollection<KeyValuePair<string, string[]>>)Unknown).CopyTo(array, arrayIndex);
}}
protected override IEnumerable<KeyValuePair<string, string[]>> EnumerateFast()
{{
{Each(loop.Headers, header => $@"
if ({header.TestBit()})
{{
yield return new KeyValuePair<string, string[]>(""{header.Name}"", _{header.Identifier});
}}
")}
foreach(var kv in Unknown)
{{
yield return kv;
}}
}}
}}
")}}}
");
context.Compilation = context.Compilation.AddSyntaxTrees(syntaxTree);
}
public virtual void AfterCompile(AfterCompileContext context)
{
}
}
}

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>bd2d4d29-1bd9-40d0-bb31-337d5416b63c</ProjectGuid>
<RootNamespace>Microsoft.AspNet.Server.Kestrel.GeneratedCode</RootNamespace>
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\..\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">..\..\artifacts\bin\$(MSBuildProjectName)\</OutputPath>
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

View File

@ -0,0 +1,28 @@
{
"version": "1.0.0-*",
"description": "",
"authors": [ "" ],
"tags": [ "" ],
"projectUrl": "",
"licenseUrl": "",
"dependencies": {
"Microsoft.Framework.Runtime.Roslyn": "1.0.0-beta6-*"
},
"frameworks": {
"dnx451": {
"frameworkAssemblies": {
"System.Runtime": "4.0.10.0"
}
},
"dnxcore50": {
"dependencies": {
"System.Collections": "4.0.10-beta-*",
"System.Linq": "4.0.0-beta-*",
"System.Threading": "4.0.10-beta-*",
"Microsoft.CSharp": "4.0.0-beta-*"
}
}
}
}

View File

@ -0,0 +1,132 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
namespace Microsoft.AspNet.Server.Kestrel.Http
{
public partial class FrameRequestHeaders : FrameHeaders
{
}
public partial class FrameResponseHeaders : FrameHeaders
{
}
public abstract class FrameHeaders : IDictionary<string, string[]>
{
protected Dictionary<string, string[]> Unknown = new Dictionary<string, string[]>(StringComparer.OrdinalIgnoreCase);
protected virtual int GetCountFast()
{ throw new NotImplementedException(); }
protected virtual string[] GetValueFast(string key)
{ throw new NotImplementedException(); }
protected virtual bool TryGetValueFast(string key, out string[] value)
{ throw new NotImplementedException(); }
protected virtual void SetValueFast(string key, string[] value)
{ throw new NotImplementedException(); }
protected virtual void AddValueFast(string key, string[] value)
{ throw new NotImplementedException(); }
protected virtual bool RemoveFast(string key)
{ throw new NotImplementedException(); }
protected virtual void ClearFast()
{ throw new NotImplementedException(); }
protected virtual void CopyToFast(KeyValuePair<string, string[]>[] array, int arrayIndex)
{ throw new NotImplementedException(); }
protected virtual IEnumerable<KeyValuePair<string, string[]>> EnumerateFast()
{ throw new NotImplementedException(); }
string[] IDictionary<string, string[]>.this[string key]
{
get
{
return GetValueFast(key);
}
set
{
SetValueFast(key, value);
}
}
int ICollection<KeyValuePair<string, string[]>>.Count => GetCountFast();
bool ICollection<KeyValuePair<string, string[]>>.IsReadOnly => false;
ICollection<string> IDictionary<string, string[]>.Keys => EnumerateFast().Select(x => x.Key).ToList();
ICollection<string[]> IDictionary<string, string[]>.Values => EnumerateFast().Select(x => x.Value).ToList();
void ICollection<KeyValuePair<string, string[]>>.Add(KeyValuePair<string, string[]> item)
{
AddValueFast(item.Key, item.Value);
}
void IDictionary<string, string[]>.Add(string key, string[] value)
{
AddValueFast(key, value);
}
void ICollection<KeyValuePair<string, string[]>>.Clear()
{
ClearFast();
}
bool ICollection<KeyValuePair<string, string[]>>.Contains(KeyValuePair<string, string[]> item)
{
string[] value;
return
TryGetValueFast(item.Key, out value) &&
object.Equals(value, item.Value);
}
bool IDictionary<string, string[]>.ContainsKey(string key)
{
string[] value;
return TryGetValueFast(key, out value);
}
void ICollection<KeyValuePair<string, string[]>>.CopyTo(KeyValuePair<string, string[]>[] array, int arrayIndex)
{
CopyToFast(array, arrayIndex);
}
IEnumerator IEnumerable.GetEnumerator()
{
return EnumerateFast().GetEnumerator();
}
IEnumerator<KeyValuePair<string, string[]>> IEnumerable<KeyValuePair<string, string[]>>.GetEnumerator()
{
return EnumerateFast().GetEnumerator();
}
bool ICollection<KeyValuePair<string, string[]>>.Remove(KeyValuePair<string, string[]> item)
{
string[] value;
return
TryGetValueFast(item.Key, out value) &&
object.Equals(value, item.Value) &&
RemoveFast(item.Key);
}
bool IDictionary<string, string[]>.Remove(string key)
{
return RemoveFast(key);
}
bool IDictionary<string, string[]>.TryGetValue(string key, out string[] value)
{
return TryGetValueFast(key, out value);
}
}
}

View File

@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Microsoft.AspNet.Server.Kestrel
{
public class KnownHeaders : Microsoft.AspNet.Server.Kestrel.GeneratedCode.KnownHeaders
{
}
}

View File

@ -6,7 +6,8 @@
"url": "git://github.com/aspnet/kestrelhttpserver"
},
"dependencies": {
"Microsoft.Dnx.Runtime.Abstractions": "1.0.0-beta7-*"
"Microsoft.Dnx.Runtime.Abstractions": "1.0.0-beta7-*",
"Microsoft.AspNet.Server.Kestrel.GeneratedCode": { "version": "1.0.0-*", "type": "build" }
},
"frameworks": {
"dnx451": { },

View File

@ -0,0 +1,238 @@
using Microsoft.AspNet.Server.Kestrel.Http;
using System;
using System.Collections.Generic;
using System.Linq;
using Xunit;
namespace Microsoft.AspNet.Server.KestrelTests
{
public class FrameRequestHeadersTests
{
[Fact]
public void InitialDictionaryIsEmpty()
{
IDictionary<string, string[]> headers = new FrameRequestHeaders();
Assert.Equal(0, headers.Count);
Assert.False(headers.IsReadOnly);
}
[Fact]
public void SettingUnknownHeadersWorks()
{
IDictionary<string, string[]> headers = new FrameRequestHeaders();
headers["custom"] = new[] { "value" };
Assert.NotNull(headers["custom"]);
Assert.Equal(1, headers["custom"].Length);
Assert.Equal("value", headers["custom"][0]);
}
[Fact]
public void SettingKnownHeadersWorks()
{
IDictionary<string, string[]> headers = new FrameRequestHeaders();
headers["host"] = new[] { "value" };
Assert.NotNull(headers["host"]);
Assert.Equal(1, headers["host"].Length);
Assert.Equal("value", headers["host"][0]);
}
[Fact]
public void KnownAndCustomHeaderCountAddedTogether()
{
IDictionary<string, string[]> headers = new FrameRequestHeaders();
headers["host"] = new[] { "value" };
headers["custom"] = new[] { "value" };
Assert.Equal(2, headers.Count);
}
[Fact]
public void TryGetValueWorksForKnownAndUnknownHeaders()
{
IDictionary<string, string[]> headers = new FrameRequestHeaders();
string[] value;
Assert.False(headers.TryGetValue("host", out value));
Assert.False(headers.TryGetValue("custom", out value));
headers["host"] = new[] { "value" };
Assert.True(headers.TryGetValue("host", out value));
Assert.False(headers.TryGetValue("custom", out value));
headers["custom"] = new[] { "value" };
Assert.True(headers.TryGetValue("host", out value));
Assert.True(headers.TryGetValue("custom", out value));
}
[Fact]
public void SameExceptionThrownForMissingKey()
{
IDictionary<string, string[]> headers = new FrameRequestHeaders();
Assert.Throws<KeyNotFoundException>(() => headers["custom"]);
Assert.Throws<KeyNotFoundException>(() => headers["host"]);
}
[Fact]
public void EntriesCanBeEnumerated()
{
IDictionary<string, string[]> headers = new FrameRequestHeaders();
var v1 = new[] { "localhost" };
var v2 = new[] { "value" };
headers["host"] = v1;
headers["custom"] = v2;
Assert.Equal(
new[] {
new KeyValuePair<string, string[]>("Host", v1),
new KeyValuePair<string, string[]>("custom", v2),
},
headers);
}
[Fact]
public void KeysAndValuesCanBeEnumerated()
{
IDictionary<string, string[]> headers = new FrameRequestHeaders();
var v1 = new[] { "localhost" };
var v2 = new[] { "value" };
headers["host"] = v1;
headers["custom"] = v2;
Assert.Equal<string>(
new[] { "Host", "custom" },
headers.Keys);
Assert.Equal<string[]>(
new[] { v1, v2 },
headers.Values);
}
[Fact]
public void ContainsAndContainsKeyWork()
{
IDictionary<string, string[]> headers = new FrameRequestHeaders();
var kv1 = new KeyValuePair<string, string[]>("host", new[] { "localhost" });
var kv2 = new KeyValuePair<string, string[]>("custom", new[] { "value" });
var kv1b = new KeyValuePair<string, string[]>("host", new[] { "localhost" });
var kv2b = new KeyValuePair<string, string[]>("custom", new[] { "value" });
Assert.False(headers.ContainsKey("host"));
Assert.False(headers.ContainsKey("custom"));
Assert.False(headers.Contains(kv1));
Assert.False(headers.Contains(kv2));
headers["host"] = kv1.Value;
Assert.True(headers.ContainsKey("host"));
Assert.False(headers.ContainsKey("custom"));
Assert.True(headers.Contains(kv1));
Assert.False(headers.Contains(kv2));
Assert.False(headers.Contains(kv1b));
Assert.False(headers.Contains(kv2b));
headers["custom"] = kv2.Value;
Assert.True(headers.ContainsKey("host"));
Assert.True(headers.ContainsKey("custom"));
Assert.True(headers.Contains(kv1));
Assert.True(headers.Contains(kv2));
Assert.False(headers.Contains(kv1b));
Assert.False(headers.Contains(kv2b));
}
[Fact]
public void AddWorksLikeSetAndThrowsIfKeyExists()
{
IDictionary<string, string[]> headers = new FrameRequestHeaders();
string[] value;
Assert.False(headers.TryGetValue("host", out value));
Assert.False(headers.TryGetValue("custom", out value));
headers.Add("host", new[] { "localhost" });
headers.Add("custom", new[] { "value" });
Assert.True(headers.TryGetValue("host", out value));
Assert.True(headers.TryGetValue("custom", out value));
Assert.Throws<ArgumentException>(() => headers.Add("host", new[] { "localhost" }));
Assert.Throws<ArgumentException>(() => headers.Add("custom", new[] { "value" }));
Assert.True(headers.TryGetValue("host", out value));
Assert.True(headers.TryGetValue("custom", out value));
}
[Fact]
public void ClearRemovesAllHeaders()
{
IDictionary<string, string[]> headers = new FrameRequestHeaders();
headers.Add("host", new[] { "localhost" });
headers.Add("custom", new[] { "value" });
string[] value;
Assert.Equal(2, headers.Count);
Assert.True(headers.TryGetValue("host", out value));
Assert.True(headers.TryGetValue("custom", out value));
headers.Clear();
Assert.Equal(0, headers.Count);
Assert.False(headers.TryGetValue("host", out value));
Assert.False(headers.TryGetValue("custom", out value));
}
[Fact]
public void RemoveTakesHeadersOutOfDictionary()
{
IDictionary<string, string[]> headers = new FrameRequestHeaders();
headers.Add("host", new[] { "localhost" });
headers.Add("custom", new[] { "value" });
string[] value;
Assert.Equal(2, headers.Count);
Assert.True(headers.TryGetValue("host", out value));
Assert.True(headers.TryGetValue("custom", out value));
Assert.True(headers.Remove("host"));
Assert.False(headers.Remove("host"));
Assert.Equal(1, headers.Count);
Assert.False(headers.TryGetValue("host", out value));
Assert.True(headers.TryGetValue("custom", out value));
Assert.True(headers.Remove("custom"));
Assert.False(headers.Remove("custom"));
Assert.Equal(0, headers.Count);
Assert.False(headers.TryGetValue("host", out value));
Assert.False(headers.TryGetValue("custom", out value));
}
[Fact]
public void CopyToMovesDataIntoArray()
{
IDictionary<string, string[]> headers = new FrameRequestHeaders();
headers.Add("host", new[] { "localhost" });
headers.Add("custom", new[] { "value" });
var entries = new KeyValuePair<string, string[]>[4];
headers.CopyTo(entries, 1);
Assert.Null(entries[0].Key);
Assert.Null(entries[0].Value);
Assert.Equal("Host", entries[1].Key);
Assert.NotNull(entries[1].Value);
Assert.Equal("custom", entries[2].Key);
Assert.NotNull(entries[2].Value);
Assert.Null(entries[3].Key);
Assert.Null(entries[3].Value);
}
}
}