parent
0cf972cc43
commit
7209cab5e9
|
|
@ -0,0 +1,134 @@
|
||||||
|
// 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.Collections.Generic;
|
||||||
|
using BenchmarkDotNet.Attributes;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
|
{
|
||||||
|
public class JumpTableMultipleEntryBenchmark
|
||||||
|
{
|
||||||
|
private string[] _strings;
|
||||||
|
private PathSegment[] _segments;
|
||||||
|
|
||||||
|
private JumpTable _linearSearch;
|
||||||
|
private JumpTable _dictionary;
|
||||||
|
|
||||||
|
// All factors of 100 to support sampling
|
||||||
|
[Params(2, 5, 10, 25, 50, 100)]
|
||||||
|
public int Count;
|
||||||
|
|
||||||
|
[GlobalSetup]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
_strings = GetStrings(100);
|
||||||
|
_segments = new PathSegment[100];
|
||||||
|
|
||||||
|
for (var i = 0; i < _strings.Length; i++)
|
||||||
|
{
|
||||||
|
_segments[i] = new PathSegment(0, _strings[i].Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
var samples = new int[Count];
|
||||||
|
for (var i = 0; i < samples.Length; i++)
|
||||||
|
{
|
||||||
|
samples[i] = i * (_strings.Length / Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
var entries = new List<(string text, int _)>();
|
||||||
|
for (var i = 0; i < samples.Length; i++)
|
||||||
|
{
|
||||||
|
entries.Add((_strings[samples[i]], i));
|
||||||
|
}
|
||||||
|
|
||||||
|
_linearSearch = new LinearSearchJumpTable(0, -1, entries.ToArray());
|
||||||
|
_dictionary = new DictionaryJumpTable(0, -1, entries.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
// This baseline is similar to SingleEntryJumpTable. We just want
|
||||||
|
// something stable to compare against.
|
||||||
|
[Benchmark(Baseline = true, OperationsPerInvoke = 100)]
|
||||||
|
public int Baseline()
|
||||||
|
{
|
||||||
|
var strings = _strings;
|
||||||
|
var segments = _segments;
|
||||||
|
|
||||||
|
var destination = 0;
|
||||||
|
for (var i = 0; i < strings.Length; i++)
|
||||||
|
{
|
||||||
|
var @string = strings[i];
|
||||||
|
var segment = segments[i];
|
||||||
|
|
||||||
|
destination = segment.Length == 0 ? -1 :
|
||||||
|
segment.Length != @string.Length ? 1 :
|
||||||
|
string.Compare(
|
||||||
|
@string,
|
||||||
|
segment.Start,
|
||||||
|
@string,
|
||||||
|
0,
|
||||||
|
@string.Length,
|
||||||
|
StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
return destination;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Benchmark(OperationsPerInvoke = 100)]
|
||||||
|
public int LinearSearch()
|
||||||
|
{
|
||||||
|
var strings = _strings;
|
||||||
|
var segments = _segments;
|
||||||
|
|
||||||
|
var destination = 0;
|
||||||
|
for (var i = 0; i < strings.Length; i++)
|
||||||
|
{
|
||||||
|
destination = _linearSearch.GetDestination(strings[i], segments[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return destination;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Benchmark(OperationsPerInvoke = 100)]
|
||||||
|
public int Dictionary()
|
||||||
|
{
|
||||||
|
var strings = _strings;
|
||||||
|
var segments = _segments;
|
||||||
|
|
||||||
|
var destination = 0;
|
||||||
|
for (var i = 0; i < strings.Length; i++)
|
||||||
|
{
|
||||||
|
destination = _dictionary.GetDestination(strings[i], segments[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return destination;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string[] GetStrings(int count)
|
||||||
|
{
|
||||||
|
var strings = new string[count];
|
||||||
|
for (var i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
var guid = Guid.NewGuid().ToString();
|
||||||
|
|
||||||
|
// Between 5 and 36 characters
|
||||||
|
var text = guid.Substring(0, Math.Max(5, Math.Min(count, 36)));
|
||||||
|
if (char.IsDigit(text[0]))
|
||||||
|
{
|
||||||
|
// Convert first character to a letter.
|
||||||
|
text = ((char)(text[0] + ('G' - '0'))) + text.Substring(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i % 2 == 0)
|
||||||
|
{
|
||||||
|
// Lowercase half of them
|
||||||
|
text = text.ToLowerInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
strings[i] = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
// 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 BenchmarkDotNet.Attributes;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
|
{
|
||||||
|
public class JumpTableSingleEntryBenchmark
|
||||||
|
{
|
||||||
|
private JumpTable _table;
|
||||||
|
private string[] _strings;
|
||||||
|
private PathSegment[] _segments;
|
||||||
|
|
||||||
|
[GlobalSetup]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
_table = new SingleEntryJumpTable(0, -1, "hello-world", 1);
|
||||||
|
_strings = new string[]
|
||||||
|
{
|
||||||
|
"index/foo/2",
|
||||||
|
"index/hello-world1/2",
|
||||||
|
"index/hello-world/2",
|
||||||
|
"index//2",
|
||||||
|
"index/hillo-goodbye/2",
|
||||||
|
};
|
||||||
|
_segments = new PathSegment[]
|
||||||
|
{
|
||||||
|
new PathSegment(6, 3),
|
||||||
|
new PathSegment(6, 12),
|
||||||
|
new PathSegment(6, 11),
|
||||||
|
new PathSegment(6, 0),
|
||||||
|
new PathSegment(6, 13),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[Benchmark(Baseline = true, OperationsPerInvoke = 5)]
|
||||||
|
public int Baseline()
|
||||||
|
{
|
||||||
|
var strings = _strings;
|
||||||
|
var segments = _segments;
|
||||||
|
|
||||||
|
var destination = 0;
|
||||||
|
for (var i = 0; i < strings.Length; i++)
|
||||||
|
{
|
||||||
|
var @string = strings[i];
|
||||||
|
var segment = segments[i];
|
||||||
|
|
||||||
|
destination = segment.Length == 0 ? -1 :
|
||||||
|
segment.Length != 11 ? 1 :
|
||||||
|
string.Compare(
|
||||||
|
@string,
|
||||||
|
segment.Start,
|
||||||
|
"hello-world",
|
||||||
|
0,
|
||||||
|
segment.Length,
|
||||||
|
StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
return destination;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Benchmark(OperationsPerInvoke = 5)]
|
||||||
|
public int Implementation()
|
||||||
|
{
|
||||||
|
var strings = _strings;
|
||||||
|
var segments = _segments;
|
||||||
|
|
||||||
|
var destination = 0;
|
||||||
|
for (var i = 0; i < strings.Length; i++)
|
||||||
|
{
|
||||||
|
destination = _table.GetDestination(strings[i], segments[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return destination;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
// 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 BenchmarkDotNet.Attributes;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
|
{
|
||||||
|
public class JumpTableZeroEntryBenchmark
|
||||||
|
{
|
||||||
|
private JumpTable _table;
|
||||||
|
private string[] _strings;
|
||||||
|
private PathSegment[] _segments;
|
||||||
|
|
||||||
|
[GlobalSetup]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
_table = new ZeroEntryJumpTable(0, -1);
|
||||||
|
_strings = new string[]
|
||||||
|
{
|
||||||
|
"index/foo/2",
|
||||||
|
"index/hello-world1/2",
|
||||||
|
"index/hello-world/2",
|
||||||
|
"index//2",
|
||||||
|
"index/hillo-goodbye/2",
|
||||||
|
};
|
||||||
|
_segments = new PathSegment[]
|
||||||
|
{
|
||||||
|
new PathSegment(6, 3),
|
||||||
|
new PathSegment(6, 12),
|
||||||
|
new PathSegment(6, 11),
|
||||||
|
new PathSegment(6, 0),
|
||||||
|
new PathSegment(6, 13),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[Benchmark(Baseline=true, OperationsPerInvoke = 5)]
|
||||||
|
public int Baseline()
|
||||||
|
{
|
||||||
|
var strings = _strings;
|
||||||
|
var segments = _segments;
|
||||||
|
|
||||||
|
var destination = 0;
|
||||||
|
for (var i = 0; i < strings.Length; i++)
|
||||||
|
{
|
||||||
|
destination = segments[i].Length == 0 ? -1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return destination;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Benchmark(OperationsPerInvoke = 5)]
|
||||||
|
public int Implementation()
|
||||||
|
{
|
||||||
|
var strings = _strings;
|
||||||
|
var segments = _segments;
|
||||||
|
|
||||||
|
var destination = 0;
|
||||||
|
for (var i = 0; i < strings.Length; i++)
|
||||||
|
{
|
||||||
|
destination = _table.GetDestination(strings[i], segments[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return destination;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
// 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.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
|
{
|
||||||
|
internal class DictionaryJumpTable : JumpTable
|
||||||
|
{
|
||||||
|
private readonly int _defaultDestination;
|
||||||
|
private readonly int _exitDestination;
|
||||||
|
private readonly Dictionary<string, int> _dictionary;
|
||||||
|
|
||||||
|
public DictionaryJumpTable(
|
||||||
|
int defaultDestination,
|
||||||
|
int exitDestination,
|
||||||
|
(string text, int destination)[] entries)
|
||||||
|
{
|
||||||
|
_defaultDestination = defaultDestination;
|
||||||
|
_exitDestination = exitDestination;
|
||||||
|
|
||||||
|
_dictionary = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
for (var i = 0; i < entries.Length; i++)
|
||||||
|
{
|
||||||
|
_dictionary.Add(entries[i].text, entries[i].destination);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetDestination(string path, PathSegment segment)
|
||||||
|
{
|
||||||
|
if (segment.Length == 0)
|
||||||
|
{
|
||||||
|
return _exitDestination;
|
||||||
|
}
|
||||||
|
|
||||||
|
var text = path.Substring(segment.Start, segment.Length);
|
||||||
|
if (_dictionary.TryGetValue(text, out var destination))
|
||||||
|
{
|
||||||
|
return destination;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _defaultDestination;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string DebuggerToString()
|
||||||
|
{
|
||||||
|
var builder = new StringBuilder();
|
||||||
|
builder.Append("{ ");
|
||||||
|
|
||||||
|
builder.Append(string.Join(", ", _dictionary.Select(kvp => $"{kvp.Key}: {kvp.Value}")));
|
||||||
|
|
||||||
|
builder.Append("$+: ");
|
||||||
|
builder.Append(_defaultDestination);
|
||||||
|
builder.Append(", ");
|
||||||
|
|
||||||
|
builder.Append("$0: ");
|
||||||
|
builder.Append(_defaultDestination);
|
||||||
|
|
||||||
|
builder.Append(" }");
|
||||||
|
|
||||||
|
|
||||||
|
return builder.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
// 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.Diagnostics;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
|
{
|
||||||
|
[DebuggerDisplay("{DebuggerToString(),nq}")]
|
||||||
|
internal abstract class JumpTable
|
||||||
|
{
|
||||||
|
public abstract int GetDestination(string path, PathSegment segment);
|
||||||
|
|
||||||
|
public virtual string DebuggerToString()
|
||||||
|
{
|
||||||
|
return GetType().Name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
// 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.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
|
{
|
||||||
|
internal class JumpTableBuilder
|
||||||
|
{
|
||||||
|
public static readonly int InvalidDestination = -1;
|
||||||
|
|
||||||
|
private readonly List<(string text, int destination)> _entries = new List<(string text, int destination)>();
|
||||||
|
|
||||||
|
// The destination state when none of the text entries match.
|
||||||
|
public int DefaultDestination { get; set; } = InvalidDestination;
|
||||||
|
|
||||||
|
// The destination state for a zero-length segment. This is a special
|
||||||
|
// case because parameters don't match a zero-length segment.
|
||||||
|
public int ExitDestination { get; set; } = InvalidDestination;
|
||||||
|
|
||||||
|
public void AddEntry(string text, int destination)
|
||||||
|
{
|
||||||
|
_entries.Add((text, destination));
|
||||||
|
}
|
||||||
|
|
||||||
|
public JumpTable Build()
|
||||||
|
{
|
||||||
|
if (DefaultDestination == InvalidDestination)
|
||||||
|
{
|
||||||
|
var message = $"{nameof(DefaultDestination)} is not set. Please report this as a bug.";
|
||||||
|
throw new InvalidOperationException(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ExitDestination == InvalidDestination)
|
||||||
|
{
|
||||||
|
var message = $"{nameof(ExitDestination)} is not set. Please report this as a bug.";
|
||||||
|
throw new InvalidOperationException(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The JumpTable implementation is chosen based on the number of entries. Right
|
||||||
|
// now this is simple and minimal.
|
||||||
|
if (_entries.Count == 0)
|
||||||
|
{
|
||||||
|
return new ZeroEntryJumpTable(DefaultDestination, ExitDestination);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_entries.Count == 1)
|
||||||
|
{
|
||||||
|
var entry = _entries[0];
|
||||||
|
return new SingleEntryJumpTable(DefaultDestination, ExitDestination, entry.text, entry.destination);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_entries.Count < 10)
|
||||||
|
{
|
||||||
|
return new LinearSearchJumpTable(DefaultDestination, ExitDestination, _entries.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DictionaryJumpTable(DefaultDestination, ExitDestination, _entries.ToArray());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
// 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.Linq;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
|
{
|
||||||
|
internal class LinearSearchJumpTable : JumpTable
|
||||||
|
{
|
||||||
|
private readonly int _defaultDestination;
|
||||||
|
private readonly int _exitDestination;
|
||||||
|
private readonly (string text, int destination)[] _entries;
|
||||||
|
|
||||||
|
public LinearSearchJumpTable(
|
||||||
|
int defaultDestination,
|
||||||
|
int exitDestination,
|
||||||
|
(string text, int destination)[] entries)
|
||||||
|
{
|
||||||
|
_defaultDestination = defaultDestination;
|
||||||
|
_exitDestination = exitDestination;
|
||||||
|
_entries = entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetDestination(string path, PathSegment segment)
|
||||||
|
{
|
||||||
|
if (segment.Length == 0)
|
||||||
|
{
|
||||||
|
return _exitDestination;
|
||||||
|
}
|
||||||
|
|
||||||
|
var entries = _entries;
|
||||||
|
for (var i = 0; i < entries.Length; i++)
|
||||||
|
{
|
||||||
|
var text = entries[i].text;
|
||||||
|
if (segment.Length == text.Length &&
|
||||||
|
string.Compare(
|
||||||
|
path,
|
||||||
|
segment.Start,
|
||||||
|
text,
|
||||||
|
0,
|
||||||
|
segment.Length,
|
||||||
|
StringComparison.OrdinalIgnoreCase) == 0)
|
||||||
|
{
|
||||||
|
return entries[i].destination;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return _defaultDestination;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string DebuggerToString()
|
||||||
|
{
|
||||||
|
var builder = new StringBuilder();
|
||||||
|
builder.Append("{ ");
|
||||||
|
|
||||||
|
builder.Append(string.Join(", ", _entries.Select(e => $"{e.text}: {e.destination}")));
|
||||||
|
|
||||||
|
builder.Append("$+: ");
|
||||||
|
builder.Append(_defaultDestination);
|
||||||
|
builder.Append(", ");
|
||||||
|
|
||||||
|
builder.Append("$0: ");
|
||||||
|
builder.Append(_defaultDestination);
|
||||||
|
|
||||||
|
builder.Append(" }");
|
||||||
|
|
||||||
|
return builder.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
|
{
|
||||||
|
internal class SingleEntryJumpTable : JumpTable
|
||||||
|
{
|
||||||
|
private readonly int _defaultDestination;
|
||||||
|
private readonly int _exitDestination;
|
||||||
|
private readonly string _text;
|
||||||
|
private readonly int _destination;
|
||||||
|
|
||||||
|
public SingleEntryJumpTable(
|
||||||
|
int defaultDestination,
|
||||||
|
int exitDestination,
|
||||||
|
string text,
|
||||||
|
int destination)
|
||||||
|
{
|
||||||
|
_defaultDestination = defaultDestination;
|
||||||
|
_exitDestination = exitDestination;
|
||||||
|
_text = text;
|
||||||
|
_destination = destination;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetDestination(string path, PathSegment segment)
|
||||||
|
{
|
||||||
|
if (segment.Length == 0)
|
||||||
|
{
|
||||||
|
return _exitDestination;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (segment.Length == _text.Length &&
|
||||||
|
string.Compare(
|
||||||
|
path,
|
||||||
|
segment.Start,
|
||||||
|
_text,
|
||||||
|
0,
|
||||||
|
segment.Length,
|
||||||
|
StringComparison.OrdinalIgnoreCase) == 0)
|
||||||
|
{
|
||||||
|
return _destination;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _defaultDestination;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string DebuggerToString()
|
||||||
|
{
|
||||||
|
return $"{{ {_text}: {_destination}, $+: {_defaultDestination}, $0: {_exitDestination} }}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
|
{
|
||||||
|
internal class ZeroEntryJumpTable : JumpTable
|
||||||
|
{
|
||||||
|
private readonly int _defaultDestination;
|
||||||
|
private readonly int _exitDestination;
|
||||||
|
|
||||||
|
public ZeroEntryJumpTable(int defaultDestination, int exitDestination)
|
||||||
|
{
|
||||||
|
_defaultDestination = defaultDestination;
|
||||||
|
_exitDestination = exitDestination;
|
||||||
|
}
|
||||||
|
|
||||||
|
public unsafe override int GetDestination(string path, PathSegment segment)
|
||||||
|
{
|
||||||
|
return segment.Length == 0 ? _exitDestination : _defaultDestination;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string DebuggerToString()
|
||||||
|
{
|
||||||
|
return $"{{ $+: {_defaultDestination}, $0: {_exitDestination} }}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -38,7 +38,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
|
|
||||||
for (var i = 0; i < count; i++)
|
for (var i = 0; i < count; i++)
|
||||||
{
|
{
|
||||||
current = states[current].Transitions.GetDestination(buffer, i, path);
|
current = states[current].Transitions.GetDestination(path, buffer[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
var matches = new List<(Endpoint, RouteValueDictionary)>();
|
var matches = new List<(Endpoint, RouteValueDictionary)>();
|
||||||
|
|
@ -88,64 +88,5 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
public Endpoint Endpoint;
|
public Endpoint Endpoint;
|
||||||
public string[] Parameters;
|
public string[] Parameters;
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract class JumpTable
|
|
||||||
{
|
|
||||||
public unsafe abstract int GetDestination(PathSegment* segments, int depth, string path);
|
|
||||||
}
|
|
||||||
|
|
||||||
public class JumpTableBuilder
|
|
||||||
{
|
|
||||||
private readonly List<(string text, int destination)> _entries = new List<(string text, int destination)>();
|
|
||||||
|
|
||||||
public int Depth { get; set; }
|
|
||||||
|
|
||||||
public int Exit { get; set; }
|
|
||||||
|
|
||||||
public void AddEntry(string text, int destination)
|
|
||||||
{
|
|
||||||
_entries.Add((text, destination));
|
|
||||||
}
|
|
||||||
|
|
||||||
public JumpTable Build()
|
|
||||||
{
|
|
||||||
return new SimpleJumpTable(Depth, Exit, _entries.ToArray());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class SimpleJumpTable : JumpTable
|
|
||||||
{
|
|
||||||
private readonly (string text, int destination)[] _entries;
|
|
||||||
private readonly int _depth;
|
|
||||||
private readonly int _exit;
|
|
||||||
|
|
||||||
public SimpleJumpTable(int depth, int exit, (string text, int destination)[] entries)
|
|
||||||
{
|
|
||||||
_depth = depth;
|
|
||||||
_exit = exit;
|
|
||||||
_entries = entries;
|
|
||||||
}
|
|
||||||
|
|
||||||
public unsafe override int GetDestination(PathSegment* segments, int depth, string path)
|
|
||||||
{
|
|
||||||
for (var i = 0; i < _entries.Length; i++)
|
|
||||||
{
|
|
||||||
var segment = segments[depth];
|
|
||||||
if (segment.Length == _entries[i].text.Length &&
|
|
||||||
string.Compare(
|
|
||||||
path,
|
|
||||||
segment.Start,
|
|
||||||
_entries[i].text,
|
|
||||||
0,
|
|
||||||
segment.Length,
|
|
||||||
StringComparison.OrdinalIgnoreCase) == 0)
|
|
||||||
{
|
|
||||||
return _entries[i].destination;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return _exit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -109,13 +109,18 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
|
|
||||||
var exit = states.Count;
|
var exit = states.Count;
|
||||||
states.Add(new State() { IsAccepting = false, Matches = Array.Empty<Candidate>(), });
|
states.Add(new State() { IsAccepting = false, Matches = Array.Empty<Candidate>(), });
|
||||||
tables.Add(new JumpTableBuilder() { Exit = exit, });
|
tables.Add(new JumpTableBuilder() { DefaultDestination = exit, ExitDestination = exit, });
|
||||||
|
|
||||||
for (var i = 0; i < tables.Count; i++)
|
for (var i = 0; i < tables.Count; i++)
|
||||||
{
|
{
|
||||||
if (tables[i].Exit == -1)
|
if (tables[i].DefaultDestination == JumpTableBuilder.InvalidDestination)
|
||||||
{
|
{
|
||||||
tables[i].Exit = exit;
|
tables[i].DefaultDestination = exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tables[i].ExitDestination == JumpTableBuilder.InvalidDestination)
|
||||||
|
{
|
||||||
|
tables[i].ExitDestination = exit;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -158,7 +163,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
IsAccepting = node.Matches.Count > 0,
|
IsAccepting = node.Matches.Count > 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
var table = new JumpTableBuilder() { Depth = node.Depth, };
|
var table = new JumpTableBuilder();
|
||||||
tables.Add(table);
|
tables.Add(table);
|
||||||
|
|
||||||
foreach (var kvp in node.Literals)
|
foreach (var kvp in node.Literals)
|
||||||
|
|
@ -172,13 +177,13 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
table.AddEntry(kvp.Key, transition);
|
table.AddEntry(kvp.Key, transition);
|
||||||
}
|
}
|
||||||
|
|
||||||
var exitIndex = -1;
|
var defaultIndex = -1;
|
||||||
if (node.Literals.TryGetValue("*", out var exit))
|
if (node.Literals.TryGetValue("*", out var exit))
|
||||||
{
|
{
|
||||||
exitIndex = AddNode(exit, states, tables);
|
defaultIndex = AddNode(exit, states, tables);
|
||||||
}
|
}
|
||||||
|
|
||||||
table.Exit = exitIndex;
|
table.DefaultDestination = defaultIndex;
|
||||||
return index;
|
return index;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
|
{
|
||||||
|
public class DictionaryJumpTableTest : MultipleEntryJumpTableTest
|
||||||
|
{
|
||||||
|
internal override JumpTable CreateTable(
|
||||||
|
int defaultDestination,
|
||||||
|
int exitDestination,
|
||||||
|
params (string text, int destination)[] entries)
|
||||||
|
{
|
||||||
|
return new DictionaryJumpTable(defaultDestination, exitDestination, entries);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
|
{
|
||||||
|
public class LinearSearchJumpTableTest : MultipleEntryJumpTableTest
|
||||||
|
{
|
||||||
|
internal override JumpTable CreateTable(
|
||||||
|
int defaultDestination,
|
||||||
|
int existDestination,
|
||||||
|
params (string text, int destination)[] entries)
|
||||||
|
{
|
||||||
|
return new LinearSearchJumpTable(defaultDestination, existDestination, entries);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
// 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 Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
|
{
|
||||||
|
public abstract class MultipleEntryJumpTableTest
|
||||||
|
{
|
||||||
|
internal abstract JumpTable CreateTable(
|
||||||
|
int defaultDestination,
|
||||||
|
int exitDestination,
|
||||||
|
params (string text, int destination)[] entries);
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetDestination_ZeroLengthSegment_JumpsToExit()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var table = CreateTable(0, 1, ("text", 2));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = table.GetDestination("ignored", new PathSegment(0, 0));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(1, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetDestination_NonMatchingSegment_JumpsToDefault()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var table = CreateTable(0, 1, ("text", 2));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = table.GetDestination("text", new PathSegment(1, 2));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(0, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetDestination_SegmentMatchingText_JumpsToDestination()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var table = CreateTable(0, 1, ("text", 2));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = table.GetDestination("some-text", new PathSegment(5, 4));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(2, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetDestination_SegmentMatchingTextIgnoreCase_JumpsToDestination()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var table = CreateTable(0, 1, ("text", 2));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = table.GetDestination("some-tExt", new PathSegment(5, 4));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(2, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetDestination_SegmentMatchingTextIgnoreCase_MultipleEntries()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var table = CreateTable(0, 1, ("tezt", 2), ("text", 3));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = table.GetDestination("some-tExt", new PathSegment(5, 4));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(3, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
// 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 Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
|
{
|
||||||
|
public class SingleEntryJumpTableTest
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void GetDestination_ZeroLengthSegment_JumpsToExit()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var table = new SingleEntryJumpTable(0, 1, "text", 2);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = table.GetDestination("ignored", new PathSegment(0, 0));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(1, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetDestination_NonMatchingSegment_JumpsToDefault()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var table = new SingleEntryJumpTable(0, 1, "text", 2);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = table.GetDestination("text", new PathSegment(1, 2));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(0, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetDestination_SegmentMatchingText_JumpsToDestination()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var table = new SingleEntryJumpTable(0, 1, "text", 2);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = table.GetDestination("some-text", new PathSegment(5, 4));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(2, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetDestination_SegmentMatchingTextIgnoreCase_JumpsToDestination()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var table = new SingleEntryJumpTable(0, 1, "text", 2);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = table.GetDestination("some-tExt", new PathSegment(5, 4));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(2, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
// 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 Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
|
{
|
||||||
|
public class ZeroEntryJumpTableTest
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void GetDestination_ZeroLengthSegment_JumpsToExit()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var table = new ZeroEntryJumpTable(0, 1);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = table.GetDestination("ignored", new PathSegment(0, 0));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(1, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetDestination_SegmentWithLength_JumpsToDefault()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var table = new ZeroEntryJumpTable(0, 1);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = table.GetDestination("ignored", new PathSegment(0, 1));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(0, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue