From bdaabf0b7b2ceb611ef6c549d9996cb34e79d1c2 Mon Sep 17 00:00:00 2001 From: Atif Aziz Date: Thu, 20 Oct 2022 19:26:31 +0200 Subject: [PATCH 01/33] Add "Batch" overloads to get pooled buckets --- MoreLinq.Test/BatchTest.cs | 141 +++++++++++++++---- MoreLinq/Experimental/Batch.cs | 241 +++++++++++++++++++++++++++++++++ MoreLinq/MoreLinq.csproj | 10 +- 3 files changed, 365 insertions(+), 27 deletions(-) create mode 100644 MoreLinq/Experimental/Batch.cs diff --git a/MoreLinq.Test/BatchTest.cs b/MoreLinq.Test/BatchTest.cs index c5c9186e7..f62e03aca 100644 --- a/MoreLinq.Test/BatchTest.cs +++ b/MoreLinq.Test/BatchTest.cs @@ -21,26 +21,52 @@ namespace MoreLinq.Test using NUnit.Framework; [TestFixture] - public class BatchTest + public class BatchTest : BatchBaseTest { + protected override IEnumerable> Batch(IEnumerable source, int size) => + source.Batch(size); + + [Test] + public void BatchSequenceYieldsListsOfBatches() + { + var result = new[] { 1, 2, 3 }.Batch(2); + + using var reader = result.Read(); + Assert.That(reader.Read(), Is.InstanceOf(typeof(IList))); + Assert.That(reader.Read(), Is.InstanceOf(typeof(IList))); + reader.ReadEnd(); + } + + [Test] + public void BatchSequenceTransformingResult() + { + var result = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }.Batch(4, batch => batch.Sum()); + result.AssertSequenceEqual(10, 26, 9); + } + } + + public abstract class BatchBaseTest + { + protected abstract IEnumerable> Batch(IEnumerable source, int size); + [Test] public void BatchZeroSize() { AssertThrowsArgument.OutOfRangeException("size",() => - new object[0].Batch(0)); + Batch(new object[0], 0)); } [Test] public void BatchNegativeSize() { AssertThrowsArgument.OutOfRangeException("size",() => - new object[0].Batch(-1)); + Batch(new object[0], -1)); } [Test] public void BatchEvenlyDivisibleSequence() { - var result = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }.Batch(3); + var result = Batch(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }, 3); using var reader = result.Read(); reader.Read().AssertSequenceEqual(1, 2, 3); @@ -52,7 +78,7 @@ public void BatchEvenlyDivisibleSequence() [Test] public void BatchUnevenlyDivisibleSequence() { - var result = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }.Batch(4); + var result = Batch(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }, 4); using var reader = result.Read(); reader.Read().AssertSequenceEqual(1, 2, 3, 4); @@ -61,28 +87,10 @@ public void BatchUnevenlyDivisibleSequence() reader.ReadEnd(); } - [Test] - public void BatchSequenceTransformingResult() - { - var result = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }.Batch(4, batch => batch.Sum()); - result.AssertSequenceEqual(10, 26, 9); - } - - [Test] - public void BatchSequenceYieldsListsOfBatches() - { - var result = new[] { 1, 2, 3 }.Batch(2); - - using var reader = result.Read(); - Assert.That(reader.Read(), Is.InstanceOf(typeof(IList))); - Assert.That(reader.Read(), Is.InstanceOf(typeof(IList))); - reader.ReadEnd(); - } - [Test] public void BatchSequencesAreIndependentInstances() { - var result = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }.Batch(4); + var result = Batch(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }, 4); using var reader = result.Read(); var first = reader.Read(); @@ -141,3 +149,88 @@ public void BatchEmptySource(SourceKind kind) } } } + +#if NETCOREAPP3_1_OR_GREATER + +namespace MoreLinq.Test +{ + using System; + using System.Buffers; + using System.Collections.Generic; + using MoreLinq.Experimental; + using NUnit.Framework; + + [TestFixture] + public class BatchArrayPoolTest : BatchBaseTest + { + protected override IEnumerable> Batch(IEnumerable source, int size) => + from b in source.Batch(size, ArrayPool.Shared) + select b.Bucket.Take(b.Length); + + [Test] + public void BatchReturnsNewArraysWhenUnreturnedToPool() + { + var pairs = Enumerable.Range(1, 100) + .Batch(13, ArrayPool.Shared) + .Pairwise(ValueTuple.Create); + + foreach (var ((prev, _), (curr, _)) in pairs) + Assert.That(curr, Is.Not.SameAs(prev)); + } + + [Test] + public void BatchReusesReturnedArraysFromPool() + { + int[] previousBucket = null; + var pool = ArrayPool.Shared; + + foreach (var (bucket, _) in Enumerable.Range(1, 100) + .Batch(13, pool)) + { + if (previousBucket is { } somePreviousBucket) + Assert.That(bucket, Is.SameAs(somePreviousBucket)); + + previousBucket = bucket; + + pool.Return(bucket); + } + } + } + + public class BatchMemoryPoolTest : BatchBaseTest + { + protected override IEnumerable> Batch(IEnumerable source, int size) => + from b in source.Batch(size, MemoryPool.Shared) + select Elements(b.Bucket.Memory).Take(b.Length); + + static IEnumerable Elements(Memory memory) + { + for (var i = 0; i < memory.Length; i++) + yield return memory.Span[i]; + } + + [TestCase(false)] + [TestCase(true)] + public void BatchMemoryReuseFromPool(bool disposeBucket) + { + foreach (var (i, (bucket, _)) in Enumerable.Range(1, 100) + .Batch(13, MemoryPool.Shared) + .Index()) + { + var memory = bucket.Memory; + + Assert.That(memory.Length, Is.GreaterThan(13)); + + if (i is 0) + memory.Span.Fill(42); + else + Assert.That(memory.Span[^1], Is.EqualTo(disposeBucket ? 42 : 0)); + + if (disposeBucket) + bucket.Dispose(); + } + } + } +} + +#endif diff --git a/MoreLinq/Experimental/Batch.cs b/MoreLinq/Experimental/Batch.cs new file mode 100644 index 000000000..a877fe411 --- /dev/null +++ b/MoreLinq/Experimental/Batch.cs @@ -0,0 +1,241 @@ +#region License and Terms +// MoreLINQ - Extensions to LINQ to Objects +// Copyright (c) 2022 Atif Aziz. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#endregion + +#if !NO_MEMORY + +namespace MoreLinq.Experimental +{ + using System; + using System.Buffers; + using System.Collections.Generic; + using System.Linq; + + static partial class ExperimentalEnumerable + { + /// + /// Batches the source sequence into sized buckets using a memory pool + /// to rent memory to back each bucket. + /// + /// Type of elements in sequence. + /// The source sequence. + /// Size of buckets. + /// The memory pool used to rent memory for each bucket. + /// A sequence of equally sized buckets containing elements of the source collection. + /// + /// + /// This operator uses deferred execution and streams its results + /// (buckets are streamed but their content buffered). + /// + /// + /// Each bucket is backed by rented memory that may be at least + /// in length. The second element paired with + /// each bucket is the actual length of the bucket that is valid to use. + /// The rented memory should be disposed to return it to the pool given + /// in the argument. If it is returned as each + /// bucket is retrieved during iteration then there is a good chance that + /// the same memory will be reused for subsequent buckets. This can save + /// allocations for very large buckets. + /// + /// + /// When more than one bucket is streamed, all buckets except the last + /// is guaranteed to have elements. The last + /// bucket may be smaller depending on the remaining elements in the + /// sequence. + /// Each bucket is pre-allocated to elements. + /// If is set to a very large value, e.g. + /// to effectively disable batching by just + /// hoping for a single bucket, then it can lead to memory exhaustion + /// (). + /// + /// + + public static IEnumerable<(IMemoryOwner Bucket, int Length)> + Batch(this IEnumerable source, int size, MemoryPool pool) + { + if (source == null) throw new ArgumentNullException(nameof(source)); + if (pool == null) throw new ArgumentNullException(nameof(pool)); + if (size <= 0) throw new ArgumentOutOfRangeException(nameof(size)); + + switch (source) + { + case ICollection { Count: 0 }: + { + return Enumerable.Empty<(IMemoryOwner, int)>(); + } + case ICollection collection when collection.Count <= size: + { + return Batch(collection.Count); + } + case IReadOnlyCollection { Count: 0 }: + { + return Enumerable.Empty<(IMemoryOwner, int)>(); + } + case IReadOnlyList list when list.Count <= size: + { + return _(); IEnumerable<(IMemoryOwner, int)> _() + { + var bucket = pool.Rent(list.Count); + for (var i = 0; i < list.Count; i++) + bucket.Memory.Span[i] = list[i]; + yield return (bucket, list.Count); + } + } + case IReadOnlyCollection collection when collection.Count <= size: + { + return Batch(collection.Count); + } + default: + { + return Batch(size); + } + + IEnumerable<(IMemoryOwner, int)> Batch(int size) + { + IMemoryOwner? bucket = null; + var count = 0; + + foreach (var item in source) + { + bucket ??= pool.Rent(size); + bucket.Memory.Span[count++] = item; + + // The bucket is fully buffered before it's yielded + if (count != size) + continue; + + yield return (bucket, size); + + bucket = null; + count = 0; + } + + // Return the last bucket with all remaining elements + if (bucket is { } someBucket && count > 0) + yield return (someBucket, count); + } + } + } + + /// + /// Batches the source sequence into sized buckets using a array pool + /// to rent an array to back each bucket. + /// + /// Type of elements in sequence. + /// The source sequence. + /// Size of buckets. + /// The pool used to rent the array for each bucket. + /// A sequence of equally sized buckets containing elements of the source collection. + /// + /// + /// This operator uses deferred execution and streams its results + /// (buckets are streamed but their content buffered). + /// + /// + /// Each bucket is backed by a rented array that may be at least + /// in length. The second element paired with + /// each bucket is the actual length of the bucket that is valid to use. + /// The rented array should be returned to the pool sent as the + /// argument. If it is returned as each bucket + /// is retrieved during iteration then there is a good chance that the + /// same array allocation will be reused for subsequent buckets. This + /// can save allocations for very large buckets. + /// + /// + /// When more than one bucket is streamed, all buckets except the last + /// is guaranteed to have elements. The last + /// bucket may be smaller depending on the remaining elements in the + /// sequence. + /// Each bucket is pre-allocated to elements. + /// If is set to a very large value, e.g. + /// to effectively disable batching by just + /// hoping for a single bucket, then it can lead to memory exhaustion + /// (). + /// + /// + + public static IEnumerable<(T[] Bucket, int Length)> + Batch(this IEnumerable source, int size, ArrayPool pool) + { + if (source == null) throw new ArgumentNullException(nameof(source)); + if (pool == null) throw new ArgumentNullException(nameof(pool)); + if (size <= 0) throw new ArgumentOutOfRangeException(nameof(size)); + + switch (source) + { + case ICollection { Count: 0 }: + { + return Enumerable.Empty<(T[], int)>(); + } + case ICollection collection when collection.Count <= size: + { + var bucket = pool.Rent(collection.Count); + collection.CopyTo(bucket, 0); + return MoreEnumerable.Return((bucket, collection.Count)); + } + case IReadOnlyCollection { Count: 0 }: + { + return Enumerable.Empty<(T[], int)>(); + } + case IReadOnlyList list when list.Count <= size: + { + return _(); IEnumerable<(T[], int)> _() + { + var bucket = pool.Rent(list.Count); + for (var i = 0; i < list.Count; i++) + bucket[i] = list[i]; + yield return (bucket, list.Count); + } + } + case IReadOnlyCollection collection when collection.Count <= size: + { + return Batch(collection.Count); + } + default: + { + return Batch(size); + } + } + + IEnumerable<(T[], int)> Batch(int size) + { + T[]? bucket = null; + var count = 0; + + foreach (var item in source) + { + bucket ??= pool.Rent(size); + bucket[count++] = item; + + // The bucket is fully buffered before it's yielded + if (count != size) + continue; + + yield return (bucket, size); + + bucket = null; + count = 0; + } + + // Return the last bucket with all remaining elements + if (bucket is { } someBucket && count > 0) + yield return (someBucket, count); + } + } + } +} + +#endif // !NO_MEMORY diff --git a/MoreLinq/MoreLinq.csproj b/MoreLinq/MoreLinq.csproj index 252535bf1..80b9f534e 100644 --- a/MoreLinq/MoreLinq.csproj +++ b/MoreLinq/MoreLinq.csproj @@ -119,7 +119,7 @@ en-US 3.3.2 MoreLINQ Developers. - net451;netstandard1.0;netstandard2.0 + net451;netstandard1.0;netstandard2.0;netstandard2.1 enable @@ -189,12 +189,16 @@ - + $(DefineConstants);MORELINQ + + $(DefineConstants);NO_MEMORY + + - $(DefineConstants);MORELINQ;NO_SERIALIZATION_ATTRIBUTES;NO_EXCEPTION_SERIALIZATION;NO_TRACING;NO_COM;NO_ASYNC + $(DefineConstants);MORELINQ;NO_MEMORY;NO_SERIALIZATION_ATTRIBUTES;NO_EXCEPTION_SERIALIZATION;NO_TRACING;NO_COM;NO_ASYNC From 4d8464beba8fe8a90e02fffb2dc1558be4798204 Mon Sep 17 00:00:00 2001 From: Atif Aziz Date: Fri, 21 Oct 2022 00:26:10 +0200 Subject: [PATCH 02/33] Refactor to return buckets cursor --- MoreLinq.Test/BatchTest.cs | 191 ++++++++++++++----------- MoreLinq/Experimental/Batch.cs | 248 ++++++++++++++++++++++++++++----- 2 files changed, 327 insertions(+), 112 deletions(-) diff --git a/MoreLinq.Test/BatchTest.cs b/MoreLinq.Test/BatchTest.cs index f62e03aca..44ef75149 100644 --- a/MoreLinq.Test/BatchTest.cs +++ b/MoreLinq.Test/BatchTest.cs @@ -21,52 +21,26 @@ namespace MoreLinq.Test using NUnit.Framework; [TestFixture] - public class BatchTest : BatchBaseTest + public class BatchTest { - protected override IEnumerable> Batch(IEnumerable source, int size) => - source.Batch(size); - - [Test] - public void BatchSequenceYieldsListsOfBatches() - { - var result = new[] { 1, 2, 3 }.Batch(2); - - using var reader = result.Read(); - Assert.That(reader.Read(), Is.InstanceOf(typeof(IList))); - Assert.That(reader.Read(), Is.InstanceOf(typeof(IList))); - reader.ReadEnd(); - } - - [Test] - public void BatchSequenceTransformingResult() - { - var result = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }.Batch(4, batch => batch.Sum()); - result.AssertSequenceEqual(10, 26, 9); - } - } - - public abstract class BatchBaseTest - { - protected abstract IEnumerable> Batch(IEnumerable source, int size); - [Test] public void BatchZeroSize() { AssertThrowsArgument.OutOfRangeException("size",() => - Batch(new object[0], 0)); + new object[0].Batch(0)); } [Test] public void BatchNegativeSize() { AssertThrowsArgument.OutOfRangeException("size",() => - Batch(new object[0], -1)); + new object[0].Batch(-1)); } [Test] public void BatchEvenlyDivisibleSequence() { - var result = Batch(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }, 3); + var result = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }.Batch(3); using var reader = result.Read(); reader.Read().AssertSequenceEqual(1, 2, 3); @@ -78,7 +52,7 @@ public void BatchEvenlyDivisibleSequence() [Test] public void BatchUnevenlyDivisibleSequence() { - var result = Batch(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }, 4); + var result = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }.Batch(4); using var reader = result.Read(); reader.Read().AssertSequenceEqual(1, 2, 3, 4); @@ -87,10 +61,28 @@ public void BatchUnevenlyDivisibleSequence() reader.ReadEnd(); } + [Test] + public void BatchSequenceTransformingResult() + { + var result = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }.Batch(4, batch => batch.Sum()); + result.AssertSequenceEqual(10, 26, 9); + } + + [Test] + public void BatchSequenceYieldsListsOfBatches() + { + var result = new[] { 1, 2, 3 }.Batch(2); + + using var reader = result.Read(); + Assert.That(reader.Read(), Is.InstanceOf(typeof(IList))); + Assert.That(reader.Read(), Is.InstanceOf(typeof(IList))); + reader.ReadEnd(); + } + [Test] public void BatchSequencesAreIndependentInstances() { - var result = Batch(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }, 4); + var result = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }.Batch(4); using var reader = result.Read(); var first = reader.Read(); @@ -161,76 +153,117 @@ namespace MoreLinq.Test using NUnit.Framework; [TestFixture] - public class BatchArrayPoolTest : BatchBaseTest + public abstract class BatchPoolTest { - protected override IEnumerable> Batch(IEnumerable source, int size) => - from b in source.Batch(size, ArrayPool.Shared) - select b.Bucket.Take(b.Length); + protected abstract IBatchBucket Batch(IEnumerable source, int size); [Test] - public void BatchReturnsNewArraysWhenUnreturnedToPool() + public void BatchZeroSize() { - var pairs = Enumerable.Range(1, 100) - .Batch(13, ArrayPool.Shared) - .Pairwise(ValueTuple.Create); - - foreach (var ((prev, _), (curr, _)) in pairs) - Assert.That(curr, Is.Not.SameAs(prev)); + AssertThrowsArgument.OutOfRangeException("size",() => + Batch(new object[0], 0)); } [Test] - public void BatchReusesReturnedArraysFromPool() + public void BatchNegativeSize() { - int[] previousBucket = null; - var pool = ArrayPool.Shared; + AssertThrowsArgument.OutOfRangeException("size",() => + Batch(new object[0], -1)); + } - foreach (var (bucket, _) in Enumerable.Range(1, 100) - .Batch(13, pool)) - { - if (previousBucket is { } somePreviousBucket) - Assert.That(bucket, Is.SameAs(somePreviousBucket)); + void AssertNext(IBatchBucket bucket, params T[] items) + { + Assert.That(bucket.MoveNext(), Is.True); - previousBucket = bucket; + Assert.That(bucket.Count, Is.EqualTo(items.Length)); - pool.Return(bucket); + foreach (var (i, item) in items.Index()) + { + Assert.That(bucket.Contains(item)); + Assert.That(bucket.IndexOf(item), Is.EqualTo(i)); } + + bucket.AssertSequenceEqual(items); + bucket.AsSpan().ToArray().SequenceEqual(items); } - } - public class BatchMemoryPoolTest : BatchBaseTest - { - protected override IEnumerable> Batch(IEnumerable source, int size) => - from b in source.Batch(size, MemoryPool.Shared) - select Elements(b.Bucket.Memory).Take(b.Length); + [Test] + public void BatchEvenlyDivisibleSequence() + { + using var result = Batch(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }, 3); + + AssertNext(result, 1, 2, 3); + AssertNext(result, 4, 5, 6); + AssertNext(result, 7, 8, 9); + Assert.That(result.MoveNext(), Is.False); + } - static IEnumerable Elements(Memory memory) + [Test] + public void BatchUnevenlyDivisibleSequence() { - for (var i = 0; i < memory.Length; i++) - yield return memory.Span[i]; + using var result = Batch(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }, 4); + + AssertNext(result, 1, 2, 3, 4); + AssertNext(result, 5, 6, 7, 8); + AssertNext(result, 9); + Assert.That(result.MoveNext(), Is.False); } - [TestCase(false)] - [TestCase(true)] - public void BatchMemoryReuseFromPool(bool disposeBucket) + [Test] + public void BatchIsLazy() { - foreach (var (i, (bucket, _)) in Enumerable.Range(1, 100) - .Batch(13, MemoryPool.Shared) - .Index()) - { - var memory = bucket.Memory; + new BreakingSequence().Batch(1); + } - Assert.That(memory.Length, Is.GreaterThan(13)); + [TestCase(SourceKind.BreakingList , 0)] + [TestCase(SourceKind.BreakingReadOnlyList, 0)] + [TestCase(SourceKind.BreakingList , 1)] + [TestCase(SourceKind.BreakingReadOnlyList, 1)] + [TestCase(SourceKind.BreakingList , 2)] + [TestCase(SourceKind.BreakingReadOnlyList, 2)] + public void BatchCollectionSmallerThanSize(SourceKind kind, int oversize) + { + var xs = new[] { 1, 2, 3, 4, 5 }; + using var result = Batch(xs.ToSourceKind(kind), xs.Length + oversize); - if (i is 0) - memory.Span.Fill(42); - else - Assert.That(memory.Span[^1], Is.EqualTo(disposeBucket ? 42 : 0)); + AssertNext(result, 1, 2, 3, 4, 5); + Assert.That(result.MoveNext(), Is.False); + } - if (disposeBucket) - bucket.Dispose(); - } + [Test] + public void BatchReadOnlyCollectionSmallerThanSize() + { + var collection = ReadOnlyCollection.From(1, 2, 3, 4, 5); + using var result = Batch(collection, collection.Count * 2); + Assert.That(result.MoveNext(), Is.True); + Assert.That(result.Count, Is.EqualTo(5)); + result.AssertSequenceEqual(1, 2, 3, 4, 5); + Assert.That(result.MoveNext(), Is.False); + } + + [TestCase(SourceKind.Sequence)] + [TestCase(SourceKind.BreakingList)] + [TestCase(SourceKind.BreakingReadOnlyList)] + [TestCase(SourceKind.BreakingReadOnlyCollection)] + [TestCase(SourceKind.BreakingCollection)] + public void BatchEmptySource(SourceKind kind) + { + using var result = Batch(Enumerable.Empty().ToSourceKind(kind), 100); + Assert.That(result.MoveNext(), Is.False); } } + + public class BatchPooledArrayTest : BatchPoolTest + { + protected override IBatchBucket Batch(IEnumerable source, int size) => + source.Batch(size, ArrayPool.Create()); + } + + public class BatchPooledMemoryTest : BatchPoolTest + { + protected override IBatchBucket Batch(IEnumerable source, int size) => + source.Batch(size, MemoryPool.Shared); + } } -#endif +#endif // NETCOREAPP3_1_OR_GREATER diff --git a/MoreLinq/Experimental/Batch.cs b/MoreLinq/Experimental/Batch.cs index a877fe411..e74561dbd 100644 --- a/MoreLinq/Experimental/Batch.cs +++ b/MoreLinq/Experimental/Batch.cs @@ -21,9 +21,70 @@ namespace MoreLinq.Experimental { using System; using System.Buffers; + using System.Collections; using System.Collections.Generic; + using System.Diagnostics; using System.Linq; + /// + /// Represents a bucket returned by one of the Batch overloads of + /// . + /// + /// Type of elements in the bucket + + public interface IBatchBucket : IDisposable, IList + { + /// + /// Returns a new span over the bucket elements. + /// + + Span AsSpan(); + + /// + /// Update this instance with the next set of elements from the source. + /// + /// + /// A Boolean that is true if this instance was updated with + /// new elements; otherwise false to indicate that the end of + /// the bucket source has been reached. + /// + + bool MoveNext(); + + int IList.IndexOf(T item) + { + var comparer = EqualityComparer.Default; + + for (var i = 0; i < Count; i++) + { + if (comparer.Equals(this[i], item)) + return i; + } + + return -1; + } + + bool ICollection.Contains(T item) => IndexOf(item) >= 0; + + void ICollection.CopyTo(T[] array, int arrayIndex) + { + if (arrayIndex < 0) throw new ArgumentOutOfRangeException(nameof(arrayIndex), arrayIndex, null); + if (arrayIndex + Count > array.Length) throw new ArgumentException(null, nameof(arrayIndex)); + + for (int i = 0, j = arrayIndex; i < Count; i++, j++) + array[j] = this[i]; + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + void IList.Insert(int index, T item) => throw new NotSupportedException(); + void IList.RemoveAt(int index) => throw new NotSupportedException(); + void ICollection.Add(T item) => throw new NotSupportedException(); + void ICollection.Clear() => throw new NotSupportedException(); + bool ICollection.Remove(T item) => throw new NotSupportedException(); + bool ICollection.IsReadOnly => true; + } + static partial class ExperimentalEnumerable { /// @@ -34,7 +95,10 @@ static partial class ExperimentalEnumerable /// The source sequence. /// Size of buckets. /// The memory pool used to rent memory for each bucket. - /// A sequence of equally sized buckets containing elements of the source collection. + /// + /// A that can be used to enumerate + /// equally sized buckets containing elements of the source collection. + /// /// /// /// This operator uses deferred execution and streams its results @@ -42,13 +106,7 @@ static partial class ExperimentalEnumerable /// /// /// Each bucket is backed by rented memory that may be at least - /// in length. The second element paired with - /// each bucket is the actual length of the bucket that is valid to use. - /// The rented memory should be disposed to return it to the pool given - /// in the argument. If it is returned as each - /// bucket is retrieved during iteration then there is a good chance that - /// the same memory will be reused for subsequent buckets. This can save - /// allocations for very large buckets. + /// in length. /// /// /// When more than one bucket is streamed, all buckets except the last @@ -63,30 +121,41 @@ static partial class ExperimentalEnumerable /// /// - public static IEnumerable<(IMemoryOwner Bucket, int Length)> + public static IBatchBucket Batch(this IEnumerable source, int size, MemoryPool pool) { if (source == null) throw new ArgumentNullException(nameof(source)); if (pool == null) throw new ArgumentNullException(nameof(pool)); if (size <= 0) throw new ArgumentOutOfRangeException(nameof(size)); + IBatchBucket Cursor(IEnumerator<(IMemoryOwner, int)> source) => + new RentedMemoryBatchBucket(source); + + IEnumerator<(IMemoryOwner, int)> Empty() { yield break; } + switch (source) { case ICollection { Count: 0 }: { - return Enumerable.Empty<(IMemoryOwner, int)>(); + return Cursor(Empty()); } - case ICollection collection when collection.Count <= size: + case IList list when list.Count <= size: { - return Batch(collection.Count); + return Cursor(_()); IEnumerator<(IMemoryOwner, int)> _() + { + var bucket = pool.Rent(list.Count); + for (var i = 0; i < list.Count; i++) + bucket.Memory.Span[i] = list[i]; + yield return (bucket, list.Count); + } } case IReadOnlyCollection { Count: 0 }: { - return Enumerable.Empty<(IMemoryOwner, int)>(); + return Cursor(Empty()); } case IReadOnlyList list when list.Count <= size: { - return _(); IEnumerable<(IMemoryOwner, int)> _() + return Cursor(_()); IEnumerator<(IMemoryOwner, int)> _() { var bucket = pool.Rent(list.Count); for (var i = 0; i < list.Count; i++) @@ -96,14 +165,14 @@ static partial class ExperimentalEnumerable } case IReadOnlyCollection collection when collection.Count <= size: { - return Batch(collection.Count); + return Cursor(Batch(collection.Count)); } default: { - return Batch(size); + return Cursor(Batch(size)); } - IEnumerable<(IMemoryOwner, int)> Batch(int size) + IEnumerator<(IMemoryOwner, int)> Batch(int size) { IMemoryOwner? bucket = null; var count = 0; @@ -130,6 +199,61 @@ static partial class ExperimentalEnumerable } } + sealed class RentedMemoryBatchBucket : IBatchBucket + { + bool _started; + IEnumerator<(IMemoryOwner Bucket, int Length)>? _enumerator; + + public RentedMemoryBatchBucket(IEnumerator<(IMemoryOwner, int)> enumerator) => + _enumerator = enumerator; + + public Span AsSpan() => Memory.Span; + + public bool MoveNext() + { + if (_enumerator is { } enumerator) + { + if (_started) + enumerator.Current.Bucket.Dispose(); + else + _started = true; + + if (!enumerator.MoveNext()) + { + enumerator.Dispose(); + _enumerator = null; + return false; + } + + return true; + } + + return false; + } + + Memory Memory => _started && _enumerator?.Current.Bucket is { Memory: var v } ? v : throw new InvalidOperationException(); + + public int Count => _started && _enumerator?.Current.Length is { } v ? v : throw new InvalidOperationException(); + + public T this[int index] + { + get => index >= 0 && index < Count ? Memory.Span[index] : throw new IndexOutOfRangeException(); + set => throw new NotSupportedException(); + } + + public void Dispose() + { + _enumerator?.Dispose(); + _enumerator = null; + } + + public IEnumerator GetEnumerator() + { + for (var i = 0; i < Count; i++) + yield return this[i]; + } + } + /// /// Batches the source sequence into sized buckets using a array pool /// to rent an array to back each bucket. @@ -138,7 +262,10 @@ static partial class ExperimentalEnumerable /// The source sequence. /// Size of buckets. /// The pool used to rent the array for each bucket. - /// A sequence of equally sized buckets containing elements of the source collection. + /// + /// A that can be used to enumerate + /// equally sized buckets containing elements of the source collection. + /// /// /// /// This operator uses deferred execution and streams its results @@ -146,13 +273,7 @@ static partial class ExperimentalEnumerable /// /// /// Each bucket is backed by a rented array that may be at least - /// in length. The second element paired with - /// each bucket is the actual length of the bucket that is valid to use. - /// The rented array should be returned to the pool sent as the - /// argument. If it is returned as each bucket - /// is retrieved during iteration then there is a good chance that the - /// same array allocation will be reused for subsequent buckets. This - /// can save allocations for very large buckets. + /// in length. /// /// /// When more than one bucket is streamed, all buckets except the last @@ -167,32 +288,37 @@ static partial class ExperimentalEnumerable /// /// - public static IEnumerable<(T[] Bucket, int Length)> + public static IBatchBucket Batch(this IEnumerable source, int size, ArrayPool pool) { if (source == null) throw new ArgumentNullException(nameof(source)); if (pool == null) throw new ArgumentNullException(nameof(pool)); if (size <= 0) throw new ArgumentOutOfRangeException(nameof(size)); + IBatchBucket Cursor(IEnumerator<(T[], int)> source) => + new RentedArrayBatchBucket(source, pool); + + IEnumerator<(T[], int)> Empty() { yield break; } + switch (source) { case ICollection { Count: 0 }: { - return Enumerable.Empty<(T[], int)>(); + return Cursor(Empty()); } case ICollection collection when collection.Count <= size: { var bucket = pool.Rent(collection.Count); collection.CopyTo(bucket, 0); - return MoreEnumerable.Return((bucket, collection.Count)); + return Cursor(MoreEnumerable.Return((bucket, collection.Count)).GetEnumerator()); } case IReadOnlyCollection { Count: 0 }: { - return Enumerable.Empty<(T[], int)>(); + return Cursor(Empty()); } case IReadOnlyList list when list.Count <= size: { - return _(); IEnumerable<(T[], int)> _() + return Cursor(_()); IEnumerator<(T[], int)> _() { var bucket = pool.Rent(list.Count); for (var i = 0; i < list.Count; i++) @@ -202,15 +328,15 @@ static partial class ExperimentalEnumerable } case IReadOnlyCollection collection when collection.Count <= size: { - return Batch(collection.Count); + return Cursor(Batch(collection.Count)); } default: { - return Batch(size); + return Cursor(Batch(size)); } } - IEnumerable<(T[], int)> Batch(int size) + IEnumerator<(T[], int)> Batch(int size) { T[]? bucket = null; var count = 0; @@ -235,6 +361,62 @@ static partial class ExperimentalEnumerable yield return (someBucket, count); } } + + sealed class RentedArrayBatchBucket : IBatchBucket + { + bool _started; + IEnumerator<(T[] Bucket, int Length)>? _enumerator; + ArrayPool? _pool; + + public RentedArrayBatchBucket(IEnumerator<(T[], int)> enumerator, ArrayPool pool) => + (_enumerator, _pool) = (enumerator, pool); + + public Span AsSpan() => Array.AsSpan(); + + public bool MoveNext() + { + if (_enumerator is { } enumerator) + { + Debug.Assert(_pool is not null); + if (_started) + _pool.Return(enumerator.Current.Bucket); + else + _started = true; + + if (!enumerator.MoveNext()) + { + enumerator.Dispose(); + _enumerator = null; + _pool = null; + return false; + } + + return true; + } + + return false; + } + + T[] Array => _started && _enumerator?.Current.Bucket is { } v ? v : throw new InvalidOperationException(); + + public int Count => _started && _enumerator?.Current.Length is { } v ? v : throw new InvalidOperationException(); + + public T this[int index] + { + get => index >= 0 && index < Count ? Array[index] : throw new IndexOutOfRangeException(); + set => throw new NotSupportedException(); + + } + + public void Dispose() + { + _enumerator?.Dispose(); + _enumerator = null; + _pool = null; + } + + public IEnumerator GetEnumerator() => Array.Take(Count).GetEnumerator(); + } } } From 123a12ea6049d752d01a080c0e6bb18ddcb7cb42 Mon Sep 17 00:00:00 2001 From: Atif Aziz Date: Thu, 27 Oct 2022 12:28:05 +0200 Subject: [PATCH 03/33] Rename batch bucket into more general list view --- MoreLinq.Test/BatchTest.cs | 8 +-- MoreLinq/Experimental/Batch.cs | 84 +++++------------------------ MoreLinq/Experimental/IListView.cs | 85 ++++++++++++++++++++++++++++++ MoreLinq/MoreLinq.csproj | 4 +- 4 files changed, 103 insertions(+), 78 deletions(-) create mode 100644 MoreLinq/Experimental/IListView.cs diff --git a/MoreLinq.Test/BatchTest.cs b/MoreLinq.Test/BatchTest.cs index 44ef75149..96f7e9f70 100644 --- a/MoreLinq.Test/BatchTest.cs +++ b/MoreLinq.Test/BatchTest.cs @@ -155,7 +155,7 @@ namespace MoreLinq.Test [TestFixture] public abstract class BatchPoolTest { - protected abstract IBatchBucket Batch(IEnumerable source, int size); + protected abstract IListView Batch(IEnumerable source, int size); [Test] public void BatchZeroSize() @@ -171,7 +171,7 @@ public void BatchNegativeSize() Batch(new object[0], -1)); } - void AssertNext(IBatchBucket bucket, params T[] items) + void AssertNext(IListView bucket, params T[] items) { Assert.That(bucket.MoveNext(), Is.True); @@ -255,13 +255,13 @@ public void BatchEmptySource(SourceKind kind) public class BatchPooledArrayTest : BatchPoolTest { - protected override IBatchBucket Batch(IEnumerable source, int size) => + protected override IListView Batch(IEnumerable source, int size) => source.Batch(size, ArrayPool.Create()); } public class BatchPooledMemoryTest : BatchPoolTest { - protected override IBatchBucket Batch(IEnumerable source, int size) => + protected override IListView Batch(IEnumerable source, int size) => source.Batch(size, MemoryPool.Shared); } } diff --git a/MoreLinq/Experimental/Batch.cs b/MoreLinq/Experimental/Batch.cs index e74561dbd..0291fd62b 100644 --- a/MoreLinq/Experimental/Batch.cs +++ b/MoreLinq/Experimental/Batch.cs @@ -21,70 +21,10 @@ namespace MoreLinq.Experimental { using System; using System.Buffers; - using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Linq; - /// - /// Represents a bucket returned by one of the Batch overloads of - /// . - /// - /// Type of elements in the bucket - - public interface IBatchBucket : IDisposable, IList - { - /// - /// Returns a new span over the bucket elements. - /// - - Span AsSpan(); - - /// - /// Update this instance with the next set of elements from the source. - /// - /// - /// A Boolean that is true if this instance was updated with - /// new elements; otherwise false to indicate that the end of - /// the bucket source has been reached. - /// - - bool MoveNext(); - - int IList.IndexOf(T item) - { - var comparer = EqualityComparer.Default; - - for (var i = 0; i < Count; i++) - { - if (comparer.Equals(this[i], item)) - return i; - } - - return -1; - } - - bool ICollection.Contains(T item) => IndexOf(item) >= 0; - - void ICollection.CopyTo(T[] array, int arrayIndex) - { - if (arrayIndex < 0) throw new ArgumentOutOfRangeException(nameof(arrayIndex), arrayIndex, null); - if (arrayIndex + Count > array.Length) throw new ArgumentException(null, nameof(arrayIndex)); - - for (int i = 0, j = arrayIndex; i < Count; i++, j++) - array[j] = this[i]; - } - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - - void IList.Insert(int index, T item) => throw new NotSupportedException(); - void IList.RemoveAt(int index) => throw new NotSupportedException(); - void ICollection.Add(T item) => throw new NotSupportedException(); - void ICollection.Clear() => throw new NotSupportedException(); - bool ICollection.Remove(T item) => throw new NotSupportedException(); - bool ICollection.IsReadOnly => true; - } - static partial class ExperimentalEnumerable { /// @@ -96,7 +36,7 @@ static partial class ExperimentalEnumerable /// Size of buckets. /// The memory pool used to rent memory for each bucket. /// - /// A that can be used to enumerate + /// A that can be used to enumerate /// equally sized buckets containing elements of the source collection. /// /// @@ -121,15 +61,15 @@ static partial class ExperimentalEnumerable /// /// - public static IBatchBucket + public static IListView Batch(this IEnumerable source, int size, MemoryPool pool) { if (source == null) throw new ArgumentNullException(nameof(source)); if (pool == null) throw new ArgumentNullException(nameof(pool)); if (size <= 0) throw new ArgumentOutOfRangeException(nameof(size)); - IBatchBucket Cursor(IEnumerator<(IMemoryOwner, int)> source) => - new RentedMemoryBatchBucket(source); + IListView Cursor(IEnumerator<(IMemoryOwner, int)> source) => + new RentedMemoryView(source); IEnumerator<(IMemoryOwner, int)> Empty() { yield break; } @@ -199,12 +139,12 @@ IBatchBucket Cursor(IEnumerator<(IMemoryOwner, int)> source) => } } - sealed class RentedMemoryBatchBucket : IBatchBucket + sealed class RentedMemoryView : IListView { bool _started; IEnumerator<(IMemoryOwner Bucket, int Length)>? _enumerator; - public RentedMemoryBatchBucket(IEnumerator<(IMemoryOwner, int)> enumerator) => + public RentedMemoryView(IEnumerator<(IMemoryOwner, int)> enumerator) => _enumerator = enumerator; public Span AsSpan() => Memory.Span; @@ -263,7 +203,7 @@ public IEnumerator GetEnumerator() /// Size of buckets. /// The pool used to rent the array for each bucket. /// - /// A that can be used to enumerate + /// A that can be used to enumerate /// equally sized buckets containing elements of the source collection. /// /// @@ -288,15 +228,15 @@ public IEnumerator GetEnumerator() /// /// - public static IBatchBucket + public static IListView Batch(this IEnumerable source, int size, ArrayPool pool) { if (source == null) throw new ArgumentNullException(nameof(source)); if (pool == null) throw new ArgumentNullException(nameof(pool)); if (size <= 0) throw new ArgumentOutOfRangeException(nameof(size)); - IBatchBucket Cursor(IEnumerator<(T[], int)> source) => - new RentedArrayBatchBucket(source, pool); + IListView Cursor(IEnumerator<(T[], int)> source) => + new RentedArrayView(source, pool); IEnumerator<(T[], int)> Empty() { yield break; } @@ -362,13 +302,13 @@ IBatchBucket Cursor(IEnumerator<(T[], int)> source) => } } - sealed class RentedArrayBatchBucket : IBatchBucket + sealed class RentedArrayView : IListView { bool _started; IEnumerator<(T[] Bucket, int Length)>? _enumerator; ArrayPool? _pool; - public RentedArrayBatchBucket(IEnumerator<(T[], int)> enumerator, ArrayPool pool) => + public RentedArrayView(IEnumerator<(T[], int)> enumerator, ArrayPool pool) => (_enumerator, _pool) = (enumerator, pool); public Span AsSpan() => Array.AsSpan(); diff --git a/MoreLinq/Experimental/IListView.cs b/MoreLinq/Experimental/IListView.cs new file mode 100644 index 000000000..1370a4ddf --- /dev/null +++ b/MoreLinq/Experimental/IListView.cs @@ -0,0 +1,85 @@ +#region License and Terms +// MoreLINQ - Extensions to LINQ to Objects +// Copyright (c) 2022 Atif Aziz. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#endregion + +#if !NO_MEMORY && !NO_TRAITS && !NO_TRAITS + +namespace MoreLinq.Experimental +{ + using System; + using System.Collections; + using System.Collections.Generic; + + /// + /// Represents an updateable list view of a larger result. + /// + /// Type of elements in the bucket + + public interface IListView : IDisposable, IList + { + /// + /// Returns a new span over the bucket elements. + /// + + Span AsSpan(); + + /// + /// Update this instance with the next set of elements from the source. + /// + /// + /// A Boolean that is true if this instance was updated with + /// new elements; otherwise false to indicate that the end of + /// the bucket source has been reached. + /// + + bool MoveNext(); + + int IList.IndexOf(T item) + { + var comparer = EqualityComparer.Default; + + for (var i = 0; i < Count; i++) + { + if (comparer.Equals(this[i], item)) + return i; + } + + return -1; + } + + bool ICollection.Contains(T item) => IndexOf(item) >= 0; + + void ICollection.CopyTo(T[] array, int arrayIndex) + { + if (arrayIndex < 0) throw new ArgumentOutOfRangeException(nameof(arrayIndex), arrayIndex, null); + if (arrayIndex + Count > array.Length) throw new ArgumentException(null, nameof(arrayIndex)); + + for (int i = 0, j = arrayIndex; i < Count; i++, j++) + array[j] = this[i]; + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + void IList.Insert(int index, T item) => throw new NotSupportedException(); + void IList.RemoveAt(int index) => throw new NotSupportedException(); + void ICollection.Add(T item) => throw new NotSupportedException(); + void ICollection.Clear() => throw new NotSupportedException(); + bool ICollection.Remove(T item) => throw new NotSupportedException(); + bool ICollection.IsReadOnly => true; + } +} + +#endif // !NO_MEMORY && !NO_TRAITS diff --git a/MoreLinq/MoreLinq.csproj b/MoreLinq/MoreLinq.csproj index 80b9f534e..eb9c4ca45 100644 --- a/MoreLinq/MoreLinq.csproj +++ b/MoreLinq/MoreLinq.csproj @@ -194,11 +194,11 @@ - $(DefineConstants);NO_MEMORY + $(DefineConstants);NO_TRAITS;NO_MEMORY - $(DefineConstants);MORELINQ;NO_MEMORY;NO_SERIALIZATION_ATTRIBUTES;NO_EXCEPTION_SERIALIZATION;NO_TRACING;NO_COM;NO_ASYNC + $(DefineConstants);MORELINQ;NO_MEMORY;NO_TRAITS;NO_SERIALIZATION_ATTRIBUTES;NO_EXCEPTION_SERIALIZATION;NO_TRACING;NO_COM;NO_ASYNC From b49d27d23eeaee6dc3eb0cd8a0abda54a98a1ef4 Mon Sep 17 00:00:00 2001 From: Atif Aziz Date: Thu, 27 Oct 2022 18:32:22 +0200 Subject: [PATCH 04/33] Add test to assert in-place updates --- MoreLinq.Test/BatchTest.cs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/MoreLinq.Test/BatchTest.cs b/MoreLinq.Test/BatchTest.cs index 96f7e9f70..bdfdb651c 100644 --- a/MoreLinq.Test/BatchTest.cs +++ b/MoreLinq.Test/BatchTest.cs @@ -251,6 +251,30 @@ public void BatchEmptySource(SourceKind kind) using var result = Batch(Enumerable.Empty().ToSourceKind(kind), 100); Assert.That(result.MoveNext(), Is.False); } + + [Test] + public void BatchResultUpdatesInPlaceOnEachMoveNext() + { + const int scale = 2; + + using var result = Batch(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }, 3); + + var query = + from n in result + where n % 2 == 0 + select n * scale; + + Assert.That(result.MoveNext(), Is.True); + query.AssertSequenceEqual(2 * scale); + + Assert.That(result.MoveNext(), Is.True); + query.AssertSequenceEqual(4 * scale, 6 * scale); + + Assert.That(result.MoveNext(), Is.True); + query.AssertSequenceEqual(8 * scale); + + Assert.That(result.MoveNext(), Is.False); + } } public class BatchPooledArrayTest : BatchPoolTest From ff023a224f5668c00f2ef19cfe4243ecd7bb28ef Mon Sep 17 00:00:00 2001 From: Atif Aziz Date: Thu, 27 Oct 2022 20:22:59 +0200 Subject: [PATCH 05/33] Use test pool to assert rentals/returns --- MoreLinq.Test/BatchTest.cs | 80 +++++++++++++++++++++++++++++++++++++- 1 file changed, 78 insertions(+), 2 deletions(-) diff --git a/MoreLinq.Test/BatchTest.cs b/MoreLinq.Test/BatchTest.cs index bdfdb651c..6e73dabca 100644 --- a/MoreLinq.Test/BatchTest.cs +++ b/MoreLinq.Test/BatchTest.cs @@ -280,13 +280,89 @@ from n in result public class BatchPooledArrayTest : BatchPoolTest { protected override IListView Batch(IEnumerable source, int size) => - source.Batch(size, ArrayPool.Create()); + source.Batch(size, new TestArrayPool()); } public class BatchPooledMemoryTest : BatchPoolTest { protected override IListView Batch(IEnumerable source, int size) => - source.Batch(size, MemoryPool.Shared); + source.Batch(size, new TestMemoryPool(new TestArrayPool())); + + sealed class TestMemoryPool : MemoryPool + { + readonly ArrayPool _pool; + + public TestMemoryPool(ArrayPool pool) => _pool = pool; + + protected override void Dispose(bool disposing) { } // NOP + + public override IMemoryOwner Rent(int minBufferSize = -1) => + minBufferSize >= 0 + ? new MemoryOwner(_pool, _pool.Rent(minBufferSize)) + : throw new NotSupportedException(); + + public override int MaxBufferSize => + // https://github.com/dotnet/runtime/blob/v7.0.0-rc.2.22472.3/src/libraries/System.Memory/src/System/Buffers/ArrayMemoryPool.cs#L10 + 2_147_483_591; + + sealed class MemoryOwner : IMemoryOwner + { + ArrayPool _pool; + T[] _rental; + + public MemoryOwner(ArrayPool pool, T[] rental) => + (_pool, _rental) = (pool, rental); + + public Memory Memory => _rental is { } rental ? new Memory(rental) + : throw new ObjectDisposedException(null); + + public void Dispose() + { + if (_rental is { } array && _pool is { } pool) + { + _rental = null; + _pool = null; + pool.Return(array); + } + } + } + } + } + + /// + /// An implementation for testing purposes that holds only + /// one array in the pool. + /// + + sealed class TestArrayPool : ArrayPool + { + T[] _pooledArray; + T[] _rentedArray; + + public override T[] Rent(int minimumLength) + { + if (_pooledArray is null && _rentedArray is null) + _pooledArray = new T[minimumLength * 2]; + + if (_pooledArray is null) + throw new InvalidOperationException("The pool is exhausted."); + + (_pooledArray, _rentedArray) = (null, _pooledArray); + + return _rentedArray; + } + + public override void Return(T[] array, bool clearArray = false) + { + if (_rentedArray is null) + throw new InvalidOperationException("Cannot return when nothing has been rented from this pool."); + + if (array != _rentedArray) + throw new InvalidOperationException("Cannot return what has not been rented from this pool."); + + _pooledArray = array; + _rentedArray = null; + } } } From 9fb0a91db6c398f8b3fc3250db3af3bdee877260 Mon Sep 17 00:00:00 2001 From: Atif Aziz Date: Thu, 27 Oct 2022 20:38:45 +0200 Subject: [PATCH 06/33] Make helper static --- MoreLinq.Test/BatchTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MoreLinq.Test/BatchTest.cs b/MoreLinq.Test/BatchTest.cs index 6e73dabca..d64bc23e7 100644 --- a/MoreLinq.Test/BatchTest.cs +++ b/MoreLinq.Test/BatchTest.cs @@ -171,7 +171,7 @@ public void BatchNegativeSize() Batch(new object[0], -1)); } - void AssertNext(IListView bucket, params T[] items) + static void AssertNext(IListView bucket, params T[] items) { Assert.That(bucket.MoveNext(), Is.True); From c2d98fa879750a51512f90b0520bbf5ca27e76a5 Mon Sep 17 00:00:00 2001 From: Atif Aziz Date: Thu, 27 Oct 2022 20:42:52 +0200 Subject: [PATCH 07/33] Fix code formatting --- MoreLinq.Test/BatchTest.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/MoreLinq.Test/BatchTest.cs b/MoreLinq.Test/BatchTest.cs index d64bc23e7..abb5b7047 100644 --- a/MoreLinq.Test/BatchTest.cs +++ b/MoreLinq.Test/BatchTest.cs @@ -160,15 +160,13 @@ public abstract class BatchPoolTest [Test] public void BatchZeroSize() { - AssertThrowsArgument.OutOfRangeException("size",() => - Batch(new object[0], 0)); + AssertThrowsArgument.OutOfRangeException("size", () => Batch(new object[0], 0)); } [Test] public void BatchNegativeSize() { - AssertThrowsArgument.OutOfRangeException("size",() => - Batch(new object[0], -1)); + AssertThrowsArgument.OutOfRangeException("size", () => Batch(new object[0], -1)); } static void AssertNext(IListView bucket, params T[] items) From 851cde005f88bba69c96d740697aee820030655c Mon Sep 17 00:00:00 2001 From: Atif Aziz Date: Thu, 27 Oct 2022 21:59:32 +0200 Subject: [PATCH 08/33] Test last return to the pool --- MoreLinq.Test/BatchTest.cs | 92 +++++++++++++++++++------------------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/MoreLinq.Test/BatchTest.cs b/MoreLinq.Test/BatchTest.cs index abb5b7047..933370f24 100644 --- a/MoreLinq.Test/BatchTest.cs +++ b/MoreLinq.Test/BatchTest.cs @@ -155,7 +155,12 @@ namespace MoreLinq.Test [TestFixture] public abstract class BatchPoolTest { - protected abstract IListView Batch(IEnumerable source, int size); + protected abstract void AssertBatch(IEnumerable source, int size, Action> asserter); + + void Batch(IEnumerable source, int size) => Batch(source, size, delegate { }); + + void Batch(IEnumerable source, int size, Action> asserter) => + AssertBatch(source, size, asserter + (bucket => Assert.That(bucket.MoveNext(), Is.False))); [Test] public void BatchZeroSize() @@ -169,7 +174,7 @@ public void BatchNegativeSize() AssertThrowsArgument.OutOfRangeException("size", () => Batch(new object[0], -1)); } - static void AssertNext(IListView bucket, params T[] items) + static Action> AssertNext(params T[] items) => bucket => { Assert.That(bucket.MoveNext(), Is.True); @@ -183,28 +188,20 @@ static void AssertNext(IListView bucket, params T[] items) bucket.AssertSequenceEqual(items); bucket.AsSpan().ToArray().SequenceEqual(items); - } + }; [Test] public void BatchEvenlyDivisibleSequence() { - using var result = Batch(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }, 3); - - AssertNext(result, 1, 2, 3); - AssertNext(result, 4, 5, 6); - AssertNext(result, 7, 8, 9); - Assert.That(result.MoveNext(), Is.False); + var input = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; + Batch(input, 3, AssertNext(1, 2, 3) + AssertNext(4, 5, 6) + AssertNext(7, 8, 9)); } [Test] public void BatchUnevenlyDivisibleSequence() { - using var result = Batch(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }, 4); - - AssertNext(result, 1, 2, 3, 4); - AssertNext(result, 5, 6, 7, 8); - AssertNext(result, 9); - Assert.That(result.MoveNext(), Is.False); + var input = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; + Batch(input, 4, AssertNext(1, 2, 3, 4) + AssertNext(5, 6, 7, 8) + AssertNext(9)); } [Test] @@ -222,21 +219,14 @@ public void BatchIsLazy() public void BatchCollectionSmallerThanSize(SourceKind kind, int oversize) { var xs = new[] { 1, 2, 3, 4, 5 }; - using var result = Batch(xs.ToSourceKind(kind), xs.Length + oversize); - - AssertNext(result, 1, 2, 3, 4, 5); - Assert.That(result.MoveNext(), Is.False); + Batch(xs.ToSourceKind(kind), xs.Length + oversize, AssertNext(1, 2, 3, 4, 5)); } [Test] public void BatchReadOnlyCollectionSmallerThanSize() { var collection = ReadOnlyCollection.From(1, 2, 3, 4, 5); - using var result = Batch(collection, collection.Count * 2); - Assert.That(result.MoveNext(), Is.True); - Assert.That(result.Count, Is.EqualTo(5)); - result.AssertSequenceEqual(1, 2, 3, 4, 5); - Assert.That(result.MoveNext(), Is.False); + Batch(collection, collection.Count * 2, AssertNext(1, 2, 3, 4, 5)); } [TestCase(SourceKind.Sequence)] @@ -246,51 +236,59 @@ public void BatchReadOnlyCollectionSmallerThanSize() [TestCase(SourceKind.BreakingCollection)] public void BatchEmptySource(SourceKind kind) { - using var result = Batch(Enumerable.Empty().ToSourceKind(kind), 100); - Assert.That(result.MoveNext(), Is.False); + Batch(Enumerable.Empty().ToSourceKind(kind), 100, delegate { }); } [Test] public void BatchResultUpdatesInPlaceOnEachMoveNext() { - const int scale = 2; - - using var result = Batch(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }, 3); + Batch(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }, 3, result => + { + const int scale = 2; - var query = - from n in result - where n % 2 == 0 - select n * scale; + var query = + from n in result + where n % 2 == 0 + select n * scale; - Assert.That(result.MoveNext(), Is.True); - query.AssertSequenceEqual(2 * scale); + Assert.That(result.MoveNext(), Is.True); + query.AssertSequenceEqual(2 * scale); - Assert.That(result.MoveNext(), Is.True); - query.AssertSequenceEqual(4 * scale, 6 * scale); + Assert.That(result.MoveNext(), Is.True); + query.AssertSequenceEqual(4 * scale, 6 * scale); - Assert.That(result.MoveNext(), Is.True); - query.AssertSequenceEqual(8 * scale); + Assert.That(result.MoveNext(), Is.True); + query.AssertSequenceEqual(8 * scale); - Assert.That(result.MoveNext(), Is.False); + Assert.That(result.MoveNext(), Is.False); + }); } } public class BatchPooledArrayTest : BatchPoolTest { - protected override IListView Batch(IEnumerable source, int size) => - source.Batch(size, new TestArrayPool()); + protected override void AssertBatch(IEnumerable source, int size, Action> asserter) + { + var pool = new TestArrayPool(); + asserter(source.Batch(size, pool)); + Assert.That(pool.HasRented, Is.False); + } } public class BatchPooledMemoryTest : BatchPoolTest { - protected override IListView Batch(IEnumerable source, int size) => - source.Batch(size, new TestMemoryPool(new TestArrayPool())); + protected override void AssertBatch(IEnumerable source, int size, Action> asserter) + { + var pool = new TestMemoryPool(); + asserter(source.Batch(size, pool)); + Assert.That(pool.HasRented, Is.False); + } sealed class TestMemoryPool : MemoryPool { - readonly ArrayPool _pool; + readonly TestArrayPool _pool = new(); - public TestMemoryPool(ArrayPool pool) => _pool = pool; + public bool HasRented => _pool.HasRented; protected override void Dispose(bool disposing) { } // NOP @@ -337,6 +335,8 @@ sealed class TestArrayPool : ArrayPool T[] _pooledArray; T[] _rentedArray; + public bool HasRented => _rentedArray is not null; + public override T[] Rent(int minimumLength) { if (_pooledArray is null && _rentedArray is null) From fa81c4841fcb667db43d1bf6c647965695561b89 Mon Sep 17 00:00:00 2001 From: Atif Aziz Date: Thu, 27 Oct 2022 22:11:37 +0200 Subject: [PATCH 09/33] Fix disposal bugs --- MoreLinq.Test/BatchTest.cs | 8 ++++++++ MoreLinq/Experimental/Batch.cs | 20 +++++++++++++++----- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/MoreLinq.Test/BatchTest.cs b/MoreLinq.Test/BatchTest.cs index 933370f24..9f6e244a2 100644 --- a/MoreLinq.Test/BatchTest.cs +++ b/MoreLinq.Test/BatchTest.cs @@ -204,6 +204,14 @@ public void BatchUnevenlyDivisibleSequence() Batch(input, 4, AssertNext(1, 2, 3, 4) + AssertNext(5, 6, 7, 8) + AssertNext(9)); } + [Test] + public void BatchDisposeMidway() + { + var input = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; + Batch(input, 4, AssertNext(1, 2, 3, 4) + (result => result.Dispose())); + } + + [Test] public void BatchIsLazy() { diff --git a/MoreLinq/Experimental/Batch.cs b/MoreLinq/Experimental/Batch.cs index 0291fd62b..8b256efcf 100644 --- a/MoreLinq/Experimental/Batch.cs +++ b/MoreLinq/Experimental/Batch.cs @@ -183,8 +183,13 @@ public T this[int index] public void Dispose() { - _enumerator?.Dispose(); - _enumerator = null; + if (_enumerator is { } enumerator) + { + _enumerator = null; + if (_started) + enumerator.Current.Bucket.Dispose(); + enumerator.Dispose(); + } } public IEnumerator GetEnumerator() @@ -350,9 +355,14 @@ public T this[int index] public void Dispose() { - _enumerator?.Dispose(); - _enumerator = null; - _pool = null; + if (_enumerator is { } enumerator) + { + Debug.Assert(_pool is not null); + _pool.Return(enumerator.Current.Bucket); + enumerator.Dispose(); + _enumerator = null; + _pool = null; + } } public IEnumerator GetEnumerator() => Array.Take(Count).GetEnumerator(); From 02c1c1c8bcff97a9ec589ee9318e4ec29346777a Mon Sep 17 00:00:00 2001 From: Atif Aziz Date: Thu, 27 Oct 2022 22:15:01 +0200 Subject: [PATCH 10/33] Test disposal of source --- MoreLinq.Test/BatchTest.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/MoreLinq.Test/BatchTest.cs b/MoreLinq.Test/BatchTest.cs index 9f6e244a2..736dc6038 100644 --- a/MoreLinq.Test/BatchTest.cs +++ b/MoreLinq.Test/BatchTest.cs @@ -193,21 +193,21 @@ static Action> AssertNext(params T[] items) => bucket => [Test] public void BatchEvenlyDivisibleSequence() { - var input = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; + using var input = TestingSequence.Of(1, 2, 3, 4, 5, 6, 7, 8, 9); Batch(input, 3, AssertNext(1, 2, 3) + AssertNext(4, 5, 6) + AssertNext(7, 8, 9)); } [Test] public void BatchUnevenlyDivisibleSequence() { - var input = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; + using var input = TestingSequence.Of(1, 2, 3, 4, 5, 6, 7, 8, 9); Batch(input, 4, AssertNext(1, 2, 3, 4) + AssertNext(5, 6, 7, 8) + AssertNext(9)); } [Test] public void BatchDisposeMidway() { - var input = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; + using var input = TestingSequence.Of(1, 2, 3, 4, 5, 6, 7, 8, 9); Batch(input, 4, AssertNext(1, 2, 3, 4) + (result => result.Dispose())); } @@ -250,7 +250,9 @@ public void BatchEmptySource(SourceKind kind) [Test] public void BatchResultUpdatesInPlaceOnEachMoveNext() { - Batch(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }, 3, result => + var input = TestingSequence.Of(1, 2, 3, 4, 5, 6, 7, 8, 9); + + Batch(input, 3, result => { const int scale = 2; From 8701b88892f5475edea9d0317898742b0e249b81 Mon Sep 17 00:00:00 2001 From: Atif Aziz Date: Sat, 29 Oct 2022 11:46:13 +0200 Subject: [PATCH 11/33] Review name and virtual members --- MoreLinq.Test/BatchTest.cs | 34 ++++++------- MoreLinq/Experimental/Batch.cs | 49 +++++++++---------- .../{IListView.cs => CurrentList.cs} | 47 ++++++++++++++---- 3 files changed, 77 insertions(+), 53 deletions(-) rename MoreLinq/Experimental/{IListView.cs => CurrentList.cs} (62%) diff --git a/MoreLinq.Test/BatchTest.cs b/MoreLinq.Test/BatchTest.cs index 736dc6038..ced197a0d 100644 --- a/MoreLinq.Test/BatchTest.cs +++ b/MoreLinq.Test/BatchTest.cs @@ -155,12 +155,12 @@ namespace MoreLinq.Test [TestFixture] public abstract class BatchPoolTest { - protected abstract void AssertBatch(IEnumerable source, int size, Action> asserter); + protected abstract void AssertBatch(IEnumerable source, int size, Action> asserter); void Batch(IEnumerable source, int size) => Batch(source, size, delegate { }); - void Batch(IEnumerable source, int size, Action> asserter) => - AssertBatch(source, size, asserter + (bucket => Assert.That(bucket.MoveNext(), Is.False))); + void Batch(IEnumerable source, int size, Action> asserter) => + AssertBatch(source, size, asserter + (bucket => Assert.That(bucket.UpdateWithNext(), Is.False))); [Test] public void BatchZeroSize() @@ -174,20 +174,20 @@ public void BatchNegativeSize() AssertThrowsArgument.OutOfRangeException("size", () => Batch(new object[0], -1)); } - static Action> AssertNext(params T[] items) => bucket => + static Action> AssertNext(params T[] items) => bucket => { - Assert.That(bucket.MoveNext(), Is.True); + Assert.That(bucket.UpdateWithNext(), Is.True); - Assert.That(bucket.Count, Is.EqualTo(items.Length)); + Assert.That(bucket.CurrentItems.Count, Is.EqualTo(items.Length)); foreach (var (i, item) in items.Index()) { - Assert.That(bucket.Contains(item)); - Assert.That(bucket.IndexOf(item), Is.EqualTo(i)); + Assert.That(bucket.CurrentItems.Contains(item)); + Assert.That(bucket.CurrentItems.IndexOf(item), Is.EqualTo(i)); } - bucket.AssertSequenceEqual(items); - bucket.AsSpan().ToArray().SequenceEqual(items); + bucket.CurrentItems.AssertSequenceEqual(items); + bucket.CurrentItemsSpan.ToArray().SequenceEqual(items); }; [Test] @@ -257,27 +257,27 @@ public void BatchResultUpdatesInPlaceOnEachMoveNext() const int scale = 2; var query = - from n in result + from n in result.CurrentItems where n % 2 == 0 select n * scale; - Assert.That(result.MoveNext(), Is.True); + Assert.That(result.UpdateWithNext(), Is.True); query.AssertSequenceEqual(2 * scale); - Assert.That(result.MoveNext(), Is.True); + Assert.That(result.UpdateWithNext(), Is.True); query.AssertSequenceEqual(4 * scale, 6 * scale); - Assert.That(result.MoveNext(), Is.True); + Assert.That(result.UpdateWithNext(), Is.True); query.AssertSequenceEqual(8 * scale); - Assert.That(result.MoveNext(), Is.False); + Assert.That(result.UpdateWithNext(), Is.False); }); } } public class BatchPooledArrayTest : BatchPoolTest { - protected override void AssertBatch(IEnumerable source, int size, Action> asserter) + protected override void AssertBatch(IEnumerable source, int size, Action> asserter) { var pool = new TestArrayPool(); asserter(source.Batch(size, pool)); @@ -287,7 +287,7 @@ protected override void AssertBatch(IEnumerable source, int size, Action(IEnumerable source, int size, Action> asserter) + protected override void AssertBatch(IEnumerable source, int size, Action> asserter) { var pool = new TestMemoryPool(); asserter(source.Batch(size, pool)); diff --git a/MoreLinq/Experimental/Batch.cs b/MoreLinq/Experimental/Batch.cs index 8b256efcf..2d6849221 100644 --- a/MoreLinq/Experimental/Batch.cs +++ b/MoreLinq/Experimental/Batch.cs @@ -23,7 +23,6 @@ namespace MoreLinq.Experimental using System.Buffers; using System.Collections.Generic; using System.Diagnostics; - using System.Linq; static partial class ExperimentalEnumerable { @@ -36,7 +35,7 @@ static partial class ExperimentalEnumerable /// Size of buckets. /// The memory pool used to rent memory for each bucket. /// - /// A that can be used to enumerate + /// A that can be used to enumerate /// equally sized buckets containing elements of the source collection. /// /// @@ -61,15 +60,15 @@ static partial class ExperimentalEnumerable /// /// - public static IListView + public static ICurrentList Batch(this IEnumerable source, int size, MemoryPool pool) { if (source == null) throw new ArgumentNullException(nameof(source)); if (pool == null) throw new ArgumentNullException(nameof(pool)); if (size <= 0) throw new ArgumentOutOfRangeException(nameof(size)); - IListView Cursor(IEnumerator<(IMemoryOwner, int)> source) => - new RentedMemoryView(source); + ICurrentList Cursor(IEnumerator<(IMemoryOwner, int)> source) => + new CurrentBucketMemory(source); IEnumerator<(IMemoryOwner, int)> Empty() { yield break; } @@ -139,17 +138,17 @@ IListView Cursor(IEnumerator<(IMemoryOwner, int)> source) => } } - sealed class RentedMemoryView : IListView + sealed class CurrentBucketMemory : CurrentList { bool _started; IEnumerator<(IMemoryOwner Bucket, int Length)>? _enumerator; - public RentedMemoryView(IEnumerator<(IMemoryOwner, int)> enumerator) => + public CurrentBucketMemory(IEnumerator<(IMemoryOwner, int)> enumerator) => _enumerator = enumerator; - public Span AsSpan() => Memory.Span; + public override Span CurrentItemsSpan => Memory.Span; - public bool MoveNext() + public override bool UpdateWithNext() { if (_enumerator is { } enumerator) { @@ -173,15 +172,15 @@ public bool MoveNext() Memory Memory => _started && _enumerator?.Current.Bucket is { Memory: var v } ? v : throw new InvalidOperationException(); - public int Count => _started && _enumerator?.Current.Length is { } v ? v : throw new InvalidOperationException(); + public override int Count => _started && _enumerator?.Current.Length is { } v ? v : throw new InvalidOperationException(); - public T this[int index] + public override T this[int index] { get => index >= 0 && index < Count ? Memory.Span[index] : throw new IndexOutOfRangeException(); set => throw new NotSupportedException(); } - public void Dispose() + public override void Dispose() { if (_enumerator is { } enumerator) { @@ -192,7 +191,7 @@ public void Dispose() } } - public IEnumerator GetEnumerator() + public override IEnumerator GetEnumerator() { for (var i = 0; i < Count; i++) yield return this[i]; @@ -208,7 +207,7 @@ public IEnumerator GetEnumerator() /// Size of buckets. /// The pool used to rent the array for each bucket. /// - /// A that can be used to enumerate + /// A that can be used to enumerate /// equally sized buckets containing elements of the source collection. /// /// @@ -233,15 +232,15 @@ public IEnumerator GetEnumerator() /// /// - public static IListView + public static ICurrentList Batch(this IEnumerable source, int size, ArrayPool pool) { if (source == null) throw new ArgumentNullException(nameof(source)); if (pool == null) throw new ArgumentNullException(nameof(pool)); if (size <= 0) throw new ArgumentOutOfRangeException(nameof(size)); - IListView Cursor(IEnumerator<(T[], int)> source) => - new RentedArrayView(source, pool); + ICurrentList Cursor(IEnumerator<(T[], int)> source) => + new CurrentBucketArray(source, pool); IEnumerator<(T[], int)> Empty() { yield break; } @@ -307,18 +306,18 @@ IListView Cursor(IEnumerator<(T[], int)> source) => } } - sealed class RentedArrayView : IListView + sealed class CurrentBucketArray : CurrentList { bool _started; IEnumerator<(T[] Bucket, int Length)>? _enumerator; ArrayPool? _pool; - public RentedArrayView(IEnumerator<(T[], int)> enumerator, ArrayPool pool) => + public CurrentBucketArray(IEnumerator<(T[], int)> enumerator, ArrayPool pool) => (_enumerator, _pool) = (enumerator, pool); - public Span AsSpan() => Array.AsSpan(); + public override Span CurrentItemsSpan => Array.AsSpan(); - public bool MoveNext() + public override bool UpdateWithNext() { if (_enumerator is { } enumerator) { @@ -344,16 +343,16 @@ public bool MoveNext() T[] Array => _started && _enumerator?.Current.Bucket is { } v ? v : throw new InvalidOperationException(); - public int Count => _started && _enumerator?.Current.Length is { } v ? v : throw new InvalidOperationException(); + public override int Count => _started && _enumerator?.Current.Length is { } v ? v : throw new InvalidOperationException(); - public T this[int index] + public override T this[int index] { get => index >= 0 && index < Count ? Array[index] : throw new IndexOutOfRangeException(); set => throw new NotSupportedException(); } - public void Dispose() + public override void Dispose() { if (_enumerator is { } enumerator) { @@ -364,8 +363,6 @@ public void Dispose() _pool = null; } } - - public IEnumerator GetEnumerator() => Array.Take(Count).GetEnumerator(); } } } diff --git a/MoreLinq/Experimental/IListView.cs b/MoreLinq/Experimental/CurrentList.cs similarity index 62% rename from MoreLinq/Experimental/IListView.cs rename to MoreLinq/Experimental/CurrentList.cs index 1370a4ddf..64bd60a47 100644 --- a/MoreLinq/Experimental/IListView.cs +++ b/MoreLinq/Experimental/CurrentList.cs @@ -22,19 +22,32 @@ namespace MoreLinq.Experimental using System; using System.Collections; using System.Collections.Generic; + using System.Linq; /// - /// Represents an updateable list view of a larger result. + /// Represents a list that is the current view of a larger result and which + /// is updated in-place (thus current) as it is moved through the overall + /// result. /// - /// Type of elements in the bucket + /// Type of elements in the bucket. - public interface IListView : IDisposable, IList + public interface ICurrentList : IDisposable { /// - /// Returns a new span over the bucket elements. + /// Gets the current items of the list. /// + /// + /// The returned list is updated in-place when + /// is called. + /// - Span AsSpan(); + IList CurrentItems { get; } + + /// + /// Gets the current items of the list as . + /// + + Span CurrentItemsSpan { get; } /// /// Update this instance with the next set of elements from the source. @@ -45,9 +58,23 @@ public interface IListView : IDisposable, IList /// the bucket source has been reached. /// - bool MoveNext(); + bool UpdateWithNext(); + } + + abstract class CurrentList : ICurrentList, IList + { + public abstract bool UpdateWithNext(); + public abstract void Dispose(); + + public abstract Span CurrentItemsSpan { get; } + public abstract int Count { get; } + public abstract T this[int index] { get; set; } + + public virtual IList CurrentItems => this; + + public virtual bool IsReadOnly => false; - int IList.IndexOf(T item) + public virtual int IndexOf(T item) { var comparer = EqualityComparer.Default; @@ -60,9 +87,9 @@ int IList.IndexOf(T item) return -1; } - bool ICollection.Contains(T item) => IndexOf(item) >= 0; + public virtual bool Contains(T item) => IndexOf(item) >= 0; - void ICollection.CopyTo(T[] array, int arrayIndex) + public virtual void CopyTo(T[] array, int arrayIndex) { if (arrayIndex < 0) throw new ArgumentOutOfRangeException(nameof(arrayIndex), arrayIndex, null); if (arrayIndex + Count > array.Length) throw new ArgumentException(null, nameof(arrayIndex)); @@ -71,6 +98,7 @@ void ICollection.CopyTo(T[] array, int arrayIndex) array[j] = this[i]; } + public virtual IEnumerator GetEnumerator() => CurrentItems.Take(Count).GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); void IList.Insert(int index, T item) => throw new NotSupportedException(); @@ -78,7 +106,6 @@ void ICollection.CopyTo(T[] array, int arrayIndex) void ICollection.Add(T item) => throw new NotSupportedException(); void ICollection.Clear() => throw new NotSupportedException(); bool ICollection.Remove(T item) => throw new NotSupportedException(); - bool ICollection.IsReadOnly => true; } } From b2c3e02584ff85cd3a0f5ba032b9a42e9c78079e Mon Sep 17 00:00:00 2001 From: Atif Aziz Date: Sat, 29 Oct 2022 11:47:05 +0200 Subject: [PATCH 12/33] Add overload with current list query that returns a sequence --- MoreLinq.Test/BatchTest.cs | 33 +++++++++++++++++ MoreLinq/Experimental/Batch.cs | 66 ++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+) diff --git a/MoreLinq.Test/BatchTest.cs b/MoreLinq.Test/BatchTest.cs index ced197a0d..ad5d6d8f5 100644 --- a/MoreLinq.Test/BatchTest.cs +++ b/MoreLinq.Test/BatchTest.cs @@ -273,6 +273,39 @@ from n in result.CurrentItems Assert.That(result.UpdateWithNext(), Is.False); }); } + + [Test] + public void BatchFilterBucket() + { + const int scale = 2; + var input = TestingSequence.Of(1, 2, 3, 4, 5, 6, 7, 8, 9); + + var result = input.Batch(3, new TestArrayPool(), + current => from n in current.CurrentItems + where n % 2 == 0 + select n * scale, + query => query.ToArray()); + + using var reader = result.Read(); + reader.Read().AssertSequenceEqual(2 * scale); + reader.Read().AssertSequenceEqual(4 * scale, 6 * scale); + reader.Read().AssertSequenceEqual(8 * scale); + reader.ReadEnd(); + } + + [Test] + public void BatchSumBucket() + { + var input = TestingSequence.Of(1, 2, 3, 4, 5, 6, 7, 8, 9); + + var result = input.Batch(3, new TestArrayPool(), current => current.CurrentItems, q => q.Sum()); + + using var reader = result.Read(); + Assert.That(reader.Read(), Is.EqualTo(1 + 2 + 3)); + Assert.That(reader.Read(), Is.EqualTo(4 + 5 + 6)); + Assert.That(reader.Read(), Is.EqualTo(7 + 8 + 9)); + reader.ReadEnd(); + } } public class BatchPooledArrayTest : BatchPoolTest diff --git a/MoreLinq/Experimental/Batch.cs b/MoreLinq/Experimental/Batch.cs index 2d6849221..e8252e97a 100644 --- a/MoreLinq/Experimental/Batch.cs +++ b/MoreLinq/Experimental/Batch.cs @@ -198,6 +198,72 @@ public override IEnumerator GetEnumerator() } } + /// + /// Batches the source sequence into sized buckets using an array pool + /// to rent arrays to back each bucket and returns a sequence of + /// elements projected from each bucket. + /// + /// + /// Type of elements in sequence. + /// + /// Type of elements in the sequence returned by . + /// + /// Type of elements of the resulting sequence. + /// + /// The source sequence. + /// Size of buckets. + /// The pool used to rent the array for each bucket. + /// A function that projects a query over + /// all buckets. + /// A function that projects a result from + /// the input sequence produced over a bucket. + /// + /// A sequence whose elements are projected from each bucket (returned by + /// ). + /// + /// + /// + /// This operator uses deferred execution and streams its results + /// (buckets are streamed but their content buffered). + /// + /// + /// Each bucket is backed by a rented array that may be at least + /// in length. + /// + /// + /// When more than one bucket is streamed, all buckets except the last + /// is guaranteed to have elements. The last + /// bucket may be smaller depending on the remaining elements in the + /// sequence. + /// Each bucket is pre-allocated to elements. + /// If is set to a very large value, e.g. + /// to effectively disable batching by just + /// hoping for a single bucket, then it can lead to memory exhaustion + /// (). + /// + /// + + public static IEnumerable + Batch( + this IEnumerable source, int size, ArrayPool pool, + Func, IEnumerable> querySelector, + Func, TResult> resultSelector) + { + using var current = source.Batch(size, pool); + if (current.UpdateWithNext()) + { + var query = querySelector(current); + do + { + var result = resultSelector(query); + if (result is IEnumerable && ReferenceEquals(result, query)) + throw new InvalidOperationException(); + yield return result; + } + while (current.UpdateWithNext()); + } + } + /// /// Batches the source sequence into sized buckets using a array pool /// to rent an array to back each bucket. From 5d45266543ce02ea0403c8d6f5cc41b0d1f602d0 Mon Sep 17 00:00:00 2001 From: Atif Aziz Date: Sat, 29 Oct 2022 13:20:23 +0200 Subject: [PATCH 13/33] Keep just the array pool version --- MoreLinq.Test/BatchTest.cs | 261 ++++++++++++--------------------- MoreLinq/Experimental/Batch.cs | 197 ++----------------------- 2 files changed, 108 insertions(+), 350 deletions(-) diff --git a/MoreLinq.Test/BatchTest.cs b/MoreLinq.Test/BatchTest.cs index ad5d6d8f5..9bb8d6152 100644 --- a/MoreLinq.Test/BatchTest.cs +++ b/MoreLinq.Test/BatchTest.cs @@ -23,18 +23,12 @@ namespace MoreLinq.Test [TestFixture] public class BatchTest { - [Test] - public void BatchZeroSize() - { - AssertThrowsArgument.OutOfRangeException("size",() => - new object[0].Batch(0)); - } - - [Test] - public void BatchNegativeSize() + [TestCase(0)] + [TestCase(-1)] + public void BatchBadSize(int size) { - AssertThrowsArgument.OutOfRangeException("size",() => - new object[0].Batch(-1)); + AssertThrowsArgument.OutOfRangeException("size", () => + new object[0].Batch(size)); } [Test] @@ -153,69 +147,59 @@ namespace MoreLinq.Test using NUnit.Framework; [TestFixture] - public abstract class BatchPoolTest + public class BatchPoolTest { - protected abstract void AssertBatch(IEnumerable source, int size, Action> asserter); - - void Batch(IEnumerable source, int size) => Batch(source, size, delegate { }); - - void Batch(IEnumerable source, int size, Action> asserter) => - AssertBatch(source, size, asserter + (bucket => Assert.That(bucket.UpdateWithNext(), Is.False))); - - [Test] - public void BatchZeroSize() - { - AssertThrowsArgument.OutOfRangeException("size", () => Batch(new object[0], 0)); - } - - [Test] - public void BatchNegativeSize() + [TestCase(0)] + [TestCase(-1)] + public void BatchBadSize(int size) { - AssertThrowsArgument.OutOfRangeException("size", () => Batch(new object[0], -1)); + AssertThrowsArgument.OutOfRangeException("size", () => + new object[0].Batch(size, ArrayPool.Shared, + BreakingFunc.Of, IEnumerable>(), + BreakingFunc.Of, object>())); } - static Action> AssertNext(params T[] items) => bucket => - { - Assert.That(bucket.UpdateWithNext(), Is.True); - - Assert.That(bucket.CurrentItems.Count, Is.EqualTo(items.Length)); - - foreach (var (i, item) in items.Index()) - { - Assert.That(bucket.CurrentItems.Contains(item)); - Assert.That(bucket.CurrentItems.IndexOf(item), Is.EqualTo(i)); - } - - bucket.CurrentItems.AssertSequenceEqual(items); - bucket.CurrentItemsSpan.ToArray().SequenceEqual(items); - }; - [Test] public void BatchEvenlyDivisibleSequence() { using var input = TestingSequence.Of(1, 2, 3, 4, 5, 6, 7, 8, 9); - Batch(input, 3, AssertNext(1, 2, 3) + AssertNext(4, 5, 6) + AssertNext(7, 8, 9)); + using var pool = new TestArrayPool(); + + var result = input.Batch(3, pool, + current => current.CurrentItems, + items => items.ToArray()); + + using var reader = result.Read(); + reader.Read().AssertSequenceEqual(1, 2, 3); + reader.Read().AssertSequenceEqual(4, 5, 6); + reader.Read().AssertSequenceEqual(7, 8, 9); + reader.ReadEnd(); } [Test] public void BatchUnevenlyDivisibleSequence() { using var input = TestingSequence.Of(1, 2, 3, 4, 5, 6, 7, 8, 9); - Batch(input, 4, AssertNext(1, 2, 3, 4) + AssertNext(5, 6, 7, 8) + AssertNext(9)); - } + using var pool = new TestArrayPool(); - [Test] - public void BatchDisposeMidway() - { - using var input = TestingSequence.Of(1, 2, 3, 4, 5, 6, 7, 8, 9); - Batch(input, 4, AssertNext(1, 2, 3, 4) + (result => result.Dispose())); - } + var result = input.Batch(4, pool, + current => current.CurrentItems, + items => items.ToArray()); + using var reader = result.Read(); + reader.Read().AssertSequenceEqual(1, 2, 3, 4); + reader.Read().AssertSequenceEqual(5, 6, 7, 8); + reader.Read().AssertSequenceEqual(9); + reader.ReadEnd(); + } [Test] public void BatchIsLazy() { - new BreakingSequence().Batch(1); + var input = new BreakingSequence(); + _ = input.Batch(1, ArrayPool.Shared, + BreakingFunc.Of, IEnumerable>(), + BreakingFunc.Of, object>()); } [TestCase(SourceKind.BreakingList , 0)] @@ -227,14 +211,30 @@ public void BatchIsLazy() public void BatchCollectionSmallerThanSize(SourceKind kind, int oversize) { var xs = new[] { 1, 2, 3, 4, 5 }; - Batch(xs.ToSourceKind(kind), xs.Length + oversize, AssertNext(1, 2, 3, 4, 5)); + using var pool = new TestArrayPool(); + + var result = xs.ToSourceKind(kind) + .Batch(xs.Length + oversize, pool, + current => current.CurrentItems, items => items); + + using var reader = result.Read(); + reader.Read().AssertSequenceEqual(1, 2, 3, 4, 5); + reader.ReadEnd(); } [Test] public void BatchReadOnlyCollectionSmallerThanSize() { var collection = ReadOnlyCollection.From(1, 2, 3, 4, 5); - Batch(collection, collection.Count * 2, AssertNext(1, 2, 3, 4, 5)); + using var pool = new TestArrayPool(); + + var result = collection.Batch(collection.Count * 2, pool, + current => current.CurrentItems, + items => items.ToArray()); + + using var reader = result.Read(); + reader.Read().AssertSequenceEqual(1, 2, 3, 4, 5); + reader.ReadEnd(); } [TestCase(SourceKind.Sequence)] @@ -244,34 +244,14 @@ public void BatchReadOnlyCollectionSmallerThanSize() [TestCase(SourceKind.BreakingCollection)] public void BatchEmptySource(SourceKind kind) { - Batch(Enumerable.Empty().ToSourceKind(kind), 100, delegate { }); - } + using var pool = new TestArrayPool(); - [Test] - public void BatchResultUpdatesInPlaceOnEachMoveNext() - { - var input = TestingSequence.Of(1, 2, 3, 4, 5, 6, 7, 8, 9); + var result = Enumerable.Empty() + .ToSourceKind(kind) + .Batch(100, pool, + current => current.CurrentItems, items => items); - Batch(input, 3, result => - { - const int scale = 2; - - var query = - from n in result.CurrentItems - where n % 2 == 0 - select n * scale; - - Assert.That(result.UpdateWithNext(), Is.True); - query.AssertSequenceEqual(2 * scale); - - Assert.That(result.UpdateWithNext(), Is.True); - query.AssertSequenceEqual(4 * scale, 6 * scale); - - Assert.That(result.UpdateWithNext(), Is.True); - query.AssertSequenceEqual(8 * scale); - - Assert.That(result.UpdateWithNext(), Is.False); - }); + Assert.That(result, Is.Empty); } [Test] @@ -279,8 +259,9 @@ public void BatchFilterBucket() { const int scale = 2; var input = TestingSequence.Of(1, 2, 3, 4, 5, 6, 7, 8, 9); + using var pool = new TestArrayPool(); - var result = input.Batch(3, new TestArrayPool(), + var result = input.Batch(3, pool, current => from n in current.CurrentItems where n % 2 == 0 select n * scale, @@ -297,8 +278,9 @@ public void BatchFilterBucket() public void BatchSumBucket() { var input = TestingSequence.Of(1, 2, 3, 4, 5, 6, 7, 8, 9); + using var pool = new TestArrayPool(); - var result = input.Batch(3, new TestArrayPool(), current => current.CurrentItems, q => q.Sum()); + var result = input.Batch(3, pool, current => current.CurrentItems, q => q.Sum()); using var reader = result.Read(); Assert.That(reader.Read(), Is.EqualTo(1 + 2 + 3)); @@ -306,103 +288,44 @@ public void BatchSumBucket() Assert.That(reader.Read(), Is.EqualTo(7 + 8 + 9)); reader.ReadEnd(); } - } - - public class BatchPooledArrayTest : BatchPoolTest - { - protected override void AssertBatch(IEnumerable source, int size, Action> asserter) - { - var pool = new TestArrayPool(); - asserter(source.Batch(size, pool)); - Assert.That(pool.HasRented, Is.False); - } - } - public class BatchPooledMemoryTest : BatchPoolTest - { - protected override void AssertBatch(IEnumerable source, int size, Action> asserter) - { - var pool = new TestMemoryPool(); - asserter(source.Batch(size, pool)); - Assert.That(pool.HasRented, Is.False); - } + /// + /// An implementation for testing purposes that holds only + /// one array in the pool and ensures that it is returned when the pool is disposed. + /// - sealed class TestMemoryPool : MemoryPool + sealed class TestArrayPool : ArrayPool, IDisposable { - readonly TestArrayPool _pool = new(); - - public bool HasRented => _pool.HasRented; - - protected override void Dispose(bool disposing) { } // NOP + T[] _pooledArray; + T[] _rentedArray; - public override IMemoryOwner Rent(int minBufferSize = -1) => - minBufferSize >= 0 - ? new MemoryOwner(_pool, _pool.Rent(minBufferSize)) - : throw new NotSupportedException(); - - public override int MaxBufferSize => - // https://github.com/dotnet/runtime/blob/v7.0.0-rc.2.22472.3/src/libraries/System.Memory/src/System/Buffers/ArrayMemoryPool.cs#L10 - 2_147_483_591; - - sealed class MemoryOwner : IMemoryOwner + public override T[] Rent(int minimumLength) { - ArrayPool _pool; - T[] _rental; - - public MemoryOwner(ArrayPool pool, T[] rental) => - (_pool, _rental) = (pool, rental); - - public Memory Memory => _rental is { } rental ? new Memory(rental) - : throw new ObjectDisposedException(null); - - public void Dispose() - { - if (_rental is { } array && _pool is { } pool) - { - _rental = null; - _pool = null; - pool.Return(array); - } - } - } - } - } + if (_pooledArray is null && _rentedArray is null) + _pooledArray = new T[minimumLength * 2]; - /// - /// An implementation for testing purposes that holds only - /// one array in the pool. - /// + if (_pooledArray is null) + throw new InvalidOperationException("The pool is exhausted."); - sealed class TestArrayPool : ArrayPool - { - T[] _pooledArray; - T[] _rentedArray; - - public bool HasRented => _rentedArray is not null; - - public override T[] Rent(int minimumLength) - { - if (_pooledArray is null && _rentedArray is null) - _pooledArray = new T[minimumLength * 2]; - - if (_pooledArray is null) - throw new InvalidOperationException("The pool is exhausted."); + (_pooledArray, _rentedArray) = (null, _pooledArray); - (_pooledArray, _rentedArray) = (null, _pooledArray); + return _rentedArray; + } - return _rentedArray; - } + public override void Return(T[] array, bool clearArray = false) + { + if (_rentedArray is null) + throw new InvalidOperationException("Cannot return when nothing has been rented from this pool."); - public override void Return(T[] array, bool clearArray = false) - { - if (_rentedArray is null) - throw new InvalidOperationException("Cannot return when nothing has been rented from this pool."); + if (array != _rentedArray) + throw new InvalidOperationException("Cannot return what has not been rented from this pool."); - if (array != _rentedArray) - throw new InvalidOperationException("Cannot return what has not been rented from this pool."); + _pooledArray = array; + _rentedArray = null; + } - _pooledArray = array; - _rentedArray = null; + public void Dispose() => + Assert.That(_rentedArray, Is.Null); } } } diff --git a/MoreLinq/Experimental/Batch.cs b/MoreLinq/Experimental/Batch.cs index e8252e97a..89e9cf7ca 100644 --- a/MoreLinq/Experimental/Batch.cs +++ b/MoreLinq/Experimental/Batch.cs @@ -26,178 +26,6 @@ namespace MoreLinq.Experimental static partial class ExperimentalEnumerable { - /// - /// Batches the source sequence into sized buckets using a memory pool - /// to rent memory to back each bucket. - /// - /// Type of elements in sequence. - /// The source sequence. - /// Size of buckets. - /// The memory pool used to rent memory for each bucket. - /// - /// A that can be used to enumerate - /// equally sized buckets containing elements of the source collection. - /// - /// - /// - /// This operator uses deferred execution and streams its results - /// (buckets are streamed but their content buffered). - /// - /// - /// Each bucket is backed by rented memory that may be at least - /// in length. - /// - /// - /// When more than one bucket is streamed, all buckets except the last - /// is guaranteed to have elements. The last - /// bucket may be smaller depending on the remaining elements in the - /// sequence. - /// Each bucket is pre-allocated to elements. - /// If is set to a very large value, e.g. - /// to effectively disable batching by just - /// hoping for a single bucket, then it can lead to memory exhaustion - /// (). - /// - /// - - public static ICurrentList - Batch(this IEnumerable source, int size, MemoryPool pool) - { - if (source == null) throw new ArgumentNullException(nameof(source)); - if (pool == null) throw new ArgumentNullException(nameof(pool)); - if (size <= 0) throw new ArgumentOutOfRangeException(nameof(size)); - - ICurrentList Cursor(IEnumerator<(IMemoryOwner, int)> source) => - new CurrentBucketMemory(source); - - IEnumerator<(IMemoryOwner, int)> Empty() { yield break; } - - switch (source) - { - case ICollection { Count: 0 }: - { - return Cursor(Empty()); - } - case IList list when list.Count <= size: - { - return Cursor(_()); IEnumerator<(IMemoryOwner, int)> _() - { - var bucket = pool.Rent(list.Count); - for (var i = 0; i < list.Count; i++) - bucket.Memory.Span[i] = list[i]; - yield return (bucket, list.Count); - } - } - case IReadOnlyCollection { Count: 0 }: - { - return Cursor(Empty()); - } - case IReadOnlyList list when list.Count <= size: - { - return Cursor(_()); IEnumerator<(IMemoryOwner, int)> _() - { - var bucket = pool.Rent(list.Count); - for (var i = 0; i < list.Count; i++) - bucket.Memory.Span[i] = list[i]; - yield return (bucket, list.Count); - } - } - case IReadOnlyCollection collection when collection.Count <= size: - { - return Cursor(Batch(collection.Count)); - } - default: - { - return Cursor(Batch(size)); - } - - IEnumerator<(IMemoryOwner, int)> Batch(int size) - { - IMemoryOwner? bucket = null; - var count = 0; - - foreach (var item in source) - { - bucket ??= pool.Rent(size); - bucket.Memory.Span[count++] = item; - - // The bucket is fully buffered before it's yielded - if (count != size) - continue; - - yield return (bucket, size); - - bucket = null; - count = 0; - } - - // Return the last bucket with all remaining elements - if (bucket is { } someBucket && count > 0) - yield return (someBucket, count); - } - } - } - - sealed class CurrentBucketMemory : CurrentList - { - bool _started; - IEnumerator<(IMemoryOwner Bucket, int Length)>? _enumerator; - - public CurrentBucketMemory(IEnumerator<(IMemoryOwner, int)> enumerator) => - _enumerator = enumerator; - - public override Span CurrentItemsSpan => Memory.Span; - - public override bool UpdateWithNext() - { - if (_enumerator is { } enumerator) - { - if (_started) - enumerator.Current.Bucket.Dispose(); - else - _started = true; - - if (!enumerator.MoveNext()) - { - enumerator.Dispose(); - _enumerator = null; - return false; - } - - return true; - } - - return false; - } - - Memory Memory => _started && _enumerator?.Current.Bucket is { Memory: var v } ? v : throw new InvalidOperationException(); - - public override int Count => _started && _enumerator?.Current.Length is { } v ? v : throw new InvalidOperationException(); - - public override T this[int index] - { - get => index >= 0 && index < Count ? Memory.Span[index] : throw new IndexOutOfRangeException(); - set => throw new NotSupportedException(); - } - - public override void Dispose() - { - if (_enumerator is { } enumerator) - { - _enumerator = null; - if (_started) - enumerator.Current.Bucket.Dispose(); - enumerator.Dispose(); - } - } - - public override IEnumerator GetEnumerator() - { - for (var i = 0; i < Count; i++) - yield return this[i]; - } - } - /// /// Batches the source sequence into sized buckets using an array pool /// to rent arrays to back each bucket and returns a sequence of @@ -249,18 +77,25 @@ public static IEnumerable Func, IEnumerable> querySelector, Func, TResult> resultSelector) { - using var current = source.Batch(size, pool); - if (current.UpdateWithNext()) + if (source == null) throw new ArgumentNullException(nameof(source)); + if (pool == null) throw new ArgumentNullException(nameof(pool)); + if (size <= 0) throw new ArgumentOutOfRangeException(nameof(size)); + if (querySelector == null) throw new ArgumentNullException(nameof(querySelector)); + if (resultSelector == null) throw new ArgumentNullException(nameof(resultSelector)); + + return _(); IEnumerable _() { - var query = querySelector(current); - do + using var current = source.Batch(size, pool); + + if (current.UpdateWithNext()) { - var result = resultSelector(query); - if (result is IEnumerable && ReferenceEquals(result, query)) - throw new InvalidOperationException(); - yield return result; + var query = querySelector(current); + do + { + yield return resultSelector(query); + } + while (current.UpdateWithNext()); } - while (current.UpdateWithNext()); } } From 0a4a3a7e5657de36eae10a4745b61dd49b196697 Mon Sep 17 00:00:00 2001 From: Atif Aziz Date: Sat, 29 Oct 2022 13:26:11 +0200 Subject: [PATCH 14/33] Fix type parameter doc comment --- MoreLinq/Experimental/CurrentList.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MoreLinq/Experimental/CurrentList.cs b/MoreLinq/Experimental/CurrentList.cs index 64bd60a47..d5488325a 100644 --- a/MoreLinq/Experimental/CurrentList.cs +++ b/MoreLinq/Experimental/CurrentList.cs @@ -29,7 +29,7 @@ namespace MoreLinq.Experimental /// is updated in-place (thus current) as it is moved through the overall /// result. /// - /// Type of elements in the bucket. + /// Type of elements in the list. public interface ICurrentList : IDisposable { From aec464cb2ec82a62933fd3874b2e7acad09fecc9 Mon Sep 17 00:00:00 2001 From: Atif Aziz Date: Sat, 29 Oct 2022 13:42:16 +0200 Subject: [PATCH 15/33] Split current list from its provider --- MoreLinq.Test/BatchTest.cs | 14 +++++------ MoreLinq/Experimental/Batch.cs | 22 +++++++++-------- MoreLinq/Experimental/CurrentList.cs | 35 +++++++++++++++------------- 3 files changed, 38 insertions(+), 33 deletions(-) diff --git a/MoreLinq.Test/BatchTest.cs b/MoreLinq.Test/BatchTest.cs index 9bb8d6152..f710e2038 100644 --- a/MoreLinq.Test/BatchTest.cs +++ b/MoreLinq.Test/BatchTest.cs @@ -166,7 +166,7 @@ public void BatchEvenlyDivisibleSequence() using var pool = new TestArrayPool(); var result = input.Batch(3, pool, - current => current.CurrentItems, + current => current, items => items.ToArray()); using var reader = result.Read(); @@ -183,7 +183,7 @@ public void BatchUnevenlyDivisibleSequence() using var pool = new TestArrayPool(); var result = input.Batch(4, pool, - current => current.CurrentItems, + current => current, items => items.ToArray()); using var reader = result.Read(); @@ -215,7 +215,7 @@ public void BatchCollectionSmallerThanSize(SourceKind kind, int oversize) var result = xs.ToSourceKind(kind) .Batch(xs.Length + oversize, pool, - current => current.CurrentItems, items => items); + current => current, items => items); using var reader = result.Read(); reader.Read().AssertSequenceEqual(1, 2, 3, 4, 5); @@ -229,7 +229,7 @@ public void BatchReadOnlyCollectionSmallerThanSize() using var pool = new TestArrayPool(); var result = collection.Batch(collection.Count * 2, pool, - current => current.CurrentItems, + current => current, items => items.ToArray()); using var reader = result.Read(); @@ -249,7 +249,7 @@ public void BatchEmptySource(SourceKind kind) var result = Enumerable.Empty() .ToSourceKind(kind) .Batch(100, pool, - current => current.CurrentItems, items => items); + current => current, items => items); Assert.That(result, Is.Empty); } @@ -262,7 +262,7 @@ public void BatchFilterBucket() using var pool = new TestArrayPool(); var result = input.Batch(3, pool, - current => from n in current.CurrentItems + current => from n in current where n % 2 == 0 select n * scale, query => query.ToArray()); @@ -280,7 +280,7 @@ public void BatchSumBucket() var input = TestingSequence.Of(1, 2, 3, 4, 5, 6, 7, 8, 9); using var pool = new TestArrayPool(); - var result = input.Batch(3, pool, current => current.CurrentItems, q => q.Sum()); + var result = input.Batch(3, pool, current => current, q => q.Sum()); using var reader = result.Read(); Assert.That(reader.Read(), Is.EqualTo(1 + 2 + 3)); diff --git a/MoreLinq/Experimental/Batch.cs b/MoreLinq/Experimental/Batch.cs index 89e9cf7ca..54f2b2ae7 100644 --- a/MoreLinq/Experimental/Batch.cs +++ b/MoreLinq/Experimental/Batch.cs @@ -85,16 +85,16 @@ public static IEnumerable return _(); IEnumerable _() { - using var current = source.Batch(size, pool); + using var batch = source.Batch(size, pool); - if (current.UpdateWithNext()) + if (batch.UpdateWithNext()) { - var query = querySelector(current); + var query = querySelector(batch.CurrentList); do { yield return resultSelector(query); } - while (current.UpdateWithNext()); + while (batch.UpdateWithNext()); } } } @@ -133,14 +133,14 @@ public static IEnumerable /// /// - public static ICurrentList + public static ICurrentListProvider Batch(this IEnumerable source, int size, ArrayPool pool) { if (source == null) throw new ArgumentNullException(nameof(source)); if (pool == null) throw new ArgumentNullException(nameof(pool)); if (size <= 0) throw new ArgumentOutOfRangeException(nameof(size)); - ICurrentList Cursor(IEnumerator<(T[], int)> source) => + ICurrentListProvider Cursor(IEnumerator<(T[], int)> source) => new CurrentBucketArray(source, pool); IEnumerator<(T[], int)> Empty() { yield break; } @@ -207,7 +207,7 @@ ICurrentList Cursor(IEnumerator<(T[], int)> source) => } } - sealed class CurrentBucketArray : CurrentList + sealed class CurrentBucketArray : CurrentList, ICurrentListProvider { bool _started; IEnumerator<(T[] Bucket, int Length)>? _enumerator; @@ -216,9 +216,11 @@ sealed class CurrentBucketArray : CurrentList public CurrentBucketArray(IEnumerator<(T[], int)> enumerator, ArrayPool pool) => (_enumerator, _pool) = (enumerator, pool); - public override Span CurrentItemsSpan => Array.AsSpan(); + public override Span AsSpan => Array.AsSpan(); - public override bool UpdateWithNext() + ICurrentList ICurrentListProvider.CurrentList => this; + + public bool UpdateWithNext() { if (_enumerator is { } enumerator) { @@ -253,7 +255,7 @@ public override T this[int index] } - public override void Dispose() + public void Dispose() { if (_enumerator is { } enumerator) { diff --git a/MoreLinq/Experimental/CurrentList.cs b/MoreLinq/Experimental/CurrentList.cs index d5488325a..944884ba2 100644 --- a/MoreLinq/Experimental/CurrentList.cs +++ b/MoreLinq/Experimental/CurrentList.cs @@ -31,7 +31,21 @@ namespace MoreLinq.Experimental /// /// Type of elements in the list. - public interface ICurrentList : IDisposable + public interface ICurrentList : IList + { + /// + /// Gets the current items of the list as . + /// + + Span AsSpan { get; } + } + + /// + /// A provider of current list that updates it in-place. + /// + /// Type of elements in the list. + + public interface ICurrentListProvider : IDisposable { /// /// Gets the current items of the list. @@ -41,13 +55,7 @@ public interface ICurrentList : IDisposable /// is called. /// - IList CurrentItems { get; } - - /// - /// Gets the current items of the list as . - /// - - Span CurrentItemsSpan { get; } + ICurrentList CurrentList { get; } /// /// Update this instance with the next set of elements from the source. @@ -61,17 +69,12 @@ public interface ICurrentList : IDisposable bool UpdateWithNext(); } - abstract class CurrentList : ICurrentList, IList + abstract class CurrentList : ICurrentList { - public abstract bool UpdateWithNext(); - public abstract void Dispose(); - - public abstract Span CurrentItemsSpan { get; } + public abstract Span AsSpan { get; } public abstract int Count { get; } public abstract T this[int index] { get; set; } - public virtual IList CurrentItems => this; - public virtual bool IsReadOnly => false; public virtual int IndexOf(T item) @@ -98,7 +101,7 @@ public virtual void CopyTo(T[] array, int arrayIndex) array[j] = this[i]; } - public virtual IEnumerator GetEnumerator() => CurrentItems.Take(Count).GetEnumerator(); + public virtual IEnumerator GetEnumerator() => this.Take(Count).GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); void IList.Insert(int index, T item) => throw new NotSupportedException(); From 3175ce988dc13115a93bd86ce76d7a0dc5d70655 Mon Sep 17 00:00:00 2001 From: Atif Aziz Date: Sat, 29 Oct 2022 13:52:45 +0200 Subject: [PATCH 16/33] Retain just the simpler overload publicly --- MoreLinq/Experimental/Batch.cs | 36 +--------------------------- MoreLinq/Experimental/CurrentList.cs | 2 +- 2 files changed, 2 insertions(+), 36 deletions(-) diff --git a/MoreLinq/Experimental/Batch.cs b/MoreLinq/Experimental/Batch.cs index 54f2b2ae7..baffaf212 100644 --- a/MoreLinq/Experimental/Batch.cs +++ b/MoreLinq/Experimental/Batch.cs @@ -99,41 +99,7 @@ public static IEnumerable } } - /// - /// Batches the source sequence into sized buckets using a array pool - /// to rent an array to back each bucket. - /// - /// Type of elements in sequence. - /// The source sequence. - /// Size of buckets. - /// The pool used to rent the array for each bucket. - /// - /// A that can be used to enumerate - /// equally sized buckets containing elements of the source collection. - /// - /// - /// - /// This operator uses deferred execution and streams its results - /// (buckets are streamed but their content buffered). - /// - /// - /// Each bucket is backed by a rented array that may be at least - /// in length. - /// - /// - /// When more than one bucket is streamed, all buckets except the last - /// is guaranteed to have elements. The last - /// bucket may be smaller depending on the remaining elements in the - /// sequence. - /// Each bucket is pre-allocated to elements. - /// If is set to a very large value, e.g. - /// to effectively disable batching by just - /// hoping for a single bucket, then it can lead to memory exhaustion - /// (). - /// - /// - - public static ICurrentListProvider + static ICurrentListProvider Batch(this IEnumerable source, int size, ArrayPool pool) { if (source == null) throw new ArgumentNullException(nameof(source)); diff --git a/MoreLinq/Experimental/CurrentList.cs b/MoreLinq/Experimental/CurrentList.cs index 944884ba2..5bc215f13 100644 --- a/MoreLinq/Experimental/CurrentList.cs +++ b/MoreLinq/Experimental/CurrentList.cs @@ -45,7 +45,7 @@ public interface ICurrentList : IList /// /// Type of elements in the list. - public interface ICurrentListProvider : IDisposable + interface ICurrentListProvider : IDisposable { /// /// Gets the current items of the list. From 07e7ce65524a6a3aed15fc6369c3b2b9ca4e23a8 Mon Sep 17 00:00:00 2001 From: Atif Aziz Date: Sat, 29 Oct 2022 15:19:22 +0200 Subject: [PATCH 17/33] Add simpler overload --- MoreLinq.Test/BatchTest.cs | 21 +++++-------- MoreLinq/Experimental/Batch.cs | 55 ++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 14 deletions(-) diff --git a/MoreLinq.Test/BatchTest.cs b/MoreLinq.Test/BatchTest.cs index f710e2038..13dd2f6d5 100644 --- a/MoreLinq.Test/BatchTest.cs +++ b/MoreLinq.Test/BatchTest.cs @@ -165,9 +165,7 @@ public void BatchEvenlyDivisibleSequence() using var input = TestingSequence.Of(1, 2, 3, 4, 5, 6, 7, 8, 9); using var pool = new TestArrayPool(); - var result = input.Batch(3, pool, - current => current, - items => items.ToArray()); + var result = input.Batch(3, pool, Enumerable.ToArray); using var reader = result.Read(); reader.Read().AssertSequenceEqual(1, 2, 3); @@ -182,9 +180,7 @@ public void BatchUnevenlyDivisibleSequence() using var input = TestingSequence.Of(1, 2, 3, 4, 5, 6, 7, 8, 9); using var pool = new TestArrayPool(); - var result = input.Batch(4, pool, - current => current, - items => items.ToArray()); + var result = input.Batch(4, pool, Enumerable.ToArray); using var reader = result.Read(); reader.Read().AssertSequenceEqual(1, 2, 3, 4); @@ -214,8 +210,7 @@ public void BatchCollectionSmallerThanSize(SourceKind kind, int oversize) using var pool = new TestArrayPool(); var result = xs.ToSourceKind(kind) - .Batch(xs.Length + oversize, pool, - current => current, items => items); + .Batch(xs.Length + oversize, pool, Enumerable.ToArray); using var reader = result.Read(); reader.Read().AssertSequenceEqual(1, 2, 3, 4, 5); @@ -229,8 +224,7 @@ public void BatchReadOnlyCollectionSmallerThanSize() using var pool = new TestArrayPool(); var result = collection.Batch(collection.Count * 2, pool, - current => current, - items => items.ToArray()); + Enumerable.ToArray); using var reader = result.Read(); reader.Read().AssertSequenceEqual(1, 2, 3, 4, 5); @@ -248,8 +242,7 @@ public void BatchEmptySource(SourceKind kind) var result = Enumerable.Empty() .ToSourceKind(kind) - .Batch(100, pool, - current => current, items => items); + .Batch(100, pool, Enumerable.ToArray); Assert.That(result, Is.Empty); } @@ -265,7 +258,7 @@ public void BatchFilterBucket() current => from n in current where n % 2 == 0 select n * scale, - query => query.ToArray()); + Enumerable.ToArray); using var reader = result.Read(); reader.Read().AssertSequenceEqual(2 * scale); @@ -280,7 +273,7 @@ public void BatchSumBucket() var input = TestingSequence.Of(1, 2, 3, 4, 5, 6, 7, 8, 9); using var pool = new TestArrayPool(); - var result = input.Batch(3, pool, current => current, q => q.Sum()); + var result = input.Batch(3, pool, Enumerable.Sum); using var reader = result.Read(); Assert.That(reader.Read(), Is.EqualTo(1 + 2 + 3)); diff --git a/MoreLinq/Experimental/Batch.cs b/MoreLinq/Experimental/Batch.cs index baffaf212..3591db64e 100644 --- a/MoreLinq/Experimental/Batch.cs +++ b/MoreLinq/Experimental/Batch.cs @@ -26,6 +26,61 @@ namespace MoreLinq.Experimental static partial class ExperimentalEnumerable { + /// + /// Batches the source sequence into sized buckets using an array pool + /// to rent arrays to back each bucket and returns a sequence of + /// elements projected from each bucket. + /// + /// + /// Type of elements in sequence. + /// + /// Type of elements of the resulting sequence. + /// + /// The source sequence. + /// Size of buckets. + /// The pool used to rent the array for each bucket. + /// A function that projects a result from + /// the current bucket. + /// + /// A sequence whose elements are projected from each bucket (returned by + /// ). + /// + /// + /// + /// This operator uses deferred execution and streams its results + /// (buckets are streamed but their content buffered). + /// + /// + /// Each bucket is backed by a rented array that may be at least + /// in length. + /// + /// + /// When more than one bucket is streamed, all buckets except the last + /// is guaranteed to have elements. The last + /// bucket may be smaller depending on the remaining elements in the + /// sequence. + /// Each bucket is pre-allocated to elements. + /// If is set to a very large value, e.g. + /// to effectively disable batching by just + /// hoping for a single bucket, then it can lead to memory exhaustion + /// (). + /// + /// + + public static IEnumerable + Batch(this IEnumerable source, int size, + ArrayPool pool, + Func, TResult> resultSelector) + { + if (source == null) throw new ArgumentNullException(nameof(source)); + if (pool == null) throw new ArgumentNullException(nameof(pool)); + if (size <= 0) throw new ArgumentOutOfRangeException(nameof(size)); + if (resultSelector == null) throw new ArgumentNullException(nameof(resultSelector)); + + return source.Batch(size, pool, current => current, + current => resultSelector((ICurrentList)current)); + } + /// /// Batches the source sequence into sized buckets using an array pool /// to rent arrays to back each bucket and returns a sequence of From 1e703669a37734800ca0740f1bcf848161641efa Mon Sep 17 00:00:00 2001 From: Atif Aziz Date: Sat, 29 Oct 2022 15:38:42 +0200 Subject: [PATCH 18/33] Add test to assert in-place updates --- MoreLinq.Test/BatchTest.cs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/MoreLinq.Test/BatchTest.cs b/MoreLinq.Test/BatchTest.cs index 13dd2f6d5..bfc849072 100644 --- a/MoreLinq.Test/BatchTest.cs +++ b/MoreLinq.Test/BatchTest.cs @@ -282,6 +282,29 @@ public void BatchSumBucket() reader.ReadEnd(); } + /// + /// This test does not exercise the intended usage! + /// + + [Test] + public void BatchUpdatesCurrentListInPlace() + { + var input = TestingSequence.Of(1, 2, 3, 4, 5, 6, 7, 8, 9); + using var pool = new TestArrayPool(); + + var result = input.Batch(4, pool, current => current, current => (ICurrentList)current); + + using var reader = result.Read(); + var current = reader.Read(); + current.AssertSequenceEqual(1, 2, 3, 4); + _ = reader.Read(); + current.AssertSequenceEqual(5, 6, 7, 8); + _ = reader.Read(); + current.AssertSequenceEqual(9); + + reader.ReadEnd(); + } + /// /// An implementation for testing purposes that holds only /// one array in the pool and ensures that it is returned when the pool is disposed. From 55daf666f411322356333f50408e5bb81af6667a Mon Sep 17 00:00:00 2001 From: Atif Aziz Date: Sat, 29 Oct 2022 15:39:51 +0200 Subject: [PATCH 19/33] Fix current list provider implementation name --- MoreLinq/Experimental/Batch.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/MoreLinq/Experimental/Batch.cs b/MoreLinq/Experimental/Batch.cs index 3591db64e..5ca366fd0 100644 --- a/MoreLinq/Experimental/Batch.cs +++ b/MoreLinq/Experimental/Batch.cs @@ -162,7 +162,7 @@ static ICurrentListProvider if (size <= 0) throw new ArgumentOutOfRangeException(nameof(size)); ICurrentListProvider Cursor(IEnumerator<(T[], int)> source) => - new CurrentBucketArray(source, pool); + new CurrentPoolArrayProvider(source, pool); IEnumerator<(T[], int)> Empty() { yield break; } @@ -228,13 +228,13 @@ ICurrentListProvider Cursor(IEnumerator<(T[], int)> source) => } } - sealed class CurrentBucketArray : CurrentList, ICurrentListProvider + sealed class CurrentPoolArrayProvider : CurrentList, ICurrentListProvider { bool _started; - IEnumerator<(T[] Bucket, int Length)>? _enumerator; + IEnumerator<(T[] Array, int Length)>? _enumerator; ArrayPool? _pool; - public CurrentBucketArray(IEnumerator<(T[], int)> enumerator, ArrayPool pool) => + public CurrentPoolArrayProvider(IEnumerator<(T[], int)> enumerator, ArrayPool pool) => (_enumerator, _pool) = (enumerator, pool); public override Span AsSpan => Array.AsSpan(); @@ -247,7 +247,7 @@ public bool UpdateWithNext() { Debug.Assert(_pool is not null); if (_started) - _pool.Return(enumerator.Current.Bucket); + _pool.Return(enumerator.Current.Array); else _started = true; @@ -265,7 +265,7 @@ public bool UpdateWithNext() return false; } - T[] Array => _started && _enumerator?.Current.Bucket is { } v ? v : throw new InvalidOperationException(); + T[] Array => _started && _enumerator?.Current.Array is { } v ? v : throw new InvalidOperationException(); public override int Count => _started && _enumerator?.Current.Length is { } v ? v : throw new InvalidOperationException(); @@ -281,7 +281,7 @@ public void Dispose() if (_enumerator is { } enumerator) { Debug.Assert(_pool is not null); - _pool.Return(enumerator.Current.Bucket); + _pool.Return(enumerator.Current.Array); enumerator.Dispose(); _enumerator = null; _pool = null; From cca8965febbec430eb088834f392319ff36606fa Mon Sep 17 00:00:00 2001 From: Atif Aziz Date: Sat, 29 Oct 2022 16:31:27 +0200 Subject: [PATCH 20/33] Call query selector before iterating source --- MoreLinq.Test/BatchTest.cs | 45 ++++++++++++++++++++++++++ MoreLinq/Experimental/Batch.cs | 59 ++++++++++++++++------------------ 2 files changed, 73 insertions(+), 31 deletions(-) diff --git a/MoreLinq.Test/BatchTest.cs b/MoreLinq.Test/BatchTest.cs index bfc849072..6bac1c6e6 100644 --- a/MoreLinq.Test/BatchTest.cs +++ b/MoreLinq.Test/BatchTest.cs @@ -303,6 +303,51 @@ public void BatchUpdatesCurrentListInPlace() current.AssertSequenceEqual(9); reader.ReadEnd(); + + Assert.That(current, Is.Empty); + } + + [Test] + public void BatchCallsQuerySelectorBeforeIteratingSource() + { + var iterations = 0; + IEnumerable Source() + { + iterations++; + yield break; + } + + var input = Source(); + using var pool = new TestArrayPool(); + var initIterations = -1; + + var result = input.Batch(4, pool, + current => + { + initIterations = iterations; + return current; + }, + _ => 0); + + using var enumerator = result.GetEnumerator(); + Assert.That(enumerator.MoveNext(), Is.False); + Assert.That(initIterations, Is.Zero); + } + + [Test] + public void BatchQueryCurrentList() + { + var input = TestingSequence.Of(1, 2, 3, 4, 5, 6, 7, 8, 9); + using var pool = new TestArrayPool(); + int[] queryCurrentList = null; + + var result = input.Batch(4, pool, current => queryCurrentList = current.ToArray(), _ => 0); + + using var reader = result.Read(); + _ = reader.Read(); + Assert.That(queryCurrentList, Is.Not.Null); + Assert.That(queryCurrentList, Is.Empty); + } /// diff --git a/MoreLinq/Experimental/Batch.cs b/MoreLinq/Experimental/Batch.cs index 5ca366fd0..5bdcda780 100644 --- a/MoreLinq/Experimental/Batch.cs +++ b/MoreLinq/Experimental/Batch.cs @@ -141,16 +141,9 @@ public static IEnumerable return _(); IEnumerable _() { using var batch = source.Batch(size, pool); - - if (batch.UpdateWithNext()) - { - var query = querySelector(batch.CurrentList); - do - { - yield return resultSelector(query); - } - while (batch.UpdateWithNext()); - } + var query = querySelector(batch.CurrentList); + while (batch.UpdateWithNext()) + yield return resultSelector(query); } } @@ -230,60 +223,64 @@ ICurrentListProvider Cursor(IEnumerator<(T[], int)> source) => sealed class CurrentPoolArrayProvider : CurrentList, ICurrentListProvider { - bool _started; - IEnumerator<(T[] Array, int Length)>? _enumerator; + bool _rented; + T[] _array = Array.Empty(); + int _count; + IEnumerator<(T[], int)>? _rental; ArrayPool? _pool; - public CurrentPoolArrayProvider(IEnumerator<(T[], int)> enumerator, ArrayPool pool) => - (_enumerator, _pool) = (enumerator, pool); + public CurrentPoolArrayProvider(IEnumerator<(T[], int)> rental, ArrayPool pool) => + (_rental, _pool) = (rental, pool); - public override Span AsSpan => Array.AsSpan(); + public override Span AsSpan => _array.AsSpan(); ICurrentList ICurrentListProvider.CurrentList => this; public bool UpdateWithNext() { - if (_enumerator is { } enumerator) + if (_rental is { Current: var (array, _) } rental) { Debug.Assert(_pool is not null); - if (_started) - _pool.Return(enumerator.Current.Array); - else - _started = true; + if (_rented) + { + _pool.Return(array); + _rented = false; + } - if (!enumerator.MoveNext()) + if (!rental.MoveNext()) { - enumerator.Dispose(); - _enumerator = null; - _pool = null; + Dispose(); return false; } + _rented = true; + (_array, _count) = rental.Current; return true; } return false; } - T[] Array => _started && _enumerator?.Current.Array is { } v ? v : throw new InvalidOperationException(); - - public override int Count => _started && _enumerator?.Current.Length is { } v ? v : throw new InvalidOperationException(); + public override int Count => _count; public override T this[int index] { - get => index >= 0 && index < Count ? Array[index] : throw new IndexOutOfRangeException(); + get => index >= 0 && index < Count ? _array[index] : throw new IndexOutOfRangeException(); set => throw new NotSupportedException(); } public void Dispose() { - if (_enumerator is { } enumerator) + if (_rental is { Current: var (array, _) } enumerator) { Debug.Assert(_pool is not null); - _pool.Return(enumerator.Current.Array); + if (_rented) + _pool.Return(array); enumerator.Dispose(); - _enumerator = null; + _array = Array.Empty(); + _count = 0; + _rental = null; _pool = null; } } From ff226ef938e36af349c7142983713ad0be1bd9cb Mon Sep 17 00:00:00 2001 From: Atif Aziz Date: Sat, 29 Oct 2022 16:39:49 +0200 Subject: [PATCH 21/33] Rename query to bucket --- MoreLinq.Test/BatchTest.cs | 12 ++++++------ MoreLinq/Experimental/Batch.cs | 23 +++++++++++++---------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/MoreLinq.Test/BatchTest.cs b/MoreLinq.Test/BatchTest.cs index 6bac1c6e6..f0292ebcc 100644 --- a/MoreLinq.Test/BatchTest.cs +++ b/MoreLinq.Test/BatchTest.cs @@ -308,7 +308,7 @@ public void BatchUpdatesCurrentListInPlace() } [Test] - public void BatchCallsQuerySelectorBeforeIteratingSource() + public void BatchCallsBucketSelectorBeforeIteratingSource() { var iterations = 0; IEnumerable Source() @@ -335,18 +335,18 @@ IEnumerable Source() } [Test] - public void BatchQueryCurrentList() + public void BatchBucketSelectorCurrentList() { var input = TestingSequence.Of(1, 2, 3, 4, 5, 6, 7, 8, 9); using var pool = new TestArrayPool(); - int[] queryCurrentList = null; + int[] bucketSelectorItems = null; - var result = input.Batch(4, pool, current => queryCurrentList = current.ToArray(), _ => 0); + var result = input.Batch(4, pool, current => bucketSelectorItems = current.ToArray(), _ => 0); using var reader = result.Read(); _ = reader.Read(); - Assert.That(queryCurrentList, Is.Not.Null); - Assert.That(queryCurrentList, Is.Empty); + Assert.That(bucketSelectorItems, Is.Not.Null); + Assert.That(bucketSelectorItems, Is.Empty); } diff --git a/MoreLinq/Experimental/Batch.cs b/MoreLinq/Experimental/Batch.cs index 5bdcda780..8fd35c612 100644 --- a/MoreLinq/Experimental/Batch.cs +++ b/MoreLinq/Experimental/Batch.cs @@ -88,16 +88,19 @@ public static IEnumerable /// /// /// Type of elements in sequence. - /// - /// Type of elements in the sequence returned by . + /// + /// Type of elements in the sequence returned by . /// /// Type of elements of the resulting sequence. /// /// The source sequence. /// Size of buckets. /// The pool used to rent the array for each bucket. - /// A function that projects a query over - /// all buckets. + /// A function that returns a sequence + /// projection to use for each bucket. It is called initially before + /// iterating over , but the resulting + /// projection is evaluated for each bucket. + /// /// A function that projects a result from /// the input sequence produced over a bucket. /// @@ -127,23 +130,23 @@ public static IEnumerable /// public static IEnumerable - Batch( + Batch( this IEnumerable source, int size, ArrayPool pool, - Func, IEnumerable> querySelector, - Func, TResult> resultSelector) + Func, IEnumerable> bucketSelector, + Func, TResult> resultSelector) { if (source == null) throw new ArgumentNullException(nameof(source)); if (pool == null) throw new ArgumentNullException(nameof(pool)); if (size <= 0) throw new ArgumentOutOfRangeException(nameof(size)); - if (querySelector == null) throw new ArgumentNullException(nameof(querySelector)); + if (bucketSelector == null) throw new ArgumentNullException(nameof(bucketSelector)); if (resultSelector == null) throw new ArgumentNullException(nameof(resultSelector)); return _(); IEnumerable _() { using var batch = source.Batch(size, pool); - var query = querySelector(batch.CurrentList); + var bucket = bucketSelector(batch.CurrentList); while (batch.UpdateWithNext()) - yield return resultSelector(query); + yield return resultSelector(bucket); } } From 70bb45b8a41016af2d43b791b68d785d9cce8b8b Mon Sep 17 00:00:00 2001 From: Atif Aziz Date: Sat, 29 Oct 2022 16:41:11 +0200 Subject: [PATCH 22/33] Rename query to bucket projection --- MoreLinq/Experimental/Batch.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/MoreLinq/Experimental/Batch.cs b/MoreLinq/Experimental/Batch.cs index 8fd35c612..2e0727498 100644 --- a/MoreLinq/Experimental/Batch.cs +++ b/MoreLinq/Experimental/Batch.cs @@ -89,16 +89,16 @@ public static IEnumerable /// /// Type of elements in sequence. /// - /// Type of elements in the sequence returned by . + /// Type of elements in the sequence returned by . /// /// Type of elements of the resulting sequence. /// /// The source sequence. /// Size of buckets. /// The pool used to rent the array for each bucket. - /// A function that returns a sequence - /// projection to use for each bucket. It is called initially before - /// iterating over , but the resulting + /// A function that returns a + /// sequence projection to use for each bucket. It is called initially + /// before iterating over , but the resulting /// projection is evaluated for each bucket. /// /// A function that projects a result from @@ -132,19 +132,19 @@ public static IEnumerable public static IEnumerable Batch( this IEnumerable source, int size, ArrayPool pool, - Func, IEnumerable> bucketSelector, + Func, IEnumerable> bucketProjectionSelector, Func, TResult> resultSelector) { if (source == null) throw new ArgumentNullException(nameof(source)); if (pool == null) throw new ArgumentNullException(nameof(pool)); if (size <= 0) throw new ArgumentOutOfRangeException(nameof(size)); - if (bucketSelector == null) throw new ArgumentNullException(nameof(bucketSelector)); + if (bucketProjectionSelector == null) throw new ArgumentNullException(nameof(bucketProjectionSelector)); if (resultSelector == null) throw new ArgumentNullException(nameof(resultSelector)); return _(); IEnumerable _() { using var batch = source.Batch(size, pool); - var bucket = bucketSelector(batch.CurrentList); + var bucket = bucketProjectionSelector(batch.CurrentList); while (batch.UpdateWithNext()) yield return resultSelector(bucket); } From e707297acce9120729f1d9d4c6cbdc0b7189597d Mon Sep 17 00:00:00 2001 From: Atif Aziz Date: Sat, 29 Oct 2022 17:10:40 +0200 Subject: [PATCH 23/33] Revise conditional constants --- MoreLinq/Experimental/Batch.cs | 4 ++-- MoreLinq/Experimental/CurrentList.cs | 4 ++-- MoreLinq/MoreLinq.csproj | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/MoreLinq/Experimental/Batch.cs b/MoreLinq/Experimental/Batch.cs index 2e0727498..dd7b4f3a4 100644 --- a/MoreLinq/Experimental/Batch.cs +++ b/MoreLinq/Experimental/Batch.cs @@ -15,7 +15,7 @@ // limitations under the License. #endregion -#if !NO_MEMORY +#if !NO_BUFFERS namespace MoreLinq.Experimental { @@ -291,4 +291,4 @@ public void Dispose() } } -#endif // !NO_MEMORY +#endif // !NO_BUFFERS diff --git a/MoreLinq/Experimental/CurrentList.cs b/MoreLinq/Experimental/CurrentList.cs index 5bc215f13..55b7a3589 100644 --- a/MoreLinq/Experimental/CurrentList.cs +++ b/MoreLinq/Experimental/CurrentList.cs @@ -15,7 +15,7 @@ // limitations under the License. #endregion -#if !NO_MEMORY && !NO_TRAITS && !NO_TRAITS +#if !NO_BUFFERS namespace MoreLinq.Experimental { @@ -112,4 +112,4 @@ public virtual void CopyTo(T[] array, int arrayIndex) } } -#endif // !NO_MEMORY && !NO_TRAITS +#endif // !NO_BUFFERS diff --git a/MoreLinq/MoreLinq.csproj b/MoreLinq/MoreLinq.csproj index eb9c4ca45..d8838ab43 100644 --- a/MoreLinq/MoreLinq.csproj +++ b/MoreLinq/MoreLinq.csproj @@ -194,11 +194,11 @@ - $(DefineConstants);NO_TRAITS;NO_MEMORY + $(DefineConstants);NO_BUFFERS - $(DefineConstants);MORELINQ;NO_MEMORY;NO_TRAITS;NO_SERIALIZATION_ATTRIBUTES;NO_EXCEPTION_SERIALIZATION;NO_TRACING;NO_COM;NO_ASYNC + $(DefineConstants);MORELINQ;NO_BUFFERS;NO_SERIALIZATION_ATTRIBUTES;NO_EXCEPTION_SERIALIZATION;NO_TRACING;NO_COM;NO_ASYNC From d61ec0397e456ac4b57fef77e4433f5ad7a9ba14 Mon Sep 17 00:00:00 2001 From: Atif Aziz Date: Sat, 29 Oct 2022 17:24:45 +0200 Subject: [PATCH 24/33] Remove extra blank line --- MoreLinq.Test/BatchTest.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/MoreLinq.Test/BatchTest.cs b/MoreLinq.Test/BatchTest.cs index f0292ebcc..e83e2b448 100644 --- a/MoreLinq.Test/BatchTest.cs +++ b/MoreLinq.Test/BatchTest.cs @@ -347,7 +347,6 @@ public void BatchBucketSelectorCurrentList() _ = reader.Read(); Assert.That(bucketSelectorItems, Is.Not.Null); Assert.That(bucketSelectorItems, Is.Empty); - } /// From 61dbc8438c7c4b9d771d18fe341efc58cb193f94 Mon Sep 17 00:00:00 2001 From: Atif Aziz Date: Sat, 29 Oct 2022 19:11:45 +0200 Subject: [PATCH 25/33] Remove "AsSpan" from current list --- MoreLinq/Experimental/CurrentList.cs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/MoreLinq/Experimental/CurrentList.cs b/MoreLinq/Experimental/CurrentList.cs index 55b7a3589..a8936a69c 100644 --- a/MoreLinq/Experimental/CurrentList.cs +++ b/MoreLinq/Experimental/CurrentList.cs @@ -31,14 +31,7 @@ namespace MoreLinq.Experimental /// /// Type of elements in the list. - public interface ICurrentList : IList - { - /// - /// Gets the current items of the list as . - /// - - Span AsSpan { get; } - } + public interface ICurrentList : IList { } /// /// A provider of current list that updates it in-place. From 41fcf7472af84047bb98702edda21571641fd436 Mon Sep 17 00:00:00 2001 From: Atif Aziz Date: Sun, 30 Oct 2022 11:53:30 +0100 Subject: [PATCH 26/33] Update overload count in read-me doc --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 420dc324d..0104175ee 100644 --- a/README.md +++ b/README.md @@ -141,7 +141,7 @@ the third-last element and so on. Batches the source sequence into sized buckets. -This method has 2 overloads. +This method has 4 overloads, 2 of which are experimental. ### Cartesian From b555c3dd88e093afdd68b851e45996e9d0fd6e41 Mon Sep 17 00:00:00 2001 From: Atif Aziz Date: Mon, 31 Oct 2022 20:49:31 +0100 Subject: [PATCH 27/33] Reuse "Enumerable.Empty" enumerator --- .config/dotnet-tools.json | 2 +- MoreLinq/Experimental/Batch.cs | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index fb2643e5b..51fdd4883 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -9,7 +9,7 @@ ] }, "dotnet-reportgenerator-globaltool": { - "version": "4.6.4", + "version": "5.1.10", "commands": [ "reportgenerator" ] diff --git a/MoreLinq/Experimental/Batch.cs b/MoreLinq/Experimental/Batch.cs index dd7b4f3a4..774f6071f 100644 --- a/MoreLinq/Experimental/Batch.cs +++ b/MoreLinq/Experimental/Batch.cs @@ -23,6 +23,7 @@ namespace MoreLinq.Experimental using System.Buffers; using System.Collections.Generic; using System.Diagnostics; + using System.Linq; static partial class ExperimentalEnumerable { @@ -160,13 +161,11 @@ static ICurrentListProvider ICurrentListProvider Cursor(IEnumerator<(T[], int)> source) => new CurrentPoolArrayProvider(source, pool); - IEnumerator<(T[], int)> Empty() { yield break; } - switch (source) { case ICollection { Count: 0 }: { - return Cursor(Empty()); + return Cursor(Enumerable.Empty <(T[], int)>().GetEnumerator()); } case ICollection collection when collection.Count <= size: { @@ -176,7 +175,7 @@ ICurrentListProvider Cursor(IEnumerator<(T[], int)> source) => } case IReadOnlyCollection { Count: 0 }: { - return Cursor(Empty()); + return Cursor(Enumerable.Empty <(T[], int)>().GetEnumerator()); } case IReadOnlyList list when list.Count <= size: { From be1f763c0338ae058c1b47a7f8999dee71a36e5b Mon Sep 17 00:00:00 2001 From: Atif Aziz Date: Mon, 31 Oct 2022 22:59:44 +0100 Subject: [PATCH 28/33] Remove "AsSpan" from current list abstract These changes belonged with: > commit 61dbc8438c7c4b9d771d18fe341efc58cb193f94 > Author: Atif Aziz > Date: Sat Oct 29 19:11:45 2022 +0200 > > Remove "AsSpan" from current list --- MoreLinq/Experimental/Batch.cs | 2 -- MoreLinq/Experimental/CurrentList.cs | 1 - 2 files changed, 3 deletions(-) diff --git a/MoreLinq/Experimental/Batch.cs b/MoreLinq/Experimental/Batch.cs index 774f6071f..1d2561511 100644 --- a/MoreLinq/Experimental/Batch.cs +++ b/MoreLinq/Experimental/Batch.cs @@ -234,8 +234,6 @@ sealed class CurrentPoolArrayProvider : CurrentList, ICurrentListProvider< public CurrentPoolArrayProvider(IEnumerator<(T[], int)> rental, ArrayPool pool) => (_rental, _pool) = (rental, pool); - public override Span AsSpan => _array.AsSpan(); - ICurrentList ICurrentListProvider.CurrentList => this; public bool UpdateWithNext() diff --git a/MoreLinq/Experimental/CurrentList.cs b/MoreLinq/Experimental/CurrentList.cs index a8936a69c..8fcae447b 100644 --- a/MoreLinq/Experimental/CurrentList.cs +++ b/MoreLinq/Experimental/CurrentList.cs @@ -64,7 +64,6 @@ interface ICurrentListProvider : IDisposable abstract class CurrentList : ICurrentList { - public abstract Span AsSpan { get; } public abstract int Count { get; } public abstract T this[int index] { get; set; } From d1cd8ccb91fbfc57542c47a00a190c198733962e Mon Sep 17 00:00:00 2001 From: Atif Aziz Date: Mon, 31 Oct 2022 23:09:05 +0100 Subject: [PATCH 29/33] Fix doc about bucket streaming/buffering --- MoreLinq/Experimental/Batch.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/MoreLinq/Experimental/Batch.cs b/MoreLinq/Experimental/Batch.cs index 1d2561511..b6dff235c 100644 --- a/MoreLinq/Experimental/Batch.cs +++ b/MoreLinq/Experimental/Batch.cs @@ -49,14 +49,15 @@ static partial class ExperimentalEnumerable /// /// /// This operator uses deferred execution and streams its results - /// (buckets are streamed but their content buffered). + /// (each bucket to is however + /// buffered). /// /// /// Each bucket is backed by a rented array that may be at least /// in length. /// /// - /// When more than one bucket is streamed, all buckets except the last + /// When more than one bucket is produced, all buckets except the last /// is guaranteed to have elements. The last /// bucket may be smaller depending on the remaining elements in the /// sequence. @@ -111,14 +112,14 @@ public static IEnumerable /// /// /// This operator uses deferred execution and streams its results - /// (buckets are streamed but their content buffered). + /// (each bucket is however buffered). /// /// /// Each bucket is backed by a rented array that may be at least /// in length. /// /// - /// When more than one bucket is streamed, all buckets except the last + /// When more than one bucket is produced, all buckets except the last /// is guaranteed to have elements. The last /// bucket may be smaller depending on the remaining elements in the /// sequence. From 0fc8901e53efa1cd11eac489d5bebcab4e5d0d0a Mon Sep 17 00:00:00 2001 From: Atif Aziz Date: Mon, 31 Oct 2022 23:32:50 +0100 Subject: [PATCH 30/33] Rename list in current list types to buffer --- MoreLinq.Test/BatchTest.cs | 6 +++--- MoreLinq/Experimental/Batch.cs | 16 ++++++++-------- .../{CurrentList.cs => CurrentBuffer.cs} | 12 ++++++------ 3 files changed, 17 insertions(+), 17 deletions(-) rename MoreLinq/Experimental/{CurrentList.cs => CurrentBuffer.cs} (90%) diff --git a/MoreLinq.Test/BatchTest.cs b/MoreLinq.Test/BatchTest.cs index e83e2b448..a459ee2dd 100644 --- a/MoreLinq.Test/BatchTest.cs +++ b/MoreLinq.Test/BatchTest.cs @@ -155,7 +155,7 @@ public void BatchBadSize(int size) { AssertThrowsArgument.OutOfRangeException("size", () => new object[0].Batch(size, ArrayPool.Shared, - BreakingFunc.Of, IEnumerable>(), + BreakingFunc.Of, IEnumerable>(), BreakingFunc.Of, object>())); } @@ -194,7 +194,7 @@ public void BatchIsLazy() { var input = new BreakingSequence(); _ = input.Batch(1, ArrayPool.Shared, - BreakingFunc.Of, IEnumerable>(), + BreakingFunc.Of, IEnumerable>(), BreakingFunc.Of, object>()); } @@ -292,7 +292,7 @@ public void BatchUpdatesCurrentListInPlace() var input = TestingSequence.Of(1, 2, 3, 4, 5, 6, 7, 8, 9); using var pool = new TestArrayPool(); - var result = input.Batch(4, pool, current => current, current => (ICurrentList)current); + var result = input.Batch(4, pool, current => current, current => (ICurrentBuffer)current); using var reader = result.Read(); var current = reader.Read(); diff --git a/MoreLinq/Experimental/Batch.cs b/MoreLinq/Experimental/Batch.cs index b6dff235c..fd687215d 100644 --- a/MoreLinq/Experimental/Batch.cs +++ b/MoreLinq/Experimental/Batch.cs @@ -72,7 +72,7 @@ static partial class ExperimentalEnumerable public static IEnumerable Batch(this IEnumerable source, int size, ArrayPool pool, - Func, TResult> resultSelector) + Func, TResult> resultSelector) { if (source == null) throw new ArgumentNullException(nameof(source)); if (pool == null) throw new ArgumentNullException(nameof(pool)); @@ -80,7 +80,7 @@ public static IEnumerable if (resultSelector == null) throw new ArgumentNullException(nameof(resultSelector)); return source.Batch(size, pool, current => current, - current => resultSelector((ICurrentList)current)); + current => resultSelector((ICurrentBuffer)current)); } /// @@ -134,7 +134,7 @@ public static IEnumerable public static IEnumerable Batch( this IEnumerable source, int size, ArrayPool pool, - Func, IEnumerable> bucketProjectionSelector, + Func, IEnumerable> bucketProjectionSelector, Func, TResult> resultSelector) { if (source == null) throw new ArgumentNullException(nameof(source)); @@ -146,20 +146,20 @@ public static IEnumerable return _(); IEnumerable _() { using var batch = source.Batch(size, pool); - var bucket = bucketProjectionSelector(batch.CurrentList); + var bucket = bucketProjectionSelector(batch.CurrentBuffer); while (batch.UpdateWithNext()) yield return resultSelector(bucket); } } - static ICurrentListProvider + static ICurrentBufferProvider Batch(this IEnumerable source, int size, ArrayPool pool) { if (source == null) throw new ArgumentNullException(nameof(source)); if (pool == null) throw new ArgumentNullException(nameof(pool)); if (size <= 0) throw new ArgumentOutOfRangeException(nameof(size)); - ICurrentListProvider Cursor(IEnumerator<(T[], int)> source) => + ICurrentBufferProvider Cursor(IEnumerator<(T[], int)> source) => new CurrentPoolArrayProvider(source, pool); switch (source) @@ -224,7 +224,7 @@ ICurrentListProvider Cursor(IEnumerator<(T[], int)> source) => } } - sealed class CurrentPoolArrayProvider : CurrentList, ICurrentListProvider + sealed class CurrentPoolArrayProvider : CurrentBuffer, ICurrentBufferProvider { bool _rented; T[] _array = Array.Empty(); @@ -235,7 +235,7 @@ sealed class CurrentPoolArrayProvider : CurrentList, ICurrentListProvider< public CurrentPoolArrayProvider(IEnumerator<(T[], int)> rental, ArrayPool pool) => (_rental, _pool) = (rental, pool); - ICurrentList ICurrentListProvider.CurrentList => this; + ICurrentBuffer ICurrentBufferProvider.CurrentBuffer => this; public bool UpdateWithNext() { diff --git a/MoreLinq/Experimental/CurrentList.cs b/MoreLinq/Experimental/CurrentBuffer.cs similarity index 90% rename from MoreLinq/Experimental/CurrentList.cs rename to MoreLinq/Experimental/CurrentBuffer.cs index 8fcae447b..e6cb526a1 100644 --- a/MoreLinq/Experimental/CurrentList.cs +++ b/MoreLinq/Experimental/CurrentBuffer.cs @@ -25,20 +25,20 @@ namespace MoreLinq.Experimental using System.Linq; /// - /// Represents a list that is the current view of a larger result and which + /// Represents a current buffered view of a larger result and which /// is updated in-place (thus current) as it is moved through the overall /// result. /// /// Type of elements in the list. - public interface ICurrentList : IList { } + public interface ICurrentBuffer : IList { } /// - /// A provider of current list that updates it in-place. + /// A provider of current buffer that updates it in-place. /// /// Type of elements in the list. - interface ICurrentListProvider : IDisposable + interface ICurrentBufferProvider : IDisposable { /// /// Gets the current items of the list. @@ -48,7 +48,7 @@ interface ICurrentListProvider : IDisposable /// is called. /// - ICurrentList CurrentList { get; } + ICurrentBuffer CurrentBuffer { get; } /// /// Update this instance with the next set of elements from the source. @@ -62,7 +62,7 @@ interface ICurrentListProvider : IDisposable bool UpdateWithNext(); } - abstract class CurrentList : ICurrentList + abstract class CurrentBuffer : ICurrentBuffer { public abstract int Count { get; } public abstract T this[int index] { get; set; } From 43f75826e76e3cbb9f23e27adeb311e5737d0d71 Mon Sep 17 00:00:00 2001 From: Atif Aziz Date: Sat, 12 Nov 2022 12:45:45 +0100 Subject: [PATCH 31/33] Expand "bucketProjectionSelector" doc Co-authored-by: Stuart Turner --- MoreLinq/Experimental/Batch.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/MoreLinq/Experimental/Batch.cs b/MoreLinq/Experimental/Batch.cs index fd687215d..1c626f1c7 100644 --- a/MoreLinq/Experimental/Batch.cs +++ b/MoreLinq/Experimental/Batch.cs @@ -101,7 +101,9 @@ public static IEnumerable /// A function that returns a /// sequence projection to use for each bucket. It is called initially /// before iterating over , but the resulting - /// projection is evaluated for each bucket. + /// projection is evaluated for each bucket. This has the same effect as + /// calling for each bucket, + /// but allows initialization of the transformation to happen only once. /// /// A function that projects a result from /// the input sequence produced over a bucket. From bdf76a761dae32312644b0396e1dd9997e4a71a7 Mon Sep 17 00:00:00 2001 From: Atif Aziz Date: Sat, 12 Nov 2022 12:49:34 +0100 Subject: [PATCH 32/33] Reword remarks note to read clearer. Co-authored-by: Stuart Turner --- MoreLinq/Experimental/Batch.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MoreLinq/Experimental/Batch.cs b/MoreLinq/Experimental/Batch.cs index 1c626f1c7..003dd6675 100644 --- a/MoreLinq/Experimental/Batch.cs +++ b/MoreLinq/Experimental/Batch.cs @@ -49,7 +49,7 @@ static partial class ExperimentalEnumerable /// /// /// This operator uses deferred execution and streams its results - /// (each bucket to is however + /// (however, each bucket provided to is /// buffered). /// /// From ec82d1be31a5aca1ccdcd817de4618289ebf0566 Mon Sep 17 00:00:00 2001 From: Atif Aziz Date: Sat, 12 Nov 2022 12:49:44 +0100 Subject: [PATCH 33/33] Reword remarks note to read clearer. Co-authored-by: Stuart Turner --- MoreLinq/Experimental/Batch.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MoreLinq/Experimental/Batch.cs b/MoreLinq/Experimental/Batch.cs index 003dd6675..ae40954ff 100644 --- a/MoreLinq/Experimental/Batch.cs +++ b/MoreLinq/Experimental/Batch.cs @@ -114,7 +114,7 @@ public static IEnumerable /// /// /// This operator uses deferred execution and streams its results - /// (each bucket is however buffered). + /// (however, each bucket is buffered). /// /// /// Each bucket is backed by a rented array that may be at least