Skip to content

Commit daf9480

Browse files
committed
Improve SemanticVersion Performance
1 parent 795d14d commit daf9480

2 files changed

Lines changed: 79 additions & 100 deletions

File tree

dev/DevWinUI.Base/Common/SemanticVersion/ReadOnlyList.cs

Lines changed: 27 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -2,113 +2,60 @@
22

33
namespace DevWinUI;
44

5-
internal static partial class ReadOnlyList
5+
internal sealed partial class ReadOnlyList<T> : IReadOnlyList<T>
66
{
7-
public static ReadOnlyList<T> From<T>(IEnumerable<T> items)
8-
{
9-
var list = new ReadOnlyList<T>(items);
10-
list.Freeze();
11-
return list;
12-
}
13-
}
14-
15-
internal sealed partial class ReadOnlyList<T> : IList<T>, IReadOnlyList<T>
16-
{
17-
private readonly List<T> _items;
7+
private T[] _items = [];
8+
private int _count;
189
private bool _frozen;
1910

2011
public ReadOnlyList()
2112
{
22-
_items = [];
2313
}
2414

2515
public ReadOnlyList(int capacity)
2616
{
27-
_items = new List<T>(capacity);
28-
}
29-
30-
public ReadOnlyList(IEnumerable<T> items)
31-
{
32-
_items = new List<T>(items);
17+
if (capacity > 0)
18+
{
19+
_items = new T[capacity];
20+
}
3321
}
3422

3523
public T this[int index]
3624
{
37-
get => _items[index];
38-
set
25+
get
3926
{
40-
CheckFrozen();
41-
_items[index] = value;
27+
if ((uint)index >= (uint)_count)
28+
throw new ArgumentOutOfRangeException(nameof(index));
29+
30+
return _items[index];
4231
}
4332
}
4433

45-
public int Count => _items.Count;
46-
47-
public bool IsReadOnly => _frozen || ((ICollection<T>)_items).IsReadOnly;
34+
public int Count => _count;
4835

4936
public void Add(T item)
5037
{
51-
CheckFrozen();
52-
_items.Add(item);
53-
}
54-
55-
public void Clear()
56-
{
57-
CheckFrozen();
58-
_items.Clear();
59-
}
60-
61-
public bool Contains(T item)
62-
{
63-
return _items.Contains(item);
64-
}
65-
66-
public void CopyTo(T[] array, int arrayIndex)
67-
{
68-
_items.CopyTo(array, arrayIndex);
69-
}
70-
71-
public void Freeze()
72-
{
73-
_frozen = true;
74-
}
38+
if (_frozen)
39+
throw new InvalidOperationException("The collection is frozen");
7540

76-
public int IndexOf(T item)
77-
{
78-
return _items.IndexOf(item);
79-
}
41+
if (_count == _items.Length)
42+
{
43+
Array.Resize(ref _items, _items.Length == 0 ? 2 : _items.Length * 2);
44+
}
8045

81-
public void Insert(int index, T item)
82-
{
83-
CheckFrozen();
84-
_items.Insert(index, item);
46+
_items[_count++] = item;
8547
}
8648

87-
public bool Remove(T item)
88-
{
89-
CheckFrozen();
90-
return _items.Remove(item);
91-
}
92-
93-
public void RemoveAt(int index)
94-
{
95-
CheckFrozen();
96-
_items.RemoveAt(index);
97-
}
49+
public void Freeze() => _frozen = true;
9850

9951
public IEnumerator<T> GetEnumerator()
10052
{
101-
return _items.GetEnumerator();
102-
}
103-
104-
IEnumerator IEnumerable.GetEnumerator()
105-
{
106-
return _items.GetEnumerator();
53+
for (var i = 0; i < _count; i++)
54+
{
55+
yield return _items[i];
56+
}
10757
}
10858

109-
private void CheckFrozen()
110-
{
111-
if (_frozen)
112-
throw new InvalidOperationException("The collection is frozen");
113-
}
59+
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
11460
}
61+

dev/DevWinUI.Base/Common/SemanticVersion/SemanticVersion.cs

Lines changed: 52 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ namespace DevWinUI;
2929
/// </example>
3030
// https://github.com/semver/semver/blob/master/semver.md
3131
// https://github.com/semver/semver/blob/master/semver.svg
32-
public sealed class SemanticVersion : IFormattable, IComparable, IComparable<SemanticVersion>, IEquatable<SemanticVersion>, IParsable<SemanticVersion>, ISpanParsable<SemanticVersion>
32+
public sealed partial class SemanticVersion : IFormattable, IComparable, IComparable<SemanticVersion>, IEquatable<SemanticVersion>, IParsable<SemanticVersion>, ISpanParsable<SemanticVersion>
3333
{
3434
private static readonly IReadOnlyList<string> EmptyArray = Array.Empty<string>();
3535

@@ -41,6 +41,17 @@ public SemanticVersion(int major, int minor, int patch)
4141
Patch = patch;
4242
}
4343

44+
// Internal constructor used by the parser. The labels are already validated and frozen,
45+
// so this bypasses the per-label validation and list rebuilding done by the public constructors.
46+
private SemanticVersion(int major, int minor, int patch, IReadOnlyList<string> prereleaseLabels, IReadOnlyList<string> metadata)
47+
{
48+
Major = major;
49+
Minor = minor;
50+
Patch = patch;
51+
PrereleaseLabels = prereleaseLabels;
52+
Metadata = metadata;
53+
}
54+
4455
/// <summary>Creates a new semantic version with the specified major, minor, patch numbers and prerelease label.</summary>
4556
public SemanticVersion(int major, int minor, int patch, string? prereleaseLabel)
4657
: this(major, minor, patch, prereleaseLabel, metadata: null)
@@ -80,7 +91,7 @@ public SemanticVersion(int major, int minor, int patch, IEnumerable<string>? pre
8091
if (label is null || !IsPrereleaseIdentifier(label.AsSpan()))
8192
throw new ArgumentException($"Label '{label}' is not valid", nameof(prereleaseLabel));
8293

83-
labels ??= [];
94+
labels ??= prereleaseLabel.TryGetNonEnumeratedCount(out var count) ? new ReadOnlyList<string>(count) : [];
8495
labels.Add(label);
8596
}
8697

