Skip to content

Commit 9984a8b

Browse files
authored
Merge pull request #382 from fsprojects/repo-assist/perf-while-bang-cleanup-20260409-5ddbcb4cb509276b
[Repo Assist] perf: use while! in iter/fold/reduce/mapFold/tryLast/Drop/Truncate
2 parents 53ee24c + 179386e commit 9984a8b

File tree

4 files changed

+66
-88
lines changed

4 files changed

+66
-88
lines changed

AGENTS.md

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ FSharp.Control.TaskSeq is an F# library providing a `taskSeq` computation expres
1010
- `src/FSharp.Control.TaskSeq.Test/` — xUnit test project (net10.0)
1111
- `src/FSharp.Control.TaskSeq.SmokeTests/` — Smoke/integration tests
1212
- `src/FSharp.Control.TaskSeq.sln` — Solution file
13-
- `Version.props`Single source of truth for the package version
13+
- `Version.props`Package version (derived automatically from `release-notes.txt`)
1414
- `build.cmd` — Windows build/test script used by CI
1515

1616
## Build
@@ -102,12 +102,28 @@ All workflows are in `.github/workflows/`:
102102

103103
## Release Notes
104104

105-
**Required**: Every PR that adds features, fixes bugs, or makes user-visible changes **must** include an update to `release-notes.txt`. Add a bullet under the appropriate version heading (currently `0.5.0`). The format is:
105+
`release-notes.txt` is the **single source of truth** for the package version. `Version.props` extracts the version automatically by finding the first line that matches a `X.Y.Z` semver pattern. The `Unreleased` section at the top of the file is skipped because it does not match this pattern.
106+
107+
**Format requirements:**
108+
109+
- The file **must** always begin with a heading line `Unreleased` (after the optional `Release notes:` header). This section holds in-progress changes before they are assigned a version number. It must always be present, even if empty.
110+
- Below `Unreleased`, versioned sections are listed in descending order (`1.0.0`, `0.7.0`, …). The topmost versioned section determines the package version.
111+
- To bump the version, add a new version heading between `Unreleased` and the previous version.
112+
113+
Example:
106114

107115
```
108-
0.5.0
116+
Release notes:
117+
118+
Unreleased
119+
- upcoming change description
120+
121+
1.1.0
109122
- adds TaskSeq.myFunction and TaskSeq.myFunctionAsync, #<issue>
110123
- fixes <description>, #<issue>
124+
125+
1.0.0
126+
- adds TaskSeq.withCancellation, #167
111127
```
112128

113-
If you are bumping to a new version, also update `Version.props`. PRs that touch library source (`src/FSharp.Control.TaskSeq/`) without updating `release-notes.txt` are incomplete.
129+
**Required**: Every PR that adds features, fixes bugs, or makes user-visible changes **must** add a bullet under the `Unreleased` heading in `release-notes.txt`. PRs that touch library source (`src/FSharp.Control.TaskSeq/`) without updating `release-notes.txt` are incomplete.

Version.props

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<Project>
22
<PropertyGroup>
3-
<!-- updating this version will trigger a publish after merge to 'main' -->
4-
<Version>1.0.0</Version>
3+
<!-- Version is extracted from the first versioned section heading in release-notes.txt -->
4+
<Version>$([System.Text.RegularExpressions.Regex]::Match($([System.IO.File]::ReadAllText(`$(MSBuildThisFileDirectory)release-notes.txt`)), `(?m)^(\d+\.\d+\.\d+)`).Groups.get_Item(1).Value)</Version>
55
</PropertyGroup>
6-
</Project>
6+
</Project>

release-notes.txt

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,24 @@
11

22
Release notes:
33

4+
Unreleased
5+
6+
1.1.1
7+
- perf: use while! in groupBy, countBy, partition, except, exceptOfSeq to eliminate redundant mutable 'go' variables and initial MoveNextAsync calls
8+
49
1.1.0
510
- adds TaskSeq.chooseV, TaskSeq.chooseVAsync, #385
611

