From 696807f1eb578cf6b804e969f43e19b650c0daf1 Mon Sep 17 00:00:00 2001 From: Relay Date: Tue, 1 Sep 2026 00:26:38 -0400 Subject: [PATCH] perf: avoid repeated orchestration history scans for tracing --- src/Shared/Grpc/Tracing/TraceHelper.cs | 6 + .../Grpc/GrpcDurableTaskWorker.Processor.cs | 166 ++++++++---------- src/Worker/Grpc/TracingHistoryEventIndex.cs | 48 +++++ test/Worker/Grpc.Tests/TraceHelperTests.cs | 33 ++++ .../TracingHistoryEventIndexTests.cs | 62 +++++++ 5 files changed, 224 insertions(+), 91 deletions(-) create mode 100644 src/Worker/Grpc/TracingHistoryEventIndex.cs create mode 100644 test/Worker/Grpc.Tests/TraceHelperTests.cs create mode 100644 test/Worker/Grpc.Tests/TracingHistoryEventIndexTests.cs diff --git a/src/Shared/Grpc/Tracing/TraceHelper.cs b/src/Shared/Grpc/Tracing/TraceHelper.cs index 1283ff12..42ea0564 100644 --- a/src/Shared/Grpc/Tracing/TraceHelper.cs +++ b/src/Shared/Grpc/Tracing/TraceHelper.cs @@ -20,6 +20,12 @@ static class TraceHelper static readonly ActivitySource ActivityTraceSource = new ActivitySource(Source); + /// + /// Gets whether any listener is subscribed to Durable Task tracing activities. + /// + /// when tracing work can produce activities; otherwise, . + public static bool HasListeners() => ActivityTraceSource.HasListeners(); + /// /// Starts a new trace activity for scheduling an orchestration from the client. /// diff --git a/src/Worker/Grpc/GrpcDurableTaskWorker.Processor.cs b/src/Worker/Grpc/GrpcDurableTaskWorker.Processor.cs index 5dd18d52..0fad52f6 100644 --- a/src/Worker/Grpc/GrpcDurableTaskWorker.Processor.cs +++ b/src/Worker/Grpc/GrpcDurableTaskWorker.Processor.cs @@ -602,105 +602,89 @@ async Task OnRunOrchestratorAsync( string completionToken, CancellationToken cancellationToken) { - var executionStartedEvent = - request - .NewEvents - .Concat(request.PastEvents) - .Where(e => e.EventTypeCase == P.HistoryEvent.EventTypeOneofCase.ExecutionStarted) - .Select(e => e.ExecutionStarted) - .FirstOrDefault(); - - Activity? traceActivity = TraceHelper.StartTraceActivityForOrchestrationExecution( - executionStartedEvent, - request.OrchestrationTraceContext); - - if (executionStartedEvent is not null) + Activity? traceActivity = null; + if (TraceHelper.HasListeners()) { - P.HistoryEvent? GetSuborchestrationInstanceCreatedEvent(int eventId) + var executionStartedEvent = + request + .NewEvents + .Concat(request.PastEvents) + .Where(e => e.EventTypeCase == P.HistoryEvent.EventTypeOneofCase.ExecutionStarted) + .Select(e => e.ExecutionStarted) + .FirstOrDefault(); + + traceActivity = TraceHelper.StartTraceActivityForOrchestrationExecution( + executionStartedEvent, + request.OrchestrationTraceContext); + + if (executionStartedEvent is not null) { - var subOrchestrationEvent = - request - .PastEvents - .Where(x => x.EventTypeCase == P.HistoryEvent.EventTypeOneofCase.SubOrchestrationInstanceCreated) - .FirstOrDefault(x => x.EventId == eventId); + TracingHistoryEventIndex historyEventIndex = new(request.PastEvents); - return subOrchestrationEvent; - } - - P.HistoryEvent? GetTaskScheduledEvent(int eventId) - { - var taskScheduledEvent = - request - .PastEvents - .Where(x => x.EventTypeCase == P.HistoryEvent.EventTypeOneofCase.TaskScheduled) - .LastOrDefault(x => x.EventId == eventId); - - return taskScheduledEvent; - } - - foreach (var newEvent in request.NewEvents) - { - switch (newEvent.EventTypeCase) + foreach (var newEvent in request.NewEvents) { - case P.HistoryEvent.EventTypeOneofCase.SubOrchestrationInstanceCompleted: - { - P.HistoryEvent? subOrchestrationInstanceCreatedEvent = - GetSuborchestrationInstanceCreatedEvent( - newEvent.SubOrchestrationInstanceCompleted.TaskScheduledId); - - TraceHelper.EmitTraceActivityForSubOrchestrationCompleted( - request.InstanceId, - subOrchestrationInstanceCreatedEvent, - subOrchestrationInstanceCreatedEvent?.SubOrchestrationInstanceCreated); - break; - } - - case P.HistoryEvent.EventTypeOneofCase.SubOrchestrationInstanceFailed: - { - P.HistoryEvent? subOrchestrationInstanceCreatedEvent = - GetSuborchestrationInstanceCreatedEvent( - newEvent.SubOrchestrationInstanceFailed.TaskScheduledId); - - TraceHelper.EmitTraceActivityForSubOrchestrationFailed( - request.InstanceId, - subOrchestrationInstanceCreatedEvent, - subOrchestrationInstanceCreatedEvent?.SubOrchestrationInstanceCreated, - newEvent.SubOrchestrationInstanceFailed); - break; - } - - case P.HistoryEvent.EventTypeOneofCase.TaskCompleted: - { - P.HistoryEvent? taskScheduledEvent = - GetTaskScheduledEvent(newEvent.TaskCompleted.TaskScheduledId); - - TraceHelper.EmitTraceActivityForTaskCompleted( - request.InstanceId, - taskScheduledEvent, - taskScheduledEvent?.TaskScheduled); - break; - } + switch (newEvent.EventTypeCase) + { + case P.HistoryEvent.EventTypeOneofCase.SubOrchestrationInstanceCompleted: + { + P.HistoryEvent? subOrchestrationInstanceCreatedEvent = + historyEventIndex.GetSubOrchestrationInstanceCreatedEvent( + newEvent.SubOrchestrationInstanceCompleted.TaskScheduledId); + + TraceHelper.EmitTraceActivityForSubOrchestrationCompleted( + request.InstanceId, + subOrchestrationInstanceCreatedEvent, + subOrchestrationInstanceCreatedEvent?.SubOrchestrationInstanceCreated); + break; + } + + case P.HistoryEvent.EventTypeOneofCase.SubOrchestrationInstanceFailed: + { + P.HistoryEvent? subOrchestrationInstanceCreatedEvent = + historyEventIndex.GetSubOrchestrationInstanceCreatedEvent( + newEvent.SubOrchestrationInstanceFailed.TaskScheduledId); + + TraceHelper.EmitTraceActivityForSubOrchestrationFailed( + request.InstanceId, + subOrchestrationInstanceCreatedEvent, + subOrchestrationInstanceCreatedEvent?.SubOrchestrationInstanceCreated, + newEvent.SubOrchestrationInstanceFailed); + break; + } + + case P.HistoryEvent.EventTypeOneofCase.TaskCompleted: + { + P.HistoryEvent? taskScheduledEvent = + historyEventIndex.GetTaskScheduledEvent(newEvent.TaskCompleted.TaskScheduledId); - case P.HistoryEvent.EventTypeOneofCase.TaskFailed: - { - P.HistoryEvent? taskScheduledEvent = - GetTaskScheduledEvent(newEvent.TaskFailed.TaskScheduledId); + TraceHelper.EmitTraceActivityForTaskCompleted( + request.InstanceId, + taskScheduledEvent, + taskScheduledEvent?.TaskScheduled); + break; + } - TraceHelper.EmitTraceActivityForTaskFailed( + case P.HistoryEvent.EventTypeOneofCase.TaskFailed: + { + P.HistoryEvent? taskScheduledEvent = + historyEventIndex.GetTaskScheduledEvent(newEvent.TaskFailed.TaskScheduledId); + + TraceHelper.EmitTraceActivityForTaskFailed( + request.InstanceId, + taskScheduledEvent, + taskScheduledEvent?.TaskScheduled, + newEvent.TaskFailed); + break; + } + + case P.HistoryEvent.EventTypeOneofCase.TimerFired: + TraceHelper.EmitTraceActivityForTimer( request.InstanceId, - taskScheduledEvent, - taskScheduledEvent?.TaskScheduled, - newEvent.TaskFailed); + executionStartedEvent.Name, + newEvent.Timestamp.ToDateTime(), + newEvent.TimerFired); break; - } - - case P.HistoryEvent.EventTypeOneofCase.TimerFired: - TraceHelper.EmitTraceActivityForTimer( - request.InstanceId, - executionStartedEvent.Name, - newEvent.Timestamp.ToDateTime(), - newEvent.TimerFired); - break; + } } } } diff --git a/src/Worker/Grpc/TracingHistoryEventIndex.cs b/src/Worker/Grpc/TracingHistoryEventIndex.cs new file mode 100644 index 00000000..5c1e6209 --- /dev/null +++ b/src/Worker/Grpc/TracingHistoryEventIndex.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using P = Microsoft.DurableTask.Protobuf; + +namespace Microsoft.DurableTask.Worker.Grpc; + +/// +/// Indexes the orchestration history events used to reconstruct tracing spans. +/// +sealed class TracingHistoryEventIndex +{ + readonly Dictionary subOrchestrationCreatedEvents = new(); + readonly Dictionary taskScheduledEvents = new(); + + public TracingHistoryEventIndex(IEnumerable pastEvents) + { + foreach (P.HistoryEvent historyEvent in pastEvents) + { + switch (historyEvent.EventTypeCase) + { + case P.HistoryEvent.EventTypeOneofCase.SubOrchestrationInstanceCreated: + // Preserve the previous FirstOrDefault semantics for duplicate IDs. + if (!this.subOrchestrationCreatedEvents.ContainsKey(historyEvent.EventId)) + { + this.subOrchestrationCreatedEvents.Add(historyEvent.EventId, historyEvent); + } + + break; + + case P.HistoryEvent.EventTypeOneofCase.TaskScheduled: + // Preserve the previous LastOrDefault semantics for duplicate IDs. + this.taskScheduledEvents[historyEvent.EventId] = historyEvent; + break; + } + } + } + + public P.HistoryEvent? GetSubOrchestrationInstanceCreatedEvent(int eventId) + => this.subOrchestrationCreatedEvents.TryGetValue(eventId, out P.HistoryEvent? historyEvent) + ? historyEvent + : null; + + public P.HistoryEvent? GetTaskScheduledEvent(int eventId) + => this.taskScheduledEvents.TryGetValue(eventId, out P.HistoryEvent? historyEvent) + ? historyEvent + : null; +} diff --git a/test/Worker/Grpc.Tests/TraceHelperTests.cs b/test/Worker/Grpc.Tests/TraceHelperTests.cs new file mode 100644 index 00000000..2d42122a --- /dev/null +++ b/test/Worker/Grpc.Tests/TraceHelperTests.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Diagnostics; +using Microsoft.DurableTask.Tracing; + +namespace Microsoft.DurableTask.Worker.Grpc.Tests; + +public class TraceHelperTests +{ + [Fact] + public void HasListeners_TracksMatchingActivityListener() + { + bool initialHasListeners = TraceHelper.HasListeners(); + ActivityListener listener = new() + { + ShouldListenTo = source => source.Name == "Microsoft.DurableTask", + }; + + try + { + ActivitySource.AddActivityListener(listener); + + TraceHelper.HasListeners().Should().BeTrue(); + } + finally + { + listener.Dispose(); + } + + TraceHelper.HasListeners().Should().Be(initialHasListeners); + } +} diff --git a/test/Worker/Grpc.Tests/TracingHistoryEventIndexTests.cs b/test/Worker/Grpc.Tests/TracingHistoryEventIndexTests.cs new file mode 100644 index 00000000..9bbd1ad2 --- /dev/null +++ b/test/Worker/Grpc.Tests/TracingHistoryEventIndexTests.cs @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using P = Microsoft.DurableTask.Protobuf; + +namespace Microsoft.DurableTask.Worker.Grpc.Tests; + +public class TracingHistoryEventIndexTests +{ + [Fact] + public void GetSubOrchestrationInstanceCreatedEvent_DuplicateIds_ReturnsFirstEvent() + { + P.HistoryEvent first = new() + { + EventId = 7, + SubOrchestrationInstanceCreated = new P.SubOrchestrationInstanceCreatedEvent { Name = "first" }, + }; + P.HistoryEvent second = new() + { + EventId = 7, + SubOrchestrationInstanceCreated = new P.SubOrchestrationInstanceCreatedEvent { Name = "second" }, + }; + + TracingHistoryEventIndex index = new([first, second]); + + index.GetSubOrchestrationInstanceCreatedEvent(7).Should().BeSameAs(first); + } + + [Fact] + public void GetTaskScheduledEvent_DuplicateIds_ReturnsLastEvent() + { + P.HistoryEvent first = new() + { + EventId = 11, + TaskScheduled = new P.TaskScheduledEvent { Name = "first" }, + }; + P.HistoryEvent second = new() + { + EventId = 11, + TaskScheduled = new P.TaskScheduledEvent { Name = "second" }, + }; + + TracingHistoryEventIndex index = new([first, second]); + + index.GetTaskScheduledEvent(11).Should().BeSameAs(second); + } + + [Fact] + public void Lookups_MissingIds_ReturnNull() + { + P.HistoryEvent unrelated = new() + { + EventId = 3, + TimerCreated = new P.TimerCreatedEvent(), + }; + + TracingHistoryEventIndex index = new([unrelated]); + + index.GetSubOrchestrationInstanceCreatedEvent(3).Should().BeNull(); + index.GetTaskScheduledEvent(3).Should().BeNull(); + } +}