Skip to content

Commit

Permalink
Feature/optimizations (#3)
Browse files Browse the repository at this point in the history
* Initial parameter cache optimization.

* Reduced the overhead of clearing the sensitive data.

* Refactored the parameter cache so it only allocates a single entry per name/value pair.

* Simplified the API for use of ParameterCache.

* Fixed bug: value length not being respected correctly.

* Added a sandbox console app.

* Optimized "always false" condition.

* Added some examples of using Json and Dictionary parameter providers.

* Updated the README from the blog post.
  • Loading branch information
mwadams authored Oct 7, 2022
1 parent c097886 commit 7c8122c
Show file tree
Hide file tree
Showing 16 changed files with 333 additions and 212 deletions.
145 changes: 76 additions & 69 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,8 @@ As you can see, there is a significant benefit to using the Corvus implementatio
| ExtractParametersCorvusTavis | 495.2 ns | NA | 0.50 | 0.1450 | 608 B | 0.55 |
| ExtractParametersCorvus | 174.6 ns | NA | 0.18 | - | - | 0.00 |

## Parameter Extraction

### Using the Tavis API
## Tavis API
### Parameter Extraction

```csharp
UriTemplate template = new("http://example.com/Glimpse.axd?n=glimpse_ajax&parentRequestId={parentRequestId}{&hash,callback}");
Expand All @@ -43,52 +42,7 @@ Uri uri = new ("http://example.com/Glimpse.axd?n=glimpse_ajax&parentRequestId=12
IDictionary<string, object?>? result = template.GetParameters(uri);
```

### Using the low-allocation API directly

The lowest-level access makes use of a callback, which is fed the parameters as they are found.

If the `reset` flag is set, you should disregard any parameters that have previously been sent, and start again. (This is typically the case where a partial match fails, and is restarted.)

In order to manage the cache/reset process for you, we provide a `ParameterCache` type. You can rent an instance, and use it to accumulate the results for you. You can then enumerate the result set, and return the resource that have been rented for you.

```csharp
var state = ParameterCache.Rent(5);
IUriTemplateParser corvusTemplate = UriTemplateParserFactory.CreateParser("http://example.com/Glimpse.axd?n=glimpse_ajax&parentRequestId={parentRequestId}{&hash,callback}");

corvusTemplate!.ParseUri("http://example.com/Glimpse.axd?n=glimpse_ajax&parentRequestId=123232323&hash=23ADE34FAE&callback=http%3A%2F%2Fexample.com%2Fcallback", ParameterCache.HandleParameters, ref state);

state.EnumerateParameters(HandleFinalParameterSet);

state.Return();

void HandleFinalParameterSet(ReadOnlySpan<char> name, ReadOnlySpan<char> value)
{
if (name.SequenceEqual("parentRequestId"))
{
Assert.True(value.SequenceEqual("123232323"), $"parentRequestId was {value}");
count++;
}
else if (name.SequenceEqual("hash"))
{
Assert.True(value.SequenceEqual("23ADE34FAE"), $"hash was {value}");
count++;
}
else if (name.SequenceEqual("callback"))
{
Assert.True(value.SequenceEqual("http%3A%2F%2Fexample.com%2Fcallback"), $"callback was {value}");
count++;
}
else
{
Assert.True(false, $"Unexpected parameter: (name: '{name}', value: '{value}')");
}
}
```


## URI Resolution

### Using the Tavis API
### URI Resolution

Replacing a path segment parameter,

Expand Down Expand Up @@ -186,47 +140,100 @@ public void TestExtremeEncoding()
}
```

### Using the low-allocation API directly
Our `Corvus.UriTemplates.TavisApi` implementation is built over an underlying low-allocation API.

## Low allocation API

### Extracting parameter values from a URI by matching it to a URI template

To create an instance of a parser for a URI template, call one of the `CreateParser()` overloads, passing it your URI template.

The low-allocation library provides a generic class `UriTemplateResolver<TParameterProvider, TParameterPayload>` for URI template resolution.
```csharp
IUriTemplateParser UriTemplateParserFactory.CreateParser(string uriTemplate);
```

The `TParameterProvider` is a type which implements `ITemplateParameterProvider<TParameterPayload>`, to process a parameter payload according to a variable specification.
or

This allows you to process parameters as efficiently as possible, based on the types you need to support.
```csharp
IUriTemplateParser UriTemplateParserFactory.CreateParser(ReadOnlySpan<char> uriTemplate);
```

The package `Corvus.UriTemplates.Resolvers.Json` contains a `JsonTemplateResolver` that takes a parameter set based on a `System.Text.Json.JsonElement` of `JsonValueKind.Object`. Its properties become the named parameters.
You would typically have some initialization code that is called once to build your parsers from your templates (either derived statically or from some configuration)

```csharp
using JsonDocument jsonValues = JsonDocument.Parse("{ \"foo\": \"bar\", \"bar\": \"baz\", \"baz\": \"bob\" }");
object? nullState = default;
JsonUriTemplateResolver.TryResolveResult(UriTemplate.AsSpan(), false, jsonValues.RootElement, HandleResult, ref nullState);
private const string UriTemplate = "http://example.com/Glimpse.axd?n=glimpse_ajax&parentRequestId={parentRequestId}{&hash,callback}";

