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
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; }
}
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; }
}
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
<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
<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'">
/// <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
/// <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
#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()
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>
/// <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));
}
/// <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));
}
/// <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
{
[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.
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
--- /dev/null
+// 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
}).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)
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;
}
}
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;
}
}
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);
}
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));
}
}
<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" />
--- /dev/null
+// 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);
+ }
+ }
+ }
+}
+
+