712
1.0.0
8-
- perf: use while! in groupBy, countBy, partition, except, exceptOfSeq to eliminate redundant mutable 'go' variables and initial MoveNextAsync calls
913
- adds taskSeqDynamic computation expression and TaskSeqDynamic/TaskSeqDynamicInfo types for dynamic (FSI-compatible) resumable code, fixing issue where taskSeq would raise NotImplementedException in F# Interactive, #246
14+
- perf: simplify iter, fold, reduce, mapFold, tryLast, skipOrTake (Drop/Truncate) to use while! and remove manual go-flag and initial MoveNextAsync pre-advance, matching the pattern already used by sum/sumBy/average
1015
- perf: toResizeArrayAsync (and therefore toArrayAsync, toListAsync, toResizeArrayAsync, toIListAsync) uses a direct loop instead of going through iter, avoiding a lambda and DU allocation per call
1116
- perf: tryItem uses a simpler loop that skips the redundant inner index check on every iteration
1217
- perf: TaskSeq.chunkBy and chunkByAsync reuse the ResizeArray buffer between chunks, reducing allocations on sequences with many chunk boundaries
1318
- fixes: TaskSeq.insertAt, insertManyAt, removeAt, removeManyAt, updateAt now raise ArgumentNullException (not NullReferenceException) when given a null source; insertManyAt also validates the values argument
1419
- refactor: simplify lengthBy and lengthBeforeMax to use while! and remove the redundant mutable 'go' and initial MoveNextAsync
1520
- refactor: simplify tryTail inner loop to use while!, removing redundant mutable 'go' flag and initial MoveNextAsync
1621
- adds TaskSeq.distinctUntilChangedWith and TaskSeq.distinctUntilChangedWithAsync, #345
17-
- adds TaskSeq.withCancellation, #167
1822
- adds TaskSeq.replicateInfinite, replicateInfiniteAsync, replicateUntilNoneAsync, #345
1923
- adds TaskSeq.firstOrDefault, lastOrDefault, #345
2024
- adds TaskSeq.splitAt, #345
@@ -25,6 +29,9 @@ Release notes:
2529
- docs: adds missing XML <returns> documentation tags to singleton, isEmpty, length, lengthOrMax, lengthBy, and lengthByAsync
2630
- test: adds 70 new tests to TaskSeq.Fold.Tests.fs covering call-count assertions, folder-not-called-on-empty, ordering, null initial state, and fold/foldAsync equivalence
2731

32+
1.0.0
33+
- adds TaskSeq.withCancellation, #167
34+
2835
0.7.0
2936
- performance: TaskSeq.exists, existsAsync, contains no longer allocate an intermediate Option value
3037
- test: adds 67 tests for TaskSeq.lengthOrMax (previously untested)

src/FSharp.Control.TaskSeq/TaskSeqInternal.fs

Lines changed: 34 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -362,68 +362,49 @@ module internal TaskSeqInternal =
362362

363363
task {
364364
use e = source.GetAsyncEnumerator CancellationToken.None
365-
let mutable go = true
366-
let! step = e.MoveNextAsync()
367-
go <- step
368365

369-
// this ensures that the inner loop is optimized for the closure
370-
// though perhaps we need to split into individual functions after all to use
371-
// InlineIfLambda?
366+
// Each branch keeps its own while! loop so the match dispatch is hoisted out and
367+
// the JIT sees a tight, single-case loop (same pattern as sum/sumBy etc.).
372368
match action with
373369
| CountableAction action ->
374370
let mutable i = 0
375371

376-
while go do
377-
do action i e.Current
378-
let! step = e.MoveNextAsync()
372+
while! e.MoveNextAsync() do
373+
action i e.Current
379374
i <- i + 1
380-
go <- step
381375

382376
| SimpleAction action ->
383-
while go do
384-
do action e.Current
385-
let! step = e.MoveNextAsync()
386-
go <- step
377+
while! e.MoveNextAsync() do
378+
action e.Current
387379

388380
| AsyncCountableAction action ->
389381
let mutable i = 0
390382

391-
while go do
383+
while! e.MoveNextAsync() do
392384
do! action i e.Current
393-
let! step = e.MoveNextAsync()
394385
i <- i + 1
395-
go <- step
396386

397387
| AsyncSimpleAction action ->
398-
while go do
388+
while! e.MoveNextAsync() do
399389
do! action e.Current
400-
let! step = e.MoveNextAsync()
401-
go <- step
402390
}
403391

404392
let fold folder initial (source: TaskSeq<_>) =
405393
checkNonNull (nameof source) source
406394

407395
task {
408396
use e = source.GetAsyncEnumerator CancellationToken.None
409-
let mutable go = true
410397
let mutable result = initial
411-
let! step = e.MoveNextAsync()
412-
go <- step
413398

414399
match folder with
415400
| FolderAction folder ->
416-
while go do
401+
while! e.MoveNextAsync() do
417402
result <- folder result e.Current
418-
let! step = e.MoveNextAsync()
419-
go <- step
420403

421404
| AsyncFolderAction folder ->
422-
while go do
405+
while! e.MoveNextAsync() do
423406
let! tempResult = folder result e.Current
424407
result <- tempResult
425-
let! step = e.MoveNextAsync()
426-
go <- step
427408

428409
return result
429410
}
@@ -462,22 +443,16 @@ module internal TaskSeqInternal =
462443
raiseEmptySeq ()
463444

464445
let mutable result = e.Current
465-
let! step = e.MoveNextAsync()
466-
let mutable go = step
467446

468447
match folder with
469448
| FolderAction folder ->
470-
while go do
449+
while! e.MoveNextAsync() do
471450
result <- folder result e.Current
472-
let! step = e.MoveNextAsync()
473-
go <- step
474451

475452
| AsyncFolderAction folder ->
476-
while go do
453+
while! e.MoveNextAsync() do
477454
let! tempResult = folder result e.Current
478455
result <- tempResult
479-
let! step = e.MoveNextAsync()
480-
go <- step
481456

482457
return result
483458
}
@@ -487,28 +462,21 @@ module internal TaskSeqInternal =
487462

488463
task {
489464
use e = source.GetAsyncEnumerator CancellationToken.None
490-
let mutable go = true
491465
let mutable state = initial
492466
let results = ResizeArray()
493-
let! step = e.MoveNextAsync()
494-
go <- step
495467

496468
match folder with
497469
| MapFolderAction folder ->
498-
while go do
470+
while! e.MoveNextAsync() do
499471
let result, newState = folder state e.Current
500472
results.Add result
501473
state <- newState
502-
let! step = e.MoveNextAsync()
503-
go <- step
504474

505475
| AsyncMapFolderAction folder ->
506-
while go do
476+
while! e.MoveNextAsync() do
507477
let! (result, newState) = folder state e.Current
508478
results.Add result
509479
state <- newState
510-
let! step = e.MoveNextAsync()
511-
go <- step
512480

513481
return results.ToArray(), state
514482
}
@@ -813,15 +781,10 @@ module internal TaskSeqInternal =
813781

814782
task {
815783
use e = source.GetAsyncEnumerator CancellationToken.None
816-
let mutable go = true
817784
let mutable last = ValueNone
818-
let! step = e.MoveNextAsync()
819-
go <- step
820785

821-
while go do
786+
while! e.MoveNextAsync() do
822787
last <- ValueSome e.Current
823-
let! step = e.MoveNextAsync()
824-
go <- step
825788

826789
match last with
827790
| ValueSome value -> return Some value
@@ -1255,24 +1218,19 @@ module internal TaskSeqInternal =
12551218
else
12561219
taskSeq {
12571220
use e = source.GetAsyncEnumerator CancellationToken.None
1221+
let mutable i = 0
1222+
let mutable cont = true
12581223

1259-
let! step = e.MoveNextAsync()
1260-
let mutable cont = step
1261-
let mutable pos = 0
1262-
1263-
// skip, or stop looping if we reached the end
1264-
while cont do
1265-
pos <- pos + 1
1266-
1267-
if pos < count then
1268-
let! moveNext = e.MoveNextAsync()
1269-
cont <- moveNext
1270-
else
1271-
cont <- false
1224+
// advance past 'count' elements; stop early if the source is shorter
1225+
while cont && i < count do
1226+
let! hasMore = e.MoveNextAsync()
1227+
if hasMore then i <- i + 1 else cont <- false
12721228

1273-
// return the rest
1274-
while! e.MoveNextAsync() do
1275-
yield e.Current
1229+
// return remaining elements; enumerator is at element (count-1) so one
1230+
// more MoveNext is needed to reach element (count)
1231+
if cont then
1232+
while! e.MoveNextAsync() do
1233+
yield e.Current
12761234

12771235
}
12781236
| Take ->
@@ -1299,19 +1257,16 @@ module internal TaskSeqInternal =
12991257
else
13001258
taskSeq {
13011259
use e = source.GetAsyncEnumerator CancellationToken.None
1260+
let mutable yielded = 0
1261+
let mutable cont = true
13021262

1303-
let! step = e.MoveNextAsync()
1304-
let mutable cont = step
1305-
let mutable pos = 0
1306-
1307-
// return items until we've exhausted the seq
1308-
while cont do
1309-
yield e.Current
1310-
pos <- pos + 1
1263+
// yield up to 'count' elements; stop when exhausted or limit reached
1264+
while cont && yielded < count do
1265+
let! hasMore = e.MoveNextAsync()
13111266

1312-
if pos < count then
1313-
let! moveNext = e.MoveNextAsync()
1314-
cont <- moveNext
1267+
if hasMore then
1268+
yield e.Current
1269+
yielded <- yielded + 1
13151270
else
13161271
cont <- false
13171272

0 commit comments

Comments
 (0)