From: Tarek Mahmoud Sayed Date: Tue, 17 Aug 2021 04:08:12 +0000 (-0700) Subject: Add more Metrics API overloads taking tags (#56940) X-Git-Tag: accepted/tizen/unified/20220110.054933~350 X-Git-Url: http://review.tizen.org/git/?a=commitdiff_plain;h=3d2bf6e47b5aac8827515f958dc88faf121c06c9;p=platform%2Fupstream%2Fdotnet%2Fruntime.git Add more Metrics API overloads taking tags (#56940) * Add more Metrics API overloads taking tags --- diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/ref/System.Diagnostics.DiagnosticSourceActivity.cs b/src/libraries/System.Diagnostics.DiagnosticSource/ref/System.Diagnostics.DiagnosticSourceActivity.cs index 68fa62b..fd218a7 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/ref/System.Diagnostics.DiagnosticSourceActivity.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/ref/System.Diagnostics.DiagnosticSourceActivity.cs @@ -267,6 +267,38 @@ namespace System.Diagnostics public static DistributedContextPropagator CreatePassThroughPropagator() { throw null; } public static DistributedContextPropagator CreateNoOutputPropagator() { throw null; } } + [System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)] + public struct TagList : System.Collections.Generic.IList>, System.Collections.Generic.IReadOnlyList> + { + public TagList(System.ReadOnlySpan> tagList) : this() { throw null; } + public readonly int Count => throw null; + public readonly bool IsReadOnly => throw null; + public System.Collections.Generic.KeyValuePair this[int index] + { + readonly get { { throw null; } } + set { { throw null; } } + } + public void Add(string key, object? value) { throw null; } + public void Add(System.Collections.Generic.KeyValuePair tag) { throw null; } + public readonly void CopyTo(System.Span> tags) { throw null; } + public void Insert(int index, System.Collections.Generic.KeyValuePair item) { throw null; } + public void RemoveAt(int index) { throw null; } + public void Clear() { throw null; } + public readonly bool Contains(System.Collections.Generic.KeyValuePair item) { throw null; } + public readonly void CopyTo(System.Collections.Generic.KeyValuePair[] array, int arrayIndex) { throw null; } + public bool Remove(System.Collections.Generic.KeyValuePair item) { throw null; } + public readonly System.Collections.Generic.IEnumerator> GetEnumerator() { throw null; } + readonly System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; } + public readonly int IndexOf(System.Collections.Generic.KeyValuePair item) { throw null; } + public struct Enumerator : System.Collections.Generic.IEnumerator>, System.Collections.IEnumerator + { + public System.Collections.Generic.KeyValuePair Current => throw null; + object System.Collections.IEnumerator.Current => throw null; + public void Dispose() { throw null; } + public bool MoveNext() { throw null; } + public void Reset() { throw null; } + } + } } namespace System.Diagnostics.Metrics @@ -279,6 +311,7 @@ namespace System.Diagnostics.Metrics public void Add(T delta, System.Collections.Generic.KeyValuePair tag1, System.Collections.Generic.KeyValuePair tag2, System.Collections.Generic.KeyValuePair tag3) { throw null; } public void Add(T delta, ReadOnlySpan> tags) { throw null; } public void Add(T delta, params System.Collections.Generic.KeyValuePair[] tags) { throw null; } + public void Add(T delta, in TagList tagList) { throw null; } internal Counter(Meter meter, string name, string? unit, string? description) : base(meter, name, unit, description) { throw null; } } @@ -289,6 +322,7 @@ namespace System.Diagnostics.Metrics public void Record(T value, System.Collections.Generic.KeyValuePair tag) { throw null; } public void Record(T value, System.Collections.Generic.KeyValuePair tag1, System.Collections.Generic.KeyValuePair tag2) { throw null; } public void Record(T value, System.Collections.Generic.KeyValuePair tag1, System.Collections.Generic.KeyValuePair tag2, System.Collections.Generic.KeyValuePair tag3) { throw null; } + public void Record(T value, in TagList tagList) { throw null; } public void Record(T value, ReadOnlySpan> tags) { throw null; } public void Record(T value, params System.Collections.Generic.KeyValuePair[] tags) { throw null; } } @@ -310,6 +344,7 @@ namespace System.Diagnostics.Metrics protected void RecordMeasurement(T measurement, System.Collections.Generic.KeyValuePair tag) { throw null; } protected void RecordMeasurement(T measurement, System.Collections.Generic.KeyValuePair tag1, System.Collections.Generic.KeyValuePair tag2) { throw null; } protected void RecordMeasurement(T measurement, System.Collections.Generic.KeyValuePair tag1, System.Collections.Generic.KeyValuePair tag2, System.Collections.Generic.KeyValuePair tag3) { throw null; } + protected void RecordMeasurement(T measurement, in TagList tagList) { throw null; } protected void RecordMeasurement(T measurement, ReadOnlySpan> tags) { throw null; } } public readonly struct Measurement where T : struct diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/src/Resources/Strings.resx b/src/libraries/System.Diagnostics.DiagnosticSource/src/Resources/Strings.resx index 2de05b8..0d9ad07 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/src/Resources/Strings.resx +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/Resources/Strings.resx @@ -168,4 +168,7 @@ {0} is unsupported type for this operation. The only supported types are byte, short, int, long, float, double, and decimal. + + Destination buffer is not long enough to copy all the items in the list. + \ No newline at end of file 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 c51511a..bac2f7c 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/src/System.Diagnostics.DiagnosticSource.csproj +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System.Diagnostics.DiagnosticSource.csproj @@ -72,10 +72,11 @@ System.Diagnostics.DiagnosticSource + - + diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/Counter.cs b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/Counter.cs index 3e7b3d1..d107bd7 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/Counter.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/Counter.cs @@ -66,5 +66,12 @@ namespace System.Diagnostics.Metrics /// The increment measurement. /// A list of key-value pair tags associated with the measurement. public void Add(T delta, params KeyValuePair[] tags) => RecordMeasurement(delta, tags.AsSpan()); + + /// + /// Record the increment value of the measurement. + /// + /// The measurement value. + /// A of tags associated with the measurement. + public void Add(T delta, in TagList tagList) => RecordMeasurement(delta, in tagList); } } \ No newline at end of file diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/Histogram.cs b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/Histogram.cs index 6bb4711..1000815 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/Histogram.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/Histogram.cs @@ -66,5 +66,12 @@ namespace System.Diagnostics.Metrics /// The measurement value. /// A list of key-value pair tags associated with the measurement. public void Record(T value, params KeyValuePair[] tags) => RecordMeasurement(value, tags.AsSpan()); + + /// + /// Record a measurement value. + /// + /// The measurement value. + /// A of tags associated with the measurement. + public void Record(T value, in TagList tagList) => RecordMeasurement(value, in tagList); } } \ No newline at end of file diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/Instrument.cs b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/Instrument.cs index 542a6dd..d79c22f 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/Instrument.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/Instrument.cs @@ -13,11 +13,7 @@ namespace System.Diagnostics.Metrics #endif public abstract class Instrument { -#if NO_ARRAY_EMPTY_SUPPORT - internal static KeyValuePair[] EmptyTags { get; } = new KeyValuePair[0]; -#else internal static KeyValuePair[] EmptyTags => Array.Empty>(); -#endif // NO_ARRAY_EMPTY_SUPPORT // The SyncObject is used to synchronize the following operations: // - Instrument.Publish() 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 cad3bff..ffac3c0 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 @@ -7,12 +7,44 @@ using System.Runtime.CompilerServices; namespace System.Diagnostics.Metrics { + // We define a separate structure for the different number of tags. + // The reason is, the performance is critical for the Metrics APIs that accept tags parameters. + // We are trying to reduce big tags structure initialization inside the APIs when using fewer tags. + [StructLayout(LayoutKind.Sequential)] - internal struct TagsBag + internal struct OneTagBag + { + internal KeyValuePair Tag1; + internal OneTagBag(KeyValuePair tag) + { + Tag1 = tag; + } + } + + [StructLayout(LayoutKind.Sequential)] + internal struct TwoTagsBag + { + internal KeyValuePair Tag1; + internal KeyValuePair Tag2; + internal TwoTagsBag(KeyValuePair tag1, KeyValuePair tag2) + { + Tag1 = tag1; + Tag2 = tag2; + } + } + + [StructLayout(LayoutKind.Sequential)] + internal struct ThreeTagsBag { internal KeyValuePair Tag1; internal KeyValuePair Tag2; internal KeyValuePair Tag3; + internal ThreeTagsBag(KeyValuePair tag1, KeyValuePair tag2, KeyValuePair tag3) + { + Tag1 = tag1; + Tag2 = tag2; + Tag3 = tag3; + } } /// @@ -30,8 +62,7 @@ namespace System.Diagnostics.Metrics /// A key-value pair tag associated with the measurement. protected void RecordMeasurement(T measurement, KeyValuePair tag) { - TagsBag tags; - tags.Tag1 = tag; + OneTagBag tags = new OneTagBag(tag); RecordMeasurement(measurement, MemoryMarshal.CreateReadOnlySpan(ref tags.Tag1, 1)); } @@ -44,9 +75,7 @@ namespace System.Diagnostics.Metrics /// A second key-value pair tag associated with the measurement. protected void RecordMeasurement(T measurement, KeyValuePair tag1, KeyValuePair tag2) { - TagsBag tags; - tags.Tag1 = tag1; - tags.Tag2 = tag2; + TwoTagsBag tags = new TwoTagsBag(tag1, tag2); RecordMeasurement(measurement, MemoryMarshal.CreateReadOnlySpan(ref tags.Tag1, 2)); } @@ -60,12 +89,26 @@ namespace System.Diagnostics.Metrics /// A third key-value pair tag associated with the measurement. protected void RecordMeasurement(T measurement, KeyValuePair tag1, KeyValuePair tag2, KeyValuePair tag3) { - TagsBag tags; - tags.Tag1 = tag1; - tags.Tag2 = tag2; - tags.Tag3 = tag3; + ThreeTagsBag tags = new ThreeTagsBag(tag1, tag2, tag3); RecordMeasurement(measurement, MemoryMarshal.CreateReadOnlySpan(ref tags.Tag1, 3)); } + + /// + /// Record the measurement by notifying all objects which listening to this instrument. + /// + /// 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().Slice(0, tagList.Count)); + return; + } + + RecordMeasurement(measurement, MemoryMarshal.CreateReadOnlySpan(ref Unsafe.AsRef(in tagList.Tag1), tagList.Count)); + } } } \ No newline at end of file diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/Instrument.netfx.cs b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/Instrument.netfx.cs index 4367b11..2dc030f 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/Instrument.netfx.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/Instrument.netfx.cs @@ -19,7 +19,7 @@ namespace System.Diagnostics.Metrics { [ThreadStatic] private KeyValuePair[] ts_tags; - private const int MaxTagsCount = 3; + private const int MaxTagsCount = 8; /// /// Record the measurement by notifying all objects which listening to this instrument. @@ -68,5 +68,44 @@ namespace System.Diagnostics.Metrics RecordMeasurement(measurement, tags.AsSpan().Slice(0, 3)); ts_tags = tags; } + + /// + /// Record the measurement by notifying all objects which listening to this instrument. + /// + /// 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().Slice(0, tagList.Count)); + return; + } + + tags = ts_tags ?? new KeyValuePair[MaxTagsCount]; + Debug.Assert(tagList.Count <= MaxTagsCount); + switch (tagList.Count) + { + case 8: tags[7] = tagList.Tag8; goto case 7; + case 7: tags[6] = tagList.Tag7; goto case 6; + case 6: tags[5] = tagList.Tag6; goto case 5; + case 5: tags[4] = tagList.Tag5; goto case 4; + case 4: tags[3] = tagList.Tag4; goto case 3; + case 3: tags[2] = tagList.Tag3; goto case 2; + case 2: tags[1] = tagList.Tag2; goto case 1; + case 1: tags[0] = tagList.Tag1; break; + case 0: return; // no need to report anything + default: + Debug.Assert(false); + return; + } + + ts_tags = null; + + RecordMeasurement(measurement, tags.AsSpan().Slice(0, tagList.Count)); + + ts_tags = tags; + } } } \ No newline at end of file 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.cs new file mode 100644 index 0000000..ab7f416 --- /dev/null +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/TagList.cs @@ -0,0 +1,556 @@ +// 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.Threading; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace System.Diagnostics +{ + // This struct is purposed to store a list of tags. It avoids allocating any memory till we have more than eight tags to store, then it will create an array at that time. + // To avoid the allocations, the struct define eight fields Tag1, Tag2,...,Tag8 to store up to eight tags. If need to store more than eight tags, it will create + // a managed array at that time. + // The main consumer of this struct is the Metrics APIs which create a span from this struct to send it with the reported measurements. + // As we need to have this struct work on NetFX too, we couldn't use any .NET collection as we need to create a span from such collection. + // Instead, we use regular managed array and we expand it as needed. It is easy to create a span from such managed array without allocating more memory. + + /// + /// 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. + /// +#if ALLOW_PARTIALLY_TRUSTED_CALLERS + [System.Security.SecuritySafeCriticalAttribute] +#endif + [StructLayout(LayoutKind.Sequential)] + public struct TagList : IList>, IReadOnlyList> + { + internal KeyValuePair Tag1; + internal KeyValuePair Tag2; + internal KeyValuePair Tag3; + internal KeyValuePair Tag4; + internal KeyValuePair Tag5; + internal KeyValuePair Tag6; + internal KeyValuePair Tag7; + internal KeyValuePair Tag8; + private int _tagsCount; + private KeyValuePair[]? _overflowTags; + private const int OverflowAdditionalCapacity = 8; + + /// + /// Initializes a new instance of the TagList structure using the specified . + /// + /// A span of tags to initialize the list with. + public TagList(ReadOnlySpan> tagList) : this() + { + _tagsCount = tagList.Length; + switch (_tagsCount) + { + case 8: + Tag8 = tagList[7]; + goto case 7; + + case 7: + Tag7 = tagList[6]; + goto case 6; + + case 6: + Tag6 = tagList[5]; + goto case 5; + + case 5: + Tag5 = tagList[4]; + goto case 4; + + case 4: + Tag4 = tagList[3]; + goto case 3; + + case 3: + Tag3 = tagList[2]; + goto case 2; + + case 2: + Tag2 = tagList[1]; + goto case 1; + + case 1: + Tag1 = tagList[0]; + break; + + case 0: return; + + default: + Debug.Assert(_tagsCount > 8); + _overflowTags = new KeyValuePair[_tagsCount + OverflowAdditionalCapacity]; // Add extra slots for more tags to add if needed + tagList.CopyTo(_overflowTags); + break; + } + } + + /// + /// 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 + { + if ((uint)index >= (uint)_tagsCount) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } + + if (_overflowTags is not null) + { + Debug.Assert(index < _overflowTags.Length); + return _overflowTags[index]; + } + + Debug.Assert(index <= 7); + + return index switch + { + 0 => Tag1, + 1 => Tag2, + 2 => Tag3, + 3 => Tag4, + 4 => Tag5, + 5 => Tag6, + 6 => Tag7, + 7 => Tag8, + _ => default, // we shouldn't come here anyway. + }; + } + + set + { + if ((uint)index >= (uint)_tagsCount) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } + + if (_overflowTags is not null) + { + Debug.Assert(index < _overflowTags.Length); + _overflowTags[index] = value; + return; + } + + switch (index) + { + case 0: Tag1 = value; break; + case 1: Tag2 = value; break; + case 2: Tag3 = value; break; + case 3: Tag4 = value; break; + case 4: Tag5 = value; break; + case 5: Tag6 = value; break; + case 6: Tag7 = value; break; + case 7: Tag8 = value; break; + default: + Debug.Assert(false); + break; + } + } + } + + /// + /// 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) + { + if (_overflowTags is not null) + { + if (_tagsCount == _overflowTags.Length) + { + Array.Resize(ref _overflowTags, _tagsCount + OverflowAdditionalCapacity); + } + + _overflowTags[_tagsCount++] = tag; + return; + } + + Debug.Assert(_tagsCount <= 8); + + switch (_tagsCount) + { + case 0: Tag1 = tag; break; + case 1: Tag2 = tag; break; + case 2: Tag3 = tag; break; + case 3: Tag4 = tag; break; + case 4: Tag5 = tag; break; + case 5: Tag6 = tag; break; + case 6: Tag7 = tag; break; + case 7: Tag8 = tag; break; + case 8: + Debug.Assert(_overflowTags is null); + MoveTagsToTheArray(); + Debug.Assert(_overflowTags is not null); + _overflowTags[8] = tag; + break; + default: + // We shouldn't come here. + Debug.Assert(_overflowTags is null); + return; + } + _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); + } + + if (_overflowTags is not null) + { + _overflowTags.AsSpan().Slice(0, _tagsCount).CopyTo(tags); + return; + } + + Debug.Assert(_tagsCount <= 8); + + switch (_tagsCount) + { + case 0: break; + case 8: tags[7] = Tag8; goto case 7; + case 7: tags[6] = Tag7; goto case 6; + case 6: tags[5] = Tag6; goto case 5; + case 5: tags[4] = Tag5; goto case 4; + case 4: tags[3] = Tag4; goto case 3; + case 3: tags[2] = Tag3; goto case 2; + case 2: tags[1] = Tag2; goto case 1; + case 1: tags[0] = Tag1; break; + } + } + + /// + /// 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) + { + if (array is null) + { + throw new ArgumentNullException(nameof(array)); + } + + if ((uint)arrayIndex >= array.Length) + { + throw new ArgumentOutOfRangeException(nameof(arrayIndex)); + } + + CopyTo(array.AsSpan().Slice(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 ((uint)index > (uint)_tagsCount) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } + + if (index == _tagsCount) + { + Add(item); + return; + } + + if (_tagsCount == 8 && _overflowTags is null) + { + MoveTagsToTheArray(); + Debug.Assert(_overflowTags is not null); + } + + if (_overflowTags is not null) + { + if (_tagsCount == _overflowTags.Length) + { + Array.Resize(ref _overflowTags, _tagsCount + OverflowAdditionalCapacity); + } + + for (int i = _tagsCount; i > index; i--) + { + _overflowTags[i] = _overflowTags[i - 1]; + } + _overflowTags[index] = item; + _tagsCount++; + return; + } + + Debug.Assert(_tagsCount < 8 && index < 7); + + switch (index) + { + case 0: Tag8 = Tag7; Tag7 = Tag6; Tag6 = Tag5; Tag5 = Tag4; Tag4 = Tag3; Tag3 = Tag2; Tag2 = Tag1; Tag1 = item; break; + case 1: Tag8 = Tag7; Tag7 = Tag6; Tag6 = Tag5; Tag5 = Tag4; Tag4 = Tag3; Tag3 = Tag2; Tag2 = item; break; + case 2: Tag8 = Tag7; Tag7 = Tag6; Tag6 = Tag5; Tag5 = Tag4; Tag4 = Tag3; Tag3 = item; break; + case 3: Tag8 = Tag7; Tag7 = Tag6; Tag6 = Tag5; Tag5 = Tag4; Tag4 = item; break; + case 4: Tag8 = Tag7; Tag7 = Tag6; Tag6 = Tag5; Tag5 = item; break; + case 5: Tag8 = Tag7; Tag7 = Tag6; Tag6 = item; break; + case 6: Tag8 = Tag7; Tag7 = item; break; + default: + Debug.Assert(false); // we shouldn't come here + return; + } + _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) + { + if ((uint)index >= (uint)_tagsCount) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } + + if (_overflowTags is not null) + { + for (int i = index; i < _tagsCount - 1; i++) + { + _overflowTags[i] = _overflowTags[i + 1]; + } + + _tagsCount--; + return; + } + + Debug.Assert(_tagsCount <= 8 && index <= 7); + + switch (index) + { + case 0: Tag1 = Tag2; goto case 1; + case 1: Tag2 = Tag3; goto case 2; + case 2: Tag3 = Tag4; goto case 3; + case 3: Tag4 = Tag5; goto case 4; + case 4: Tag5 = Tag6; goto case 5; + case 5: Tag6 = Tag7; goto case 6; + case 6: Tag7 = Tag8; break; + case 7: break; + } + _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) + { + if (_overflowTags is not null) + { + for (int i = 0; i < _tagsCount; i++) + { + if (TagsEqual(_overflowTags[i], item)) + { + return i; + } + } + return -1; + } + + Debug.Assert(_tagsCount <= 8); + + switch (_tagsCount) + { + case 1: if (TagsEqual(Tag1, item)) { return 0; }; + break; + case 2: if (TagsEqual(Tag1, item)) { return 0; } + if (TagsEqual(Tag2, item)) { return 1; }; + break; + case 3: if (TagsEqual(Tag1, item)) { return 0; } + if (TagsEqual(Tag2, item)) { return 1; }; + if (TagsEqual(Tag3, item)) { return 2; }; + break; + case 4: if (TagsEqual(Tag1, item)) { return 0; } + if (TagsEqual(Tag2, item)) { return 1; }; + if (TagsEqual(Tag3, item)) { return 2; }; + if (TagsEqual(Tag4, item)) { return 3; }; + break; + case 5: if (TagsEqual(Tag1, item)) { return 0; } + if (TagsEqual(Tag2, item)) { return 1; }; + if (TagsEqual(Tag3, item)) { return 2; }; + if (TagsEqual(Tag4, item)) { return 3; }; + if (TagsEqual(Tag5, item)) { return 4; }; + break; + case 6: if (TagsEqual(Tag1, item)) { return 0; } + if (TagsEqual(Tag2, item)) { return 1; }; + if (TagsEqual(Tag3, item)) { return 2; }; + if (TagsEqual(Tag4, item)) { return 3; }; + if (TagsEqual(Tag5, item)) { return 4; }; + if (TagsEqual(Tag6, item)) { return 5; }; + break; + case 7: if (TagsEqual(Tag1, item)) { return 0; } + if (TagsEqual(Tag2, item)) { return 1; }; + if (TagsEqual(Tag3, item)) { return 2; }; + if (TagsEqual(Tag4, item)) { return 3; }; + if (TagsEqual(Tag5, item)) { return 4; }; + if (TagsEqual(Tag6, item)) { return 5; }; + if (TagsEqual(Tag7, item)) { return 6; }; + break; + case 8: if (TagsEqual(Tag1, item)) { return 0; } + if (TagsEqual(Tag2, item)) { return 1; }; + if (TagsEqual(Tag3, item)) { return 2; }; + if (TagsEqual(Tag4, item)) { return 3; }; + if (TagsEqual(Tag5, item)) { return 4; }; + if (TagsEqual(Tag6, item)) { return 5; }; + if (TagsEqual(Tag7, item)) { return 6; }; + if (TagsEqual(Tag8, item)) { return 7; }; + break; + } + + return -1; + } + + internal readonly KeyValuePair[]? Tags => _overflowTags; + + private static bool TagsEqual(KeyValuePair tag1, KeyValuePair tag2) + { + // Keys always of string type so using equality operator would be enough. + if (tag1.Key != tag2.Key) + { + return false; + } + + // Values are of Object type which need to call Equals method on them. + if (tag1.Value is null) + { + if (tag2.Value is not null) + { + return false; + } + } + else + { + if (!tag1.Value.Equals(tag2.Value)) + { + return false; + } + } + + return true; + } + + private void MoveTagsToTheArray() + { + _overflowTags = new KeyValuePair[16]; + _overflowTags[0] = Tag1; + _overflowTags[1] = Tag2; + _overflowTags[2] = Tag3; + _overflowTags[3] = Tag4; + _overflowTags[4] = Tag5; + _overflowTags[5] = Tag6; + _overflowTags[6] = Tag7; + _overflowTags[7] = Tag8; + } + + public struct Enumerator : IEnumerator>, 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; + } + } +} \ No newline at end of file diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/tests/MetricsTests.cs b/src/libraries/System.Diagnostics.DiagnosticSource/tests/MetricsTests.cs index f4b90d3..c0da50f 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/tests/MetricsTests.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/tests/MetricsTests.cs @@ -807,6 +807,94 @@ namespace System.Diagnostics.Metrics.Tests }).Dispose(); } + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public void TestRecordingMeasurementsWithTagList() + { + RemoteExecutor.Invoke(() => { + + Meter meter = new Meter("RecordingMeasurementsWithTagList"); + + using (MeterListener listener = new MeterListener()) + { + Counter counter = meter.CreateCounter("Counter"); + Histogram histogram = meter.CreateHistogram("histogram"); + + listener.EnableMeasurementEvents(counter, counter); + listener.EnableMeasurementEvents(histogram, histogram); + + KeyValuePair[] expectedTags = null; + + listener.SetMeasurementEventCallback((inst, measurement, tags, state) => { + for (int i = 0; i < expectedTags.Length; i++) + { + Assert.Equal(expectedTags[i], tags[i]); + } + }); + + // 0 Tags + + expectedTags = new KeyValuePair[0]; + counter.Add(10, new TagList()); + histogram.Record(10, new TagList()); + + // 1 Tags + expectedTags = new KeyValuePair[] { new KeyValuePair("Key1", "Value1") }; + counter.Add(10, new TagList() { expectedTags[0] }); + histogram.Record(10, new TagList() { new KeyValuePair("Key1", "Value1") }); + + // 2 Tags + expectedTags = new List> + { + {"Key1", "Value1"}, + {"Key2", "Value2"} + }.ToArray(); + + counter.Add(10, new TagList() { expectedTags[0], expectedTags[1] }); + histogram.Record(10, new TagList() { expectedTags[0], expectedTags[1] }); + + // 8 Tags + expectedTags = new List> + { + { "Key1", "Value1" }, + { "Key2", "Value2" }, + { "Key3", "Value3" }, + { "Key4", "Value4" }, + { "Key5", "Value5" }, + { "Key6", "Value6" }, + { "Key7", "Value7" }, + { "Key8", "Value8" }, + }.ToArray(); + + counter.Add(10, new TagList() { expectedTags[0], expectedTags[1], expectedTags[2], expectedTags[3], expectedTags[4], expectedTags[5], expectedTags[6], expectedTags[7] }); + histogram.Record(10, new TagList() { expectedTags[0], expectedTags[1], expectedTags[2], expectedTags[3], expectedTags[4], expectedTags[5], expectedTags[6], expectedTags[7] }); + + // 13 Tags + expectedTags = new List> + { + { "Key1", "Value1" }, + { "Key2", "Value2" }, + { "Key3", "Value3" }, + { "Key4", "Value4" }, + { "Key5", "Value5" }, + { "Key6", "Value6" }, + { "Key7", "Value7" }, + { "Key8", "Value8" }, + { "Key9", "Value9" }, + { "Key10", "Value10" }, + { "Key11", "Value11" }, + { "Key12", "Value12" }, + { "Key13", "Value13" }, + }.ToArray(); + + counter.Add(10, new TagList() { expectedTags[0], expectedTags[1], expectedTags[2], expectedTags[3], expectedTags[4], expectedTags[5], expectedTags[6], expectedTags[7], + expectedTags[8], expectedTags[9], expectedTags[10], expectedTags[11], expectedTags[12] }); + histogram.Record(10, new TagList() { expectedTags[0], expectedTags[1], expectedTags[2], expectedTags[3], expectedTags[4], expectedTags[5], expectedTags[6], expectedTags[7], + expectedTags[8], expectedTags[9], expectedTags[10], expectedTags[11], expectedTags[12] }); + } + + }).Dispose(); + } + private void PublishCounterMeasurement(Counter counter, T value, KeyValuePair[] tags) where T : struct { switch (tags.Length) @@ -816,6 +904,11 @@ namespace System.Diagnostics.Metrics.Tests case 2: counter.Add(value, tags[0], tags[1]); break; case 3: counter.Add(value, tags[0], tags[1], tags[2]); break; case 4: counter.Add(value, tags[0], tags[1], tags[2], tags[3]); break; + case 5: counter.Add(value, tags[0], tags[1], tags[2], tags[3], tags[4]); break; + case 6: counter.Add(value, tags[0], tags[1], tags[2], tags[3], tags[4], tags[5]); break; + case 7: counter.Add(value, tags[0], tags[1], tags[2], tags[3], tags[4], tags[5], tags[6]); break; + case 8: counter.Add(value, tags[0], tags[1], tags[2], tags[3], tags[4], tags[5], tags[6], tags[7]); break; + case 9: counter.Add(value, tags[0], tags[1], tags[2], tags[3], tags[4], tags[5], tags[6], tags[7], tags[8]); break; default: counter.Add(value, tags); break; } } @@ -829,6 +922,11 @@ namespace System.Diagnostics.Metrics.Tests case 2: histogram.Record(value, tags[0], tags[1]); break; case 3: histogram.Record(value, tags[0], tags[1], tags[2]); break; case 4: histogram.Record(value, tags[0], tags[1], tags[2], tags[3]); break; + case 5: histogram.Record(value, tags[0], tags[1], tags[2], tags[3], tags[4]); break; + case 6: histogram.Record(value, tags[0], tags[1], tags[2], tags[3], tags[4], tags[5]); break; + case 7: histogram.Record(value, tags[0], tags[1], tags[2], tags[3], tags[4], tags[5], tags[6]); break; + case 8: histogram.Record(value, tags[0], tags[1], tags[2], tags[3], tags[4], tags[5], tags[6], tags[7]); break; + case 9: histogram.Record(value, tags[0], tags[1], tags[2], tags[3], tags[4], tags[5], tags[6], tags[7], tags[8]); break; default: histogram.Record(value, tags); break; } } @@ -940,13 +1038,86 @@ namespace System.Diagnostics.Metrics.Tests listener.Start(); expectedValue = record(instrument, expectedValue, expectedTags); - expectedTags = new KeyValuePair[] { new KeyValuePair("K1", "V1") }; + expectedTags = new List> + { + { "K1", "V1" }, + }.ToArray(); + expectedValue = record(instrument, expectedValue, expectedTags); + expectedTags = new List> + { + { "K1", "V1" }, + { "K2", "V2" }, + }.ToArray(); expectedValue = record(instrument, expectedValue, expectedTags); - expectedTags = new KeyValuePair[] { new KeyValuePair("K1", "V1"), new KeyValuePair("K2", "V2") }; + expectedTags = new List> + { + { "K1", "V1" }, + { "K2", "V2" }, + { "K3", "V3" }, + }.ToArray(); expectedValue = record(instrument, expectedValue, expectedTags); - expectedTags = new KeyValuePair[] { new KeyValuePair("K1", "V1"), new KeyValuePair("K2", "V2"), new KeyValuePair("K3", "V3") }; + expectedTags = new List> + { + { "K1", "V1" }, + { "K2", "V2" }, + { "K3", "V3" }, + { "K4", "V4" }, + }.ToArray(); expectedValue = record(instrument, expectedValue, expectedTags); - expectedTags = new KeyValuePair[] { new KeyValuePair("K1", "V1"), new KeyValuePair("K2", "V2"), new KeyValuePair("K3", "V3"), new KeyValuePair("K4", "V4") }; + expectedTags = new List> + { + { "K1", "V1" }, + { "K2", "V2" }, + { "K3", "V3" }, + { "K4", "V4" }, + { "K5", "V5" }, + }.ToArray(); + expectedValue = record(instrument, expectedValue, expectedTags); + expectedTags = new List> + { + { "K1", "V1" }, + { "K2", "V2" }, + { "K3", "V3" }, + { "K4", "V4" }, + { "K5", "V5" }, + { "K6", "V6" }, + }.ToArray(); + expectedValue = record(instrument, expectedValue, expectedTags); + expectedTags = new List> + { + { "K1", "V1" }, + { "K2", "V2" }, + { "K3", "V3" }, + { "K4", "V4" }, + { "K5", "V5" }, + { "K6", "V6" }, + { "K7", "V7" }, + }.ToArray(); + expectedValue = record(instrument, expectedValue, expectedTags); + expectedTags = new List> + { + { "K1", "V1" }, + { "K2", "V2" }, + { "K3", "V3" }, + { "K4", "V4" }, + { "K5", "V5" }, + { "K6", "V6" }, + { "K7", "V7" }, + { "K8", "V8" }, + }.ToArray(); + expectedValue = record(instrument, expectedValue, expectedTags); + expectedTags = new List> + { + { "K1", "V1" }, + { "K2", "V2" }, + { "K3", "V3" }, + { "K4", "V4" }, + { "K5", "V5" }, + { "K6", "V6" }, + { "K7", "V7" }, + { "K8", "V8" }, + { "K9", "V9" }, + }.ToArray(); expectedValue = record(instrument, expectedValue, expectedTags); } @@ -963,6 +1134,9 @@ namespace System.Diagnostics.Metrics.Tests Assert.True(false, "We encountered unsupported type"); return default; } - + } + public static class DiagnosticsCollectionExtensions + { + public static void Add(this ICollection> collection, T1 item1, T2 item2) => collection?.Add(new KeyValuePair(item1, item2)); } } diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/tests/System.Diagnostics.DiagnosticSource.Tests.csproj b/src/libraries/System.Diagnostics.DiagnosticSource/tests/System.Diagnostics.DiagnosticSource.Tests.csproj index 24c8442..00b5834 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/tests/System.Diagnostics.DiagnosticSource.Tests.csproj +++ b/src/libraries/System.Diagnostics.DiagnosticSource/tests/System.Diagnostics.DiagnosticSource.Tests.csproj @@ -29,6 +29,7 @@ + diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/tests/TagListTests.cs b/src/libraries/System.Diagnostics.DiagnosticSource/tests/TagListTests.cs new file mode 100644 index 0000000..46dcb9c --- /dev/null +++ b/src/libraries/System.Diagnostics.DiagnosticSource/tests/TagListTests.cs @@ -0,0 +1,337 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; +using System.Collections.Generic; +using System.Diagnostics; + +namespace System.Diagnostics.Tests +{ + public class TagListTests + { + [Fact] + public void TestConstruction() + { + for (int i = 0; i < 30; i++) + { + CreateTagList(i, out TagList tagList); + ValidateTags(in tagList, i); + Assert.False(tagList.IsReadOnly); + + KeyValuePair[] array = new KeyValuePair[tagList.Count]; + tagList.CopyTo(array); + TagList list = new TagList(array.AsSpan()); + ValidateTags(in tagList, i); + } + } + + [Fact] + public void TestInlineInitialization() + { + TagList list = new TagList + { + { "Some Key", "Some Value" }, + { "Some Other Key", 42 } + }; + Assert.Equal("Some Key", list[0].Key); + Assert.Equal("Some Value", list[0].Value); + Assert.Equal("Some Other Key", list[1].Key); + Assert.Equal(42, list[1].Value); + } + + [Fact] + public void TestClear() + { + for (int i = 0; i < 30; i++) + { + CreateTagList(i, out TagList tagList); + Assert.Equal(i, tagList.Count); + tagList.Clear(); + Assert.Equal(0, tagList.Count); + } + } + + [Fact] + public void TestSearchOperations() + { + for (int i = 0; i < 30; i++) + { + CreateTagList(i, out TagList tagList); + KeyValuePair[] array = new KeyValuePair[tagList.Count]; + tagList.CopyTo(array); + + for (int j = 0; j < array.Length; j++) + { + Assert.True(tagList.Contains(array[j])); + Assert.Equal(j, tagList.IndexOf(array[j])); + } + + Assert.False(tagList.Contains(new KeyValuePair("Not Exist Key", "Not Exist Value"))); + Assert.Equal(-1, tagList.IndexOf(new KeyValuePair("Not Exist Other Key", "Not Exist Other Value"))); + } + } + + [Fact] + public void TestCopyTo() + { + for (int i = 0; i < 20; i++) + { + CreateTagList(i, out TagList tagList); + KeyValuePair[] array = new KeyValuePair[tagList.Count]; + tagList.CopyTo(array.AsSpan()); + ValidateTags(tagList, array); + array = new KeyValuePair[tagList.Count]; + tagList.CopyTo(array); + ValidateTags(tagList, array); + } + } + + [Fact] + public void TestInsert() + { + TagList list = new TagList(); + Assert.Equal(0, list.Count); + list.Insert(0, new KeyValuePair("Key0", 0)); + Assert.Equal(1, list.Count); + Assert.Equal("Key0", list[0].Key); + Assert.Equal(0, list[0].Value); + + // Insert at the end + for (int i = 1; i < 20; i++) + { + list.Insert(i, new KeyValuePair("Key" + i, i)); + Assert.Equal(i + 1, list.Count); + Assert.Equal("Key" + i, list[i].Key); + Assert.Equal(i, list[i].Value); + } + + // Insert at begining + int count = list.Count; + for (int i = 1; i < 10; i++) + { + list.Insert(0, new KeyValuePair("Key-" + i, i + count)); + Assert.Equal(count + i, list.Count); + Assert.Equal("Key-" + i, list[0].Key); + Assert.Equal(i + count, list[0].Value); + } + + // Insert in the middle + count = list.Count; + int pos = count / 2; + + KeyValuePair firstItem = list[0]; + KeyValuePair lastItem = list[count - 1]; + + for (int i = 1; i < 10; i++) + { + list.Insert(pos, new KeyValuePair("Key+" + i, i + count)); + Assert.Equal(count + i, list.Count); + Assert.Equal("Key+" + i, list[pos].Key); + Assert.Equal(i + count, list[pos].Value); + + Assert.Equal(firstItem.Key, list[0].Key); + Assert.Equal(firstItem.Value, list[0].Value); + Assert.Equal(lastItem.Key, list[list.Count - 1].Key); + Assert.Equal(lastItem.Value, list[list.Count - 1].Value); + } + + // Test insert when having less than 8 tags + list = new TagList(); + Assert.Equal(0, list.Count); + + list.Insert(0, new KeyValuePair("Key!0", 0)); + Assert.Equal(1, list.Count); + Assert.Equal("Key!0", list[0].Key); + Assert.Equal(0, list[0].Value); + + list.Insert(1, new KeyValuePair("Key!1", 100)); + Assert.Equal(2, list.Count); + Assert.Equal("Key!1", list[1].Key); + Assert.Equal(100, list[1].Value); + + list.Insert(0, new KeyValuePair("Key!00", 1000)); + Assert.Equal(3, list.Count); + Assert.Equal("Key!00", list[0].Key); + Assert.Equal(1000, list[0].Value); + + list.Insert(3, new KeyValuePair("Key!300", 3000)); + Assert.Equal(4, list.Count); + Assert.Equal("Key!300", list[3].Key); + Assert.Equal(3000, list[3].Value); + + for (int i = 1; i < 10; i++) + { + list.Insert(2, new KeyValuePair("Key!200" +i , i * 200)); + Assert.Equal(4 + i, list.Count); + Assert.Equal("Key!200" + i, list[2].Key); + Assert.Equal(i * 200, list[2].Value); + } + } + + [Fact] + public void TestRemove() + { + TagList list = new TagList(); + // Test first with up to 8 tags + for (int i = 1; i <= 8; i++) + { + KeyValuePair kvp = new KeyValuePair("k" + i, "v" + i); + list.Add(kvp); + Assert.Equal(i, list.Count); + Assert.True(list.Contains(kvp)); + Assert.Equal(i - 1, list.IndexOf(kvp)); + } + + // Now remove items + + int count = list.Count; + for (int i = 1; i <= 8; i++) + { + KeyValuePair kvp = new KeyValuePair("k" + i, "v" + i); + Assert.True(list.Remove(kvp)); + Assert.Equal(count - i, list.Count); + Assert.False(list.Contains(kvp)); + Assert.Equal(-1, list.IndexOf(kvp)); + } + + Assert.Equal(0, list.Count); + + // Now we want to test more than 8 tags and test RemoveAt too + for (int i = 1; i <= 20; i++) + { + KeyValuePair kvp1 = new KeyValuePair("k-" + i, "v" + i); + KeyValuePair kvp2 = new KeyValuePair("k-" + i * 100, "v" + i * 100); + KeyValuePair kvp3 = new KeyValuePair("k-" + i * 1000, "v" + i * 1000); + + // We add 3 then remove 2. + list.Add(kvp1); + list.Add(kvp2); + list.Add(kvp3); + + // Now remove 1 + Assert.True(list.Contains(kvp3)); + Assert.True(list.Remove(kvp3)); + Assert.False(list.Contains(kvp3)); + + int index = list.IndexOf(kvp2); + Assert.True(index >= 0); + Assert.True(list.Contains(kvp2)); + list.RemoveAt(index); + Assert.False(list.Contains(kvp2)); + + Assert.True(list.Contains(kvp1)); + } + + Assert.Equal(20, list.Count); + } + + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(8)] + [InlineData(10)] + [InlineData(100)] + public void TestEnumerator(int count) + { + CreateTagList(count, out TagList tagList); + KeyValuePair[] array = new KeyValuePair[tagList.Count]; + tagList.CopyTo(array); + + Assert.Equal(count, tagList.Count); + int i = 0; + foreach (KeyValuePair kvp in tagList) + { + Assert.Equal(array[i].Key, kvp.Key); + Assert.Equal(array[i].Value, kvp.Value); + i++; + } + Assert.Equal(i, tagList.Count); + + IEnumerator> enumerator = tagList.GetEnumerator(); + i = 0; + while (enumerator.MoveNext()) + { + Assert.Equal(array[i].Key, enumerator.Current.Key); + Assert.Equal(array[i].Value, enumerator.Current.Value); + i++; + } + Assert.Equal(i, tagList.Count); + } + + [Theory] + [InlineData(2)] + [InlineData(7)] + [InlineData(9)] + [InlineData(20)] + public void TestIndex(int count) + { + CreateTagList(count, out TagList tagList); + Assert.Equal(count, tagList.Count); + + ValidateTags(in tagList, count); // It calls the indexer getter. + + for (int i = 0; i < count; i++) + { + tagList[i] = new KeyValuePair("NewKey" + i, i); + Assert.Equal("NewKey" + i, tagList[i].Key); + Assert.Equal(i, tagList[i].Value); + } + } + + [Fact] + public void TestNegativeCases() + { + TagList list = new TagList { new KeyValuePair("1", 1), new KeyValuePair("2", 2) } ; + KeyValuePair kvp = default; + + Assert.Throws(() => kvp = list[2]); + Assert.Throws(() => list[2] = kvp); + Assert.Throws(() => kvp = list[-1]); + Assert.Throws(() => list[-2] = kvp); + + KeyValuePair[] array = new KeyValuePair[1]; + Assert.Throws(() => list.CopyTo(array.AsSpan())); + Assert.Throws(() => list.CopyTo(array, 0)); + Assert.Throws(() => list.CopyTo(array, 1)); + Assert.Throws(() => list.CopyTo(array, -1)); + Assert.Throws(() => list.CopyTo(null, 0)); + + Assert.Throws(() => list.Insert(-1, default)); + Assert.Throws(() => list.Insert(3, default)); + + Assert.Throws(() => list.RemoveAt(-1)); + Assert.Throws(() => list.RemoveAt(2)); + } + + private void ValidateTags(in TagList tagList, KeyValuePair[] array) + { + Assert.True(tagList.Count <= array.Length); + for (int i = 0; i < tagList.Count; i++) + { + Assert.Equal(array[i].Key, tagList[i].Key); + Assert.Equal(array[i].Value, tagList[i].Value); + } + } + + private void ValidateTags(in TagList tagList, int tagsCount) + { + Assert.Equal(tagsCount, tagList.Count); + for (int i = 0; i < tagList.Count; i++) + { + Assert.Equal("Key"+i, tagList[i].Key); + Assert.Equal("Value"+i, tagList[i].Value); + } + } + + private void CreateTagList(int tagsCount, out TagList tagList) + { + tagList = new TagList(); + for (int i = 0; i < tagsCount; i++) + { + tagList.Add("Key" + i, "Value" + i); + } + } + } +} + +