perf(worker): exact-format timestamp parse and compiled status-field accessors on the event path

This commit is contained in:
Joseph Doherty
2026-08-15 16:59:52 -04:00
parent f4a6cb1db2
commit 94fdc18c3c
4 changed files with 386 additions and 36 deletions
@@ -1,4 +1,5 @@
using System;
using System.Collections.Concurrent;
using System.Globalization;
using Google.Protobuf.WellKnownTypes;
using ZB.MOM.WW.MxGateway.Contracts.Proto;
@@ -9,6 +10,18 @@ namespace ZB.MOM.WW.MxGateway.Worker.MxAccess;
/// <summary>Maps MXAccess COM events to protobuf MxEvent messages.</summary>
public sealed class MxAccessEventMapper
{
/// <summary>
/// Per-culture cache of the explicit timestamp formats
/// <see cref="TryParseSourceTimestamp"/> tries before the general parse
/// chain. Keyed by <see cref="CultureInfo"/> because the format set is
/// derived from the culture's own patterns and the worker host's culture
/// is fixed in practice, so the derivation runs once.
/// </summary>
private static readonly ConcurrentDictionary<CultureInfo, string[]> TimestampFormatCache = new();
/// <summary>Cached factory delegate so the dictionary lookup allocates nothing on the event path.</summary>
private static readonly Func<CultureInfo, string[]> TimestampFormatFactory = BuildTimestampFormats;
private readonly VariantConverter variantConverter;
private readonly MxStatusProxyConverter statusProxyConverter;
@@ -353,6 +366,23 @@ public sealed class MxAccessEventMapper
/// wall-clock UTC). The parsed value is therefore taken as local time
/// and converted to UTC. Tries the worker host's culture first
/// (MXAccess formats with the host locale), then the invariant culture.
/// <para>
/// PERF: every OnDataChange carries one of these strings, and the
/// general <see cref="DateTime.TryParse(string, IFormatProvider, DateTimeStyles, out DateTime)"/>
/// has to walk the culture's whole pattern set to recognize it. The
/// shapes MXAccess actually emits are tried first with
/// <see cref="DateTime.TryParseExact(string, string[], IFormatProvider, DateTimeStyles, out DateTime)"/>.
/// The formats are <em>derived from the current culture's own</em>
/// short-date and long-time patterns (see
/// <see cref="BuildTimestampFormats"/>) rather than hard-coded to the
/// US shapes seen in captures: a hard-coded <c>M/d/yyyy</c> would
/// reorder month and day on a day-first host relative to what the
/// general parse below returns. Deriving them means any string the
/// exact parse accepts is one the culture-aware general parse would
/// have accepted with the same value, so the fast path is a shortcut
/// and never a different answer; anything it does not match falls
/// through to the unchanged two-stage chain.
/// </para>
/// </summary>
/// <param name="text">The MXAccess timestamp string.</param>
/// <param name="utc">The parsed UTC timestamp on success.</param>
@@ -366,7 +396,10 @@ public sealed class MxAccessEventMapper
}
const DateTimeStyles styles = DateTimeStyles.AssumeLocal | DateTimeStyles.AdjustToUniversal;
if (DateTime.TryParse(text, CultureInfo.CurrentCulture, styles, out DateTime parsed)
CultureInfo culture = CultureInfo.CurrentCulture;
string[] formats = TimestampFormatCache.GetOrAdd(culture, TimestampFormatFactory);
if (DateTime.TryParseExact(text, formats, culture, styles, out DateTime parsed)
|| DateTime.TryParse(text, culture, styles, out parsed)
|| DateTime.TryParse(text, CultureInfo.InvariantCulture, styles, out parsed))
{
utc = DateTime.SpecifyKind(parsed, DateTimeKind.Utc);
@@ -376,6 +409,37 @@ public sealed class MxAccessEventMapper
return false;
}
/// <summary>
/// Builds the explicit timestamp formats for a culture: the culture's
/// short date plus its long time, and — first, because it is the shape
/// MXAccess actually fires — the same pattern with the millisecond
/// fraction MXAccess appends to the seconds ("3/26/2026 1:38:22.907 PM"
/// under en-US). No culture publishes a long-time pattern with
/// milliseconds, so the fractional sibling is derived by inserting
/// <c>.fff</c> after the seconds specifier; a pattern without a
/// two-digit seconds specifier, or one that already carries a fraction,
/// gets only the published pattern.
/// </summary>
/// <param name="culture">Culture whose patterns the formats are derived from.</param>
/// <returns>The formats to try, most likely first.</returns>
private static string[] BuildTimestampFormats(CultureInfo culture)
{
DateTimeFormatInfo formatInfo = culture.DateTimeFormat;
string shortDate = formatInfo.ShortDatePattern;
string longTime = formatInfo.LongTimePattern;
int secondsIndex = longTime.IndexOf("ss", StringComparison.Ordinal);
if (secondsIndex >= 0 && longTime.IndexOf('f') < 0 && longTime.IndexOf('F') < 0)
{
return
[
shortDate + " " + longTime.Insert(secondsIndex + 2, ".fff"),
shortDate + " " + longTime,
];
}
return [shortDate + " " + longTime];
}
private MxArray ConvertBufferedArray(
object? value,
MxDataType expectedElementDataType)