Add more Metrics API overloads taking tags (#56940)
authorTarek Mahmoud Sayed <tarekms@microsoft.com>
Tue, 17 Aug 2021 04:08:12 +0000 (21:08 -0700)
committerGitHub <noreply@github.com>
Tue, 17 Aug 2021 04:08:12 +0000 (21:08 -0700)
* Add more Metrics API overloads taking tags

12 files changed:
src/libraries/System.Diagnostics.DiagnosticSource/ref/System.Diagnostics.DiagnosticSourceActivity.cs
src/libraries/System.Diagnostics.DiagnosticSource/src/Resources/Strings.resx
src/libraries/System.Diagnostics.DiagnosticSource/src/System.Diagnostics.DiagnosticSource.csproj
src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/Counter.cs
src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/Histogram.cs
src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/Instrument.cs
src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/Instrument.netcore.cs
src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/Instrument.netfx.cs
src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/TagList.cs [new file with mode: 0644]
src/libraries/System.Diagnostics.DiagnosticSource/tests/MetricsTests.cs
src/libraries/System.Diagnostics.DiagnosticSource/tests/System.Diagnostics.DiagnosticSource.Tests.csproj
src/libraries/System.Diagnostics.DiagnosticSource/tests/TagListTests.cs [new file with mode: 0644]

index 68fa62b..fd218a7 100644 (file)
@@ -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.KeyValuePair<string, object?>>, System.Collections.Generic.IReadOnlyList<System.Collections.Generic.KeyValuePair<string, object?>>
+    {
+        public TagList(System.ReadOnlySpan<System.Collections.Generic.KeyValuePair<string, object?>> tagList) : this() { throw null; }
+        public readonly int Count => throw null;
+        public readonly bool IsReadOnly => throw null;
+        public System.Collections.Generic.KeyValuePair<string, object?> 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<string, object?> tag) { throw null; }
+        public readonly void CopyTo(System.Span<System.Collections.Generic.KeyValuePair<string, object?>> tags) { throw null; }
+        public void Insert(int index, System.Collections.Generic.KeyValuePair<string, object?> item) { throw null; }
+        public void RemoveAt(int index) { throw null; }
+        public void Clear() { throw null; }
+        public readonly bool Contains(System.Collections.Generic.KeyValuePair<string, object?> item) { throw null; }
+        public readonly void CopyTo(System.Collections.Generic.KeyValuePair<string, object?>[] array, int arrayIndex) { throw null; }
+        public bool Remove(System.Collections.Generic.KeyValuePair<string, object?> item) { throw null; }
+        public readonly System.Collections.Generic.IEnumerator<System.Collections.Generic.KeyValuePair<string, object?>> GetEnumerator() { throw null; }
+        readonly System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; }
+        public readonly int IndexOf(System.Collections.Generic.KeyValuePair<string, object?> item) { throw null; }
+        public struct Enumerator : System.Collections.Generic.IEnumerator<System.Collections.Generic.KeyValuePair<string, object?>>, System.Collections.IEnumerator
+        {
+            public System.Collections.Generic.KeyValuePair<string, object?> 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<string, object?> tag1, System.Collections.Generic.KeyValuePair<string, object?> tag2, System.Collections.Generic.KeyValuePair<string, object?> tag3)  {  throw null; }
         public void Add(T delta, ReadOnlySpan<System.Collections.Generic.KeyValuePair<string, object?>> tags) {  throw null; }
         public void Add(T delta, params System.Collections.Generic.KeyValuePair<string, object?>[] 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<string, object?> tag) { throw null; }
         public void Record(T value, System.Collections.Generic.KeyValuePair<string, object?> tag1, System.Collections.Generic.KeyValuePair<string, object?> tag2) { throw null; }
         public void Record(T value, System.Collections.Generic.KeyValuePair<string, object?> tag1, System.Collections.Generic.KeyValuePair<string, object?> tag2, System.Collections.Generic.KeyValuePair<string, object?> tag3) { throw null; }
+        public void Record(T value, in TagList tagList) { throw null; }
         public void Record(T value, ReadOnlySpan<System.Collections.Generic.KeyValuePair<string, object?>> tags) { throw null; }
         public void Record(T value, params System.Collections.Generic.KeyValuePair<string, object?>[] tags) { throw null; }
     }
