perf(metrics): cache MxEventFamily names for per-event metric tags

WorkerClient.StageWorkerEvent and StreamEvents called Enum.ToString per
event (per subscriber on the stream path). MxEventFamilyNames serves the
contract-fixed names from a frozen lookup, falling back to ToString for
undefined values so exported tag values are unchanged.
This commit is contained in:
Joseph Doherty
2026-10-22 06:30:57 -04:00
parent 6ae5ed905f
commit 68d6b5fd6f
4 changed files with 71 additions and 2 deletions
@@ -183,7 +183,7 @@ public sealed class MxAccessGatewayService(
// call is cancelled, instead of leaving it to the transport's own timeout.
await responseStream.WriteAsync(publicEvent, context.CancellationToken).ConfigureAwait(false);
metrics.RecordEventStreamSend(
publicEvent.Family.ToString(),
MxEventFamilyNames.Get(publicEvent.Family),
Stopwatch.GetElapsedTime(sendStartTimestamp));
}
}
@@ -0,0 +1,28 @@
using System.Collections.Frozen;
using ZB.MOM.WW.MxGateway.Contracts.Proto;
namespace ZB.MOM.WW.MxGateway.Server.Metrics;
/// <summary>
/// Cached <see cref="MxEventFamily"/> names for the per-event metric tags.
/// </summary>
/// <remarks>
/// <see cref="Enum.ToString()"/> allocates a new string on every call, and the event-received
/// and stream-send metrics tag every event with its family — once per event at the worker
/// boundary and once per event per subscriber on the stream. The names are fixed by the
/// contract, so they are computed once here. A value the contract does not define falls back
/// to <see cref="Enum.ToString()"/>, which is the same string the uncached call produced.
/// </remarks>
public static class MxEventFamilyNames
{
private static readonly FrozenDictionary<MxEventFamily, string> Names =
Enum.GetValues<MxEventFamily>()
.Distinct()
.ToFrozenDictionary(family => family, family => family.ToString());
/// <summary>Gets the name of an event family.</summary>
/// <param name="family">The event family.</param>
/// <returns>The family's name, identical to <see cref="Enum.ToString()"/>.</returns>
public static string Get(MxEventFamily family) =>
Names.TryGetValue(family, out string? name) ? name : family.ToString();
}
@@ -692,7 +692,7 @@ public sealed class WorkerClient : IWorkerClient
{
if (workerEvent.Event is not null)
{
_metrics?.EventReceived(SessionId, workerEvent.Event.Family.ToString());
_metrics?.EventReceived(SessionId, MxEventFamilyNames.Get(workerEvent.Event.Family));
}
if (_eventStaging.Writer.TryWrite(workerEvent))
@@ -0,0 +1,41 @@
using ZB.MOM.WW.MxGateway.Contracts.Proto;
using ZB.MOM.WW.MxGateway.Server.Metrics;
namespace ZB.MOM.WW.MxGateway.Tests.Metrics;
public sealed class MxEventFamilyNamesTests
{
/// <summary>
/// Every defined family maps to exactly the name <see cref="Enum.ToString()"/> produces, so
/// switching the per-event metric tags to the cached lookup changes no exported tag value.
/// </summary>
[Fact]
public void Get_ForEveryDefinedFamily_MatchesEnumToString()
{
foreach (MxEventFamily family in Enum.GetValues<MxEventFamily>())
{
Assert.Equal(family.ToString(), MxEventFamilyNames.Get(family));
}
}
/// <summary>A defined family returns the same cached string instance on every call (no per-event allocation).</summary>
[Fact]
public void Get_ForDefinedFamily_ReturnsCachedInstance()
{
Assert.Same(
MxEventFamilyNames.Get(MxEventFamily.OnDataChange),
MxEventFamilyNames.Get(MxEventFamily.OnDataChange));
}
/// <summary>
/// A numeric value the contract does not define (a newer worker, or a malformed frame) still
/// produces the <see cref="Enum.ToString()"/> form rather than throwing.
/// </summary>
[Fact]
public void Get_ForUndefinedValue_FallsBackToToString()
{
MxEventFamily undefined = (MxEventFamily)987654;
Assert.Equal(undefined.ToString(), MxEventFamilyNames.Get(undefined));
}
}