perf(mtconnect): stream sample-chunk parsing through XmlReader; no per-chunk document tree

This commit is contained in:
Joseph Doherty
2026-08-17 03:57:16 -04:00
parent d884934593
commit cda82c074e
3 changed files with 415 additions and 83 deletions
@@ -1,4 +1,5 @@
using System.Globalization;
using System.Xml;
using System.Xml.Linq;
namespace ZB.MOM.WW.OtOpcUa.Driver.MTConnect;
@@ -33,6 +34,23 @@ internal static class MTConnectXml
public static bool IsNamed(XElement element, string localName) =>
string.Equals(element.Name.LocalName, localName, StringComparison.Ordinal);
/// <summary>
/// Does the element the reader is positioned on have the given local name, whatever its
/// namespace?
/// </summary>
/// <remarks>
/// <see cref="MTConnectStreamsParser"/> reads its documents through a forward-only
/// <see cref="XmlReader"/> rather than a materialised tree — one sample chunk is read once
/// and thrown away, so building an <see cref="XDocument"/> for it is pure waste. The reader
/// overloads here exist so that going forward-only did not fork the two silent rules in the
/// type-level remarks: both flavours match on the local name, read attributes unqualified,
/// and produce the same error text.
/// </remarks>
/// <param name="reader">A reader positioned on an element.</param>
/// <param name="localName">The unqualified name to match.</param>
public static bool IsNamed(XmlReader reader, string localName) =>
string.Equals(reader.LocalName, localName, StringComparison.Ordinal);
/// <summary>Direct element children of <paramref name="parent"/> with the given local name.</summary>
/// <param name="parent">The element whose children to filter.</param>
/// <param name="localName">The unqualified name to match.</param>
@@ -52,19 +70,46 @@ internal static class MTConnectXml
return string.IsNullOrWhiteSpace(value) ? null : value;
}
/// <summary>
/// Reads an unqualified attribute off the element the reader is positioned on, normalizing a
/// present-but-empty value to <c>null</c>. See <see cref="IsNamed(XmlReader, string)"/> for
/// why the reader overloads exist.
/// </summary>
/// <remarks>
/// <see cref="XmlReader.GetAttribute(string)"/> matches on the attribute's <i>qualified</i>
/// name, so a prefixed <c>x:dataItemId</c> does not answer to <c>dataItemId</c> — exactly
/// what rule 2 in the type-level remarks requires, and what <see cref="XElement.Attribute"/>
/// does for a namespace-free <see cref="XName"/>.
/// </remarks>
/// <param name="reader">A reader positioned on an element.</param>
/// <param name="name">The unqualified attribute name.</param>
public static string? OptionalAttribute(XmlReader reader, string name)
{
var value = reader.GetAttribute(name);
return string.IsNullOrWhiteSpace(value) ? null : value;
}
/// <summary>Reads a required unqualified attribute, or throws naming the owner and the attribute.</summary>
/// <param name="element">The element carrying the attribute.</param>
/// <param name="name">The unqualified attribute name.</param>
/// <param name="owner">How to describe the element in the error message.</param>
/// <param name="subject">How to describe the document in the error message (e.g. "/probe response").</param>
/// <exception cref="InvalidDataException">The attribute is absent or blank.</exception>
public static string RequiredAttribute(XElement element, string name, string owner, string subject)
{
var value = OptionalAttribute(element, name);
public static string RequiredAttribute(XElement element, string name, string owner, string subject) =>
OptionalAttribute(element, name) ?? throw MissingAttribute(name, owner, subject);
return value ?? throw new InvalidDataException(
$"MTConnect {subject} is malformed: {owner} has no '{name}' attribute.");
}
/// <summary>
/// Reads a required unqualified attribute off the element the reader is positioned on, or
/// throws naming the owner and the attribute.
/// </summary>
/// <param name="reader">A reader positioned on an element.</param>
/// <param name="name">The unqualified attribute name.</param>
/// <param name="owner">How to describe the element in the error message.</param>
/// <param name="subject">How to describe the document in the error message.</param>
/// <exception cref="InvalidDataException">The attribute is absent or blank.</exception>
public static string RequiredAttribute(XmlReader reader, string name, string owner, string subject) =>
OptionalAttribute(reader, name) ?? throw MissingAttribute(name, owner, subject);
/// <summary>Reads an optional unqualified attribute as an <see cref="int"/>.</summary>
/// <param name="element">The element carrying the attribute.</param>
@@ -95,10 +140,28 @@ internal static class MTConnectXml
/// <param name="owner">How to describe the element in the error message.</param>
/// <param name="subject">How to describe the document in the error message.</param>
/// <exception cref="InvalidDataException">The attribute is absent, blank, or not an integer.</exception>
public static long RequiredLongAttribute(XElement element, string name, string owner, string subject)
{
var raw = RequiredAttribute(element, name, owner, subject);
public static long RequiredLongAttribute(XElement element, string name, string owner, string subject) =>
AsLong(RequiredAttribute(element, name, owner, subject), name, owner, subject);
/// <summary>
/// Reads a required unqualified attribute off the element the reader is positioned on, as a
/// <see cref="long"/>.
/// </summary>
/// <param name="reader">A reader positioned on an element.</param>
/// <param name="name">The unqualified attribute name.</param>
/// <param name="owner">How to describe the element in the error message.</param>
/// <param name="subject">How to describe the document in the error message.</param>
/// <exception cref="InvalidDataException">The attribute is absent, blank, or not an integer.</exception>
public static long RequiredLongAttribute(XmlReader reader, string name, string owner, string subject) =>
AsLong(RequiredAttribute(reader, name, owner, subject), name, owner, subject);
/// <summary>The one spelling of "that attribute is missing", shared by both flavours.</summary>
private static InvalidDataException MissingAttribute(string name, string owner, string subject) =>
new($"MTConnect {subject} is malformed: {owner} has no '{name}' attribute.");
/// <summary>The one spelling of "that attribute is not a number", shared by both flavours.</summary>
private static long AsLong(string raw, string name, string owner, string subject)
{
if (!long.TryParse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
{
throw new InvalidDataException(