@@ -310,6 +344,7 @@ namespace System.Diagnostics.Metrics
         protected void RecordMeasurement(T measurement, System.Collections.Generic.KeyValuePair<string, object?> tag) { throw null; }
         protected void RecordMeasurement(T measurement, System.Collections.Generic.KeyValuePair<string, object?> tag1, System.Collections.Generic.KeyValuePair<string, object?> tag2)  { throw null; }
         protected void RecordMeasurement(T measurement, System.Collections.Generic.KeyValuePair<string, object?> tag1, System.Collections.Generic.KeyValuePair<string, object?> tag2, System.Collections.Generic.KeyValuePair<string, object?> tag3)  { throw null; }
+        protected void RecordMeasurement(T measurement, in TagList tagList) { throw null; }
         protected void RecordMeasurement(T measurement, ReadOnlySpan<System.Collections.Generic.KeyValuePair<string, object?>> tags) { throw null; }
     }
     public readonly struct Measurement<T> where T : struct
index 2de05b8..0d9ad07 100644 (file)
   <data name="UnsupportedType" xml:space="preserve">
     <value>{0} is unsupported type for this operation. The only supported types are byte, short, int, long, float, double, and decimal.  </value>
   </data>
+  <data name="Arg_BufferTooSmall" xml:space="preserve">
+    <value>Destination buffer is not long enough to copy all the items in the list.</value>
+  </data>
 </root>
\ No newline at end of file
index c51511a..bac2f7c 100644 (file)
@@ -72,10 +72,11 @@ System.Diagnostics.DiagnosticSource</PackageDescription>
     <Compile Include="System\Diagnostics\Metrics\ObservableCounter.cs" />
     <Compile Include="System\Diagnostics\Metrics\ObservableGauge.cs" />
     <Compile Include="System\Diagnostics\Metrics\ObservableInstrument.cs" />
+    <Compile Include="System\Diagnostics\Metrics\RateAggregator.cs" />
     <Compile Include="System\Diagnostics\Metrics\StringSequence.cs" />
     <Compile Include="System\Diagnostics\Metrics\StringSequence.netcore.cs" Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp'" />
     <Compile Include="System\Diagnostics\Metrics\StringSequence.netfx.cs" Condition="'$(TargetFrameworkIdentifier)' != '.NETCoreApp'" />
-    <Compile Include="System\Diagnostics\Metrics\RateAggregator.cs" />
+    <Compile Include="System\Diagnostics\Metrics\TagList.cs" />
     <None Include="ActivityUserGuide.md" />
   </ItemGroup>
   <ItemGroup Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp'">
index 3e7b3d1..d107bd7 100644 (file)
@@ -66,5 +66,12 @@ namespace System.Diagnostics.Metrics
         /// <param name="delta">The increment measurement.</param>
         /// <param name="tags">A list of key-value pair tags associated with the measurement.</param>
         public void Add(T delta, params KeyValuePair<string, object?>[] tags) => RecordMeasurement(delta, tags.AsSpan());
+
+        /// <summary>
+        /// Record the increment value of the measurement.
+        /// </summary>
+        /// <param name="delta">The measurement value.</param>
+        /// <param name="tagList">A <see cref="T:System.Diagnostics.TagList" /> of tags associated with the measurement.</param>
+        public void Add(T delta, in TagList tagList) => RecordMeasurement(delta, in tagList);
     }
 }
\ No newline at end of file
index 6bb4711..1000815 100644 (file)
@@ -66,5 +66,12 @@ namespace System.Diagnostics.Metrics
         /// <param name="value">The measurement value.</param>
         /// <param name="tags">A list of key-value pair tags associated with the measurement.</param>
         public void Record(T value, params KeyValuePair<string, object?>[] tags) => RecordMeasurement(value, tags.AsSpan());
+
+        /// <summary>
+        /// Record a measurement value.
+        /// </summary>
+        /// <param name="value">The measurement value.</param>
+        /// <param name="tagList">A <see cref="T:System.Diagnostics.TagList" /> of tags associated with the measurement.</param>
+        public void Record(T value, in TagList tagList) => RecordMeasurement(value, in tagList);
     }
 }
