From 48f079af32ce72a93075148210b9cc3dcaf41fb6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 5 Dec 2025 02:16:53 +0000 Subject: [PATCH 1/6] Initial plan From 2076a3922ec15a5b97f24b14bee3ce1bf5a5e99b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 5 Dec 2025 02:48:58 +0000 Subject: [PATCH 2/6] Add AddRange method to ArrayBuilder and refactor Directory, DirectoryInfo, and Timer to use it Co-authored-by: 333fred <2371880+333fred@users.noreply.github.com> --- .../Collections/Generic/ArrayBuilder.cs | 14 +++++++ .../Collections/Generic/ArrayBuilderTests.cs | 41 +++++++++++++++++++ .../src/System/IO/Directory.cs | 18 ++++++-- .../src/System/IO/DirectoryInfo.cs | 18 ++++++-- .../src/System/Threading/Timer.cs | 10 ++++- 5 files changed, 94 insertions(+), 7 deletions(-) diff --git a/src/libraries/Common/src/System/Collections/Generic/ArrayBuilder.cs b/src/libraries/Common/src/System/Collections/Generic/ArrayBuilder.cs index ec862f46d5c3f1..b720d5a366c732 100644 --- a/src/libraries/Common/src/System/Collections/Generic/ArrayBuilder.cs +++ b/src/libraries/Common/src/System/Collections/Generic/ArrayBuilder.cs @@ -70,6 +70,20 @@ public void Add(T item) UncheckedAdd(item); } + /// + /// Adds a range of items to the backing array, resizing it as necessary. + /// + /// The items to add. + public void AddRange(IEnumerable items) + { + Debug.Assert(items != null); + + foreach (T item in items) + { + Add(item); + } + } + /// /// Gets the first item in this builder. /// diff --git a/src/libraries/Common/tests/Tests/System/Collections/Generic/ArrayBuilderTests.cs b/src/libraries/Common/tests/Tests/System/Collections/Generic/ArrayBuilderTests.cs index b82ed6509155b6..a1f5345b393aa9 100644 --- a/src/libraries/Common/tests/Tests/System/Collections/Generic/ArrayBuilderTests.cs +++ b/src/libraries/Common/tests/Tests/System/Collections/Generic/ArrayBuilderTests.cs @@ -112,6 +112,47 @@ public void UncheckedAdd(int capacity) VerifyBuilderContents(Enumerable.Repeat(default(T), capacity), builder); } + [Theory] + [MemberData(nameof(EnumerableData))] + public void AddRange(IEnumerable seed) + { + var builder = new ArrayBuilder(); + builder.AddRange(seed); + + int count = builder.Count; + T[] array = builder.ToArray(); + + Assert.Equal(count, array.Length); + Assert.Equal(seed, array); + } + + [Fact] + public void AddRange_EmptyEnumerable() + { + var builder = new ArrayBuilder(); + builder.AddRange(Enumerable.Empty()); + + Assert.Equal(0, builder.Count); + Assert.Same(Array.Empty(), builder.ToArray()); + } + + [Theory] + [MemberData(nameof(EnumerableData))] + public void AddRange_AfterAdd(IEnumerable seed) + { + var builder = new ArrayBuilder(); + builder.Add(default(T)); + builder.AddRange(seed); + + int expectedCount = 1 + seed.Count(); + Assert.Equal(expectedCount, builder.Count); + + T[] array = builder.ToArray(); + Assert.Equal(expectedCount, array.Length); + Assert.Equal(default(T), array[0]); + Assert.Equal(seed, array.Skip(1)); + } + public static TheoryData CapacityData() { var data = new TheoryData(); diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/Directory.cs b/src/libraries/System.Private.CoreLib/src/System/IO/Directory.cs index 78715de118e537..92eb76c35c8bf8 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/Directory.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/Directory.cs @@ -171,7 +171,11 @@ public static string[] GetFiles(string path, string searchPattern, SearchOption => GetFiles(path, searchPattern, EnumerationOptions.FromSearchOption(searchOption)); public static string[] GetFiles(string path, string searchPattern, EnumerationOptions enumerationOptions) - => new List(InternalEnumeratePaths(path, searchPattern, SearchTarget.Files, enumerationOptions)).ToArray(); + { + ArrayBuilder builder = default; + builder.AddRange(InternalEnumeratePaths(path, searchPattern, SearchTarget.Files, enumerationOptions)); + return builder.ToArray(); + } public static string[] GetDirectories(string path) => GetDirectories(path, "*", enumerationOptions: EnumerationOptions.Compatible); @@ -181,7 +185,11 @@ public static string[] GetDirectories(string path, string searchPattern, SearchO => GetDirectories(path, searchPattern, EnumerationOptions.FromSearchOption(searchOption)); public static string[] GetDirectories(string path, string searchPattern, EnumerationOptions enumerationOptions) - => new List(InternalEnumeratePaths(path, searchPattern, SearchTarget.Directories, enumerationOptions)).ToArray(); + { + ArrayBuilder builder = default; + builder.AddRange(InternalEnumeratePaths(path, searchPattern, SearchTarget.Directories, enumerationOptions)); + return builder.ToArray(); + } public static string[] GetFileSystemEntries(string path) => GetFileSystemEntries(path, "*", enumerationOptions: EnumerationOptions.Compatible); @@ -191,7 +199,11 @@ public static string[] GetFileSystemEntries(string path, string searchPattern, S => GetFileSystemEntries(path, searchPattern, EnumerationOptions.FromSearchOption(searchOption)); public static string[] GetFileSystemEntries(string path, string searchPattern, EnumerationOptions enumerationOptions) - => new List(InternalEnumeratePaths(path, searchPattern, SearchTarget.Both, enumerationOptions)).ToArray(); + { + ArrayBuilder builder = default; + builder.AddRange(InternalEnumeratePaths(path, searchPattern, SearchTarget.Both, enumerationOptions)); + return builder.ToArray(); + } internal static IEnumerable InternalEnumeratePaths( string path, diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/DirectoryInfo.cs b/src/libraries/System.Private.CoreLib/src/System/IO/DirectoryInfo.cs index 835ab22793730c..9ab3d9ba083105 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/DirectoryInfo.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/DirectoryInfo.cs @@ -115,7 +115,11 @@ public FileInfo[] GetFiles(string searchPattern, SearchOption searchOption) => GetFiles(searchPattern, EnumerationOptions.FromSearchOption(searchOption)); public FileInfo[] GetFiles(string searchPattern, EnumerationOptions enumerationOptions) - => new List((IEnumerable)InternalEnumerateInfos(FullPath, searchPattern, SearchTarget.Files, enumerationOptions)).ToArray(); + { + ArrayBuilder builder = default; + builder.AddRange((IEnumerable)InternalEnumerateInfos(FullPath, searchPattern, SearchTarget.Files, enumerationOptions)); + return builder.ToArray(); + } // Returns an array of strongly typed FileSystemInfo entries which will contain a listing // of all the files and directories. @@ -130,7 +134,11 @@ public FileSystemInfo[] GetFileSystemInfos(string searchPattern, SearchOption se => GetFileSystemInfos(searchPattern, EnumerationOptions.FromSearchOption(searchOption)); public FileSystemInfo[] GetFileSystemInfos(string searchPattern, EnumerationOptions enumerationOptions) - => new List(InternalEnumerateInfos(FullPath, searchPattern, SearchTarget.Both, enumerationOptions)).ToArray(); + { + ArrayBuilder builder = default; + builder.AddRange(InternalEnumerateInfos(FullPath, searchPattern, SearchTarget.Both, enumerationOptions)); + return builder.ToArray(); + } // Returns an array of Directories in the current directory. public DirectoryInfo[] GetDirectories() => GetDirectories("*", enumerationOptions: EnumerationOptions.Compatible); @@ -143,7 +151,11 @@ public DirectoryInfo[] GetDirectories(string searchPattern, SearchOption searchO => GetDirectories(searchPattern, EnumerationOptions.FromSearchOption(searchOption)); public DirectoryInfo[] GetDirectories(string searchPattern, EnumerationOptions enumerationOptions) - => new List((IEnumerable)InternalEnumerateInfos(FullPath, searchPattern, SearchTarget.Directories, enumerationOptions)).ToArray(); + { + ArrayBuilder builder = default; + builder.AddRange((IEnumerable)InternalEnumerateInfos(FullPath, searchPattern, SearchTarget.Directories, enumerationOptions)); + return builder.ToArray(); + } public IEnumerable EnumerateDirectories() => EnumerateDirectories("*", enumerationOptions: EnumerationOptions.Compatible); diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/Timer.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/Timer.cs index c9747d2cbdec55..497eb078bc57f0 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/Timer.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/Timer.cs @@ -103,7 +103,15 @@ public TimerQueueDebuggerTypeProxy(TimerQueue queue) } [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] - public TimerQueueTimer[] Items => new List(_queue.GetTimersForDebugger()).ToArray(); + public TimerQueueTimer[] Items + { + get + { + ArrayBuilder builder = default; + builder.AddRange(_queue.GetTimersForDebugger()); + return builder.ToArray(); + } + } } #endregion From c6ef6461a446b129c947adbc05adbc3b2777562a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 5 Dec 2025 02:56:19 +0000 Subject: [PATCH 3/6] Optimize ArrayBuilder.AddRange to pre-allocate for ICollection Co-authored-by: 333fred <2371880+333fred@users.noreply.github.com> --- .../Collections/Generic/ArrayBuilder.cs | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/libraries/Common/src/System/Collections/Generic/ArrayBuilder.cs b/src/libraries/Common/src/System/Collections/Generic/ArrayBuilder.cs index b720d5a366c732..1e6d11880235c2 100644 --- a/src/libraries/Common/src/System/Collections/Generic/ArrayBuilder.cs +++ b/src/libraries/Common/src/System/Collections/Generic/ArrayBuilder.cs @@ -78,9 +78,28 @@ public void AddRange(IEnumerable items) { Debug.Assert(items != null); - foreach (T item in items) + if (items is ICollection collection) { - Add(item); + int count = collection.Count; + if (count > 0) + { + if (_count + count > Capacity) + { + EnsureCapacity(_count + count); + } + + foreach (T item in collection) + { + UncheckedAdd(item); + } + } + } + else + { + foreach (T item in items) + { + Add(item); + } } } From 986817d257107aa662e08213934898b6953f3a9a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 5 Dec 2025 03:33:10 +0000 Subject: [PATCH 4/6] Add tests for ICollection optimization in ArrayBuilder.AddRange Co-authored-by: 333fred <2371880+333fred@users.noreply.github.com> --- .../Collections/Generic/ArrayBuilder.cs | 26 ++++++ .../Collections/Generic/ArrayBuilderTests.cs | 93 +++++++++++++++++++ 2 files changed, 119 insertions(+) diff --git a/src/coreclr/tools/Common/System/Collections/Generic/ArrayBuilder.cs b/src/coreclr/tools/Common/System/Collections/Generic/ArrayBuilder.cs index c947e2c762ff64..3732156e697568 100644 --- a/src/coreclr/tools/Common/System/Collections/Generic/ArrayBuilder.cs +++ b/src/coreclr/tools/Common/System/Collections/Generic/ArrayBuilder.cs @@ -87,6 +87,32 @@ public void Append(ArrayBuilder newItems) _count += newItems.Count; } + public void AddRange(IEnumerable items) + { + Debug.Assert(items != null); + + if (items is ICollection collection) + { + int count = collection.Count; + if (count > 0) + { + EnsureCapacity(_count + count); + + foreach (T item in collection) + { + _items[_count++] = item; + } + } + } + else + { + foreach (T item in items) + { + Add(item); + } + } + } + public void ZeroExtend(int numItems) { Debug.Assert(numItems >= 0); diff --git a/src/libraries/Common/tests/Tests/System/Collections/Generic/ArrayBuilderTests.cs b/src/libraries/Common/tests/Tests/System/Collections/Generic/ArrayBuilderTests.cs index a1f5345b393aa9..f33c750c736b42 100644 --- a/src/libraries/Common/tests/Tests/System/Collections/Generic/ArrayBuilderTests.cs +++ b/src/libraries/Common/tests/Tests/System/Collections/Generic/ArrayBuilderTests.cs @@ -153,6 +153,99 @@ public void AddRange_AfterAdd(IEnumerable seed) Assert.Equal(seed, array.Skip(1)); } + [Theory] + [MemberData(nameof(CountData))] + public void AddRange_ICollection_PreallocatesCapacity(int count) + { + // Use a List which implements ICollection + List collection = s_generator.GenerateEnumerable(count).ToList(); + + var builder = new ArrayBuilder(); + builder.AddRange(collection); + + Assert.Equal(count, builder.Count); + + // When adding an ICollection, capacity should be at least + // enough for the collection (0 if empty) + if (count > 0) + { + Assert.True(builder.Capacity >= count); + } + else + { + Assert.Equal(0, builder.Capacity); + } + + Assert.Equal(collection, builder.ToArray()); + } + + [Fact] + public void AddRange_ICollection_EmptyCollection() + { + // Use an empty List which implements ICollection + List emptyCollection = new List(); + + var builder = new ArrayBuilder(); + builder.AddRange(emptyCollection); + + Assert.Equal(0, builder.Count); + Assert.Equal(0, builder.Capacity); + Assert.Same(Array.Empty(), builder.ToArray()); + } + + [Theory] + [MemberData(nameof(CountData))] + public void AddRange_ICollection_AfterExistingItems(int count) + { + // Add some initial items + var builder = new ArrayBuilder(); + builder.Add(default(T)); + builder.Add(default(T)); + int initialCount = 2; + int initialCapacity = builder.Capacity; + + // Use a List which implements ICollection + List collection = s_generator.GenerateEnumerable(count).ToList(); + builder.AddRange(collection); + + int expectedCount = initialCount + count; + Assert.Equal(expectedCount, builder.Count); + + // When adding ICollection after existing items, capacity should be + // at least enough for all items + Assert.True(builder.Capacity >= expectedCount); + + T[] array = builder.ToArray(); + Assert.Equal(expectedCount, array.Length); + Assert.Equal(default(T), array[0]); + Assert.Equal(default(T), array[1]); + Assert.Equal(collection, array.Skip(2)); + } + + [Theory] + [MemberData(nameof(CountData))] + public void AddRange_NonICollection_GrowsIncrementally(int count) + { + // Use a generator method to create a pure IEnumerable that is NOT an ICollection + IEnumerable NonCollectionEnumerable() + { + for (int i = 0; i < count; i++) + { + yield return default(T); + } + } + + var builder = new ArrayBuilder(); + builder.AddRange(NonCollectionEnumerable()); + + Assert.Equal(count, builder.Count); + + // Non-ICollection path should grow incrementally (powers of 2) + Assert.Equal(CalculateExpectedCapacity(count), builder.Capacity); + + Assert.Equal(NonCollectionEnumerable(), builder.ToArray()); + } + public static TheoryData CapacityData() { var data = new TheoryData(); From 9491f524b18dd36620cbbe1e588e95abd3717c6e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Dec 2025 18:21:55 +0000 Subject: [PATCH 5/6] Use ICollection.CopyTo for better performance and revert Timer.cs change Co-authored-by: 333fred <2371880+333fred@users.noreply.github.com> --- .../Common/System/Collections/Generic/ArrayBuilder.cs | 6 ++---- .../src/System/Collections/Generic/ArrayBuilder.cs | 6 ++---- .../src/System/Threading/Timer.cs | 10 +--------- 3 files changed, 5 insertions(+), 17 deletions(-) diff --git a/src/coreclr/tools/Common/System/Collections/Generic/ArrayBuilder.cs b/src/coreclr/tools/Common/System/Collections/Generic/ArrayBuilder.cs index 3732156e697568..fc2737385c4541 100644 --- a/src/coreclr/tools/Common/System/Collections/Generic/ArrayBuilder.cs +++ b/src/coreclr/tools/Common/System/Collections/Generic/ArrayBuilder.cs @@ -98,10 +98,8 @@ public void AddRange(IEnumerable items) { EnsureCapacity(_count + count); - foreach (T item in collection) - { - _items[_count++] = item; - } + collection.CopyTo(_items, _count); + _count += count; } } else diff --git a/src/libraries/Common/src/System/Collections/Generic/ArrayBuilder.cs b/src/libraries/Common/src/System/Collections/Generic/ArrayBuilder.cs index 1e6d11880235c2..765589b4ca4dde 100644 --- a/src/libraries/Common/src/System/Collections/Generic/ArrayBuilder.cs +++ b/src/libraries/Common/src/System/Collections/Generic/ArrayBuilder.cs @@ -88,10 +88,8 @@ public void AddRange(IEnumerable items) EnsureCapacity(_count + count); } - foreach (T item in collection) - { - UncheckedAdd(item); - } + collection.CopyTo(_array!, _count); + _count += count; } } else diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/Timer.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/Timer.cs index 497eb078bc57f0..c9747d2cbdec55 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/Timer.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/Timer.cs @@ -103,15 +103,7 @@ public TimerQueueDebuggerTypeProxy(TimerQueue queue) } [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] - public TimerQueueTimer[] Items - { - get - { - ArrayBuilder builder = default; - builder.AddRange(_queue.GetTimersForDebugger()); - return builder.ToArray(); - } - } + public TimerQueueTimer[] Items => new List(_queue.GetTimersForDebugger()).ToArray(); } #endregion From 7c669d1d384fdb2f9be187bdf6c16eeb1cec8f34 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Dec 2025 18:45:36 +0000 Subject: [PATCH 6/6] Rename EnsureCapacity to Grow for clarity in Common ArrayBuilder Co-authored-by: 333fred <2371880+333fred@users.noreply.github.com> --- .../Common/src/System/Collections/Generic/ArrayBuilder.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libraries/Common/src/System/Collections/Generic/ArrayBuilder.cs b/src/libraries/Common/src/System/Collections/Generic/ArrayBuilder.cs index 765589b4ca4dde..b5419ee03ec8d5 100644 --- a/src/libraries/Common/src/System/Collections/Generic/ArrayBuilder.cs +++ b/src/libraries/Common/src/System/Collections/Generic/ArrayBuilder.cs @@ -64,7 +64,7 @@ public void Add(T item) { if (_count == Capacity) { - EnsureCapacity(_count + 1); + Grow(_count + 1); } UncheckedAdd(item); @@ -85,7 +85,7 @@ public void AddRange(IEnumerable items) { if (_count + count > Capacity) { - EnsureCapacity(_count + count); + Grow(_count + count); } collection.CopyTo(_array!, _count); @@ -167,7 +167,7 @@ public void UncheckedAdd(T item) _array![_count++] = item; } - private void EnsureCapacity(int minimum) + private void Grow(int minimum) { Debug.Assert(minimum > Capacity);