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.
This commit is contained in:
1 file changed
+35
-8
+35
-8
@@ -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
|
||||
|
||||
Reference in new issue
Block a user