\ No newline at end of file
index 542a6dd..d79c22f 100644 (file)
@@ -13,11 +13,7 @@ namespace System.Diagnostics.Metrics
 #endif
     public abstract class Instrument
     {
-#if NO_ARRAY_EMPTY_SUPPORT
-        internal static KeyValuePair<string, object?>[] EmptyTags { get; } = new KeyValuePair<string, object?>[0];
-#else
         internal static KeyValuePair<string, object?>[] EmptyTags => Array.Empty<KeyValuePair<string, object?>>();
-#endif // NO_ARRAY_EMPTY_SUPPORT
 
         // The SyncObject is used to synchronize the following operations:
         //  - Instrument.Publish()
index cad3bff..ffac3c0 100644 (file)
@@ -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<string, object?> Tag1;
+        internal OneTagBag(KeyValuePair<string, object?> tag)
+        {
+            Tag1 = tag;
+        }
+    }
+
+    [StructLayout(LayoutKind.Sequential)]
+    internal struct TwoTagsBag
+    {
+        internal KeyValuePair<string, object?> Tag1;
+        internal KeyValuePair<string, object?> Tag2;
+        internal TwoTagsBag(KeyValuePair<string, object?> tag1, KeyValuePair<string, object?> tag2)
+        {
+            Tag1 = tag1;
+            Tag2 = tag2;
+        }
+    }
+
+    [StructLayout(LayoutKind.Sequential)]
+    internal struct ThreeTagsBag
     {
         internal KeyValuePair<string, object?> Tag1;
         internal KeyValuePair<string, object?> Tag2;
         internal KeyValuePair<string, object?> Tag3;
+        internal ThreeTagsBag(KeyValuePair<string, object?> tag1, KeyValuePair<string, object?> tag2, KeyValuePair<string, object?> tag3)
+        {
+            Tag1 = tag1;
+            Tag2 = tag2;
+            Tag3 = tag3;
+        }
     }
 
     /// <summary>
@@ -30,8 +62,7 @@ namespace System.Diagnostics.Metrics
         /// <param name="tag">A key-value pair tag associated with the measurement.</param>
         protected void RecordMeasurement(T measurement, KeyValuePair<string, object?> 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
         /// <param name="tag2">A second key-value pair tag associated with the measurement.</param>
         protected void RecordMeasurement(T measurement, KeyValuePair<string, object?> tag1, KeyValuePair<string, object?> 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
         /// <param name="tag3">A third key-value pair tag associated with the measurement.</param>
         protected void RecordMeasurement(T measurement, KeyValuePair<string, object?> tag1, KeyValuePair<string, object?> tag2, KeyValuePair<string, object?> 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));
         }
