perf(worker): exact-format timestamp parse and compiled status-field accessors on the event path
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user