perf(adminui): /hosts rebuilds driver rows on the tick, not per health publish (R04 P4)
OnSnapshotChanged used to re-project every cluster group and re-render the whole page for each driver publish, per open tab. It now only marks a CoalescingDirtyFlag; the 5 s tick rebuilds when the flag is set or the config load changed (or evicted) anything. Refs #745 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
This commit is contained in:
@@ -347,6 +347,9 @@ else
|
||||
// next one. A manual Refresh landing on top of an in-flight tick is a no-op by the same guard.
|
||||
private int _tickRunning;
|
||||
|
||||
// Set by OnSnapshotChanged (bridge actor thread), consumed by TickAsync.
|
||||
private readonly CoalescingDirtyFlag _driversDirty = new();
|
||||
|
||||
// Page-lifetime cancellation: cancelled in DisposeAsync() so a tick in flight when the circuit
|
||||
// goes away aborts its queries instead of running them against a component that's gone.
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
@@ -372,8 +375,11 @@ else
|
||||
try
|
||||
{
|
||||
Refresh();
|
||||
await LoadConfigAsync();
|
||||
RebuildDriverGroups();
|
||||
var configChanged = await LoadConfigAsync();
|
||||
// Re-project the driver rows only when a health snapshot or the config actually changed
|
||||
// since the last tick (R04 P4). The snapshot handler just marks the flag, so N drivers
|
||||
// publishing every 5 s cost one rebuild per tick per tab, not N rebuilds + N full renders.
|
||||
if (_driversDirty.TryConsume() | configChanged) RebuildDriverGroups();
|
||||
StateHasChanged();
|
||||
}
|
||||
catch (Exception ex) when (ex is ObjectDisposedException or OperationCanceledException)
|
||||
@@ -397,6 +403,8 @@ else
|
||||
await Task.Yield();
|
||||
Refresh();
|
||||
await LoadConfigAsync();
|
||||
// A manual refresh always rebuilds; clear the flag so the next tick does not rebuild again.
|
||||
_driversDirty.TryConsume();
|
||||
RebuildDriverGroups();
|
||||
}
|
||||
catch (Exception ex) when (ex is ObjectDisposedException or OperationCanceledException)
|
||||
@@ -418,8 +426,11 @@ else
|
||||
// Load the per-cluster node + driver-instance config from the ConfigDB. Kept cheap and
|
||||
// swallow-on-failure so a transient DB hiccup dims the enrichment (rows fall back to id/—)
|
||||
// without crashing the page. The live health feed (the snapshot store) is independent of this.
|
||||
private async Task LoadConfigAsync()
|
||||
// Returns whether the loaded config differs from the previous load (records compare by value).
|
||||
private async Task<bool> LoadConfigAsync()
|
||||
{
|
||||
var previousNodes = _nodes;
|
||||
var previousInstances = _instances;
|
||||
try
|
||||
{
|
||||
await using var db = await DbFactory.CreateDbContextAsync(_cts.Token);
|
||||
@@ -437,8 +448,12 @@ else
|
||||
var configured = _instances
|
||||
.Select(i => i.DriverInstanceId)
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
DriverStore.RetainOnly(configured);
|
||||
var evicted = DriverStore.RetainOnly(configured);
|
||||
ResilienceStore.RetainOnly(configured);
|
||||
|
||||
return evicted > 0
|
||||
|| !_nodes.SequenceEqual(previousNodes)
|
||||
|| !_instances.SequenceEqual(previousInstances);
|
||||
}
|
||||
catch (Exception ex) when (ex is OperationCanceledException or ObjectDisposedException)
|
||||
{
|
||||
@@ -456,6 +471,7 @@ else
|
||||
Logger.LogWarning(ex, "/hosts: failed to load driver-instance config; rows show ids only.");
|
||||
_nodes = new();
|
||||
_instances = new();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -483,11 +499,12 @@ else
|
||||
return (null, null);
|
||||
}
|
||||
|
||||
// Raised by the snapshot store on the bridge actor's thread for every driver instance. Rebuild
|
||||
// from the cached config (cheap — no DB hit) and marshal onto the render sync context. A
|
||||
// brand-new driver shows by id until the next config reload (timer/manual Refresh) enriches it.
|
||||
private void OnSnapshotChanged(DriverHealthChanged _)
|
||||
=> InvokeAsync(() => { RebuildDriverGroups(); StateHasChanged(); });
|
||||
// Raised by the snapshot store on the bridge actor's thread for every driver instance. It only
|
||||
// marks the rows dirty: the 5 s tick does the rebuild + render (R04 P4). Rendering here cost one
|
||||
// full re-projection and full-page diff per driver publish per open tab (200 drivers ≈ 40/s).
|
||||
// A health change therefore shows within one tick; the open Controls dialog's DriverStatusPanel
|
||||
// subscribes to the store itself and stays live.
|
||||
private void OnSnapshotChanged(DriverHealthChanged _) => _driversDirty.Mark();
|
||||
|
||||
private void Refresh()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Hosts;
|
||||
|
||||
/// <summary>
|
||||
/// A thread-safe "something changed since you last looked" flag. A high-rate producer (the driver
|
||||
/// health store raises once per driver per publish, on the bridge actor's thread) calls
|
||||
/// <see cref="Mark"/>, which is cheap and never renders; the page's own periodic tick calls
|
||||
/// <see cref="TryConsume"/> and does the expensive re-projection + render at most once per tick,
|
||||
/// however many marks arrived in between.
|
||||
/// </summary>
|
||||
public sealed class CoalescingDirtyFlag
|
||||
{
|
||||
private int _dirty;
|
||||
|
||||
/// <summary>Records that the underlying data changed. Safe from any thread.</summary>
|
||||
public void Mark() => Volatile.Write(ref _dirty, 1);
|
||||
|
||||
/// <summary>Returns whether anything was marked since the last call, and clears the flag.</summary>
|
||||
/// <returns><see langword="true"/> when at least one <see cref="Mark"/> happened since the last consume.</returns>
|
||||
public bool TryConsume() => Interlocked.Exchange(ref _dirty, 0) == 1;
|
||||
}
|
||||
Reference in New Issue
Block a user