From bf2469c7ab928a6ab66f9a37e702814304b08a13 Mon Sep 17 00:00:00 2001 From: wallstop Date: Sat, 15 Aug 2026 01:42:44 +0000 Subject: [PATCH] perf: add MessagePipe attribution benchmarks --- .../BenchmarkHarnessRobustnessTests.cs | 38 ++ Tests/Runtime/Benchmarks/BenchmarkProtocol.cs | 4 + .../DispatchThroughputBenchmarks.cs | 97 +++- ...strationLifecycleBenchmarkContractTests.cs | 226 +++++++- .../RegistrationLifecycleBenchmarks.cs | 481 ++++++++++++++++++ docs/runbooks/perf-benchmark-methodology.md | 45 +- scripts/__tests__/unity-perf.test.js | 36 +- scripts/unity/perf-scenarios.js | 28 +- 8 files changed, 887 insertions(+), 68 deletions(-) diff --git a/Tests/Editor/Allocations/BenchmarkHarnessRobustnessTests.cs b/Tests/Editor/Allocations/BenchmarkHarnessRobustnessTests.cs index 9ef1bb6a..a3320a33 100644 --- a/Tests/Editor/Allocations/BenchmarkHarnessRobustnessTests.cs +++ b/Tests/Editor/Allocations/BenchmarkHarnessRobustnessTests.cs @@ -427,6 +427,12 @@ private static IEnumerable DispatchBaselineSetupCases() 1, 1 ).SetName("DispatchBaselineSetup_TargetedNoMatchingTarget"); + yield return new TestCaseData( + DispatchBenchmarkScenario.UntargetedFloodOneDirectHandler, + 1, + 1, + 1 + ).SetName("DispatchBaselineSetup_UntargetedOneDirectHandler"); yield return new TestCaseData( DispatchBenchmarkScenario.UntargetedFloodTwoHandlersOnePriority, 2, @@ -486,6 +492,38 @@ int expectedRegistrationBuckets ); } + [Test] + public void DirectAndTokenOneHandlerScenariosKeepDistinctRegistrationOwnership() + { + DispatchThroughputBenchmarks.DispatchScenarioContractObservation token = + DispatchThroughputBenchmarks.ConfigureAndEmitOnceForContract( + DispatchBenchmarkScenario.UntargetedFloodOneHandler + ); + DispatchThroughputBenchmarks.DispatchScenarioContractObservation direct = + DispatchThroughputBenchmarks.ConfigureAndEmitOnceForContract( + DispatchBenchmarkScenario.UntargetedFloodOneDirectHandler + ); + + Assert.AreEqual( + 1, + token.TokenRegistrations, + "The public one-handler row must retain one token-owned registration." + ); + Assert.IsFalse( + token.HasDirectUntargetedRegistration, + "The public one-handler row must not bypass the token wrapper." + ); + Assert.AreEqual( + 0, + direct.TokenRegistrations, + "The direct row must not stage token metadata." + ); + Assert.IsTrue( + direct.HasDirectUntargetedRegistration, + "The direct row must retain the handler-owned deregistration state." + ); + } + [Test] public void BenchmarkMethodologyConstantsAreLocked() { diff --git a/Tests/Runtime/Benchmarks/BenchmarkProtocol.cs b/Tests/Runtime/Benchmarks/BenchmarkProtocol.cs index 6093d509..ca359615 100644 --- a/Tests/Runtime/Benchmarks/BenchmarkProtocol.cs +++ b/Tests/Runtime/Benchmarks/BenchmarkProtocol.cs @@ -521,6 +521,8 @@ public static string Key(DispatchBenchmarkScenario scenario) { DispatchBenchmarkScenario.EmptyBusDispatch => "EmptyBus_Dispatch", DispatchBenchmarkScenario.UntargetedFloodOneHandler => "UntargetedFlood_OneHandler", + DispatchBenchmarkScenario.UntargetedFloodOneDirectHandler => + "UntargetedFlood_OneDirectHandler", DispatchBenchmarkScenario.UntargetedFloodTwoHandlersOnePriority => "UntargetedFlood_TwoHandlers_OnePriority", DispatchBenchmarkScenario.UntargetedFloodThreeHandlersOnePriority => @@ -577,6 +579,8 @@ public static string DisplayName(DispatchBenchmarkScenario scenario) DispatchBenchmarkScenario.EmptyBusDispatch => "Empty Bus Dispatch", DispatchBenchmarkScenario.UntargetedFloodOneHandler => "Untargeted Flood (One Handler)", + DispatchBenchmarkScenario.UntargetedFloodOneDirectHandler => + "Untargeted Flood (One Direct Handler)", DispatchBenchmarkScenario.UntargetedFloodTwoHandlersOnePriority => "Untargeted Flood (Two Handlers, One Priority)", DispatchBenchmarkScenario.UntargetedFloodThreeHandlersOnePriority => diff --git a/Tests/Runtime/Benchmarks/DispatchThroughputBenchmarks.cs b/Tests/Runtime/Benchmarks/DispatchThroughputBenchmarks.cs index 3a1a0001..2010c628 100644 --- a/Tests/Runtime/Benchmarks/DispatchThroughputBenchmarks.cs +++ b/Tests/Runtime/Benchmarks/DispatchThroughputBenchmarks.cs @@ -28,6 +28,7 @@ public enum DispatchBenchmarkScenario { EmptyBusDispatch, UntargetedFloodOneHandler, + UntargetedFloodOneDirectHandler, UntargetedFloodTwoHandlersOnePriority, UntargetedFloodThreeHandlersOnePriority, UntargetedFloodFourHandlersOnePriority, @@ -57,7 +58,8 @@ public enum DispatchBenchmarkScenario public sealed class DispatchThroughputBenchmarks { internal const int PublishedDispatchOrder = 0; - internal const int DeregistrationAttributionOrder = 1; + internal const int RegistrationAttributionOrder = 1; + internal const int DeregistrationAttributionOrder = 2; [Test, Performance, Category("PerfBench")] public void MessageRegistrationHandlePhysicalSize() @@ -132,6 +134,17 @@ public void DispatchBenchmark(DispatchBenchmarkScenario scenario) _ = RunScenario(scenario); } + [Test, Performance, Category("PerfBench"), Order(RegistrationAttributionOrder)] + [TestCaseSource(nameof(RegistrationAttributionBenchmarkCases))] + public void RegistrationAttributionBenchmark(RegistrationAttributionOperation operation) + { + DispatchBenchmarkResult result = RegistrationAttributionBenchmarks.RunScenario( + operation + ); + Debug.Log(result.ToStructuredLog()); + TestContext.Out.WriteLine(result.ToCsvRow()); + } + [Test, Performance, Category("PerfBench"), Order(DeregistrationAttributionOrder)] [TestCaseSource(nameof(DeregistrationAttributionBenchmarkCases))] public void DeregistrationAttributionBenchmark(DeregistrationAttributionOperation operation) @@ -238,6 +251,20 @@ DeregistrationAttributionOperation operation in Enum.GetValues( } } + private static IEnumerable RegistrationAttributionBenchmarkCases() + { + foreach ( + RegistrationAttributionOperation operation in Enum.GetValues( + typeof(RegistrationAttributionOperation) + ) + ) + { + yield return new TestCaseData(operation).SetName( + RegistrationAttributionBenchmarks.ScenarioKey(operation) + ); + } + } + private static DispatchBenchmarkResult MeasureEmitScenario( DispatchBenchmarkScenario scenario ) @@ -317,6 +344,7 @@ internal static int ExpectedHandlerInvocationsPerEmit(DispatchBenchmarkScenario case DispatchBenchmarkScenario.TargetedFloodNoMatchingTarget: return 0; case DispatchBenchmarkScenario.UntargetedFloodOneHandler: + case DispatchBenchmarkScenario.UntargetedFloodOneDirectHandler: case DispatchBenchmarkScenario.TargetedFloodOneListener: case DispatchBenchmarkScenario.BroadcastFloodOneHandler: case DispatchBenchmarkScenario.InterceptorHeavyFourInterceptors: @@ -1153,6 +1181,9 @@ InvocationCounter handlerInvocations case DispatchBenchmarkScenario.UntargetedFloodOneHandler: RegisterUntargeted(scope, handlerInvocations, 0); return; + case DispatchBenchmarkScenario.UntargetedFloodOneDirectHandler: + scope.RegisterUntargetedDirect(handlerInvocations); + return; case DispatchBenchmarkScenario.UntargetedFloodTwoHandlersOnePriority: for (int index = 0; index < 2; index++) { @@ -1281,6 +1312,7 @@ private static void EmitMany(MessageBus bus, DispatchBenchmarkScenario scenario, { case DispatchBenchmarkScenario.EmptyBusDispatch: case DispatchBenchmarkScenario.UntargetedFloodOneHandler: + case DispatchBenchmarkScenario.UntargetedFloodOneDirectHandler: case DispatchBenchmarkScenario.UntargetedFloodTwoHandlersOnePriority: case DispatchBenchmarkScenario.UntargetedFloodThreeHandlersOnePriority: case DispatchBenchmarkScenario.UntargetedFloodFourHandlersOnePriority: @@ -1362,7 +1394,9 @@ DispatchBenchmarkScenario scenario return new DispatchScenarioContractObservation( scenarioFanOut, handlerInvocations.Count, - registrationBuckets + registrationBuckets, + scope.TokenRegistrations, + scope.HasDirectUntargetedRegistration ); } @@ -1371,12 +1405,16 @@ internal readonly struct DispatchScenarioContractObservation internal DispatchScenarioContractObservation( long scenarioFanOut, long controlFanOut, - int registrationBuckets + int registrationBuckets, + int tokenRegistrations, + bool hasDirectUntargetedRegistration ) { ScenarioFanOut = scenarioFanOut; ControlFanOut = controlFanOut; RegistrationBuckets = registrationBuckets; + TokenRegistrations = tokenRegistrations; + HasDirectUntargetedRegistration = hasDirectUntargetedRegistration; } internal long ScenarioFanOut { get; } @@ -1384,6 +1422,10 @@ int registrationBuckets internal long ControlFanOut { get; } internal int RegistrationBuckets { get; } + + internal int TokenRegistrations { get; } + + internal bool HasDirectUntargetedRegistration { get; } } private static bool AllowUntargeted(ref SimpleUntargetedMessage message) @@ -1933,6 +1975,8 @@ private sealed class BenchmarkRegistrationScope : IDisposable { private readonly List _tokens = new(); private readonly List _handlers = new(); + private MessageHandler.TypedHandler.TypedHandlerDeregistrationState _directUntargetedDeregistration; + private bool _hasDirectUntargetedDeregistration; private int _nextOwner = 32000; public BenchmarkRegistrationScope() @@ -1949,6 +1993,21 @@ public BenchmarkRegistrationScope() public MessageRegistrationToken PrimaryToken { get; } + public int TokenRegistrations + { + get + { + int count = 0; + for (int index = 0; index < _tokens.Count; index++) + { + count += _tokens[index]._metadata.Count; + } + return count; + } + } + + public bool HasDirectUntargetedRegistration => _hasDirectUntargetedDeregistration; + public MessageRegistrationToken CreateToken(bool active = true) { MessageHandler handler = new(new InstanceId(_nextOwner++), Bus) { active = active }; @@ -1960,6 +2019,21 @@ public MessageRegistrationToken CreateToken(bool active = true) return token; } + public void RegisterUntargetedDirect(InvocationCounter handlerInvocations) + { + MessageHandler handler = new(new InstanceId(_nextOwner++), Bus) { active = true }; + MessageHandler.FastHandler callback = ( + ref SimpleUntargetedMessage message + ) => handlerInvocations.Increment(); + _directUntargetedDeregistration = handler.RegisterUntargetedMessageHandler( + callback, + callback, + messageBus: Bus + ); + _hasDirectUntargetedDeregistration = true; + _handlers.Add(handler); + } + public void SetAllHandlersActive(bool active) { for (int index = 0; index < _handlers.Count; index++) @@ -1970,10 +2044,21 @@ public void SetAllHandlersActive(bool active) public void Dispose() { - for (int index = _tokens.Count - 1; index >= 0; index--) + try + { + if (_hasDirectUntargetedDeregistration) + { + _directUntargetedDeregistration.Deregister(); + _hasDirectUntargetedDeregistration = false; + } + } + finally { - _tokens[index].UnregisterAll(); - _tokens[index].Dispose(); + for (int index = _tokens.Count - 1; index >= 0; index--) + { + _tokens[index].UnregisterAll(); + _tokens[index].Dispose(); + } } } } diff --git a/Tests/Runtime/Benchmarks/RegistrationLifecycleBenchmarkContractTests.cs b/Tests/Runtime/Benchmarks/RegistrationLifecycleBenchmarkContractTests.cs index 059aaafd..b929a52e 100644 --- a/Tests/Runtime/Benchmarks/RegistrationLifecycleBenchmarkContractTests.cs +++ b/Tests/Runtime/Benchmarks/RegistrationLifecycleBenchmarkContractTests.cs @@ -68,6 +68,133 @@ public void LifecycleMatrixContainsEveryOperationAtEveryRequiredCardinality() } } + [Test] + [Category("PerfBench")] + [TestCase(RegistrationAttributionOperation.DirectBus, 1, 0, 0, 0)] + [TestCase(RegistrationAttributionOperation.DirectHandler, 1, 1, 0, 1)] + [TestCase(RegistrationAttributionOperation.TokenStage, 0, 0, 1, 0)] + [TestCase(RegistrationAttributionOperation.TokenActive, 1, 1, 1, 1)] + public void RegistrationAttributionProducesExactLayerState( + RegistrationAttributionOperation operation, + int expectedBusRegistrations, + int expectedHandlerRegistrations, + int expectedTokenRegistrations, + int expectedInvocations + ) + { + RegistrationAttributionObservation observation = + RegistrationAttributionBenchmarks.ExecuteOnceForContract(operation); + + Assert.AreEqual( + operation, + observation.Operation, + $"{operation}: observation operation drifted." + ); + Assert.AreEqual( + expectedBusRegistrations, + observation.Live.BusRegistrations, + $"{operation}: live bus layer state drifted." + ); + Assert.AreEqual( + expectedHandlerRegistrations, + observation.Live.HandlerRegistrations, + $"{operation}: live handler layer state drifted." + ); + Assert.AreEqual( + expectedTokenRegistrations, + observation.Live.TokenRegistrations, + $"{operation}: live token layer state drifted." + ); + Assert.AreEqual( + expectedInvocations, + observation.Live.HandlerInvocations, + $"{operation}: live delivery count drifted." + ); + Assert.AreEqual( + 0, + observation.Final.BusRegistrations, + $"{operation}: final bus state must be empty." + ); + Assert.AreEqual( + 0, + observation.Final.HandlerRegistrations, + $"{operation}: final handler state must be empty." + ); + Assert.AreEqual( + 0, + observation.Final.TokenRegistrations, + $"{operation}: final token state must be empty." + ); + Assert.AreEqual( + 0, + observation.Final.HandlerInvocations, + $"{operation}: final state must not deliver." + ); + } + + [Test] + [Category("PerfBench")] + public void RegistrationAttributionScenarioKeysCoverEveryOperationAtFixedCycleCount() + { + RegistrationAttributionOperation[] operations = + { + RegistrationAttributionOperation.DirectBus, + RegistrationAttributionOperation.DirectHandler, + RegistrationAttributionOperation.TokenStage, + RegistrationAttributionOperation.TokenActive, + }; + CollectionAssert.AreEqual( + operations, + (RegistrationAttributionOperation[]) + Enum.GetValues(typeof(RegistrationAttributionOperation)), + "Registration attribution cases must cover every operation in declaration order." + ); + Assert.AreEqual( + 131_072, + RegistrationAttributionBenchmarks.CycleCount, + "Registration attribution must retain its fixed cycle count." + ); + Assert.AreEqual( + BenchmarkProtocol.BatchSize, + RegistrationAttributionBenchmarks.AllocationCycleCount, + "Registration attribution and comparison allocation batches must stay comparable." + ); + + string[] keys = operations + .Select(RegistrationAttributionBenchmarks.ScenarioKey) + .ToArray(); + CollectionAssert.AllItemsAreUnique( + keys, + "Registration attribution scenario keys must be unique." + ); + foreach (RegistrationAttributionOperation operation in operations) + { + StringAssert.Contains( + operation.ToString(), + RegistrationAttributionBenchmarks.ScenarioKey(operation), + $"{operation}: scenario key must name the measured registration layer." + ); + StringAssert.EndsWith( + "_131072", + RegistrationAttributionBenchmarks.ScenarioKey(operation), + $"{operation}: scenario key must encode the fixed cycle count." + ); + } + } + + [Test] + [Category("PerfBench")] + [TestCase(RegistrationAttributionOperation.DirectBus)] + [TestCase(RegistrationAttributionOperation.DirectHandler)] + [TestCase(RegistrationAttributionOperation.TokenStage)] + [TestCase(RegistrationAttributionOperation.TokenActive)] + public void RegistrationAttributionSharedCyclePathSupportsTwoConsecutiveBatches( + RegistrationAttributionOperation operation + ) + { + RegistrationAttributionBenchmarks.ExecuteCyclesForContract(operation, cycleCount: 257); + } + [Test] [Category("PerfBench")] [TestCase(DeregistrationAttributionOperation.DirectBus)] @@ -152,40 +279,93 @@ public void PublishedDispatchAndAttributionUseSameSupportedMethodOrderBoundary() MethodInfo dispatchMethod = typeof(DispatchThroughputBenchmarks).GetMethod( nameof(DispatchThroughputBenchmarks.DispatchBenchmark) ); - MethodInfo attributionMethod = typeof(DispatchThroughputBenchmarks).GetMethod( - nameof(DispatchThroughputBenchmarks.DeregistrationAttributionBenchmark) - ); + MethodInfo registrationAttributionMethod = + typeof(DispatchThroughputBenchmarks).GetMethod( + nameof(DispatchThroughputBenchmarks.RegistrationAttributionBenchmark) + ); + MethodInfo deregistrationAttributionMethod = + typeof(DispatchThroughputBenchmarks).GetMethod( + nameof(DispatchThroughputBenchmarks.DeregistrationAttributionBenchmark) + ); - Assert.IsNotNull(dispatchMethod); - Assert.IsNotNull(attributionMethod); - Assert.AreEqual(typeof(DispatchThroughputBenchmarks), dispatchMethod.DeclaringType); - Assert.AreEqual(typeof(DispatchThroughputBenchmarks), attributionMethod.DeclaringType); + Assert.IsNotNull(dispatchMethod, "Published dispatch entry point must exist."); + Assert.IsNotNull( + registrationAttributionMethod, + "Registration attribution entry point must exist." + ); + Assert.IsNotNull( + deregistrationAttributionMethod, + "Deregistration attribution entry point must exist." + ); + Assert.AreEqual( + typeof(DispatchThroughputBenchmarks), + dispatchMethod.DeclaringType, + "Published dispatch entry must remain on the ordered benchmark fixture." + ); + Assert.AreEqual( + typeof(DispatchThroughputBenchmarks), + registrationAttributionMethod.DeclaringType, + "Registration attribution entry must remain on the ordered benchmark fixture." + ); + Assert.AreEqual( + typeof(DispatchThroughputBenchmarks), + deregistrationAttributionMethod.DeclaringType, + "Deregistration attribution entry must remain on the ordered benchmark fixture." + ); OrderAttribute dispatchOrder = dispatchMethod.GetCustomAttribute(); - OrderAttribute attributionOrder = - attributionMethod.GetCustomAttribute(); - Assert.IsNotNull(dispatchOrder); - Assert.IsNotNull(attributionOrder); + OrderAttribute registrationAttributionOrder = + registrationAttributionMethod.GetCustomAttribute(); + OrderAttribute deregistrationAttributionOrder = + deregistrationAttributionMethod.GetCustomAttribute(); + Assert.IsNotNull(dispatchOrder, "Published dispatch entry must declare its order."); + Assert.IsNotNull( + registrationAttributionOrder, + "Registration attribution entry must declare its order." + ); + Assert.IsNotNull( + deregistrationAttributionOrder, + "Deregistration attribution entry must declare its order." + ); Assert.AreEqual( DispatchThroughputBenchmarks.PublishedDispatchOrder, - dispatchOrder.Order + dispatchOrder.Order, + "Published dispatch order drifted." + ); + Assert.AreEqual( + DispatchThroughputBenchmarks.RegistrationAttributionOrder, + registrationAttributionOrder.Order, + "Registration attribution order drifted." ); Assert.AreEqual( DispatchThroughputBenchmarks.DeregistrationAttributionOrder, - attributionOrder.Order + deregistrationAttributionOrder.Order, + "Deregistration attribution order drifted." ); - Assert.Less( - dispatchOrder.Order, - attributionOrder.Order, - "Published dispatch windows must complete before high-cardinality attribution." + CollectionAssert.AreEqual( + new[] { 0, 1, 2 }, + new[] + { + dispatchOrder.Order, + registrationAttributionOrder.Order, + deregistrationAttributionOrder.Order, + }, + "Published dispatch and attribution rows must retain one deterministic boundary." ); - MethodInfo[] independentAttributionEntries = typeof(DeregistrationAttributionBenchmarks) - .GetMethods( - BindingFlags.Public - | BindingFlags.NonPublic - | BindingFlags.Static - | BindingFlags.Instance + Type[] attributionTypes = + { + typeof(RegistrationAttributionBenchmarks), + typeof(DeregistrationAttributionBenchmarks), + }; + MethodInfo[] independentAttributionEntries = attributionTypes + .SelectMany(type => + type.GetMethods( + BindingFlags.Public + | BindingFlags.NonPublic + | BindingFlags.Static + | BindingFlags.Instance + ) ) .Where(method => method.GetCustomAttribute() != null) .ToArray(); diff --git a/Tests/Runtime/Benchmarks/RegistrationLifecycleBenchmarks.cs b/Tests/Runtime/Benchmarks/RegistrationLifecycleBenchmarks.cs index 116ab5c8..96a067bd 100644 --- a/Tests/Runtime/Benchmarks/RegistrationLifecycleBenchmarks.cs +++ b/Tests/Runtime/Benchmarks/RegistrationLifecycleBenchmarks.cs @@ -5,10 +5,12 @@ namespace DxMessaging.Tests.Runtime.Benchmarks using System.Collections.Generic; using System.Diagnostics; using System.Globalization; + using System.Runtime.CompilerServices; using DxMessaging.Core; using DxMessaging.Core.Internal; using DxMessaging.Core.MessageBus; using DxMessaging.Core.Messages; + using DxMessaging.Tests.Runtime; using NUnit.Framework; using UnityEngine; using Debug = UnityEngine.Debug; @@ -449,6 +451,484 @@ public void Dispose() } } + public enum RegistrationAttributionOperation + { + DirectBus, + DirectHandler, + TokenStage, + TokenActive, + } + + /// + /// Attributes the complete register/remove cycle across the bus, handler cache, disabled token, + /// and active token layers. Each row reuses one by-ref delegate and returns to an empty state + /// after every cycle. The active-token row matches the comparison suite's + /// subscribe/unsubscribe workload. + /// + internal static class RegistrationAttributionBenchmarks + { + internal const int CycleCount = 131_072; + internal const int AllocationCycleCount = BenchmarkProtocol.BatchSize; + private const int TimingTrials = 7; + private const int AllocationAttempts = 7; + + internal static RegistrationAttributionObservation ExecuteOnceForContract( + RegistrationAttributionOperation operation + ) + { + using RegistrationAttributionState state = new(operation); + using LeakWatcher watcher = new( + state.Bus, + label: $"registration attribution observed cycle ({operation})" + ); + return state.ExecuteSingleCycleWithObservation(); + } + + internal static void ExecuteCyclesForContract( + RegistrationAttributionOperation operation, + int cycleCount + ) + { + using RegistrationAttributionState state = new(operation); + using LeakWatcher watcher = new( + state.Bus, + label: $"registration attribution repeated cycles ({operation})" + ); + state.ExecuteCycles(cycleCount); + state.ExecuteCycles(cycleCount); + state.VerifyFinal(expectedCycles: cycleCount * 2); + } + + internal static DispatchBenchmarkResult RunScenario( + RegistrationAttributionOperation operation + ) + { + using (RegistrationAttributionState warmup = new(operation)) + { + warmup.ExecuteCycles(cycleCount: 16); + warmup.VerifyFinal(expectedCycles: 16); + } + + AllocationProbe.SettleHeapForMeasurement(); + double minElapsedSeconds = double.MaxValue; + for (int trial = 0; trial < TimingTrials; trial++) + { + using RegistrationAttributionState state = new(operation); + long startTimestamp = Stopwatch.GetTimestamp(); + state.ExecuteCycles(CycleCount); + long endTimestamp = Stopwatch.GetTimestamp(); + state.VerifyFinal(CycleCount); + double elapsedSeconds = + (endTimestamp - startTimestamp) / (double)Stopwatch.Frequency; + if (elapsedSeconds < minElapsedSeconds) + { + minElapsedSeconds = elapsedSeconds; + } + } + + AllocationProbe.AllocationSample allocation = MeasureAllocation(operation); + + return DispatchBenchmarkResult.ForRegistrationScenario( + ScenarioKey(operation), + runIndex: -1, + allocation.Allocations, + allocation.Bytes, + minElapsedSeconds * 1000d + ); + } + + private static AllocationProbe.AllocationSample MeasureAllocation( + RegistrationAttributionOperation operation + ) + { + AllocationProbe.SettleHeapForMeasurement(); + if (!AllocationProbe.IsFunctional) + { + return new AllocationProbe.AllocationSample( + AllocationProbe.Unmeasured, + AllocationProbe.Unmeasured + ); + } + + long minimumCount = long.MaxValue; + long minimumBytes = AllocationProbe.Unmeasured; + try + { + for (int attempt = 0; attempt < AllocationAttempts; attempt++) + { + using RegistrationAttributionState state = new(operation); + // Match BenchmarkProtocol.Measure: warm this exact bus/token state before + // opening the profiler recorder, then measure one same-sized steady-state batch. + state.ExecuteCycles(AllocationCycleCount); + AllocationProbe.AllocationSample sample; + using (AllocationProbe.Window window = AllocationProbe.BeginWindow()) + { + state.ExecuteCycles(AllocationCycleCount); + sample = window.SampleBoth(); + } + state.VerifyFinal(expectedCycles: AllocationCycleCount * 2); + if ( + AllocationProbe.ShouldReplaceMinimumAttempt( + sample.Allocations, + sample.Bytes, + minimumCount, + minimumBytes + ) + ) + { + minimumCount = sample.Allocations; + minimumBytes = sample.Bytes; + } + } + } + finally + { + AllocationProbe.SettleHeapForMeasurement(); + } + + return new AllocationProbe.AllocationSample(minimumCount, minimumBytes); + } + + internal static string ScenarioKey(RegistrationAttributionOperation operation) + { + // SYNC: scripts/unity/perf-scenarios.js mirrors these stable rendered keys. + return operation switch + { + RegistrationAttributionOperation.DirectBus => + $"RegistrationAttribution_DirectBus_{CycleCount}", + RegistrationAttributionOperation.DirectHandler => + $"RegistrationAttribution_DirectHandler_{CycleCount}", + RegistrationAttributionOperation.TokenStage => + $"RegistrationAttribution_TokenStage_{CycleCount}", + RegistrationAttributionOperation.TokenActive => + $"RegistrationAttribution_TokenActive_{CycleCount}", + _ => throw new ArgumentOutOfRangeException(nameof(operation), operation, null), + }; + } + + private readonly struct AttributionMessage : IUntargetedMessage { } + + private interface IRegistrationCyclePath + { + RegistrationAttributionStateObservation ExecuteObservedCycle( + RegistrationAttributionState state + ); + + void ExecuteCycles(RegistrationAttributionState state, int cycleCount); + } + + private sealed class DirectBusCyclePath : IRegistrationCyclePath + { + public RegistrationAttributionStateObservation ExecuteObservedCycle( + RegistrationAttributionState state + ) + { + MessageBusRegistration registration = state.RegisterDirectBus(); + RegistrationAttributionStateObservation live = state.ObserveState(); + state.DeregisterDirectBus(in registration); + return live; + } + + public void ExecuteCycles(RegistrationAttributionState state, int cycleCount) + { + for (int index = 0; index < cycleCount; index++) + { + MessageBusRegistration registration = state.RegisterDirectBus(); + state.DeregisterDirectBus(in registration); + } + } + } + + private sealed class DirectHandlerCyclePath : IRegistrationCyclePath + { + public RegistrationAttributionStateObservation ExecuteObservedCycle( + RegistrationAttributionState state + ) + { + MessageHandler.TypedHandler.TypedHandlerDeregistrationState deregistration = + state.RegisterDirectHandler(); + RegistrationAttributionStateObservation live = state.ObserveState(); + state.DeregisterDirectHandler(in deregistration); + return live; + } + + public void ExecuteCycles(RegistrationAttributionState state, int cycleCount) + { + for (int index = 0; index < cycleCount; index++) + { + MessageHandler.TypedHandler.TypedHandlerDeregistrationState deregistration = + state.RegisterDirectHandler(); + state.DeregisterDirectHandler(in deregistration); + } + } + } + + private sealed class TokenCyclePath : IRegistrationCyclePath + { + public RegistrationAttributionStateObservation ExecuteObservedCycle( + RegistrationAttributionState state + ) + { + MessageRegistrationHandle handle = state.RegisterToken(); + RegistrationAttributionStateObservation live = state.ObserveState(); + state.RemoveToken(handle); + return live; + } + + public void ExecuteCycles(RegistrationAttributionState state, int cycleCount) + { + for (int index = 0; index < cycleCount; index++) + { + MessageRegistrationHandle handle = state.RegisterToken(); + state.RemoveToken(handle); + } + } + } + + private static readonly IRegistrationCyclePath DirectBusPath = new DirectBusCyclePath(); + private static readonly IRegistrationCyclePath DirectHandlerPath = + new DirectHandlerCyclePath(); + private static readonly IRegistrationCyclePath TokenPath = new TokenCyclePath(); + + private static IRegistrationCyclePath ResolveCyclePath( + RegistrationAttributionOperation operation + ) + { + return operation switch + { + RegistrationAttributionOperation.DirectBus => DirectBusPath, + RegistrationAttributionOperation.DirectHandler => DirectHandlerPath, + RegistrationAttributionOperation.TokenStage => TokenPath, + RegistrationAttributionOperation.TokenActive => TokenPath, + _ => throw new ArgumentOutOfRangeException(nameof(operation), operation, null), + }; + } + + private sealed class RegistrationAttributionState : IDisposable + { + private readonly RegistrationAttributionOperation _operation; + private readonly IRegistrationCyclePath _cyclePath; + private readonly IDisposable _registryScope; + private readonly MessageBus _bus; + private readonly MessageHandler _handler; + private readonly MessageHandler.FastHandler _callback; + private readonly MessageRegistrationToken _token; + private int _completedCycles; + private int _handlerInvocations; + private bool _disposed; + + public RegistrationAttributionState(RegistrationAttributionOperation operation) + { + _operation = operation; + _cyclePath = ResolveCyclePath(operation); + _registryScope = MessageBus.IsolateIdleSweepRegistryForBenchmark(); + _bus = new MessageBus { DiagnosticsMode = false }; + _handler = new MessageHandler(new InstanceId(43001), _bus) { active = true }; + _callback = Handle; + if ( + operation == RegistrationAttributionOperation.TokenStage + || operation == RegistrationAttributionOperation.TokenActive + ) + { + _token = MessageRegistrationToken.Create(_handler, _bus); + _token.DiagnosticMode = false; + if (operation == RegistrationAttributionOperation.TokenActive) + { + _token.Enable(); + } + } + } + + public IMessageBus Bus => _bus; + + public RegistrationAttributionObservation ExecuteSingleCycleWithObservation() + { + RegistrationAttributionStateObservation live = _cyclePath.ExecuteObservedCycle( + this + ); + _completedCycles = 1; + RegistrationAttributionStateObservation final = ObserveState(); + return new RegistrationAttributionObservation(_operation, live, final); + } + + public void ExecuteCycles(int cycleCount) + { + if (cycleCount <= 0) + { + throw new ArgumentOutOfRangeException( + nameof(cycleCount), + cycleCount, + "Registration attribution cycle count must be positive." + ); + } + _cyclePath.ExecuteCycles(this, cycleCount); + _completedCycles = checked(_completedCycles + cycleCount); + } + + public void VerifyFinal(int expectedCycles) + { + Assert.AreEqual( + expectedCycles, + _completedCycles, + $"{_operation}: completed cycle count drifted." + ); + RegistrationAttributionStateObservation final = ObserveState(); + Assert.AreEqual( + 0, + final.BusRegistrations, + $"{_operation}: registration cycles left a bus registration live." + ); + Assert.AreEqual( + 0, + final.HandlerRegistrations, + $"{_operation}: registration cycles left a flat handler live." + ); + Assert.AreEqual( + 0, + final.TokenRegistrations, + $"{_operation}: registration cycles left token metadata live." + ); + Assert.AreEqual( + 0, + final.HandlerInvocations, + $"{_operation}: registration cycles still delivered after removal." + ); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public MessageBusRegistration RegisterDirectBus() + { + return _bus.RegisterUntargeted(_handler); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void DeregisterDirectBus(in MessageBusRegistration registration) + { + _bus.Deregister(in registration); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public MessageHandler.TypedHandler.TypedHandlerDeregistrationState RegisterDirectHandler() + { + return _handler.RegisterUntargetedMessageHandler( + _callback, + _callback, + messageBus: _bus + ); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void DeregisterDirectHandler( + in MessageHandler.TypedHandler.TypedHandlerDeregistrationState deregistration + ) + { + deregistration.Deregister(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public MessageRegistrationHandle RegisterToken() + { + return _token.RegisterUntargeted(_callback); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void RemoveToken(MessageRegistrationHandle handle) + { + _token.RemoveRegistration(handle); + } + + public RegistrationAttributionStateObservation ObserveState() + { + _handlerInvocations = 0; + AttributionMessage message = default; + _bus.UntargetedBroadcast(ref message); + return new RegistrationAttributionStateObservation( + _bus.RegisteredUntargeted, + CountHandlerRegistrations(), + _token?._metadata.Count ?? 0, + _handlerInvocations + ); + } + + private void Handle(ref AttributionMessage message) + { + _handlerInvocations++; + } + + private int CountHandlerRegistrations() + { + return _handler.CountFlatHandlers( + _bus, + priority: 0, + fastIndex: TypedSlotIndex.UntargetedHandleFast, + defaultIndex: TypedSlotIndex.UntargetedHandleDefault + ); + } + + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + try + { + _token?.Dispose(); + } + finally + { + _registryScope.Dispose(); + } + } + } + } + + public readonly struct RegistrationAttributionObservation + { + public RegistrationAttributionObservation( + RegistrationAttributionOperation operation, + RegistrationAttributionStateObservation live, + RegistrationAttributionStateObservation final + ) + { + Operation = operation; + Live = live; + Final = final; + } + + public RegistrationAttributionOperation Operation { get; } + + public RegistrationAttributionStateObservation Live { get; } + + public RegistrationAttributionStateObservation Final { get; } + } + + public readonly struct RegistrationAttributionStateObservation + { + public RegistrationAttributionStateObservation( + int busRegistrations, + int handlerRegistrations, + int tokenRegistrations, + int handlerInvocations + ) + { + BusRegistrations = busRegistrations; + HandlerRegistrations = handlerRegistrations; + TokenRegistrations = tokenRegistrations; + HandlerInvocations = handlerInvocations; + } + + public int BusRegistrations { get; } + + public int HandlerRegistrations { get; } + + public int TokenRegistrations { get; } + + public int HandlerInvocations { get; } + } + public enum DeregistrationAttributionOperation { DirectBus, @@ -518,6 +998,7 @@ DeregistrationAttributionOperation operation internal static string ScenarioKey(DeregistrationAttributionOperation operation) { + // SYNC: scripts/unity/perf-scenarios.js mirrors these stable rendered keys. return operation switch { DeregistrationAttributionOperation.DirectBus => diff --git a/docs/runbooks/perf-benchmark-methodology.md b/docs/runbooks/perf-benchmark-methodology.md index bfd9d0c8..7fd0898a 100644 --- a/docs/runbooks/perf-benchmark-methodology.md +++ b/docs/runbooks/perf-benchmark-methodology.md @@ -127,6 +127,29 @@ they are report-only -- rendered as wall clock, never gated. warm-JIT deregistration flood is repeatable and reports the MINIMUM over `WarmFloodTrials` trials. Both are wall-clock rows, symmetric with the registration floods. +- **Registration-cycle attribution.** The four `RegistrationAttribution_*_131072` + rows time complete same-type register/remove cycles through the direct bus, + direct handler, disabled token, and active token layers. Every row reuses one + cached by-ref handler, selects the operation outside the timed loop, and ends + with zero bus registrations, flat handlers, token metadata, and deliveries. + `TokenActive` matches the DxMessaging subscribe/unsubscribe comparison shape. + `TokenStage` is a sibling measurement, not a cumulative lower layer. The + `TokenActive` minus `DirectHandler` delta includes registration-object creation, + arena/handle work, teardown bookkeeping, and augmented delegate binding; do not + label it as token storage alone. Each row warms the exact cycle on throwaway + state, settles the heap once, and reports the minimum of seven fresh timing + trials. Allocation instrumentation runs afterward over seven fresh states. Each + state executes 10000 warm-up cycles before the recorder opens, then measures a + second 10000-cycle batch and reports the minimum count with bytes from that same + attempt. This matches the comparison harness's warmed allocation batch size and + state without putting profiler overhead in the latency clock. A stripped IL2CPP + player reports `n/a` for both allocation fields. +- **Token dispatch attribution.** Compare `UntargetedFlood_OneDirectHandler` with + `UntargetedFlood_OneHandler`. Both use the same by-ref fast slot, one active + handler, and exact fan-out. The first registers the user delegate directly + through `MessageHandler`; the second uses the public enabled-token path and its + `AugmentedScalarFast` callback. The token's diagnostics flag remains mutable, so + the augmented callback's per-dispatch branch is part of the public contract. - **Deregistration attribution.** The four `DeregistrationAttribution_*_131072` rows report cumulative layers over one same-type, high-cardinality population: direct bus removal; handler-cache plus bus removal; token `RemoveRegistration` @@ -146,7 +169,7 @@ they are report-only -- rendered as wall clock, never gated. exposure. Prove allocation changes with structural or short-window differential guards instead. The attribution entry and the published dispatch entry share `DispatchThroughputBenchmarks`: method-level NUnit `Order` runs every dispatch - scenario before the repeated 131072-registration attribution setup. These + scenario before the 131072-cycle and retained-population attribution setup. These diagnostic wall-clock rows are not hard regression gates. Do not compare them with the 1000-type flood rows because their registration topology differs. - **Noise control on the wall-clock floods.** A single one-shot sample of a ~1 ms @@ -240,17 +263,16 @@ apples-to-apples set every library bridge implements (or declares unsupported). ### Dispatch scenarios (DxMessaging only) -The scenario registry contains thirty DxMessaging rows: fourteen continuous-window -dispatch-throughput rows and sixteen wall-clock rows. The twenty-six established rows -are defined in +The scenario registry contains thirty-five DxMessaging rows: fifteen continuous-window +dispatch-throughput rows and twenty wall-clock rows. Twenty-seven rows are defined in [`DispatchThroughputBenchmarks.cs`](https://github.com/Ambiguous-Interactive/DxMessaging/blob/master/Tests/Runtime/Benchmarks/DispatchThroughputBenchmarks.cs). -The four diagnostic deregistration-attribution rows are defined in +The eight diagnostic registration/deregistration attribution rows are defined in [`RegistrationLifecycleBenchmarks.cs`](https://github.com/Ambiguous-Interactive/DxMessaging/blob/master/Tests/Runtime/Benchmarks/RegistrationLifecycleBenchmarks.cs). -The sixteen cold, warm-JIT, construction, marginal-registration, deregistration, and +The twenty cold, warm-JIT, construction, marginal-registration, deregistration, and attribution rows report zero throughput and wall-clock latency; see [Cold vs warm/hot modes](#cold-vs-warmhot-modes). The three marginal-registration rows report the GC-allocation cost of an additional same-type registration -- the -surface the registration allocation work reduced -- and are measurable only where the +surface where the registration allocation work was reduced -- and are measurable only where the profiler is present (an in-editor PlayMode/Mono run; the published Standalone IL2CPP leg strips it, so its allocation columns are omitted): @@ -268,6 +290,7 @@ sub-millisecond single shot without changing the reported per-bus cardinality. | ----------------------------------------------------------------- | ---------------------------------------------------------------------- | | `EmptyBus_Dispatch` | Dispatch with no registered handler. | | `UntargetedFlood_OneHandler` | One untargeted handler on one message type. | +| `UntargetedFlood_OneDirectHandler` | One direct handler without the token-owned augmented callback. | | `UntargetedFlood_TwoHandlers_OnePriority` | Two untargeted handlers sharing priority 0. | | `UntargetedFlood_ThreeHandlers_OnePriority` | Three untargeted handlers sharing priority 0. | | `UntargetedFlood_FourHandlers_OnePriority` | Four untargeted handlers sharing priority 0. | @@ -292,6 +315,10 @@ sub-millisecond single shot without changing the reported per-bus cardinality. | `BroadcastRegistration_Marginal` | Marginal cost of 1000 more broadcast handlers on one warm type/source. | | `DeregistrationFlood_1000Types_Cold` | Tearing down 1000 live registrations, JIT-inclusive (cold flood). | | `DeregistrationFlood_1000Types_WarmJit` | Tearing down the same 1000 registrations after a JIT pre-warm. | +| `RegistrationAttribution_DirectBus_131072` | Direct bus register/remove cycles for one reused handler. | +| `RegistrationAttribution_DirectHandler_131072` | Handler plus bus register/remove cycles. | +| `RegistrationAttribution_TokenStage_131072` | Disabled-token stage/remove cycles. | +| `RegistrationAttribution_TokenActive_131072` | Enabled-token register/remove cycles including lower layers. | | `DeregistrationAttribution_DirectBus_131072` | Direct built-in bus teardown for 131072 same-type registrations. | | `DeregistrationAttribution_DirectHandler_131072` | Handler-cache teardown including the direct bus layer. | | `DeregistrationAttribution_TokenRemove_131072` | Per-handle token removal including handler and bus layers. | @@ -341,10 +368,10 @@ silently desync. | `PriorityOrdered` | 1 token, 4 priorities | `UntargetedFlood_FourHandlers_FourPriorities` | No | Comparison uses one MessageHandler with four handler-store entries; the dispatch cell uses four separate tokens. Same fan-out (4), different storage. | | `Filtered` | 1 interceptor + 1 handler | `InterceptorHeavy_FourInterceptors` | No | Comparison runs one interceptor; the dispatch cell runs four. | | `PostProcess` | 1 post-processor + 1 handler | `PostProcessingHeavy_FourPostProcessors` | No | Comparison runs one post-processor; the dispatch cell runs four. | -| `SubUnsub` | register/unregister churn cycle | (none) | No | The dispatch family has no subscribe/unsubscribe-throughput scenario. | +| `SubUnsub` | register/unregister churn cycle | (none) | No | The dispatch family has no subscribe/unsubscribe throughput scenario. | **Fresh-state guarantee.** CI builds the comparison matrix into a dedicated player; -the internal benchmark player, including the 131072-registration teardown rows, +the internal benchmark player, including the 131072-cycle and teardown rows, cannot leave heap state behind for it. Every matrix row then constructs and disposes a fresh bridge. The harness does not force collections between rows; the dedicated player provides the required clean process boundary without adding GC work to each case. The DxMessaging path uses a fresh diff --git a/scripts/__tests__/unity-perf.test.js b/scripts/__tests__/unity-perf.test.js index af220dfe..302f7d7c 100644 --- a/scripts/__tests__/unity-perf.test.js +++ b/scripts/__tests__/unity-perf.test.js @@ -53,6 +53,7 @@ const REPO_ROOT = path.resolve(__dirname, "..", ".."); test("dispatch baseline scenarios keep stable order and labels", () => { const expected = [ ["EmptyBus_Dispatch", "Empty Bus Dispatch"], + ["UntargetedFlood_OneDirectHandler", "Untargeted Flood (One Direct Handler)"], ["UntargetedFlood_TwoHandlers_OnePriority", "Untargeted Flood (Two Handlers, One Priority)"], [ "UntargetedFlood_ThreeHandlers_OnePriority", @@ -68,11 +69,7 @@ test("dispatch baseline scenarios keep stable order and labels", () => { [ "MessageRegistrationTokenConstruction_1000_PrebuiltHandlerAndBus", "Registration Token Construction (1000, Prebuilt Handler + Bus)" - ], - ...["Direct Bus", "Direct Handler", "Token Remove", "Token Disable"].map((label) => [ - `DeregistrationAttribution_${label.replaceAll(" ", "")}_131072`, - `Deregistration Attribution (${label}, 131072)` - ]) + ] ]; for (const [key, label] of expected) { @@ -80,18 +77,33 @@ test("dispatch baseline scenarios keep stable order and labels", () => { assert.equal(DISPATCH_DISPLAY_NAMES[key], label); } - assert.ok( - SCENARIO_ORDER.indexOf("EmptyBus_Dispatch") < - SCENARIO_ORDER.indexOf("UntargetedFlood_OneHandler") - ); - assert.deepEqual(SCENARIO_ORDER.slice(1, 5), [ + const registrationLabel = "Registration Attribution"; + const deregistrationLabel = "Deregistration Attribution"; + const attributionLabels = { + RegistrationAttribution_DirectBus_131072: `${registrationLabel} (Direct Bus, 131072)`, + RegistrationAttribution_DirectHandler_131072: `${registrationLabel} (Direct Handler, 131072)`, + RegistrationAttribution_TokenStage_131072: `${registrationLabel} (Token Stage, 131072)`, + RegistrationAttribution_TokenActive_131072: `${registrationLabel} (Token Active, 131072)`, + DeregistrationAttribution_DirectBus_131072: `${deregistrationLabel} (Direct Bus, 131072)`, + DeregistrationAttribution_DirectHandler_131072: `${deregistrationLabel} (Direct Handler, 131072)`, + DeregistrationAttribution_TokenRemove_131072: `${deregistrationLabel} (Token Remove, 131072)`, + DeregistrationAttribution_TokenDisable_131072: `${deregistrationLabel} (Token Disable, 131072)` + }; + for (const [key, label] of Object.entries(attributionLabels)) { + assert.equal(DISPATCH_DISPLAY_NAMES[key], label, `${key} must retain its stable label`); + } + + assert.deepEqual(SCENARIO_ORDER.slice(1, 6), [ "UntargetedFlood_OneHandler", + "UntargetedFlood_OneDirectHandler", "UntargetedFlood_TwoHandlers_OnePriority", "UntargetedFlood_ThreeHandlers_OnePriority", "UntargetedFlood_FourHandlers_OnePriority" ]); assert.equal(new Set(SCENARIO_ORDER).size, SCENARIO_ORDER.length); + const attributionKeys = Object.keys(attributionLabels); + assert.deepEqual([...WALL_CLOCK_SCENARIOS].slice(-8, -4), attributionKeys.slice(0, 4)); assert.deepEqual( [...Object.keys(DISPATCH_DISPLAY_NAMES)].sort(), [...SCENARIO_ORDER].sort(), @@ -219,7 +231,7 @@ test("extractRows parses CSV and structured log lines, dedupes, skips noise", () `2026-01-01T00:00:00 UntargetedFlood_OneHandler,${PLATFORM},abc1234,0,1000000,0,12.5,2048`, `UntargetedFlood_OneHandler,${PLATFORM},abc1234,0,1000000,0,12.5,2048`, `{scenario:"TargetedFlood_OneListener",platform:"${PLATFORM}",commit:"abc1234",runIndex:1,emitsPerSec:2500000.5,gcAllocations:64,wallClockMs:8.25,gcAllocatedBytes:4096}`, - `{scenario:"DeregistrationAttribution_DirectHandler_131072",platform:"${PLATFORM}",commit:"abc1234",runIndex:-1,emitsPerSec:0,gcAllocations:-1,wallClockMs:42.5,gcAllocatedBytes:-1}`, + `{scenario:"RegistrationAttribution_TokenActive_131072",platform:"${PLATFORM}",commit:"abc1234",runIndex:-1,emitsPerSec:0,gcAllocations:10000,wallClockMs:42.5,gcAllocatedBytes:640000}`, `UnknownScenario,${PLATFORM},abc1234,0,1,0,1,0` ].join("\n"); @@ -234,7 +246,7 @@ test("extractRows parses CSV and structured log lines, dedupes, skips noise", () [ ["UntargetedFlood_OneHandler", "1000000.000", "0", "2048"], ["TargetedFlood_OneListener", "2500000.500", "64", "4096"], - ["DeregistrationAttribution_DirectHandler_131072", "0.000", "-1", "-1"] + ["RegistrationAttribution_TokenActive_131072", "0.000", "10000", "640000"] ] ); }); diff --git a/scripts/unity/perf-scenarios.js b/scripts/unity/perf-scenarios.js index 383e6f1e..bb6458d3 100644 --- a/scripts/unity/perf-scenarios.js +++ b/scripts/unity/perf-scenarios.js @@ -6,6 +6,7 @@ const COMPARISON_SCENARIO_PREFIX = "Comparison_"; const SCENARIO_DEFINITIONS = [ ["EmptyBus_Dispatch", "Empty Bus Dispatch"], ["UntargetedFlood_OneHandler", "Untargeted Flood (One Handler)"], + ["UntargetedFlood_OneDirectHandler", "Untargeted Flood (One Direct Handler)"], ["UntargetedFlood_TwoHandlers_OnePriority", "Untargeted Flood (Two Handlers, One Priority)"], ["UntargetedFlood_ThreeHandlers_OnePriority", "Untargeted Flood (Three Handlers, One Priority)"], ["UntargetedFlood_FourHandlers_OnePriority", "Untargeted Flood (Four Handlers, One Priority)"], @@ -40,26 +41,17 @@ const SCENARIO_DEFINITIONS = [ ["BroadcastRegistration_Marginal", "Broadcast Registration (Marginal, 1000 Same-Type)", true], ["DeregistrationFlood_1000Types_Cold", "Deregistration Flood (1000 Types, Cold)", true], ["DeregistrationFlood_1000Types_WarmJit", "Deregistration Flood (1000 Types, Warm JIT)", true], - [ - "DeregistrationAttribution_DirectBus_131072", - "Deregistration Attribution (Direct Bus, 131072)", + // SYNC: both attribution ScenarioKey methods in RegistrationLifecycleBenchmarks.cs own these keys. + ...["Direct Bus", "Direct Handler", "Token Stage", "Token Active"].map((label) => [ + `RegistrationAttribution_${label.replaceAll(" ", "")}_131072`, + `Registration Attribution (${label}, 131072)`, true - ], - [ - "DeregistrationAttribution_DirectHandler_131072", - "Deregistration Attribution (Direct Handler, 131072)", - true - ], - [ - "DeregistrationAttribution_TokenRemove_131072", - "Deregistration Attribution (Token Remove, 131072)", - true - ], - [ - "DeregistrationAttribution_TokenDisable_131072", - "Deregistration Attribution (Token Disable, 131072)", + ]), + ...["Direct Bus", "Direct Handler", "Token Remove", "Token Disable"].map((label) => [ + `DeregistrationAttribution_${label.replaceAll(" ", "")}_131072`, + `Deregistration Attribution (${label}, 131072)`, true - ] + ]) ]; const SCENARIO_ORDER = SCENARIO_DEFINITIONS.map(([key]) => key);