perf(ui): shared KPI cache, live-cache-backed alarm summary, coalesced debug renders

This commit is contained in:
Joseph Doherty
2026-08-14 20:53:38 -04:00
parent ee193cd2bb
commit 8c0b36b2fa
19 changed files with 1554 additions and 64 deletions
@@ -1,5 +1,6 @@
@page "/monitoring/alarms"
@attribute [Authorize(Policy = ZB.MOM.WW.ScadaBridge.Security.AuthorizationPolicies.RequireDeployment)]
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared
@using ZB.MOM.WW.ScadaBridge.CentralUI.Services
@using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites
@@ -196,15 +197,25 @@
@onkeydown='e => OnHeaderKeyDown(e, "severity")' @onkeydown:preventDefault="_preventHeaderDefault">Severity @SortGlyph("severity")</th>
</tr>
</thead>
@* Row markup is identical on both paths — the same <tr>, the same cells,
the same data-test hook. Only WHO enumerates it differs: a plain
foreach for the ordinary case, and Virtualize once the flat table
gets long enough that rendering every row per delta dominates the
circuit (arch-review WP2.4). Virtualize needs <tr> spacers or the
browser hoists its <div>s out of the table and the layout collapses. *@
<tbody>
@foreach (var row in _visibleRows)
@if (_visibleRows.Count <= VirtualizeThreshold)
{
<tr data-test="alarm-summary-row">
<td class="font-monospace small">@row.InstanceUniqueName</td>
<td>@row.Alarm.AlarmName</td>
<td><AlarmStateBadges Alarm="row.Alarm" /></td>
<td class="text-end font-monospace">@row.Alarm.Condition.Severity</td>
</tr>
@foreach (var row in _visibleRows)
{
@AlarmRow(row)
}
}
else
{
<Virtualize Items="_visibleRows" Context="row" ItemSize="37" SpacerElement="tr">
@AlarmRow(row)
</Virtualize>
}
</tbody>
</table>
@@ -222,7 +233,23 @@
// P4 (arch-review): the filtered+sorted view is memoized rather than recomputed
// on every render. RecomputeVisibleRows() refreshes it whenever the inputs change
// (a fresh snapshot in RefreshAsync, a filter change via @bind:after, or a sort).
private IReadOnlyList<AlarmSummaryRow> _visibleRows = Array.Empty<AlarmSummaryRow>();
// Concrete List because Virtualize binds ICollection<T>, not IReadOnlyList<T>.
private List<AlarmSummaryRow> _visibleRows = new();
// Above this many visible rows the flat table switches to Virtualize. Below it the
// plain foreach is cheaper than the virtualization machinery (and needs no JS), and
// an operator filtered down to a handful of alarms should never pay for either.
private const int VirtualizeThreshold = 150;
// One row template, shared by the plain and virtualized paths so the two can never
// drift visually.
private RenderFragment<AlarmSummaryRow> AlarmRow => row =>
@<tr data-test="alarm-summary-row">
<td class="font-monospace small">@row.InstanceUniqueName</td>
<td>@row.Alarm.AlarmName</td>
<td><AlarmStateBadges Alarm="row.Alarm" /></td>
<td class="text-end font-monospace">@row.Alarm.Condition.Severity</td>
</tr>;
private IReadOnlyList<string> _notReporting = Array.Empty<string>();
private AlarmRollup _rollup = new(0, 0, 0, new Dictionary<AlarmKind, int>());