Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Diagnostics;

namespace Microsoft.AspNetCore.Razor.Language;

internal sealed partial class TagHelperBinder
{
/// <summary>
/// Similar to <see cref="ImmutableArray{T}"/>, but optimized to store either a single value or an array of values.
/// </summary>
[DebuggerDisplay("{GetDebuggerDisplay(),nq}")]
[DebuggerTypeProxy(typeof(DebuggerProxy))]
private readonly struct TagHelperSet
{
public static readonly TagHelperSet Empty = default!;

private readonly object _valueOrArray;

public TagHelperSet(TagHelperDescriptor value)
{
_valueOrArray = value;
}

public TagHelperSet(TagHelperDescriptor[] array)
{
_valueOrArray = array;
}

public TagHelperDescriptor this[int index]
{
get
{
return _valueOrArray switch
{
TagHelperDescriptor[] array => array[index],
not null when index == 0 => (TagHelperDescriptor)_valueOrArray,
_ => throw new IndexOutOfRangeException(),
};
}
}

public int Count
=> _valueOrArray switch
{
TagHelperDescriptor[] array => array.Length,
null => 0,

// _valueOrArray can be an array, a single value, or null.
// So, we can avoid a type check for the single value case.
_ => 1
};

public Enumerator GetEnumerator()
=> new(this);

public struct Enumerator
{
private readonly TagHelperSet _tagHelperSet;
private int _index;

internal Enumerator(TagHelperSet tagHelperSet)
{
_tagHelperSet = tagHelperSet;
_index = -1;
}

public bool MoveNext()
{
_index++;
return _index < _tagHelperSet.Count;
}

public readonly TagHelperDescriptor Current
=> _tagHelperSet[_index];
}

private sealed class DebuggerProxy(TagHelperSet instance)
{
private readonly TagHelperSet _instance = instance;

[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
public TagHelperDescriptor[] Items
=> _instance._valueOrArray switch
{
TagHelperDescriptor[] array => array,
TagHelperDescriptor value => [value],
_ => []
};
}

private string GetDebuggerDisplay()
=> "Count " + Count;

/// <summary>
/// This is a mutable builder for <see cref="TagHelperSet"/>. However, it works differently from
/// a typical builder. First, you must call <see cref="IncreaseSize"/> to set the number of items.
/// Once you've done that for each item to be added, you can call <see cref="Add(TagHelperDescriptor)"/>
/// exactly that many times. This ensures that space allocated is exactly what's needed to
/// produce the resulting <see cref="TagHelperSet"/>.
/// </summary>
public struct Builder
{
private object? _valueOrArray;
private int _index;
private int _size;

public void IncreaseSize()
{
Debug.Assert(_valueOrArray is null, "Cannot increase size once items have been added.");
_size++;
}

public void Add(TagHelperDescriptor item)
{
Debug.Assert(_index < _size, "Cannot add more items.");

if (_size == 1)
{
// We only need to store a single value.
_valueOrArray = item;
_index = 1;
return;
}

Debug.Assert(_valueOrArray is null or TagHelperDescriptor[]);

if (_valueOrArray is not TagHelperDescriptor[] array)
{
array = new TagHelperDescriptor[_size];
_valueOrArray = array;
}

array[_index++] = item;
}

public readonly TagHelperSet ToSet()
{
Debug.Assert(_index == _size, "Must have added all items.");

return _size switch
{
0 => Empty,
1 => new((TagHelperDescriptor)_valueOrArray!),
_ => new((TagHelperDescriptor[])_valueOrArray!)
};
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ namespace Microsoft.AspNetCore.Razor.Language;
/// <summary>
/// Enables retrieval of <see cref="TagHelperBinding"/>'s.
/// </summary>
internal sealed class TagHelperBinder
internal sealed partial class TagHelperBinder
{
private readonly ImmutableArray<TagHelperDescriptor> _catchAllDescriptors;
private readonly ReadOnlyDictionary<string, ImmutableArray<TagHelperDescriptor>> _tagNameToDescriptorsMap;
private readonly TagHelperSet _catchAllDescriptors;
private readonly ReadOnlyDictionary<string, TagHelperSet> _tagNameToDescriptorsMap;

public string? TagNamePrefix { get; }
public ImmutableArray<TagHelperDescriptor> Descriptors { get; }
Expand All @@ -38,52 +38,88 @@ public TagHelperBinder(string? tagNamePrefix, ImmutableArray<TagHelperDescriptor
private static void ProcessDescriptors(
ImmutableArray<TagHelperDescriptor> descriptors,
string? tagNamePrefix,
out ReadOnlyDictionary<string, ImmutableArray<TagHelperDescriptor>> tagNameToDescriptorsMap,
out ImmutableArray<TagHelperDescriptor> catchAllDescriptors)
out ReadOnlyDictionary<string, TagHelperSet> tagNameToDescriptorsMap,
out TagHelperSet catchAllDescriptors)
{
using var catchAllBuilder = new PooledArrayBuilder<TagHelperDescriptor>();
using var pooledMap = StringDictionaryPool<ImmutableArray<TagHelperDescriptor>.Builder>.OrdinalIgnoreCase.GetPooledObject(out var mapBuilder);
using var pooledSet = HashSetPool<TagHelperDescriptor>.GetPooledObject(out var distinctSet);
// Initialize a MemoryBuilder of TagHelperSet.Builders. We need a builder for each unique tag name.
using var builders = new MemoryBuilder<TagHelperSet.Builder>(initialCapacity: 32, clearArray: true);

// Keep track of what needs to be added in the second pass.
// There will be an entry for every tag matching rule.
// Each entry consists of an index to identify a builder and the TagHelperDescriptor to add to it.
using var toAdd = new MemoryBuilder<(int, TagHelperDescriptor)>(initialCapacity: descriptors.Length * 4, clearArray: true);

// Use a special TagHelperSet.Builder to track catch-all tag helpers.
var catchAllBuilder = new TagHelperSet.Builder();

// At most, there should only be one catch-all tag helper per descriptor.
using var catchAllToAdd = new MemoryBuilder<TagHelperDescriptor>(initialCapacity: descriptors.Length, clearArray: true);

// Build a map of tag name -> tag helpers.
foreach (var descriptor in descriptors)
// The builders are indexed using a map of "tag name" to the index of the builder in the array.
using var _1 = StringDictionaryPool<int>.OrdinalIgnoreCase.GetPooledObject(out var tagNameToBuilderIndexMap);
using var _2 = HashSetPool<TagHelperDescriptor>.GetPooledObject(out var tagHelperSet);

#if NET
tagHelperSet.EnsureCapacity(descriptors.Length);
#endif

foreach (var tagHelper in descriptors)
{
if (!distinctSet.Add(descriptor))
if (!tagHelperSet.Add(tagHelper))
{
// We're already seen this descriptor, skip it.
// We've already seen this tag helper. Skip.
continue;
}

foreach (var rule in descriptor.TagMatchingRules)
foreach (var rule in tagHelper.TagMatchingRules)
{
if (rule.TagName == TagHelperMatchingConventions.ElementCatchAllName)
var tagName = rule.TagName;

if (tagName == TagHelperMatchingConventions.ElementCatchAllName)
{
// This is a catch-all descriptor, we can keep track of it separately.
catchAllBuilder.Add(descriptor);
catchAllBuilder.IncreaseSize();
catchAllToAdd.Append(tagHelper);
continue;
}
else

if (!tagNameToBuilderIndexMap.TryGetValue(tagName, out var builderIndex))
{
// This is a specific tag name, we need to add it to the map.
var tagName = tagNamePrefix + rule.TagName;
var builder = mapBuilder.GetOrAdd(tagName, _ => ImmutableArray.CreateBuilder<TagHelperDescriptor>());
builderIndex = builders.Length;
builders.Append(default(TagHelperSet.Builder));

builder.Add(descriptor);
tagNameToBuilderIndexMap.Add(tagName, builderIndex);
}

builders[builderIndex].IncreaseSize();
toAdd.Append((builderIndex, tagHelper));
}
}

// Build the final dictionary with immutable arrays.
var map = new Dictionary<string, ImmutableArray<TagHelperDescriptor>>(capacity: mapBuilder.Count, StringComparer.OrdinalIgnoreCase);
// Next, we walk through toAdd and add each descriptor to the appropriate builder.
// Because we counted first, we know that each builder will allocate exactly the
// space needed for the final result.
foreach (var (builderIndex, tagHelper) in toAdd.AsMemory().Span)
{
builders[builderIndex].Add(tagHelper);
}

foreach (var tagHelper in catchAllToAdd.AsMemory().Span)
{
catchAllBuilder.Add(tagHelper);
}

// Build the final dictionary.
var map = new Dictionary<string, TagHelperSet>(capacity: tagNameToBuilderIndexMap.Count, StringComparer.OrdinalIgnoreCase);

foreach (var (key, value) in mapBuilder)
foreach (var (tagName, builderIndex) in tagNameToBuilderIndexMap)
{
map.Add(key, value.ToImmutableAndClear());
map.Add(tagNamePrefix + tagName, builders[builderIndex].ToSet());
}

tagNameToDescriptorsMap = new ReadOnlyDictionary<string, ImmutableArray<TagHelperDescriptor>>(map);
tagNameToDescriptorsMap = new ReadOnlyDictionary<string, TagHelperSet>(map);

// Build the catch all descriptors array.
catchAllDescriptors = catchAllBuilder.ToImmutableAndClear();
// Build the "catch all" tag helpers set.
catchAllDescriptors = catchAllBuilder.ToSet();
}

/// <summary>
Expand Down Expand Up @@ -150,7 +186,7 @@ private static void ProcessDescriptors(
: null;

static void CollectBoundRulesInfo(
ImmutableArray<TagHelperDescriptor> descriptors,
TagHelperSet descriptors,
ReadOnlySpan<char> tagName,
ReadOnlySpan<char> parentTagName,
ImmutableArray<KeyValuePair<string, string>> attributes,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@
<None Include="$(SharedFilesRoot)Compiler\MSN.cshtml" CopyToOutputDirectory="PreserveNewest" />
<None Include="$(SharedFilesRoot)Compiler\BlazorServerTagHelpers.razor" CopyToOutputDirectory="PreserveNewest" />
<None Include="$(SharedFilesRoot)Compiler\taghelpers.json" CopyToOutputDirectory="PreserveNewest" />

<Compile Remove="Resources\**\*.*" />
<None Remove="Resources\**\*.*" />

<EmbeddedResource Include="Resources\**\*.*" />
<EmbeddedResource Include="$(SharedFilesRoot)\Tooling\**\*.*" Link="Resources\%(RecursiveDir)%(Filename)%(Extension)" />
<EmbeddedResource Include="$(SharedFilesRoot)\Compiler\MSN.cshtml" Link="Resources\%(FileName)%(Extension)" />
</ItemGroup>

<Import Project="..\..\..\Shared\Microsoft.AspNetCore.Razor.Serialization.Json\Microsoft.AspNetCore.Razor.Serialization.Json.projitems" Label="Shared" />
Expand Down
42 changes: 39 additions & 3 deletions src/Compiler/perf/Microbenchmarks/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@
using System;
using System.Diagnostics;
using System.Linq;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Diagnosers;
using BenchmarkDotNet.Exporters;
using BenchmarkDotNet.Exporters.Json;
using BenchmarkDotNet.Loggers;
using BenchmarkDotNet.Reports;
using BenchmarkDotNet.Running;

namespace Microsoft.AspNetCore.BenchmarkDotNet.Runner;
Expand All @@ -13,9 +19,7 @@ partial class Program
{
private static int Main(string[] args)
{
IConfig config = Debugger.IsAttached
? new DebugInProcessConfig()
: ManualConfig.CreateEmpty();
var config = GetConfig();

var summaries = BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly)
.Run(args, config);
Expand Down Expand Up @@ -54,4 +58,36 @@ private static int Fail(object o, string message)
Console.Error.WriteLine("'{0}' failed, reason: '{1}'", o, message);
return 1;
}

private static IConfig GetConfig()
{
if (Debugger.IsAttached)
{
return new DebugInProcessConfig();
}

return ManualConfig.CreateEmpty()
.WithBuildTimeout(TimeSpan.FromMinutes(15)) // for slow machines
.AddLogger(ConsoleLogger.Default) // log output to console
.AddValidator(DefaultConfig.Instance.GetValidators().ToArray()) // copy default validators
.AddAnalyser(DefaultConfig.Instance.GetAnalysers().ToArray()) // copy default analysers
.AddExporter(MarkdownExporter.GitHub) // export to GitHub markdown
.AddColumnProvider(DefaultColumnProviders.Instance) // display default columns (method name, args etc)
.AddDiagnoser(MemoryDiagnoser.Default)
.AddExporter(JsonExporter.Full)
.AddColumn(StatisticColumn.Median, StatisticColumn.Min, StatisticColumn.Max)
.WithSummaryStyle(SummaryStyle.Default.WithMaxParameterColumnWidth(36)) // the default is 20 and trims too aggressively some benchmark results
.AddDiagnoser(CreateDisassembler());
}

private static DisassemblyDiagnoser CreateDisassembler()
=> new(new DisassemblyDiagnoserConfig(
maxDepth: 1, // TODO: is depth == 1 enough?
syntax: DisassemblySyntax.Masm, // TODO: enable diffable format
printSource: false, // we are not interested in getting C#
printInstructionAddresses: false, // would make the diffing hard, however could be useful to determine alignment
exportGithubMarkdown: false,
exportHtml: false,
exportCombinedDisassemblyReport: false,
exportDiff: false));
}
Loading