-
Notifications
You must be signed in to change notification settings - Fork 56
Description
π€ This PR was created by Repo Assist, an automated AI assistant.
Summary
Replaces the asyncSeq-builder implementations of AsyncSeq.take and AsyncSeq.skip with direct IAsyncSeqEnumerator<'T> types β OptimizedTakeEnumerator and OptimizedSkipEnumerator β following the same pattern established by the existing optimised enumerators for mapAsync, filterAsync, chooseAsync, and foldAsync.
Root Cause
Both take and skip previously used the asyncSeq { } computation builder. This routes every element through the AsyncGenerator / GenerateCont machinery, allocating generator objects and dispatching through virtual calls at each step. For operations as simple as counting/discarding elements this overhead is unnecessary.
Fix
OptimizedTakeEnumerator<'T>:
- Holds the source enumerator and a mutable
remainingcounter. - On each
MoveNext()call, returnsNoneimmediately whenremaining β€ 0; otherwise delegates to the source and decrements the counter. - No allocations beyond the single object instance.
OptimizedSkipEnumerator<'T>:
- Holds the source enumerator, a mutable
toSkipcounter, and anexhaustedflag. - First
MoveNext()call drainstoSkipelements from the source; subsequent calls delegate directly to the source. - Correctly handles sequences shorter than the skip count.
Changes
src/FSharp.Control.AsyncSeq/AsyncSeq.fsβ addOptimizedTakeEnumerator,OptimizedSkipEnumerator; replaceasyncSeq {}implementations oftake/skiptests/FSharp.Control.AsyncSeq.Benchmarks/AsyncSeqBenchmarks.fsβ addAsyncSeqSliceBenchmarksclass (Take,Skip,SkipThenTakefor N=1000/10000)tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fsβ 6 new edge-case testsRELEASE_NOTES.mdβ update 4.9.0 entry
New Tests
| Test | Description |
|---|---|
AsyncSeq.take more than length returns all elements |
take 10 of a 3-element seq returns all 3 |
AsyncSeq.take raises ArgumentException for negative count |
take -1 throws |
AsyncSeq.take from infinite sequence |
take 5 of replicateInfinite terminates |
AsyncSeq.skip more than length returns empty |
skip 10 of a 3-element seq returns [] |
AsyncSeq.skip raises ArgumentException for negative count |
skip -1 throws |
AsyncSeq.take then skip roundtrip |
skip 5 |> take 10 over [1..20] returns [6..15] |
Test Status
β
Build succeeded (0 errors, pre-existing warnings only β NU1605, FS9999 for groupByAsync, FS0044 for deprecated toAsyncEnum/ofAsyncEnum in existing tests).
β
All 323 tests pass (dotnet test FSharp.Control.AsyncSeq.sln).
Generated by Repo Assist Β· β·
To install this agentic workflow, run
gh aw add githubnext/agentics/workflows/repo-assist.md@346204513ecfa08b81566450d7d599556807389f
Note
This was originally intended as a pull request, but PR creation failed. The changes have been pushed to the branch repo-assist/perf-take-skip-enumerators-b793dd82cef306c6.
Original error: Validation Failed: {"resource":"Label","code":"unprocessable","field":"data","message":"Could not resolve to a node with the global id of 'PR_kwDOAfaq5M7KqnNT'."}
To create the pull request manually:
gh pr create --title "[Repo Assist] Perf: optimise `take` and `skip` with direct enumerators" --base main --head repo-assist/perf-take-skip-enumerators-b793dd82cef306c6 --repo fsprojects/FSharp.Control.AsyncSeqShow patch preview (271 of 271 lines)
From 25957d20c4b818ea0b2f6bb8658a0a5513f1592a Mon Sep 17 00:00:00 2001
From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com>
Date: Sun, 15 Mar 2026 00:14:12 +0000
Subject: [PATCH] Perf: optimise `take` and `skip` with direct enumerators
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Replace asyncSeq-builder implementations of `take` and `skip` with
direct `IAsyncSeqEnumerator<'T>` types (OptimizedTakeEnumerator and
OptimizedSkipEnumerator) following the same pattern as the existing
optimised enumerators for mapAsync, filterAsync, chooseAsync, and
foldAsync.
The asyncSeq builder routes every element through the AsyncGenerator /
GenerateCont machinery, allocating generator objects and dispatching
through virtual calls at each step. Direct enumerators avoid this
overhead entirely β they hold only the source enumerator and a small
amount of mutable state (an int counter), with no intermediate
allocations per element.
Changes:
- AsyncSeq.fs: add OptimizedTakeEnumerator<'T> and
OptimizedSkipEnumerator<'T>; replace asyncSeq{} in take/skip
- AsyncSeqBenchmarks.fs: add AsyncSeqSliceBenchmarks (Take, Skip,
SkipThenTake benchmarks for 1 000 and 10 000 elements)
- AsyncSeqTests.fs: 6 new edge-case tests (take >length, take -1,
take from infinite, skip >length, skip -1, skip+take roundtrip)
- RELEASE_NOTES.md: update 4.9.0 entry
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
RELEASE_NOTES.md | 4 +-
src/FSharp.Control.AsyncSeq/AsyncSeq.fs | 83 +++++++++++++------
.../AsyncSeqBenchmarks.fs | 35 ++++++++
.../AsyncSeqTests.fs | 56 +++++++++++++
4 files changed, 150 insertions(+), 28 deletions(-)
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index b66939f..06d6aa8 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -3,7 +3,9 @@
* Performance: `filterAsync` β replaced `asyncSeq`-builder implem
... (truncated)