Perf: avoid repeated orchestration-history scans for tracing - #788
Perf: avoid repeated orchestration-history scans for tracing#788Bernd Verst (berndverst) wants to merge 9 commits into
Conversation
OnRunOrchestratorAsync previously scanned orchestration history to correlate tracing events unconditionally, and for every qualifying new event it rescanned the full past-events list to find the originating TaskScheduled/SubOrchestrationInstanceCreated event - O(new events x past events) work, even when no tracing listener was registered. - Add TraceHelper.HasListeners, a cheap check backed by ActivitySource.HasListeners(), and gate all tracing-correlation work in OnRunOrchestratorAsync behind it so nothing is scanned when no listener is registered. - Replace the LINQ Concat-based scan for the ExecutionStarted event with a single-pass local function. - Add TraceHistoryEventLookup, which builds one dictionary index per work item (built lazily, once) for O(1) repeated lookups instead of rescanning PastEvents per new event, while preserving the exact original first-wins (SubOrchestrationInstanceCreated) and last-wins (TaskScheduled) semantics for duplicate event IDs. No public API changes; trace output, error handling, and replay determinism are unaffected. Adds regression tests for the no-listener fast path, first/last-wins correlation semantics end-to-end, and direct unit tests for TraceHistoryEventLookup. Fixes #769 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9c538f3e-308d-48a3-af7f-b3baf90e97b2
There was a problem hiding this comment.
Pull request overview
Improves the gRPC worker’s tracing correlation performance by avoiding unnecessary orchestration-history scans when tracing is disabled and by indexing past history events for O(1) correlation lookups when tracing is enabled. This aligns with the SDK’s goal of keeping worker-side orchestration processing efficient, especially for long-running instances with large histories.
Changes:
- Added a cheap listener check (
TraceHelper.HasListeners) and gated trace-correlation work inOnRunOrchestratorAsyncbehind it. - Replaced repeated past-history scans with a per-work-item indexed lookup (
TraceHistoryEventLookup) that preserves prior first/last-wins semantics for duplicate event IDs. - Added unit and worker-level tests covering listener/no-listener behavior and duplicate-ID correlation semantics.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| test/Worker/Grpc.Tests/TraceHistoryEventLookupTests.cs | Adds focused unit tests for the new indexed lookup, including duplicate-ID first/last-wins semantics and filtering by event type. |
| test/Worker/Grpc.Tests/GrpcDurableTaskWorkerTests.cs | Adds end-to-end worker tests for the no-listener fast path and for correlation semantics under a real ActivityListener. |
| src/Worker/Grpc/GrpcDurableTaskWorker.Processor.cs | Gates tracing correlation behind HasListeners, replaces LINQ concat scan with a single-pass helper, and uses the indexed lookup for repeated correlation. |
| src/Shared/Grpc/Tracing/TraceHistoryEventLookup.cs | Introduces a lazily-built, cached dictionary index for past events to avoid O(N×M) repeated scans. |
| src/Shared/Grpc/Tracing/TraceHelper.cs | Exposes HasListeners backed by ActivitySource.HasListeners() to allow callers to skip trace-correlation work when tracing is disabled. |
…history-scan-perf
Quantitative performance impactI compared live head Let
Representative, best, and adverse cases“Representative” below means a selected ordinary shape, not production telemetry.
The no-listener ~6 ns figures are close to benchmark-harness cost and should be read as “the scan was removed,” not as a credible millions-of-operations throughput claim. Memory and GCThe lazy indexes reference existing events; they do not copy protobuf messages. On this x64 runtime, estimated incremental dictionary storage while live is approximately 28-56 B per unique indexed ID, including capacity slack/array headers but excluding the existing event objects.
Thus:
Whole-worker throughput interpretationCorrelation-stage speedup does not translate directly to orchestration throughput. Using Amdahl's law:
Those are sensitivity examples. Actual gains require telemetry for listener prevalence and the distributions of |
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 01dc5b7c-c742-45b7-ae8c-5cda9bcfbd2c
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 01dc5b7c-c742-45b7-ae8c-5cda9bcfbd2c
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.
Suppressed comments (3)
test/Worker/Grpc.Tests/TraceHistoryEventLookupTests.cs:47
- This test currently asserts that duplicate SubOrchestrationInstanceCreated event IDs throw, but the prior linear scan behavior (and the PR description) expects first-wins semantics for duplicate IDs to preserve correlation behavior.
act.Should().Throw<InvalidOperationException>()
.WithMessage("*'SubOrchestrationInstanceCreated'*event ID '2'*");
}
test/Worker/Grpc.Tests/TraceHistoryEventLookupTests.cs:28
- These tests currently assert that duplicate event IDs throw. Per the original implementation (TaskScheduled = last-wins, SubOrchestrationInstanceCreated = first-wins) and the PR description, duplicates should be tolerated with defined first/last-wins behavior rather than causing failures during tracing correlation.
This issue also appears on line 45 of the same file.
act.Should().Throw<InvalidOperationException>()
.WithMessage("*'TaskScheduled'*event ID '1'*");
}
test/Worker/Grpc.Tests/GrpcDurableTaskWorkerTests.cs:377
- These two tests rely on TraceHistoryEventLookup throwing on duplicate event IDs (and assert the work item is abandoned when a listener is registered). That behavior conflicts with the stated goal of preserving the original duplicate-ID correlation semantics (TaskScheduled last-wins, SubOrchestrationInstanceCreated first-wins). If duplicate IDs are expected to be tolerated, these tests should be reworked to validate the no-listener fast path without depending on duplicate-ID exceptions, and the listener-enabled path should validate the preserved first/last-wins correlation rather than abandonment.
// Arrange: duplicate event IDs cause TraceHistoryEventLookup to throw when it builds an index. If this
// invalid history still completes, the no-listener fast path did not invoke either lookup method.
TraceHelper.HasListeners.Should().BeFalse();
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 01dc5b7c-c742-45b7-ae8c-5cda9bcfbd2c
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.
Suppressed comments (1)
test/Grpc.IntegrationTests/TracingIntegrationTests.cs:97
- This new test is named/positioned as validating span correlation, but the assertions only check that client Activities with specific OperationName values exist. That doesn’t actually validate the correlation behavior the PR is changing (i.e., that the server activity/sub-orchestration spans are parented to the correct client scheduled spans, and that the two scheduled operations remain distinct). Strengthen the assertions to check ParentSpanId relationships (similar to the other tests in this file) so the test fails if the history lookup mis-correlates events.
// Assert
metadata.RuntimeStatus.Should().Be(OrchestrationRuntimeStatus.Completed);
activities.Should().ContainSingle(
activity => activity.Kind == ActivityKind.Client
&& activity.Source.Name == CoreActivitySourceName
…history-scan-perf
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 01dc5b7c-c742-45b7-ae8c-5cda9bcfbd2c
Updated quantitative impact after demand-shaped indexingThis supersedes the listener-on index measurements in my earlier comment. Head I measured the old repeated-scan algorithm, the prior full-history index, and the current demand-shaped index in a focused Release/.NET 10 harness on Windows x64 (AMD EPYC 7763), using real protobuf
Compared with the prior full index, the current large task case reduces index latency another 45.5% and allocation 77.3%; the large mixed case reduces latency 56.7% and allocation 77.7%. Storage is now The no-listener path is unchanged from the earlier measurement: it performs one The constructed single-early-sub case remains the adverse boundary. The old Reciprocal latency is used only as a correlation-stage throughput proxy. Whole-worker gains depend on the fraction of turn CPU previously spent in tracing correlation, listener prevalence, history sizes, and completion-batch distributions. |
…history-scan-perf
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 01dc5b7c-c742-45b7-ae8c-5cda9bcfbd2c
Summary
OnRunOrchestratorAsyncinsrc/Worker/Grpc/GrpcDurableTaskWorker.Processor.csscanned orchestration history to correlate tracing events unconditionally, and rescanned the full past-events list for every qualifying new event to find the originatingTaskScheduled/SubOrchestrationInstanceCreatedevent — O(new events × past events), even when no tracing listener was registered.This is a precise, internal-only performance fix:
TraceHelper.HasListeners, backed byActivitySource.HasListeners(), and gated tracing-correlation work behind it so history is not scanned when no listener is registered.Concatscan forExecutionStartedwith a single-pass local function.TraceHistoryEventLookup, which pre-registers only correlation IDs referenced by the current work item's new events, then resolves both scheduling-event types in one lazy history scan. Repeated lookups remain O(1), total correlation work is O(past events + new events), and temporary index storage is O(unique correlation IDs in the current work item), not O(all scheduling events in history).TaskScheduledorSubOrchestrationInstanceCreatedIDs now throw a diagnosticInvalidOperationExceptioninstead of silently selecting an ambiguous event. With tracing enabled, the invalid work item is abandoned through the existing error path; valid histories are unaffected.No public API or replay-output changes. For valid orchestration histories, trace output and orchestration behavior are unchanged.
Tests
TraceHistoryEventLookupTests: duplicate-ID invariant failures for both correlation types, no-match behavior, event-type filtering, empty input, completed and failed correlation events, lazy construction, one-pass mixed indexing, requested-ID-only storage, and per-ID/type duplicate isolation.GrpcDurableTaskWorkerTests:TracingIntegrationTests.HistoryEventLookupCorrelatesDistinctScheduledOperations: runs through the real in-process sidecar with two distinctly named activities and a sub-orchestration, then verifies exact client-span correlation for both indexes.Worker.Grpc.Tests: 165/165 passed.Grpc.IntegrationTests: 169/169 passed.Worker.Grpcbuilds acrossnetstandard2.0,net6.0,net8.0, andnet10.0with no errors.Fixes #769