+
+        /// <summary>
+        /// Record the measurement by notifying all <see cref="MeterListener" /> objects which listening to this instrument.
+        /// </summary>
+        /// <param name="measurement">The measurement value.</param>
+        /// <param name="tagList">A <see cref="T:System.Diagnostics.TagList" /> of tags associated with the measurement.</param>
+        protected void RecordMeasurement(T measurement, in TagList tagList)
+        {
+            KeyValuePair<string, object?>[]? 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
index 4367b11..2dc030f 100644 (file)
@@ -19,7 +19,7 @@ namespace System.Diagnostics.Metrics
     {
         [ThreadStatic] private KeyValuePair<string, object?>[] ts_tags;
 
-        private const int MaxTagsCount = 3;
+        private const int MaxTagsCount = 8;
 
         /// <summary>
         /// Record the measurement by notifying all <see cref="MeterListener" /> objects which listening to this instrument.
@@ -68,5 +68,44 @@ namespace System.Diagnostics.Metrics
             RecordMeasurement(measurement, tags.AsSpan().Slice(0, 3));
             ts_tags = tags;
         }
+
+         /// <summary>
+        /// Record the measurement by notifying all <see cref="MeterListener" /> objects which listening to this instrument.
+        /// </summary>
+        /// <param name="measurement">The measurement value.</param>
+        /// <param name="tagList">A <see cref="T:System.Diagnostics.TagList" /> of tags associated with the measurement.</param>
+       protected void RecordMeasurement(T measurement, in TagList tagList)
+        {
+            KeyValuePair<string, object?>[]? tags = tagList.Tags;
+            if (tags is not null)
+            {
+                RecordMeasurement(measurement, tags.AsSpan().Slice(0, tagList.Count));
+                return;
+            }
+
+            tags = ts_tags ?? new KeyValuePair<string, object?>[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 (file)
index 0000000..ab7f416
--- /dev/null
@@ -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.
+
+    /// <summary>
+    /// Represents a list of tags that can be accessed by index. Provides methods to search, sort, and manipulate lists.
+    /// </summary>
+    /// <remarks>
+    /// 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.
+    /// </remarks>
+#if ALLOW_PARTIALLY_TRUSTED_CALLERS
+    [System.Security.SecuritySafeCriticalAttribute]
+#endif
+    [StructLayout(LayoutKind.Sequential)]
+    public struct TagList : IList<KeyValuePair<string, object?>>, IReadOnlyList<KeyValuePair<string, object?>>
+    {
+        internal KeyValuePair<string, object?> Tag1;
+        internal KeyValuePair<string, object?> Tag2;
+        internal KeyValuePair<string, object?> Tag3;
+        internal KeyValuePair<string, object?> Tag4;
+        internal KeyValuePair<string, object?> Tag5;
+        internal KeyValuePair<string, object?> Tag6;
+        internal KeyValuePair<string, object?> Tag7;
+        internal KeyValuePair<string, object?> Tag8;
+        private int _tagsCount;
+        private KeyValuePair<string, object?>[]? _overflowTags;
+        private const int OverflowAdditionalCapacity = 8;
+
+        /// <summary>
+        /// Initializes a new instance of the TagList structure using the specified <paramref name="tagList" />.
+        /// </summary>
+        /// <param name="tagList">A span of tags to initialize the list with.</param>
+        public TagList(ReadOnlySpan<KeyValuePair<string, object?>> 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<string, object?>[_tagsCount + OverflowAdditionalCapacity]; // Add extra slots for more tags to add if needed
+                    tagList.CopyTo(_overflowTags);
+                    break;
+            }
+        }
+
+        /// <summary>
+        /// Gets the number of tags contained in the <see cref="T:System.Diagnostics.TagList" />.
+        /// </summary>
+        public readonly int Count => _tagsCount;
+
+        /// <summary>
+        /// Gets a value indicating whether the <see cref="T:System.Diagnostics.TagList" /> is read-only. This property will always return <see langword="false" />.
+        /// </summary>
+        public readonly bool IsReadOnly => false;
+
+        /// <summary>
+        /// Gets or sets the tags at the specified index.
+        /// </summary>
+        /// <exception cref="T:System.ArgumentOutOfRangeException"><paramref name="index" /> is not a valid index in the <see cref="T:System.Diagnostics.TagList" />.</exception>
+        public KeyValuePair<string, object?> 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;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Adds a tag with the provided <paramref name="key" /> and <paramref name="value" /> to the list.
+        /// </summary>
+        /// <param name="key">The tag key.</param>
+        /// <param name="value">The tag value.</param>
+        public void Add(string key, object? value) => Add(new KeyValuePair<string, object?>(key, value));
+
+        /// <summary>
+        /// Adds a tag to the list.
+        /// </summary>
+        /// <param name="tag">Key and value pair of the tag to add to the list.</param>
+        public void Add(KeyValuePair<string, object?> 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++;
+        }
+
+        /// <summary>
+        /// Copies the contents of this  into a destination <paramref name="tags" /> span.
+        /// Inserts an element into this <see cref="T:System.Diagnostics.TagList" /> at the specified index.
+        /// </summary>
+        /// <param name="tags">The destination <see cref="T:System.Span`1" /> object.</param>
+        /// <exception cref="T:System.ArgumentException"> <paramref name="tags" /> The number of elements in the source <see cref="T:System.Diagnostics.TagList" /> is greater than the number of elements that the destination span.</exception>
+        public readonly void CopyTo(Span<KeyValuePair<string, object?>> 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;
+            }
+        }
+
+        /// <summary>
+        /// Copies the entire <see cref="T:System.Diagnostics.TagList" /> to a compatible one-dimensional array, starting at the specified index of the target array.
+        /// </summary>
+        /// <param name="array">The one-dimensional Array that is the destination of the elements copied from <see cref="T:System.Diagnostics.TagList" />. The Array must have zero-based indexing.</param>
+        /// <param name="arrayIndex">The zero-based index in <paramref name="array" /> at which copying begins.</param>
+        /// <exception cref="T:System.ArgumentNullException"> <paramref name="array" /> is null.</exception>
+        /// <exception cref="T:System.ArgumentOutOfRangeException"> <paramref name="arrayIndex " /> is less than 0 or greater that or equal the <paramref name="array" /> length.</exception>
+        public readonly void CopyTo(KeyValuePair<string, object?>[] 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));
+        }
+
+        /// <summary>
+        /// Inserts an element into the <see cref="T:System.Diagnostics.TagList" /> at the specified index.
+        /// </summary>
+        /// <param name="index">The zero-based index at which item should be inserted.</param>
+        /// <param name="item">The tag to insert.</param>
+        /// <exception cref="T:System.ArgumentOutOfRangeException"> <paramref name="index" /> index is less than 0 or <paramref name="index" /> is greater than <see cref="M:System.Diagnostics.TagList.Count" />.</exception>
+        public void Insert(int index, KeyValuePair<string, object?> 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++;
+        }
+
+        /// <summary>
+        /// Removes the element at the specified index of the <see cref="T:System.Diagnostics.TagList" />.
+        /// </summary>
+        /// <param name="index">The zero-based index of the element to remove.</param>
+        /// <exception cref="T:System.ArgumentOutOfRangeException"> <paramref name="index" /> index is less than 0 or <paramref name="index" /> is greater than <see cref="M:System.Diagnostics.TagList.Count" />.</exception>
+        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--;
+        }
+
+        /// <summary>
+        /// Removes all elements from the <see cref="T:System.Diagnostics.TagList" />.
+        /// </summary>
+        public void Clear() => _tagsCount = 0;
+
+        /// <summary>
+        /// Determines whether an tag is in the <see cref="T:System.Diagnostics.TagList" />.
+        /// </summary>
+        /// <param name="item">The tag to locate in the <see cref="T:System.Diagnostics.TagList" />.</param>
+        /// <returns><see langword="true" /> if item is found in the <see cref="T:System.Diagnostics.TagList" />; otherwise, <see langword="false" />.</returns>
+        public readonly bool Contains(KeyValuePair<string, object?> item) => IndexOf(item) >= 0;
+
+        /// <summary>
+        /// Removes the first occurrence of a specific object from the <see cref="T:System.Diagnostics.TagList" />.
+        /// </summary>
+        /// <param name="item">The tag to remove from the <see cref="T:System.Diagnostics.TagList" />.</param>
+        /// <returns><see langword="true" /> if item is successfully removed; otherwise, <see langword="false" />. This method also returns <see langword="false" /> if item was not found in the <see cref="T:System.Diagnostics.TagList" />.</returns>
+        public bool Remove(KeyValuePair<string, object?> item)
+        {
+            int index = IndexOf(item);
+            if (index >= 0)
+            {
+                RemoveAt(index);
+                return true;
+            }
+
+            return false;
+        }
+
+        /// <summary>
+        /// Returns an enumerator that iterates through the <see cref="T:System.Diagnostics.TagList" />.
+        /// </summary>
+        /// <returns>Returns an enumerator that iterates through the <see cref="T:System.Diagnostics.TagList" />.</returns>
+        public readonly IEnumerator<KeyValuePair<string, object?>> GetEnumerator() => new Enumerator(in this);
+
+        /// <summary>
+        /// Returns an enumerator that iterates through the <see cref="T:System.Diagnostics.TagList" />.
+        /// </summary>
+        /// <returns>Returns an enumerator that iterates through the <see cref="T:System.Diagnostics.TagList" />.</returns>
+        readonly IEnumerator IEnumerable.GetEnumerator()  => new Enumerator(in this);
+
+        /// <summary>
+        /// Searches for the specified tag and returns the zero-based index of the first occurrence within the entire <see cref="T:System.Diagnostics.TagList" />.
+        /// </summary>
+        /// <param name="item">The tag to locate in the <see cref="T:System.Diagnostics.TagList" />.</param>
+        public readonly int IndexOf(KeyValuePair<string, object?> 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<string, object?>[]? Tags => _overflowTags;
+
+        private static bool TagsEqual(KeyValuePair<string, object?> tag1, KeyValuePair<string, object?> 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<string, object?>[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<KeyValuePair<string, object?>>, IEnumerator
+        {
+            private TagList _tagList;
+            private int _index;
+            internal Enumerator(in TagList tagList)
+            {
+                _index = -1;
+                _tagList = tagList;
+            }
+
+            public KeyValuePair<string, object?> 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
index f4b90d3..c0da50f 100644 (file)
@@ -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<int> counter = meter.CreateCounter<int>("Counter");
+                    Histogram<int> histogram = meter.CreateHistogram<int>("histogram");
+
+                    listener.EnableMeasurementEvents(counter, counter);
+                    listener.EnableMeasurementEvents(histogram, histogram);
+
+                    KeyValuePair<string, object?>[] expectedTags = null;
+
+                    listener.SetMeasurementEventCallback<int>((inst, measurement, tags, state) => {
+                        for (int i = 0; i < expectedTags.Length; i++)
+                        {
+                            Assert.Equal(expectedTags[i], tags[i]);
+                        }
+                    });
+
+                    // 0 Tags
+
+                    expectedTags = new KeyValuePair<string, object?>[0];
+                    counter.Add(10, new TagList());
+                    histogram.Record(10, new TagList());
+
+                    // 1 Tags
+                    expectedTags = new KeyValuePair<string, object?>[] { new KeyValuePair<string, object?>("Key1", "Value1") };
+                    counter.Add(10, new TagList() { expectedTags[0] });
+                    histogram.Record(10, new TagList() { new KeyValuePair<string, object?>("Key1", "Value1") });
+
+                    // 2 Tags
+                    expectedTags = new List<KeyValuePair<string, object?>>
+                    {
+                        {"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<KeyValuePair<string, object?>>
+                    {
+                        { "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<KeyValuePair<string, object?>>
+                    {
+                        { "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<T>(Counter<T> counter, T value, KeyValuePair<string, object?>[] 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<string, object?>[] { new KeyValuePair<string, object?>("K1", "V1") };
+            expectedTags = new List<KeyValuePair<string, object?>>
+            {
+                { "K1", "V1" },
+            }.ToArray();
+            expectedValue = record(instrument, expectedValue, expectedTags);
+            expectedTags = new List<KeyValuePair<string, object?>>
+            {
+                { "K1", "V1" },
+                { "K2", "V2" },
+            }.ToArray();
             expectedValue = record(instrument, expectedValue, expectedTags);
-            expectedTags = new KeyValuePair<string, object?>[] { new KeyValuePair<string, object?>("K1", "V1"), new KeyValuePair<string, object?>("K2", "V2") };
+            expectedTags = new List<KeyValuePair<string, object?>>
+            {
+                { "K1", "V1" },
+                { "K2", "V2" },
+                { "K3", "V3" },
+            }.ToArray();
             expectedValue = record(instrument, expectedValue, expectedTags);
-            expectedTags = new KeyValuePair<string, object?>[] { new KeyValuePair<string, object?>("K1", "V1"), new KeyValuePair<string, object?>("K2", "V2"), new KeyValuePair<string, object?>("K3", "V3") };
+            expectedTags = new List<KeyValuePair<string, object?>>
+            {
+                { "K1", "V1" },
+                { "K2", "V2" },
+                { "K3", "V3" },
+                { "K4", "V4" },
+            }.ToArray();
             expectedValue = record(instrument, expectedValue, expectedTags);
-            expectedTags = new KeyValuePair<string, object?>[] { new KeyValuePair<string, object?>("K1", "V1"), new KeyValuePair<string, object?>("K2", "V2"), new KeyValuePair<string, object?>("K3", "V3"), new KeyValuePair<string, object?>("K4", "V4") };
+            expectedTags = new List<KeyValuePair<string, object?>>
+            {
+                { "K1", "V1" },
+                { "K2", "V2" },
+                { "K3", "V3" },
+                { "K4", "V4" },
+                { "K5", "V5" },
+            }.ToArray();
+            expectedValue = record(instrument, expectedValue, expectedTags);
+            expectedTags = new List<KeyValuePair<string, object?>>
+            {
+                { "K1", "V1" },
+                { "K2", "V2" },
+                { "K3", "V3" },
+                { "K4", "V4" },
+                { "K5", "V5" },
+                { "K6", "V6" },
+            }.ToArray();
+            expectedValue = record(instrument, expectedValue, expectedTags);
+            expectedTags = new List<KeyValuePair<string, object?>>
+            {
+                { "K1", "V1" },
+                { "K2", "V2" },
+                { "K3", "V3" },
+                { "K4", "V4" },
+                { "K5", "V5" },
+                { "K6", "V6" },
+                { "K7", "V7" },
+            }.ToArray();
+            expectedValue = record(instrument, expectedValue, expectedTags);
+            expectedTags = new List<KeyValuePair<string, object?>>
+            {
+                { "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<KeyValuePair<string, object?>>
+            {
+                { "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<T1, T2>(this ICollection<KeyValuePair<T1, T2>> collection, T1 item1, T2 item2) => collection?.Add(new KeyValuePair<T1, T2>(item1, item2));
     }
 }
index 24c8442..00b5834 100644 (file)
@@ -29,6 +29,7 @@
     <Compile Include="MetricEventSourceTests.cs" />
     <Compile Include="MetricsTests.cs" />
     <Compile Include="PropagatorTests.cs" />
+    <Compile Include="TagListTests.cs" />
   </ItemGroup>
   <ItemGroup Condition="$(TargetFramework.StartsWith('net4'))">
     <Compile Include="HttpHandlerDiagnosticListenerTests.cs" />
diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/tests/TagListTests.cs b/src/libraries/System.Diagnostics.DiagnosticSource/tests/TagListTests.cs
new file mode 100644 (file)
index 0000000..46dcb9c
--- /dev/null
@@ -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<string, object?>[] array = new KeyValuePair<string, object?>[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<string, object?>[] array = new KeyValuePair<string, object?>[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<string, object?>("Not Exist Key", "Not Exist Value")));
+                Assert.Equal(-1, tagList.IndexOf(new KeyValuePair<string, object?>("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<string, object?>[] array = new KeyValuePair<string, object?>[tagList.Count];
+                tagList.CopyTo(array.AsSpan());
+                ValidateTags(tagList, array);
+                array = new KeyValuePair<string, object?>[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<string, object?>("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<string, object?>("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<string, object?>("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<string, object?> firstItem = list[0];
+            KeyValuePair<string, object?> lastItem = list[count - 1];
+
+            for (int i = 1; i < 10; i++)
+            {
+                list.Insert(pos, new KeyValuePair<string, object?>("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<string, object?>("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<string, object?>("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<string, object?>("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<string, object?>("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<string, object?>("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<string, object?> kvp = new KeyValuePair<string, object?>("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<string, object?> kvp = new KeyValuePair<string, object?>("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<string, object?> kvp1 = new KeyValuePair<string, object?>("k-" + i, "v" + i);
+                KeyValuePair<string, object?> kvp2 = new KeyValuePair<string, object?>("k-" + i * 100, "v" + i * 100);
+                KeyValuePair<string, object?> kvp3 = new KeyValuePair<string, object?>("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<string, object?>[] array = new KeyValuePair<string, object?>[tagList.Count];
+            tagList.CopyTo(array);
+
+                Assert.Equal(count, tagList.Count);
+            int i = 0;
+            foreach (KeyValuePair<string, object?> kvp in tagList)
+            {
+                Assert.Equal(array[i].Key, kvp.Key);
+                Assert.Equal(array[i].Value, kvp.Value);
+                i++;
+            }
+            Assert.Equal(i, tagList.Count);
+
+            IEnumerator<KeyValuePair<string, object?>> 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<string, object?>("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<string, object?>("1", 1), new KeyValuePair<string, object?>("2", 2) } ;
+            KeyValuePair<string, object?> kvp = default;
+
+            Assert.Throws<ArgumentOutOfRangeException>(() => kvp = list[2]);
+            Assert.Throws<ArgumentOutOfRangeException>(() => list[2] = kvp);
+            Assert.Throws<ArgumentOutOfRangeException>(() => kvp = list[-1]);
+            Assert.Throws<ArgumentOutOfRangeException>(() => list[-2] = kvp);
+
+            KeyValuePair<string, object?>[] array = new KeyValuePair<string, object?>[1];
+            Assert.Throws<ArgumentException>(() => list.CopyTo(array.AsSpan()));
+            Assert.Throws<ArgumentException>(() => list.CopyTo(array, 0));
+            Assert.Throws<ArgumentOutOfRangeException>(() => list.CopyTo(array, 1));
+            Assert.Throws<ArgumentOutOfRangeException>(() => list.CopyTo(array, -1));
+            Assert.Throws<ArgumentNullException>(() => list.CopyTo(null, 0));
+
+            Assert.Throws<ArgumentOutOfRangeException>(() => list.Insert(-1, default));
+            Assert.Throws<ArgumentOutOfRangeException>(() => list.Insert(3, default));
+
+            Assert.Throws<ArgumentOutOfRangeException>(() => list.RemoveAt(-1));
+            Assert.Throws<ArgumentOutOfRangeException>(() => list.RemoveAt(2));
+        }
+
+        private void ValidateTags(in TagList tagList, KeyValuePair<string, object?>[] 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);
+            }
+        }
+    }
+}
+
+