perf(opcua): surgical in-place tag-attribute writes (Writable/Historizing) avoid rebuild (F10b)
This commit is contained in:
@@ -84,41 +84,90 @@ public sealed class Phase7Applier
|
||||
// Any add / remove / in-place CHANGE of Equipment, ScriptedAlarm, Equipment tag, or Equipment
|
||||
// VirtualTag topology requires a real address-space rebuild — the materialise passes re-derive
|
||||
// every node from the composition, so a changed-only deploy (e.g. a renamed equipment, a
|
||||
// re-severitied alarm, a flipped tag dataType/Writable) must still rebuild or the running server
|
||||
// keeps the stale node.
|
||||
// re-severitied alarm, a flipped tag dataType) must still rebuild or the running server keeps
|
||||
// the stale node.
|
||||
// ChangedDrivers is deliberately EXCLUDED: a driver-instance config change doesn't touch the
|
||||
// address-space topology — it routes through DriverHostActor's spawn-plan in Runtime, which
|
||||
// re-spawns the affected driver actor without re-materialising any nodes.
|
||||
// F10b: a CHANGED VirtualTag whose ONLY differences are Expression/DependencyRefs/Historize is
|
||||
// node-IRRELEVANT (see VtagDeltaIsNodeIrrelevant) — its materialised node is byte-identical and
|
||||
// the vtag engine adopts those edits via VirtualTagHostActor's INDEPENDENT respawn
|
||||
// (DriverHostActor → ApplyVirtualTags), so it skips the rebuild and PRESERVES every client's
|
||||
// server-wide subscriptions. Any structural / node-affecting vtag change (Name/FolderPath/
|
||||
// DataType) — or any non-vtag change anywhere — still forces a full rebuild (safe default).
|
||||
var needsRebuild =
|
||||
// F10b (vtag skip): a CHANGED VirtualTag whose ONLY differences are Expression/DependencyRefs/
|
||||
// Historize is node-IRRELEVANT (see VtagDeltaIsNodeIrrelevant) — its materialised node is
|
||||
// byte-identical and the vtag engine adopts those edits via VirtualTagHostActor's INDEPENDENT
|
||||
// respawn (DriverHostActor → ApplyVirtualTags), so it skips the rebuild and PRESERVES every
|
||||
// client's server-wide subscriptions. Any structural / node-affecting vtag change (Name/
|
||||
// FolderPath/DataType) — or any non-vtag change anywhere — still forces a full rebuild.
|
||||
// F10b (surgical tag write): a CHANGED equipment tag whose ONLY differences are Writable /
|
||||
// IsHistorized / HistorianTagname (a plain value variable — no alarm condition node) can be
|
||||
// updated IN PLACE on the existing node via ISurgicalAddressSpaceSink.UpdateTagAttributes
|
||||
// (see TagDeltaIsSurgicalEligible), again avoiding the full rebuild and preserving subscriptions.
|
||||
// Any other tag difference (DataType/IsArray/ArrayLength/FullName/identity/alarm) — or a sink
|
||||
// that lacks the surgical capability, or a node that turns out missing — falls back to a full
|
||||
// rebuild (safe default).
|
||||
var structuralRebuild =
|
||||
plan.AddedEquipment.Count > 0 || plan.RemovedEquipment.Count > 0 || plan.ChangedEquipment.Count > 0 ||
|
||||
plan.AddedAlarms.Count > 0 || plan.RemovedAlarms.Count > 0 || plan.ChangedAlarms.Count > 0 ||
|
||||
plan.AddedEquipmentTags.Count > 0 || plan.RemovedEquipmentTags.Count > 0 || plan.ChangedEquipmentTags.Count > 0 ||
|
||||
plan.AddedEquipmentTags.Count > 0 || plan.RemovedEquipmentTags.Count > 0 ||
|
||||
plan.ChangedEquipmentTags.Any(d => !TagDeltaIsSurgicalEligible(d)) ||
|
||||
plan.AddedEquipmentVirtualTags.Count > 0 || plan.RemovedEquipmentVirtualTags.Count > 0 ||
|
||||
plan.ChangedEquipmentVirtualTags.Any(d => !VtagDeltaIsNodeIrrelevant(d));
|
||||
|
||||
if (needsRebuild)
|
||||
var surgicalTagDeltas = plan.ChangedEquipmentTags.Where(TagDeltaIsSurgicalEligible).ToList();
|
||||
var rebuilt = false;
|
||||
|
||||
if (structuralRebuild)
|
||||
{
|
||||
try
|
||||
SafeRebuild();
|
||||
rebuilt = true;
|
||||
}
|
||||
else if (surgicalTagDeltas.Count > 0)
|
||||
{
|
||||
if (_sink is ISurgicalAddressSpaceSink surgical)
|
||||
{
|
||||
_sink.RebuildAddressSpace();
|
||||
var allApplied = true;
|
||||
foreach (var d in surgicalTagDeltas)
|
||||
{
|
||||
// Compute the node id + writable + historian EXACTLY as MaterialiseEquipmentTags would
|
||||
// so the in-place update matches what a rebuild would have produced.
|
||||
var nodeId = EquipmentNodeIds.Variable(d.Current.EquipmentId, d.Current.FolderPath, d.Current.Name);
|
||||
var writable = d.Current.Writable && !d.Current.IsArray;
|
||||
var historian = d.Current.IsHistorized
|
||||
? (string.IsNullOrWhiteSpace(d.Current.HistorianTagname) ? d.Current.FullName : d.Current.HistorianTagname)
|
||||
: null;
|
||||
bool ok;
|
||||
try { ok = surgical.UpdateTagAttributes(nodeId, writable, historian); }
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Phase7Applier: surgical UpdateTagAttributes threw for {Node}", nodeId);
|
||||
ok = false;
|
||||
}
|
||||
if (!ok) { allApplied = false; break; }
|
||||
}
|
||||
if (!allApplied) { SafeRebuild(); rebuilt = true; }
|
||||
}
|
||||
catch (Exception ex)
|
||||
else
|
||||
{
|
||||
_logger.LogError(ex, "Phase7Applier: sink.RebuildAddressSpace threw");
|
||||
// Sink lacks the surgical capability ⇒ rebuild (safe default).
|
||||
SafeRebuild();
|
||||
rebuilt = true;
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"Phase7Applier: applied plan (added={Added}, removed={Removed}, changed={Changed}, rebuild={Rebuild})",
|
||||
addedCount, removedCount, changedCount, needsRebuild);
|
||||
"Phase7Applier: applied plan (added={Added}, removed={Removed}, changed={Changed}, surgicalTags={Surgical}, rebuild={Rebuild})",
|
||||
addedCount, removedCount, changedCount, rebuilt ? 0 : surgicalTagDeltas.Count, rebuilt);
|
||||
|
||||
return new Phase7ApplyOutcome(removedCount, addedCount, changedCount, needsRebuild);
|
||||
return new Phase7ApplyOutcome(removedCount, addedCount, changedCount, rebuilt);
|
||||
}
|
||||
|
||||
private void SafeRebuild()
|
||||
{
|
||||
try
|
||||
{
|
||||
_sink.RebuildAddressSpace();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Phase7Applier: sink.RebuildAddressSpace threw");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -340,6 +389,20 @@ public sealed class Phase7Applier
|
||||
Historize = d.Current.Historize,
|
||||
}).Equals(d.Current);
|
||||
|
||||
// F10b: a CHANGED equipment tag whose ONLY differences are Writable / IsHistorized / HistorianTagname
|
||||
// (a plain value variable — no alarm condition node) can be updated IN PLACE on the existing node via
|
||||
// ISurgicalAddressSpaceSink.UpdateTagAttributes, avoiding a full rebuild (preserving subscriptions).
|
||||
// DataType / IsArray / ArrayLength / FullName / DriverInstanceId / identity / alarm differences fall
|
||||
// through to a rebuild — the override-unequal default also covers any future field.
|
||||
private static bool TagDeltaIsSurgicalEligible(Phase7Plan.EquipmentTagDelta d) =>
|
||||
d.Previous.Alarm is null && d.Current.Alarm is null &&
|
||||
(d.Previous with
|
||||
{
|
||||
Writable = d.Current.Writable,
|
||||
IsHistorized = d.Current.IsHistorized,
|
||||
HistorianTagname = d.Current.HistorianTagname,
|
||||
}).Equals(d.Current);
|
||||
|
||||
/// <summary>The "no-event" condition state written to a removed equipment / alarm node before the
|
||||
/// rebuild tears it down: inactive, acked, confirmed, enabled, unshelved, severity 0, empty message.
|
||||
/// Drives Retain to false so a removed condition stops replaying on ConditionRefresh.</summary>
|
||||
|
||||
Reference in New Issue
Block a user