Skip to content

Perf: materialize streamed orchestration history incrementally - #783

Open
berndverst wants to merge 5 commits into
mainfrom
berndverst-perf-stream-history-materialize
Open

Perf: materialize streamed orchestration history incrementally#783
berndverst wants to merge 5 commits into
mainfrom
berndverst-perf-stream-history-materialize

Conversation

@berndverst

@berndverst berndverst commented Jul 24, 2026

Copy link
Copy Markdown
Member

Summary

Replaces lazy Concat/Select accumulation in streamed orchestration-history reconstruction with direct List<HistoryEvent> materialization.

The former code built a deeply nested iterator chain. Enumerating that chain dispatches each event through the preceding Concat nodes, so many small chunks produce quadratic iterator work. The new code converts and appends each event exactly once as chunks arrive.

Change

  • Streaming history is appended directly to one List<HistoryEvent>.
  • The list intentionally relies on geometric growth instead of setting an exact per-chunk capacity, which would itself reallocate and copy for every small chunk.
  • Non-streamed past events use a count-sized list.
  • New events are added with a direct loop instead of a lazy projection.
  • Event conversion, ordering, past/new classification, cancellation, and stream disposal are unchanged.

Tests

Added three focused regressions:

  • BuildRuntimeStateAsync_MultiChunkHistoryStream_MaterializesEventsInOrderAndClassifiesNewVsPast
  • BuildRuntimeStateAsync_HistoryStreamCanceledMidStream_PropagatesCancellation
  • BuildRuntimeStateAsync_ManySingleEventChunks_DoesNotExhibitQuadraticListGrowth

The focused tests and full Worker.Grpc.Tests suite pass.

No public API, wire-format, or replay-behavior changes.

Fixes #772

Replace the lazy Concat/Select chain in BuildRuntimeStateAsync with
direct List<HistoryEvent> accumulation. Previously, each streamed
history chunk extended a lazily-evaluated IEnumerable via Concat,
which is re-walked and re-converted from scratch every time it is
enumerated or chained onto again, making history reconstruction
quadratic in the number of chunks for long-running orchestrations.

Now each chunk's events are converted and appended directly into a
single accumulating list (with a capacity hint sized per chunk), and
new events are added to the runtime state via a direct foreach instead
of a lazy Select. Event conversion, ordering, new-vs-past runtime
state classification, cancellation propagation, and the public API
surface are all unchanged.

Added regression tests covering multi-chunk streamed history
materialization (ordering + new/past classification) and cancellation
propagation mid-stream.

Fixes #772

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1fdfdae0-4987-4043-b522-7229e63810d2
Copilot AI review requested due to automatic review settings July 24, 2026 22:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves worker-side orchestration replay performance by replacing lazy IEnumerable accumulation (Concat/Select) in BuildRuntimeStateAsync with eager, incremental materialization into a single List<HistoryEvent>, preventing quadratic behavior when history is streamed in many chunks.

Changes:

  • Materialize streamed and non-streamed PastEvents into a single List<HistoryEvent> as events arrive.
  • Add NewEvents to OrchestrationRuntimeState via direct iteration and conversion (no lazy projections).
  • Add regression tests covering multi-chunk streaming order/classification and cancellation propagation.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
src/Worker/Grpc/GrpcDurableTaskWorker.Processor.cs Reworks history accumulation to eagerly append converted events instead of chaining lazy Concat/Select.
test/Worker/Grpc.Tests/GrpcDurableTaskWorkerTests.cs Adds targeted tests for multi-chunk history materialization correctness and cancellation behavior.

Comment thread src/Worker/Grpc/GrpcDurableTaskWorker.Processor.cs Outdated
Address review feedback on PR #783 (issue #772): setting
List<HistoryEvent>.Capacity to the exact running total on every
streamed chunk reallocates and copies the whole list whenever
capacity needs to grow, since the Capacity setter allocates exactly
the requested size rather than growing geometrically. With many
single-event chunks this made history materialization quadratic again
- the same class of bug the original fix addressed, reintroduced via
a different mechanism.

Remove the explicit Capacity assignment and rely on List<T>.Add's own
amortized (doubling) growth strategy, which gives O(n) total
allocation across n appends regardless of how many chunks they arrive
in.

Added a regression test with 5,000 single-event chunks that asserts on
bytes allocated (via GC.GetAllocatedBytesForCurrentThread) rather than
wall-clock time, to avoid timing-based flakiness while still reliably
distinguishing O(n) amortized growth from O(n^2) per-chunk
reallocation. Verified the test fails against the prior buggy code
(~100 MB allocated for n=5,000, chunk copied) and passes against the
fix (well under the 50 MB threshold).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1fdfdae0-4987-4043-b522-7229e63810d2
Copilot AI review requested due to automatic review settings July 24, 2026 23:18
Comment thread test/Worker/Grpc.Tests/GrpcDurableTaskWorkerTests.cs Fixed
Comment thread test/Worker/Grpc.Tests/GrpcDurableTaskWorkerTests.cs Fixed

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

src/Worker/Grpc/GrpcDurableTaskWorker.Processor.cs:287

  • In the streaming path, pastEvents starts at the default capacity and grows via repeated Add calls. For large streamed chunks, this can still trigger multiple intermediate reallocations/copies within a single chunk. Since chunk.Events.Count is available, consider calling List<T>.EnsureCapacity(pastEvents.Count + chunk.Events.Count) per chunk to reduce reallocations while still preserving the intended geometric growth behavior (and it would also align with the PR description’s mention of a per-chunk capacity hint).
                pastEvents = new List<HistoryEvent>();
                await foreach (P.HistoryChunk chunk in streamResponse.ResponseStream.ReadAllAsync(cancellation))
                {
                    foreach (P.HistoryEvent protoEvent in chunk.Events)
                    {

Bernd Verst and others added 2 commits July 27, 2026 11:31
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 01dc5b7c-c742-45b7-ae8c-5cda9bcfbd2c
Copilot AI review requested due to automatic review settings July 27, 2026 18:37

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

Comment thread src/Worker/Grpc/GrpcDurableTaskWorker.Processor.cs Outdated
Comment thread src/Worker/Grpc/GrpcDurableTaskWorker.Processor.cs
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 01dc5b7c-c742-45b7-ae8c-5cda9bcfbd2c
Copilot AI review requested due to automatic review settings July 27, 2026 23:31

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Performance: materialize streamed orchestration history incrementally

2 participants