Skip to content

[Repo Assist] perf: optimize TaskSeq.replicate with direct object-expression implementation#334

Draft
github-actions[bot] wants to merge 2 commits intomainfrom
repo-assist/perf-replicate-direct-impl-69cdb72428206a1a
Draft

[Repo Assist] perf: optimize TaskSeq.replicate with direct object-expression implementation#334
github-actions[bot] wants to merge 2 commits intomainfrom
repo-assist/perf-replicate-direct-impl-69cdb72428206a1a

Conversation

@github-actions
Copy link
Contributor

🤖 This is a draft PR from Repo Assist, an automated AI assistant.

Replaces the taskSeq { for _ in 1..count do yield value } implementation of TaskSeq.replicate with a direct IAsyncEnumerable/IAsyncEnumerator object expression — matching the pattern already used by TaskSeq.empty and TaskSeq.singleton.

Root cause / motivation

The previous implementation had three layers of unnecessary overhead for what is a trivially synchronous, allocation-simple operation:

  1. State machine: the taskSeq CE generates a resumable state machine with heap allocation and object tracking.
  2. Range IEnumerable: 1..count creates a boxed IEnumerable(int) on the heap.
  3. Range IEnumerator: iterating the range allocates an IEnumerator(int).

None of this is needed — replicate never awaits anything and produces a fixed value on every element.

The fix

// Before
let replicate count value =
    raiseCannotBeNegative (nameof count) count
    taskSeq {
        for _ in 1..count do
            yield value
    }

// After
let replicate count value =
    raiseCannotBeNegative (nameof count) count
    { new IAsyncEnumerable<'T> with
        member _.GetAsyncEnumerator _ =
            let mutable i = 0
            { new IAsyncEnumerator<'T> with
                member _.MoveNextAsync() =
                    i <- i + 1
                    ValueTask(bool)(i <= count)
                member _.Current = value
                member _.DisposeAsync() = ValueTask.CompletedTask
            }
    }

Key properties of the new implementation:

  • MoveNextAsync() always returns a synchronously-completed ValueTask(bool) — the optimal fast path for ValueTask consumers.
  • DisposeAsync() returns ValueTask.CompletedTask with zero overhead.
  • GetAsyncEnumerator creates a fresh enumerator each time, so the sequence can be consumed multiple times (existing test can be consumed multiple times covers this).
  • Value semantics are preserved: the value parameter is captured by the closure at call time (existing test captures the value, not a reference covers this).

Test Status

Build: succeeded (0 warnings, 0 errors)
Fantomas format check: all F# files pass
Replicate-specific tests: 8/8 pass
Full test suite: 4,634 passed, 0 failed (2 skipped — infrastructure-only)

🤖 Generated by Repo Assist · Task 8: Performance Improvements

Generated by Repo Assist ·

To install this agentic workflow, run

gh aw add githubnext/agentics/workflows/repo-assist.md@346204513ecfa08b81566450d7d599556807389f

…entation

Replaces the taskSeq CE (which uses a resumable state machine) and a 1..count
range IEnumerable with a minimal direct IAsyncEnumerable/IAsyncEnumerator
object expression, matching the pattern used by empty and singleton.

Benefits:
- No state machine allocation
- No range IEnumerable or IEnumerator allocation
- MoveNextAsync always completes synchronously (ValueTask<bool> hot path)
- DisposeAsync is a no-op returning ValueTask.CompletedTask

All 8 existing replicate tests pass, 4634 total tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants