From c796529e1d38775f68f4d76e22c76dc44aca665e Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Fri, 28 Jun 2024 13:24:33 -0400 Subject: [PATCH] Rewrite TagList for .NET 8+ (#104132) Renamed the existing TagList.cs file to be TagList.netfx.cs, then copied it to a TagList.netcore.cs file and rewrote the guts of it to use [InlineArray] for the embedded key/value pairs. --- ...System.Diagnostics.DiagnosticSource.csproj | 3 +- .../Diagnostics/Metrics/Instrument.netcore.cs | 13 +- .../Diagnostics/Metrics/TagList.netcore.cs | 340 ++++++++++++++++++ .../Metrics/{TagList.cs => TagList.netfx.cs} | 0 4 files changed, 344 insertions(+), 12 deletions(-) create mode 100644 src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/TagList.netcore.cs rename src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/{TagList.cs => TagList.netfx.cs} (100%) diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/src/System.Diagnostics.DiagnosticSource.csproj b/src/libraries/System.Diagnostics.DiagnosticSource/src/System.Diagnostics.DiagnosticSource.csproj index 79efda4894eae..eafefa6e9fa33 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/src/System.Diagnostics.DiagnosticSource.csproj +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System.Diagnostics.DiagnosticSource.csproj @@ -74,7 +74,6 @@ System.Diagnostics.DiagnosticSource - @@ -93,6 +92,7 @@ System.Diagnostics.DiagnosticSource + @@ -105,6 +105,7 @@ System.Diagnostics.DiagnosticSource + diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/Instrument.netcore.cs b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/Instrument.netcore.cs index 2a862fcb62578..839484af1cf35 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/Instrument.netcore.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/Instrument.netcore.cs @@ -47,16 +47,7 @@ protected void RecordMeasurement(T measurement, KeyValuePair ta /// /// The measurement value. /// A of tags associated with the measurement. - protected void RecordMeasurement(T measurement, in TagList tagList) - { - KeyValuePair[]? tags = tagList.Tags; - if (tags is not null) - { - RecordMeasurement(measurement, tags.AsSpan(0, tagList.Count)); - return; - } - - RecordMeasurement(measurement, MemoryMarshal.CreateReadOnlySpan(ref Unsafe.AsRef(in tagList.Tag1), tagList.Count)); - } + protected void RecordMeasurement(T measurement, in TagList tagList) => + RecordMeasurement(measurement, tagList.Tags); } } diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/TagList.netcore.cs b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/TagList.netcore.cs new file mode 100644 index 0000000000000..6da234ec4ea68 --- /dev/null +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/TagList.netcore.cs @@ -0,0 +1,340 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; + +namespace System.Diagnostics +{ + /// + /// Represents a list of tags that can be accessed by index. Provides methods to search, sort, and manipulate lists. + /// + /// + /// TagList can be used in the scenarios which need to optimize for memory allocations. TagList will avoid allocating any memory when using up to eight tags. + /// Using more than eight tags will cause allocating memory to store the tags. + /// Public static (Shared in Visual Basic) members of this type are thread safe. Any instance members are not guaranteed to be thread safe. + /// + [StructLayout(LayoutKind.Sequential)] + public struct TagList : IList>, IReadOnlyList> + { + private const int OverflowAdditionalCapacity = 8; + + // Up to eight tags are stored in an inline array. Once there are more items than will fit in the inline array, + // an array is allocated to store all the items and the inline array is abandoned. Even if the size shrinks down + // to below eight items, the array continues to be used. + + private InlineTags _tags; + private KeyValuePair[]? _overflowTags; + private int _tagsCount; + + /// + /// Initializes a new instance of the TagList structure using the specified . + /// + /// A span of tags to initialize the list with. + public TagList(params ReadOnlySpan> tagList) : this() + { + _tagsCount = tagList.Length; + + scoped Span> tags = _tagsCount <= InlineTags.Length ? + _tags : + _overflowTags = new KeyValuePair[_tagsCount + OverflowAdditionalCapacity]; + + tagList.CopyTo(tags); + } + + /// + /// Gets the number of tags contained in the . + /// + public readonly int Count => _tagsCount; + + /// + /// Gets a value indicating whether the is read-only. This property will always return . + /// + public readonly bool IsReadOnly => false; + + /// + /// Gets or sets the tags at the specified index. + /// + /// is not a valid index in the . + public KeyValuePair this[int index] + { + readonly get + { + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)index, (uint)_tagsCount, nameof(index)); + + return _overflowTags is null ? _tags[index] : _overflowTags[index]; + } + + set + { + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)index, (uint)_tagsCount, nameof(index)); + + if (_overflowTags is null) + { + _tags[index] = value; + } + else + { + _overflowTags[index] = value; + } + } + } + + /// + /// Adds a tag with the provided and to the list. + /// + /// The tag key. + /// The tag value. + public void Add(string key, object? value) => + Add(new KeyValuePair(key, value)); + + /// + /// Adds a tag to the list. + /// + /// Key and value pair of the tag to add to the list. + public void Add(KeyValuePair tag) + { + int count = _tagsCount; + if (_overflowTags is null && (uint)count < InlineTags.Length) + { + _tags[count] = tag; + _tagsCount++; + } + else + { + AddToOverflow(tag); + } + } + + /// + /// Adds a tag to the overflow list. Slow path outlined from Add to maximize the chance for the fast path to be inlined. + /// + /// Key and value pair of the tag to add to the list. + private void AddToOverflow(KeyValuePair tag) + { + Debug.Assert(_overflowTags is not null || _tagsCount == InlineTags.Length); + + if (_overflowTags is null) + { + _overflowTags = new KeyValuePair[InlineTags.Length + OverflowAdditionalCapacity]; + ((ReadOnlySpan>)_tags).CopyTo(_overflowTags); + } + else if (_tagsCount == _overflowTags.Length) + { + Array.Resize(ref _overflowTags, _tagsCount + OverflowAdditionalCapacity); + } + + _overflowTags[_tagsCount] = tag; + _tagsCount++; + } + + /// + /// Copies the contents of this into a destination span. + /// Inserts an element into this at the specified index. + /// + /// The destination object. + /// The number of elements in the source is greater than the number of elements that the destination span. + public readonly void CopyTo(Span> tags) + { + if (tags.Length < _tagsCount) + { + throw new ArgumentException(SR.Arg_BufferTooSmall); + } + + Tags.CopyTo(tags); + } + + /// + /// Copies the entire to a compatible one-dimensional array, starting at the specified index of the target array. + /// + /// The one-dimensional Array that is the destination of the elements copied from . The Array must have zero-based indexing. + /// The zero-based index in at which copying begins. + /// is null. + /// is less than 0 or greater that or equal the length. + public readonly void CopyTo(KeyValuePair[] array, int arrayIndex) + { + ArgumentNullException.ThrowIfNull(array); + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)arrayIndex, (uint)array.Length, nameof(arrayIndex)); + + CopyTo(array.AsSpan(arrayIndex)); + } + + /// + /// Inserts an element into the at the specified index. + /// + /// The zero-based index at which item should be inserted. + /// The tag to insert. + /// index is less than 0 or is greater than . + public void Insert(int index, KeyValuePair item) + { + if (index == _tagsCount) + { + Add(item); + return; + } + + ArgumentOutOfRangeException.ThrowIfGreaterThan((uint)index, (uint)_tagsCount, nameof(index)); + + if (_tagsCount == InlineTags.Length && _overflowTags is null) + { + _overflowTags = new KeyValuePair[InlineTags.Length + OverflowAdditionalCapacity]; + ((ReadOnlySpan>)_tags).CopyTo(_overflowTags); + } + + if (_overflowTags is not null) + { + if (_tagsCount == _overflowTags.Length) + { + Array.Resize(ref _overflowTags, _tagsCount + OverflowAdditionalCapacity); + } + + _overflowTags.AsSpan(index, _tagsCount - index).CopyTo(_overflowTags.AsSpan(index + 1)); + _overflowTags[index] = item; + } + else + { + Span> tags = _tags; + tags.Slice(index, _tagsCount - index).CopyTo(tags.Slice(index + 1)); + tags[index] = item; + } + + _tagsCount++; + } + + /// + /// Removes the element at the specified index of the . + /// + /// The zero-based index of the element to remove. + /// index is less than 0 or is greater than . + public void RemoveAt(int index) + { + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)index, (uint)_tagsCount, nameof(index)); + + Span> tags = _overflowTags is not null ? _overflowTags : _tags; + tags.Slice(index + 1, _tagsCount - index - 1).CopyTo(tags.Slice(index)); + _tagsCount--; + } + + /// + /// Removes all elements from the . + /// + public void Clear() => + _tagsCount = 0; + + /// + /// Determines whether an tag is in the . + /// + /// The tag to locate in the . + /// if item is found in the ; otherwise, . + public readonly bool Contains(KeyValuePair item) => + IndexOf(item) >= 0; + + /// + /// Removes the first occurrence of a specific object from the . + /// + /// The tag to remove from the . + /// if item is successfully removed; otherwise, . This method also returns if item was not found in the . + public bool Remove(KeyValuePair item) + { + int index = IndexOf(item); + if (index >= 0) + { + RemoveAt(index); + return true; + } + + return false; + } + + /// + /// Returns an enumerator that iterates through the . + /// + /// Returns an enumerator that iterates through the . + public readonly IEnumerator> GetEnumerator() => new Enumerator(in this); + + /// + /// Returns an enumerator that iterates through the . + /// + /// Returns an enumerator that iterates through the . + readonly IEnumerator IEnumerable.GetEnumerator() => new Enumerator(in this); + + /// + /// Searches for the specified tag and returns the zero-based index of the first occurrence within the entire . + /// + /// The tag to locate in the . + public readonly int IndexOf(KeyValuePair item) + { + ReadOnlySpan> tags = + _overflowTags is not null ? _overflowTags : + _tags; + + tags = tags.Slice(0, _tagsCount); + + if (item.Value is not null) + { + for (int i = 0; i < tags.Length; i++) + { + if (item.Key == tags[i].Key && item.Value.Equals(tags[i].Value)) + { + return i; + } + } + } + else + { + for (int i = 0; i < tags.Length; i++) + { + if (item.Key == tags[i].Key && tags[i].Value is null) + { + return i; + } + } + } + + return -1; + } + + [UnscopedRef] + internal readonly ReadOnlySpan> Tags => + _overflowTags is not null ? _overflowTags.AsSpan(0, _tagsCount) : + ((ReadOnlySpan>)_tags).Slice(0, _tagsCount); + + [InlineArray(8)] + private struct InlineTags + { + public const int Length = 8; + private KeyValuePair _first; + } + + public struct Enumerator : IEnumerator> + { + private TagList _tagList; + private int _index; + + internal Enumerator(in TagList tagList) + { + _index = -1; + _tagList = tagList; + } + + public KeyValuePair Current => _tagList[_index]; + + object IEnumerator.Current => _tagList[_index]; + + public void Dispose() { _index = _tagList.Count; } + + public bool MoveNext() + { + _index++; + return _index < _tagList.Count; + } + + public void Reset() => _index = -1; + } + } +} diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/TagList.cs b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/TagList.netfx.cs similarity index 100% rename from src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/TagList.cs rename to src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/TagList.netfx.cs