@@ -99,7 +110,7 @@ public SemanticVersion(int major, int minor, int patch, IEnumerable<string>? pre
99110
if (label is null || !IsMetadataIdentifier(label.AsSpan()))
100111
throw new ArgumentException($"Label '{label}' is not valid", nameof(metadata));
101112

102-
labels ??= [];
113+
labels ??= metadata.TryGetNonEnumeratedCount(out var count) ? new ReadOnlyList<string>(count) : [];
103114
labels.Add(label);
104115
}
105116

@@ -144,32 +155,30 @@ public string ToString(string? format, IFormatProvider? formatProvider)
144155
if (IsPrerelease)
145156
{
146157
sb.Append('-');
147-
var first = true;
148-
foreach (var label in PrereleaseLabels)
158+
var labels = PrereleaseLabels;
159+
for (var i = 0; i < labels.Count; i++)
149160
{
150-
if (!first)
161+
if (i != 0)
151162
{
152163
sb.Append('.');
153164
}
154165

155-
sb.Append(label);
156-
first = false;
166+
sb.Append(labels[i]);
157167
}
158168
}
159169

160170
if (HasMetadata)
161171
{
162172
sb.Append('+');
163-
var first = true;
164-
foreach (var label in Metadata)
173+
var labels = Metadata;
174+
for (var i = 0; i < labels.Count; i++)
165175
{
166-
if (!first)
176+
if (i != 0)
167177
{
168178
sb.Append('.');
169179
}
170180

171-
sb.Append(label);
172-
first = false;
181+
sb.Append(labels[i]);
173182
}
174183
}
175184

@@ -294,7 +303,7 @@ public static bool TryParse(ReadOnlySpan<char> versionString, [NotNullWhen(retur
294303
if (index != versionString.Length)
295304
return false;
296305

297-
version = new SemanticVersion(major, minor, patch, prereleaseLabels, metadata);
306+
version = new SemanticVersion(major, minor, patch, prereleaseLabels ?? EmptyArray, metadata ?? EmptyArray);
298307
return true;
299308
}
300309

@@ -337,19 +346,24 @@ private static bool TryReadPrerelease(ReadOnlySpan<char> versionString, ref int
337346

338347
private static IReadOnlyList<string> ReadPrereleaseIdentifiers(ReadOnlySpan<char> versionString, ref int index)
339348
{
340-
var result = new List<string>();
349+
ReadOnlyList<string>? result = null;
341350
while (true)
342351
{
343352
if (TryReadPrereleaseIdentifier(versionString, ref index, out var label))
344353
{
354+
result ??= [];
345355
result.Add(label);
346356
}
347357

348358
if (!TryReadDot(versionString, ref index))
349359
break;
350360
}
351361

352-
return result.Count == 0 ? EmptyArray : ReadOnlyList.From(result);
362+
if (result is null)
363+
return EmptyArray;
364+
365+
result.Freeze();
366+
return result;
353367
}
354368

355369
private static bool TryReadMetadata(ReadOnlySpan<char> versionString, ref int index, [NotNullWhen(returnValue: true)] out IReadOnlyList<string>? labels)
@@ -368,7 +382,7 @@ private static bool TryReadMetadata(ReadOnlySpan<char> versionString, ref int in
368382

369383
private static IReadOnlyList<string> TryReadMetadataIdentifiers(ReadOnlySpan<char> versionString, ref int index)
370384
{
371-
List<string>? result = null;
385+
ReadOnlyList<string>? result = null;
372386
while (true)
373387
{
374388
if (TryReadMetadataIdentifier(versionString, ref index, out var label))
@@ -381,7 +395,11 @@ private static IReadOnlyList<string> TryReadMetadataIdentifiers(ReadOnlySpan<cha
381395
break;
382396
}
383397

384-
return result is null ? EmptyArray : ReadOnlyList.From(result);
398+
if (result is null)
399+
return EmptyArray;
400+
401+
result.Freeze();
402+
return result;
385403
}
386404

387405
private static bool IsPrereleaseIdentifier(ReadOnlySpan<char> label)
@@ -406,9 +424,13 @@ private static bool TryReadPrereleaseIdentifier(ReadOnlySpan<char> versionString
406424

407425
if (last > index)
408426
{
409-
value = versionString[index..last].ToString();
410-
if (value[0] != '0' || value.Any(c => !IsDigit(c)))
427+
var span = versionString[index..last];
428+
429+
// A numeric identifier must not have a leading zero. Identifiers that contain at
430+
// least one non-digit, or that do not start with '0', are always valid.
431+
if (span[0] != '0' || ContainsNonDigit(span))
411432
{
433+
value = span.ToString();
412434
index = last;
413435
return true;
414436
}
@@ -417,6 +439,16 @@ private static bool TryReadPrereleaseIdentifier(ReadOnlySpan<char> versionString
417439
value = default;
418440
return false;
419441

442+
static bool ContainsNonDigit(ReadOnlySpan<char> span)
443+
{
444+
foreach (var c in span)
445+
{
446+
if (!IsDigit(c))
447+
return true;
448+
}
449+
450+
return false;
451+
}
420452
}
421453

422454
private static bool IsValidLabelCharacter(char c)

0 commit comments

Comments
 (0)