You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
FSharp.Control.AsyncSeq is a core F# async sequences library exposing AsyncSeq<'T>, computation expressions, and a rich API of combinators (mapAsync, chooseAsync, append, collect, bufferByTime, etc.). Performance directly affects every F# async pipeline built on top of it.
Ad-hoc F# script: tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqPerf.fsx — manual timing for N=1,000,000 scenarios including unfoldIter, replicate, collect, bufferByTime
CI
PR builds run dotnet test -c Release (no dedicated benchmark CI job)
No automated performance regression detection in CI
Identified Performance Issues and Bottlenecks
1. Allocation-heavy async state machines
Every MoveNext() on an AsyncSeq goes through F#'s Async<'T> which allocates continuations. The inline comments in AsyncSeqPerf.fsx show historical GC pressure: GC gen0: 1114 allocations for N=1M unfoldChooseIter. Each combinator in a pipeline compounds allocation.
2. append / collect chains — O(n²) risk
The AsyncGenerator.GenerateCont.Bind logic attempts right-associativity to avoid O(n²) continuation chains. The benchmark covers chains of 10–100 appends. There is still concern about deeply nested asyncSeq { yield! ... } patterns building up GenerateCont wrappers.
The RecursiveAsyncSeq benchmark directly tests whether recursive yield! degrades super-linearly. Historical data in the fsx script shows 69 seconds for N=10,000 recursive binds before the AsyncGenerator fix, vs 97ms after.
4. mapAsyncUnorderedParallel / iterAsyncParallel
Two loose scripts at repo root (mapAsyncUnorderedParallel_test.fsx, iterAsyncParallel_cancellation_test.fsx) suggest active development on parallel combinators. These use System.Threading.Channels for concurrency and have correctness and throughput implications.
5. No ValueTask/IAsyncEnumerable(T) bridging optimisation
The library predates C# IAsyncEnumerable(T). There is a IAsyncEnumerator<'T> interface but it returns Async<'T option> (boxed F# async), not ValueTask(bool). Bridging or native support could reduce allocations significantly for hot paths.
6. bufferByTime / time-based operations
Use of Async.Sleep and Task.WhenAny inside timing combinators can create thread-pool pressure in high-throughput scenarios.
Optimization Target Priorities
Priority
Area
Rationale
High
Reduce per-element allocation in core combinators (mapAsync, chooseAsync, iterAsync)
Largest user-facing impact; measurable with existing benchmarks
High
Validate/improve asyncSeq builder O(n²) safety at larger depths
Risk of pathological regressions; benchmark only goes to 200
Medium
mapAsyncUnorderedParallel throughput and back-pressure
Active development; correctness + performance intertwined
Medium
BenchmarkDotNet CI integration (nightly or PR-triggered)
Enables regression detection without manual effort
Low
ValueTask / IAsyncEnumerable(T) adapter for .NET 5+
Large scope but high long-term value
Low
Fable compilation size / tree-shaking
JS bundle size matters for Fable consumers
Performance Engineering Gaps
No automated perf regression baseline stored in CI artefacts
BenchmarkDotNet parameters are small (max 10k/200); real workloads hit millions
No memory-profile test for long-running sequences (leaks)
No documentation on how to run benchmarks locally
AsyncSeqPerf.fsx has many commented-out tests — not integrated into any harness
Proposed Plan
Extend BenchmarkDotNet suite to cover N=100k–1M and memory allocation per element
Audit asyncSeq builder at recursion depths 500–5000 to check for regressions beyond current 200
Parallel combinator performance — add throughput benchmark for mapAsyncUnorderedParallel
Add CI benchmark step (comment-on-PR with perf delta using dotnet-benchmark action or similar)
How to Control this Workflow
Add comments to this discussion to provide feedback or adjustments to the plan.
You can also control the workflow with these commands:
gh aw disable daily-perf-improver --repo fsprojects/FSharp.Control.AsyncSeq
gh aw enable daily-perf-improver --repo fsprojects/FSharp.Control.AsyncSeq
gh aw run daily-perf-improver --repo fsprojects/FSharp.Control.AsyncSeq --repeat (number-of-repeats)
gh aw logs daily-perf-improver --repo fsprojects/FSharp.Control.AsyncSeq
What Happens Next
The next time this workflow runs, Phase 2 will be performed: it will analyse the codebase to create a build-steps/action.yml configuration and performance engineering guides under .github/copilot/instructions/.
After Phase 2 completes (and its PR is merged), Phase 3 will begin on subsequent runs to implement actual performance improvements from the plan above.
If running in repeat mode, the workflow will automatically run again to proceed to the next phase.
Humans can review this research and add comments before the workflow continues — use the comment thread below.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
-
Performance Landscape — FSharp.Control.AsyncSeq
FSharp.Control.AsyncSeqis a core F# async sequences library exposingAsyncSeq<'T>, computation expressions, and a rich API of combinators (mapAsync,chooseAsync,append,collect,bufferByTime, etc.). Performance directly affects every F# async pipeline built on top of it.Current Performance Testing Infrastructure
Tools and Harnesses
tests/FSharp.Control.AsyncSeq.Benchmarks/AsyncSeqBenchmarks.fsAsyncSeqCoreBenchmarks—unfoldAsync,replicate,mapAsync,chooseAsyncat 1k/10k elementsAsyncSeqAppendBenchmarks— chainedappendat 10/50/100 chain depthAsyncSeqBuilderBenchmarks— recursiveasyncSeq { ... }builder at 50/100/200 recursion depthtests/FSharp.Control.AsyncSeq.Tests/AsyncSeqPerf.fsx— manual timing for N=1,000,000 scenarios includingunfoldIter,replicate,collect,bufferByTimeCI
dotnet test -c Release(no dedicated benchmark CI job)Identified Performance Issues and Bottlenecks
1. Allocation-heavy async state machines
Every
MoveNext()on anAsyncSeqgoes through F#'sAsync<'T>which allocates continuations. The inline comments inAsyncSeqPerf.fsxshow historical GC pressure:GC gen0: 1114allocations for N=1MunfoldChooseIter. Each combinator in a pipeline compounds allocation.2.
append/collectchains — O(n²) riskThe
AsyncGenerator.GenerateCont.Bindlogic attempts right-associativity to avoid O(n²) continuation chains. The benchmark covers chains of 10–100 appends. There is still concern about deeply nestedasyncSeq { yield! ... }patterns building upGenerateContwrappers.3.
asyncSeqcomputation builder recursive patternsThe
RecursiveAsyncSeqbenchmark directly tests whether recursiveyield!degrades super-linearly. Historical data in the fsx script shows 69 seconds for N=10,000 recursive binds before theAsyncGeneratorfix, vs 97ms after.4.
mapAsyncUnorderedParallel/iterAsyncParallelTwo loose scripts at repo root (
mapAsyncUnorderedParallel_test.fsx,iterAsyncParallel_cancellation_test.fsx) suggest active development on parallel combinators. These useSystem.Threading.Channelsfor concurrency and have correctness and throughput implications.5. No
ValueTask/IAsyncEnumerable(T)bridging optimisationThe library predates C#
IAsyncEnumerable(T). There is aIAsyncEnumerator<'T>interface but it returnsAsync<'T option>(boxed F# async), notValueTask(bool). Bridging or native support could reduce allocations significantly for hot paths.6.
bufferByTime/ time-based operationsUse of
Async.SleepandTask.WhenAnyinside timing combinators can create thread-pool pressure in high-throughput scenarios.Optimization Target Priorities
mapAsync,chooseAsync,iterAsync)asyncSeqbuilder O(n²) safety at larger depthsmapAsyncUnorderedParallelthroughput and back-pressureValueTask/IAsyncEnumerable(T)adapter for .NET 5+Performance Engineering Gaps
AsyncSeqPerf.fsxhas many commented-out tests — not integrated into any harnessProposed Plan
asyncSeqbuilder at recursion depths 500–5000 to check for regressions beyond current 200mapAsync/chooseAsyncallocations — consider struct-based optimisations orAsync.mapinline pathmapAsyncUnorderedParalleldotnet-benchmarkaction or similar)How to Control this Workflow
Add comments to this discussion to provide feedback or adjustments to the plan.
You can also control the workflow with these commands:
gh aw disable daily-perf-improver --repo fsprojects/FSharp.Control.AsyncSeq gh aw enable daily-perf-improver --repo fsprojects/FSharp.Control.AsyncSeq gh aw run daily-perf-improver --repo fsprojects/FSharp.Control.AsyncSeq --repeat (number-of-repeats) gh aw logs daily-perf-improver --repo fsprojects/FSharp.Control.AsyncSeqWhat Happens Next
build-steps/action.ymlconfiguration and performance engineering guides under.github/copilot/instructions/.Beta Was this translation helpful? Give feedback.
All reactions