Skip to content

Added missing clearing of pooled arrays #114545

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Apr 24, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -151,6 +151,7 @@ System.Diagnostics.DiagnosticSource</PackageDescription>
<Reference Include="System.Collections" />
<Reference Include="System.Collections.Concurrent" />
<Reference Include="System.Diagnostics.Tracing" />
<Reference Include="System.Linq" />
<Reference Include="System.Memory" />
<Reference Include="System.Runtime" />
<Reference Include="System.Runtime.InteropServices" />
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@

using System.Buffers;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Linq;

namespace System.Diagnostics.Metrics
{
@@ -32,7 +32,7 @@ public Measurement(T value)
/// <param name="tags">The <see cref="KeyValuePair{TKey, TValue}"/> tags associated with the measurement.</param>
public Measurement(T value, IEnumerable<KeyValuePair<string, object?>>? tags)
{
_tags = ToArray(tags);
_tags = tags?.ToArray() ?? Instrument.EmptyTags;
Value = value;
}

@@ -98,67 +98,5 @@ public Measurement(T value, in TagList tags)
/// Gets the value of the measurement.
/// </summary>
public T Value { get; }

// Private helper to copy IEnumerable to array. We have it to avoid adding dependencies on System.Linq
private static KeyValuePair<string, object?>[] ToArray(IEnumerable<KeyValuePair<string, object?>>? tags)
{
if (tags is null)
return Instrument.EmptyTags;

KeyValuePair<string, object?>[] result;

// When the input is a collection, we can allocate a correctly sized array and copy directly into it.
if (tags is ICollection<KeyValuePair<string, object?>> collection)
{
int items = collection.Count;

if (items == 0)
return Instrument.EmptyTags;

result = new KeyValuePair<string, object?>[items];
collection.CopyTo(result, 0);
return result;
}

// In any other case, we must enumerate the input.
// We use a pooled array as a buffer to avoid allocating until we know the final size we need.
// This assumes that there are 32 or fewer tags, which is a reasonable assumption for most cases.
// In the worst case, we will grow the buffer by renting a larger array.
KeyValuePair<string, object?>[] array = ArrayPool<KeyValuePair<string, object?>>.Shared.Rent(32);
int count = 0;
int length = array.Length;

foreach (KeyValuePair<string, object?> item in tags)
{
if (count == length)
Grow(ref array, ref length);

array[count++] = item;
}

if (count == 0)
{
ArrayPool<KeyValuePair<string, object?>>.Shared.Return(array);
return Instrument.EmptyTags;
}

result = new KeyValuePair<string, object?>[count];
array.AsSpan().Slice(0, count).CopyTo(result.AsSpan());

// Note that we don't include the return of the array inside a finally block per the established guidelines for ArrayPool.
// We don't expect the above to throw an exception, but if it does it would be infrequent and the GC will collect the array.
ArrayPool<KeyValuePair<string, object?>>.Shared.Return(array);
return result;

[MethodImpl(MethodImplOptions.AggressiveInlining)]
static void Grow(ref KeyValuePair<string, object?>[] array, ref int length)
{
KeyValuePair<string, object?>[] newArray = ArrayPool<KeyValuePair<string, object?>>.Shared.Rent(length * 2);
array.CopyTo(newArray, 0);
ArrayPool<KeyValuePair<string, object?>>.Shared.Return(array);
array = newArray;
length = array.Length;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -26,6 +26,10 @@ public void MeasurementConstructionTest()
Assert.Equal(i, measurement.Value);
TagListTests.ValidateTags(new TagList(measurement.Tags), tagsArray);

measurement = new Measurement<int>(i, tagsArray.AsEnumerable());
Assert.Equal(i, measurement.Value);
TagListTests.ValidateTags(new TagList(measurement.Tags), tagsArray);

measurement = new Measurement<int>(i, tagsArray);
Assert.Equal(i, measurement.Value);
TagListTests.ValidateTags(new TagList(measurement.Tags), tagsArray);
Original file line number Diff line number Diff line change
@@ -97,5 +97,11 @@ public static ArrayPool<T> Create(int maxArrayLength, int maxArraysPerBucket) =>
/// if it's determined that the pool already has enough buffers stored.
/// </remarks>
public abstract void Return(T[] array, bool clearArray = false);

internal void Return(T[] array, int lengthToClear)
{
array.AsSpan(0, lengthToClear).Clear();
Return(array);
}
}
}
Original file line number Diff line number Diff line change
@@ -165,7 +165,24 @@ public void Dispose()
if (toReturn != null)
{
_arrayFromPool = null;

#if SYSTEM_PRIVATE_CORELIB
if (RuntimeHelpers.IsReferenceOrContainsReferences<T>())
{
ArrayPool<T>.Shared.Return(toReturn, _pos);
}
else
{
ArrayPool<T>.Shared.Return(toReturn);
}
#else
if (!typeof(T).IsPrimitive)
Copy link
Preview

Copilot AI Apr 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fallback check using !typeof(T).IsPrimitive may not accurately determine if T contains references for value types. Please add a clarifying comment that explains this assumption, given that in current usages T is always a primitive unmanaged type.

Copilot uses AI. Check for mistakes.

{
Array.Clear(toReturn, 0, _pos);
}

ArrayPool<T>.Shared.Return(toReturn);
#endif
}
}

@@ -200,7 +217,23 @@ private void Grow(int additionalCapacityRequired = 1)
_span = _arrayFromPool = array;
if (toReturn != null)
{
#if SYSTEM_PRIVATE_CORELIB
if (RuntimeHelpers.IsReferenceOrContainsReferences<T>())
{
ArrayPool<T>.Shared.Return(toReturn, _pos);
}
else
{
ArrayPool<T>.Shared.Return(toReturn);
}
#else
if (!typeof(T).IsPrimitive)
Copy link
Preview

Copilot AI Apr 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the Dispose method, the Grow method uses the !typeof(T).IsPrimitive check as a fallback. Consider adding a comment here to clarify that this condition is acceptable because T is ensured to be a primitive unmanaged type in all current contexts.

Copilot uses AI. Check for mistakes.

{
Array.Clear(toReturn, 0, _pos);
}

ArrayPool<T>.Shared.Return(toReturn);
#endif
}
}
}
Loading
Oops, something went wrong.