Skip to content

Commit d964cd9

Browse files
TagHelperCollection Part 1: The new bits (#12504)
| [Prelude](#12503) | Part 1 | [Part 2](#12505) | [Part 3](#12506) | [Part 4](#12507) | [Part 5](#12509) | This pull request represents all new code! It introduces a new immutable collection type, `TagHelperCollection`, that is designed to contain `TagHelperDescriptors`. It is built with the following principles: 1. Guarantee that collections never contain duplicate tag helpers. 2. Collections can be compared efficiently via checksums. 3. Multiple collections can be merged into a single collection efficiently, with minimal array copying. 4. Determining whether a collection contains a tag helper is efficient. > [!TIP] > Since this is all new code, I recommend reviewing commit-by-commit. Each commit represents a cohesive portion of new code. ---- CI Build: https://dev.azure.com/dnceng/internal/_build/results?buildId=2842145&view=results Toolset Run: https://dev.azure.com/dnceng/internal/_build/results?buildId=2842216&view=results
2 parents a31ef88 + cc2d4c2 commit d964cd9

23 files changed

+5999
-0
lines changed

src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TagHelperCollectionTest.cs

Lines changed: 3610 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Collections;
5+
using System.Collections.Generic;
6+
7+
namespace Microsoft.AspNetCore.Razor.Language;
8+
9+
public abstract partial class TagHelperCollection
10+
{
11+
public sealed partial class Builder
12+
{
13+
public ref struct Enumerator(Builder builder)
14+
{
15+
private int _index = -1;
16+
17+
public readonly TagHelperDescriptor Current => builder[_index];
18+
19+
public bool MoveNext()
20+
{
21+
if (_index < builder.Count - 1)
22+
{
23+
_index++;
24+
return true;
25+
}
26+
27+
return false;
28+
}
29+
30+
public void Reset()
31+
{
32+
_index = -1;
33+
}
34+
35+
public void Dispose()
36+
{
37+
Reset();
38+
}
39+
}
40+
41+
private sealed class EnumeratorImpl(Builder builder) : IEnumerator<TagHelperDescriptor>
42+
{
43+
private int _index = -1;
44+
45+
public TagHelperDescriptor Current => builder[_index];
46+
47+
object IEnumerator.Current => Current;
48+
49+
public bool MoveNext()
50+
{
51+
if (_index < builder.Count - 1)
52+
{
53+
_index++;
54+
return true;
55+
}
56+
57+
return false;
58+
}
59+
60+
public void Reset()
61+
{
62+
_index = -1;
63+
}
64+
65+
public void Dispose()
66+
{
67+
_index = -1;
68+
}
69+
}
70+
}
71+
}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Collections;
6+
using System.Collections.Generic;
7+
using System.Collections.Immutable;
8+
using System.Threading;
9+
using Microsoft.AspNetCore.Razor.PooledObjects;
10+
using Microsoft.AspNetCore.Razor.Utilities;
11+
12+
namespace Microsoft.AspNetCore.Razor.Language;
13+
14+
public abstract partial class TagHelperCollection
15+
{
16+
public sealed partial class Builder : ICollection<TagHelperDescriptor>, IReadOnlyList<TagHelperDescriptor>, IDisposable
17+
{
18+
// Create new pooled builders and sets with a larger initial capacity to limit growth.
19+
private const int InitialCapacity = 256;
20+
21+
// Builders and sets are typically large, so allow them to stay larger when returned to their pool.
22+
private const int MaximumObjectSize = 2048;
23+
24+
private static readonly ArrayBuilderPool<TagHelperDescriptor> s_arrayBuilderPool =
25+
ArrayBuilderPool<TagHelperDescriptor>.Create(InitialCapacity, MaximumObjectSize);
26+
27+
private ImmutableArray<TagHelperDescriptor>.Builder _items;
28+
private HashSet<Checksum> _set;
29+
30+
public Builder()
31+
{
32+
_items = s_arrayBuilderPool.Get();
33+
_set = ChecksumSetPool.Default.Get();
34+
}
35+
36+
public void Dispose()
37+
{
38+
var items = Interlocked.Exchange(ref _items, null!);
39+
if (items is not null)
40+
{
41+
s_arrayBuilderPool.Return(items);
42+
}
43+
44+
var set = Interlocked.Exchange(ref _set, null!);
45+
if (set is not null)
46+
{
47+
ChecksumSetPool.Default.Return(set);
48+
}
49+
}
50+
51+
public bool IsEmpty => Count == 0;
52+
53+
public int Count => _items.Count;
54+
55+
public bool IsReadOnly => false;
56+
57+
public TagHelperDescriptor this[int index]
58+
{
59+
get
60+
{
61+
ArgHelper.ThrowIfNegative(index);
62+
ArgHelper.ThrowIfGreaterThanOrEqual(index, Count);
63+
64+
return _items[index];
65+
}
66+
}
67+
68+
public bool Add(TagHelperDescriptor item)
69+
{
70+
if (!_set.Add(item.Checksum))
71+
{
72+
return false;
73+
}
74+
75+
_items.Add(item);
76+
return true;
77+
}
78+
79+
void ICollection<TagHelperDescriptor>.Add(TagHelperDescriptor item)
80+
=> Add(item);
81+
82+
public void AddRange(TagHelperCollection items)
83+
{
84+
foreach (var item in items)
85+
{
86+
if (_set.Add(item.Checksum))
87+
{
88+
_items.Add(item);
89+
}
90+
}
91+
}
92+
93+
public void AddRange(ReadOnlySpan<TagHelperDescriptor> span)
94+
{
95+
foreach (var item in span)
96+
{
97+
if (_set.Add(item.Checksum))
98+
{
99+
_items.Add(item);
100+
}
101+
}
102+
}
103+
104+
public void AddRange(IEnumerable<TagHelperDescriptor> source)
105+
{
106+
foreach (var item in source)
107+
{
108+
if (_set.Add(item.Checksum))
109+
{
110+
_items.Add(item);
111+
}
112+
}
113+
}
114+
115+
public void Clear()
116+
{
117+
_items.Clear();
118+
_set.Clear();
119+
}
120+
121+
public bool Contains(TagHelperDescriptor item)
122+
=> _set.Contains(item.Checksum);
123+
124+
public void CopyTo(TagHelperDescriptor[] array, int arrayIndex)
125+
=> _items.CopyTo(array, arrayIndex);
126+
127+
public bool Remove(TagHelperDescriptor item)
128+
=> _set.Remove(item.Checksum) && _items.Remove(item);
129+
130+
public TagHelperCollection ToCollection()
131+
{
132+
if (_items.Count == 0)
133+
{
134+
return Empty;
135+
}
136+
137+
var array = _items.ToImmutable();
138+
return new SingleSegmentCollection(array.AsMemory());
139+
}
140+
141+
public Enumerator GetEnumerator()
142+
=> new(this);
143+
144+
IEnumerator<TagHelperDescriptor> IEnumerable<TagHelperDescriptor>.GetEnumerator()
145+
=> new EnumeratorImpl(this);
146+
147+
IEnumerator IEnumerable.GetEnumerator()
148+
=> new EnumeratorImpl(this);
149+
}
150+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Collections.Generic;
5+
using Microsoft.AspNetCore.Razor.PooledObjects;
6+
using Microsoft.AspNetCore.Razor.Utilities;
7+
8+
namespace Microsoft.AspNetCore.Razor.Language;
9+
10+
public abstract partial class TagHelperCollection
11+
{
12+
private sealed class ChecksumSetPool : CustomObjectPool<HashSet<Checksum>>
13+
{
14+
private const int MaximumObjectSize = 2048;
15+
16+
public static readonly ChecksumSetPool Default = new(Policy.Instance, DefaultPoolSize);
17+
18+
private ChecksumSetPool(PooledObjectPolicy policy, Optional<int> poolSize)
19+
: base(policy, poolSize)
20+
{
21+
}
22+
23+
private sealed class Policy : PooledObjectPolicy
24+
{
25+
public static readonly Policy Instance = new();
26+
27+
private Policy()
28+
{
29+
}
30+
31+
public override HashSet<Checksum> Create()
32+
{
33+
#if NET
34+
return new(capacity: MaximumObjectSize);
35+
#else
36+
return [];
37+
#endif
38+
}
39+
40+
public override bool Return(HashSet<Checksum> set)
41+
{
42+
var count = set.Count;
43+
set.Clear();
44+
45+
if (count > MaximumObjectSize)
46+
{
47+
#if NET9_0_OR_GREATER
48+
set.TrimExcess(MaximumObjectSize);
49+
#elif NET8_0
50+
set.TrimExcess();
51+
set.EnsureCapacity(MaximumObjectSize);
52+
#else
53+
set.TrimExcess();
54+
#endif
55+
}
56+
57+
return true;
58+
}
59+
}
60+
}
61+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using Microsoft.AspNetCore.Razor.Utilities;
6+
7+
namespace Microsoft.AspNetCore.Razor.Language;
8+
9+
public abstract partial class TagHelperCollection
10+
{
11+
/// <summary>
12+
/// Represents an immutable, empty collection of tag helpers.
13+
/// </summary>
14+
private sealed class EmptyCollection : TagHelperCollection
15+
{
16+
public static readonly EmptyCollection Instance = new();
17+
18+
private EmptyCollection()
19+
{
20+
}
21+
22+
public override int Count => 0;
23+
24+
public override TagHelperDescriptor this[int index]
25+
=> throw new IndexOutOfRangeException();
26+
27+
internal override Checksum Checksum => Checksum.Null;
28+
29+
public override int IndexOf(TagHelperDescriptor item) => -1;
30+
31+
public override void CopyTo(Span<TagHelperDescriptor> destination)
32+
{
33+
// Nothing to copy.
34+
}
35+
36+
protected override int SegmentCount => 0;
37+
38+
protected override ReadOnlyMemory<TagHelperDescriptor> GetSegment(int index)
39+
=> Assumed.Unreachable<ReadOnlyMemory<TagHelperDescriptor>>();
40+
}
41+
}

0 commit comments

Comments
 (0)