From f5a9933dd88373454ef679e67d52e0c7d5ab1709 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Mon, 10 Aug 2026 17:52:23 +0200 Subject: [PATCH] docs(findings): the Beszel coalescing fix measured zero, and was reverted Second correction in the same area. Having found that three Beszel widgets account for 82% of idle DOM churn, the obvious fix was to batch their subscription updates instead of calling setState per record. Implemented, built, measured: DOM mutations / 45s 733 -> 748 React commits / 45s 55 -> 55 Identical. Beszel emits roughly one record per second and the flush interval was also one second, so each flush batched exactly one record. Coalescing at the arrival rate does nothing - arithmetic that should have come before the implementation. That also exposes a unit error in the previous commit's numbers: DOM mutations are not re-renders. The real rates are 1.22 React commits/sec for the whole board and 13.3 DOM mutations per commit, because a chart redraw touches many nodes. The stats widget's "10.4 mutations/sec" is ~0.8 renders/sec - three live-monitoring widgets each refreshing about once a second, which is what they exist to do. So 73 commits/min is not pathological, it is the feature working, and there is no defect to fix here. Reverted rather than shipped: keeping speculative complexity that measured zero - an extra timer plus unmount and system-change teardown - is the trade this work rejects everywhere else. What survives is narrower: idle churn is widget-driven rather than app-shell driven, and legitimate. The 650 MB browser heap therefore comes from interaction and multi-tab use, not idle churn, and remains unexplained. --- PHASE2-FINDINGS.md | 43 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/PHASE2-FINDINGS.md b/PHASE2-FINDINGS.md index cdb340112..64f73ae26 100644 --- a/PHASE2-FINDINGS.md +++ b/PHASE2-FINDINGS.md @@ -323,15 +323,42 @@ On the 28-item board, idle for 45 s: | downloads | 4 | 1% | | (outside any widget) | 93 | 13% | -**Three Beszel widgets account for 82% of all idle churn**, one of them for 64% on its own, at -10.4 mutations per second. +**Three Beszel widgets account for 82% of all idle churn**, one of them for 64% on its own. -The mechanism is in `packages/widgets/src/beszel/_shared/use-live-stats.ts`: the tRPC -subscription's `onData` called `setSystemStats((prev) => [...prev, record])` — one React state -update per record, each allocating a fresh 60-element array. Beszel pushes one record per second -*per system*, plus one per container, so a widget watching several systems drives a steady stream -of renders. Records are now accumulated outside React and flushed once a second, which is the -fastest a 60-second sliding-window chart can meaningfully show. Nothing is dropped. +### And then the second correction: there is no defect here + +The obvious next step was to coalesce the updates. `use-live-stats.ts` calls +`setSystemStats((prev) => [...prev, record])` in the subscription's `onData` — one React state +update per record, each allocating a fresh 60-element array. Buffering records outside React and +flushing once a second looked like an easy large win. + +**It measured exactly zero, twice over:** + +| | before | after | +| --- | --- | --- | +| DOM mutations / 45 s | 733 | 748 | +| React commits / 45 s | **55** | **55** | + +Identical commit counts. The reason is arithmetic I should have done first: Beszel emits roughly +one record per second, and the flush interval was also one second, so each flush batched precisely +one record. Coalescing at the arrival rate does nothing. + +That also exposes a unit error in the numbers above. **DOM mutations are not re-renders.** The real +figures are: + +- **1.22 React commits per second** for the whole board +- **13.3 DOM mutations per commit** — a chart redraw touches many nodes + +So "10.4 mutations per second" for the stats widget was ~0.8 *renders* per second. Three +live-monitoring widgets each refreshing about once a second, which is precisely what they exist to +do. **73 commits/min is not pathological; it is the feature working.** The change was reverted +rather than shipped, because keeping speculative complexity that measured zero — an extra timer, +plus unmount and system-change teardown to get right — is exactly the trade this work has rejected +elsewhere. + +The residual conclusion still stands and is narrower than before: idle churn is widget-driven, not +app-shell driven, and it is legitimate. The 650 MB browser heap therefore comes from *interaction* +and multi-tab use, not from idle churn, and remains unexplained. `useTimeAgo` was calling `setState` every second even when the rendered label was unchanged ("3 minutes ago" holds for a minute), so it now compares before setting. This