static void HandleResult(ReadOnlySpan<char> resolvedTemplate, ref object? state)
private static readonly IUriTemplateParser CorvusTemplate = CreateParser();

private static IUriTemplateParser CreateParser()
{
// Do what you want with the resolved template
// (Typically, you use the state you have passed in to pprovide the resolved template
// to the outside world in some form.)
return
UriTemplateParserFactory.CreateParser(UriTemplate)
}
```

There are also overloads of `TryResolveResult` which will write to an `IBufferWriter<char>` instead of providing the `ReadOnlySpan<char>` to a callback.
You can then make use of that parser to extract parameter values from a URI.

The parser uses a callback model to deliver the parameters to you (to avoid allocations). If you are used to low allocation code, you will probably recognize the pattern.

You call `EnumerateParmaeters()`, passing the URI you wish to parse (as a `ReadOnlySpan<char>`), a callback, and the initial value of a state object, which will be passed to that callback.

The callback itself is called by the parser each time a matched parameter is discovered.

Similarly, a resolver that takes parameters from a `Dictionary<string, object?>` can be found in the package `Corvus.UriTemplates.Resolvers.DictionaryOfObject`.
It is given `ReadOnlySpan<char>` instances for the name and value pairs, along with the current version of the state object. This state is passed by `ref`, so you can update its value to keep track of whatever processing you are doing with the parameters you have been passed.

Here's an example that just counts the parameters it has seen.

```csharp
using JsonDocument jsonValues = JsonDocument.Parse("{ \"foo\": \"bar\", \"bar\": \"baz\", \"baz\": \"bob\" }");
int state = 0;

CorvusTemplate.EnumerateParameters(Uri, HandleParameters, ref state);

static void HandleParameters(ReadOnlySpan<char> name, ReadOnlySpan<char> value, ref int state)
{
state++;
}
```

> There is a defaulted optional parameter to this method that lets you specific an initial capacity for the cache; if you know how many parameters you are going to match, you can tune this to minimize the amount of re-allocation required.
### Resolving a template by substituting parameter values and producing a URI

The other basic scenario is injecting parameter values into a URI template to produce a URI (or another URI template if we haven't replaced all the parameters in the template).

The underlying type that does the work is called `UriTemplateResolver<TParameterProvider,TParameterPayload>`.

The `TParameterProvider` is an `ITemplateParameterProvider<TParameterPayload>` - an interface implemented by types which convert from a source of parameter values (the `TParameterPayload`), on behalf of the `UriTemplateResolver`.

We offer two of these providers "out of the box" - the `JsonTemplateParameterProvider` (which adapts to a `JsonElement`) and the `DictionaryTemplateParameterProvider` (which adapts to an `IDictionary<string, object?>` and is used by the underlying Tavis-compatible API).

To save you having to work directly with the `UriTemplateResolver` plugging in all the necessary generic parameters, most `ITemplateParameterProvider` implements will offer a convenience type, and these are no exception.

`JsonUriTemplateResolver` and `DictionaryUriTemplateResolver` give you strongly typed `TryResolveResult` and `TryGetParameterNames` methods which you can use in your code.

Here's an example.

```csharp
const string uriTemplate = "http://example.org/location{?value*}";

using var jsonValues = JsonDocument.Parse("{\"value\": { \"foo\": \"bar\", \"bar\": 3.4, \"baz\": null }}");
Dictionary<string, string> value = new() { { "foo", "bar" }, { "bar", "baz" }, { "baz", "bob" } };
Dictionary<string, object?> parameters = new() { { "value", value } };

object? nullState = default;
JsonUriTemplateResolver.TryResolveResult(UriTemplate.AsSpan(), false, jsonValues.RootElement, HandleResult, ref nullState);

JsonUriTemplateResolver.TryResolveResult(uriTemplate.AsSpan(), false, jsonValues.RootElement, HandleResult, ref nullState);
DictionaryUriTemplateResolver.TryResolveResult(uriTemplate.AsSpan(), false, parameters, HandleResult, ref nullState);

static void HandleResult(ReadOnlySpan<char> resolvedTemplate, ref object? state)
{
// Do what you want with the resolved template
// (Typically, you use the state you have passed in to pprovide the resolved template
// to the outside world in some form.)
Console.WriteLine(resolvedTemplate.ToString());
}
```

You should examine the implementations of those types if you wish to implement your own low-allocation parameter providers.
Notice how we can use the exact same callback that receives the resolved template, for both resolvers - the callback is not dependent on the particular parameter provider.

> The Dictionary provider is somewhat faster than the JSON provider, largely because it has less work to do to extract parameter names and values. However, the JSON parameter provider offers direct support for all JSON value kinds (including encoding serialized "deeply nested" JSON values).
## Build and test

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\Corvus.UriTemplates.Resolvers.DictionaryOfObject\Corvus.UriTemplates.Resolvers.DictionaryOfObject.csproj" />
<ProjectReference Include="..\Corvus.UriTemplates.Resolvers.Json\Corvus.UriTemplates.Resolvers.Json.csproj" />
<ProjectReference Include="..\Corvus.UriTemplates\Corvus.UriTemplates.csproj" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,30 @@ public void ExtractParametersTavis()
[Benchmark]
public void ExtractParametersCorvusTavis()
{
this.corvusTavisTemplate!.GetParameters(TavisUri);
IDictionary<string, object?>? result = this.corvusTavisTemplate!.GetParameters(TavisUri);
}

/// <summary>
/// Extract parameters from a URI template using the Corvus implementation of the Tavis API.
/// </summary>
[Benchmark]
public void ExtractParametersCorvusTavisWithParameterCache()
{
int state = 0;

if (this.corvusTemplate!.EnumerateParameters(Uri, HandleParameters, ref state))
{
// We can use the state
}
else
{
// We can't use the state
}

static void HandleParameters(ReadOnlySpan<char> name, ReadOnlySpan<char> value, ref int state)
{
state++;
}
}

/// <summary>
Expand All @@ -72,12 +95,19 @@ public void ExtractParametersCorvusTavis()
[Benchmark]
public void ExtractParametersCorvus()
{
object? state = default;
this.corvusTemplate!.ParseUri(Uri, HandleParameters, ref state);
int state = 0;
this.corvusTemplate!.ParseUri(Uri, HandleParameterMatching, ref state);

static void HandleParameters(bool reset, ReadOnlySpan<char> name, ReadOnlySpan<char> value, ref object? state)
static void HandleParameterMatching(bool reset, ReadOnlySpan<char> name, ReadOnlySpan<char> value, ref int state)
{
// NOP
if (reset)
{
state = 0;
}
else
{
state++;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ namespace Corvus.UriTemplates.Benchmarking;
public class UriTemplateParameterSetting
{
private const string UriTemplate = "http://example.org/location{?value*}";
private static readonly Dictionary<string, string> Values = new() { { "foo", "bar" }, { "bar", "baz" }, { "baz", "bob" } };
private static readonly Dictionary<string, string> Value = new() { { "foo", "bar" }, { "bar", "baz" }, { "baz", "bob" } };
private static readonly Dictionary<string, object?> Parameters = new() { { "value", Value } };

private readonly JsonDocument jsonValues = JsonDocument.Parse("{ \"foo\": \"bar\", \"bar\": \"baz\", \"baz\": \"bob\" }");
private readonly JsonDocument jsonValues = JsonDocument.Parse("{\"value\": { \"foo\": \"bar\", \"bar\": \"baz\", \"baz\": \"bob\" }}");
private Tavis.UriTemplates.UriTemplate? tavisTemplate;
private TavisApi.UriTemplate? corvusTavisTemplate;

Expand Down Expand Up @@ -49,7 +50,7 @@ public Task GlobalCleanup()
[Benchmark(Baseline = true)]
public void ResolveUriTavis()
{
this.tavisTemplate!.SetParameter("value", Values);
this.tavisTemplate!.SetParameter("value", Value);
this.tavisTemplate!.Resolve();
}

Expand All @@ -59,15 +60,15 @@ public void ResolveUriTavis()
[Benchmark]
public void ResolveUriCorvusTavis()
{
this.corvusTavisTemplate!.SetParameter("value", Values);
this.corvusTavisTemplate!.SetParameter("value", Value);
this.corvusTavisTemplate!.Resolve();
}

/// <summary>
/// Resolve a URI from a template and parameter values using Corvus.UriTemplateResolver.
/// </summary>
[Benchmark]
public void ResolveUriCorvus()
public void ResolveUriCorvusJson()
{
object? nullState = default;
JsonUriTemplateResolver.TryResolveResult(UriTemplate.AsSpan(), false, this.jsonValues.RootElement, HandleResult, ref nullState);
Expand All @@ -76,4 +77,18 @@ static void HandleResult(ReadOnlySpan<char> resolvedTemplate, ref object? state)
// NOP
}
}

/// <summary>
/// Resolve a URI from a template and parameter values using Corvus.UriTemplateResolver.
/// </summary>
[Benchmark]
public void ResolveUriCorvusDictionary()
{
object? nullState = default;
DictionaryUriTemplateResolver.TryResolveResult(UriTemplate.AsSpan(), false, Parameters, HandleResult, ref nullState);
static void HandleResult(ReadOnlySpan<char> resolvedTemplate, ref object? state)
{
// NOP
}
}
}
30 changes: 30 additions & 0 deletions Solutions/Corvus.UriTemplate.Benchmarking/packages.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,21 @@
"Microsoft.SourceLink.GitHub": "1.1.1"
}
},
"Roslynator.Analyzers": {
"type": "Direct",
"requested": "[4.1.1, )",
"resolved": "4.1.1",
"contentHash": "3cPVlrB1PytlO1ztZZBOExDKQWpMZgI15ZDa0BqLu0l6xv+xIRfEpqjNRcpvUy3aLxWTkPgSKZbbaO+VoFEJ1g=="
},
"StyleCop.Analyzers": {
"type": "Direct",
"requested": "[1.2.0-beta.435, )",
"resolved": "1.2.0-beta.435",
"contentHash": "TADk7vdGXtfTnYCV7GyleaaRTQjfoSfZXprQrVMm7cSJtJbFc1QIbWPyLvrgrfGdfHbGmUPvaN4ODKNxg2jgPQ==",
"dependencies": {
"StyleCop.Analyzers.Unstable": "1.2.0.435"
}
},
"Tavis.UriTemplates": {
"type": "Direct",
"requested": "[1.1.1, )",
Expand Down Expand Up @@ -345,6 +360,11 @@
"Microsoft.NETCore.Targets": "1.0.1"
}
},
"StyleCop.Analyzers.Unstable": {
"type": "Transitive",
"resolved": "1.2.0.435",
"contentHash": "ouwPWZxbOV3SmCZxIRqHvljkSzkCyi1tDoMzQtDb/bRP8ctASV/iRJr+A2Gdj0QLaLmWnqTWDrH82/iP+X80Lg=="
},
"System.AppContext": {
"type": "Transitive",
"resolved": "4.1.0",
Expand Down Expand Up @@ -1107,6 +1127,16 @@
"System.Collections.Immutable": "[7.0.0-rc.1.22426.10, )"
}
},
"corvus.uritemplates.resolvers.dictionaryofobject": {
"type": "Project",
"dependencies": {
"CommunityToolkit.HighPerformance": "[8.0.0, )",
"Corvus.UriTemplates": "[1.0.0, )",
"Microsoft.Extensions.ObjectPool": "[7.0.0-rc.1.22427.2, )",
"System.Buffers": "[4.5.1, )",
"System.Collections.Immutable": "[7.0.0-rc.1.22426.10, )"
}
},
"corvus.uritemplates.resolvers.json": {
"type": "Project",
"dependencies": {
Expand Down
Loading

0 comments on commit 7c8122c

Please sign in to comment.