Demos

Stream inspector

Your own stream, live: what the hub sends this tab, what zstd makes of it, and what the memory budget gave its encoder.

live

Your stream

connecting quiet for 0 s 0 frames a second

…

Setting from the budget
waiting
Reserved for your encoder
waiting

The last frame, events to wire

On the wire, frame by frame

Events sent
On the wire
Ratio so far
Heartbeats

The site

0 streams open on this site

nothing reserved of a limit of : this process has no memory limit, so the budget only counts

The payload

Size
Churn

256 rows, 1,536 cells. Every 250 ms 8 cells change.

nodereq/sp50 msp99 mscpu %heap MiBerrors
node-0014,4782.989202210
node-0022,1302.026813130
node-0034,3012.510020967
node-0043,3703.596393800
node-0051,7342.6105793596
node-0063,3460.93112603
node-0071,5140.763102470
node-0082,0973.254911280
node-0091,6091.530771850
node-0101,8331.367731270
node-0112,6513.752271290
node-0124,6483.6138772750
node-0133,4251.1102442070
node-0144,1553.9124363819
node-0152,5013.039352660
node-0163,5464.466443840
node-0173,7753.7140532720
node-0181,5862.772441720
node-0191,2962.732114019
node-0203,4772.816711957
node-0213401.98223825
node-0223,5072.910303080
node-0234,3193.386523560
node-0245071.345253300
node-0252,8093.94473708
node-0263,8032.1798460
node-0272,5660.926352200
node-0281,2673.03352880
node-0291,9804.7130484400
node-0303,3433.568613930
node-0311,8943.770111760
node-0322,8892.959173530
node-0334,4271.468612220
node-0341,2772.317163095
node-0354983.158413970
node-0362,5022.644211860
node-0373,9164.436592600
node-0381,1881.547774000
node-0392,8073.8574630
node-0402,6454.470742160
node-0413051.410282950
node-0423,6581.7106812202
node-0434,6762.3142473760
node-0441,2873.55412620
node-0454,2182.96140550
node-0462,6324.674521150
node-0475544.180414040
node-0483,3582.446674160
node-0494973.963823480
node-0501,7512.6111511210
node-0513,8811.660751581
node-0524,3552.617471880
node-0534,7811.424353150
node-0543,0121.861333350
node-0552,5741.119552620
node-0562,8061.657242820
node-0572,3232.96333120
node-0583,2810.758124140
node-0591,8740.963101900
node-0602,4913.95765710
node-0612,7090.88546770
node-0622,4613.2100833260
node-0631,2331.431614040
node-0649842.4102323820
node-0654,2181.965293720
node-0662,7582.33111842
node-0671,8052.427122370
node-0681,8773.322202320
node-0693,7044.399182960
node-0702,4972.788611540
node-0714,8600.9964966
node-0724,5160.939492820
node-0735591.6128231267
node-0742,9853.336591968
node-0753142.683683970
node-0764,8643.61123960
node-0775544.343752970
node-0783,3060.760694290
node-0792,6931.137132810
node-0809082.150333440
node-0814,0564.754841427
node-0821,1953.111113800
node-0835,2403.536842140
node-0843,6024.63681750
node-0853,5213.939882880
node-0865211.680532417
node-0872,4223.78059630
node-0888081.725532060
node-0892,5192.27871324
node-0902,8902.479351520
node-0914,5334.038453450
node-0921,3013.568551900
node-0934,1612.827394160
node-0943224.37284500
node-0954,9531.737752620
node-0961,3952.235532450
node-0971,0031.688322130
node-0982,1982.135411470
node-0991,5253.680212370
node-1001,5950.953813900
node-1014,1743.618144202
node-1022,6743.155444450
node-1033,7330.99873550
node-1042,8181.13347440
node-1055132.466902170
node-1061,0993.498573200
node-1071,1401.7136361927
node-1081,0364.59111500
node-1093,7302.434472030
node-1104,5414.015181690
node-1114,7631.052502480
node-1125,1791.026483780
node-1131,5662.551521560
node-1142,8072.722222810
node-1152,7081.858662570
node-1162,5234.412191510
node-1174,0321.74629860
node-1184,1732.680233450
node-1193,0421.67282610
node-1204,3424.1108121309
node-1214,2943.59175520
node-1226082.343781522
node-1231,4942.472481570
node-1249581.987643430
node-1252,3894.137673908
node-1264,8064.143502623
node-1271,1331.371672110
node-1282,1082.274243620
node-1293,8101.731251324
node-1304,9920.837111830
node-1313,8011.034661420
node-1322,6703.563242670
node-1334,8862.950181230
node-1342,9672.524303590
node-1351,0213.87736930
node-1363,6801.345402600
node-1373,4931.351232162
node-1383,2342.55982650
node-1391,1132.081694552
node-1402,3483.752293870
node-1411,3922.042772428
node-1426372.44055957
node-1431,5241.138393250
node-1444241.82556530
node-1452,5503.082391593
node-1461,5951.27245670
node-1473,4641.337133410
node-1483,9864.379423360
node-1491,8093.515562790
node-1501,6233.048521610
node-1513,7730.973652970
node-1521,3534.124934550
node-1533,3451.88131970
node-1545923.015553300
node-1552,5711.087762377
node-1562,2173.748512050
node-1571,1230.942892580
node-1583,9703.9126383760
node-1591,5271.573672790
node-1603,4991.667502110
node-1614,5554.124681150
node-1624,0033.223883040
node-1631,3321.075802880
node-1644,0931.5104193324
node-1654,9853.590172550
node-1662,3470.676492548
node-1673,9381.6119811730
node-1683,0963.542542050
node-1692,8162.814473830
node-1701,9913.72363170
node-1713021.74215940
node-1724,0620.942801520
node-1732,1611.2120263317
node-1746593.282161780
node-1753,3710.7100424655
node-1761,2073.476631920
node-1775411.644632332
node-1782,4972.97124200
node-1795,1342.458151240
node-1802,8173.736621750
node-1811,9103.22726630
node-1823663.488551460
node-1832,6150.961762820
node-1844,3784.224222870
node-1851,9723.5103172020
node-1864,4941.15424277
node-1876732.128271530
node-1885,0992.937231504
node-1893,3802.334362630
node-1902,2861.162392310
node-1912,9292.847682274
node-1922,1444.183843940
node-1933,4541.58184880
node-1943,3251.987462900
node-1951,7712.99045660
node-1963,4921.692921260
node-1971,0850.83386520
node-1982,3433.355842340
node-1992,3852.719182120
node-2004,6933.779321600
node-2013,3652.512491250
node-2022,1882.96393680
node-2033,7940.915303230
node-2043,4581.494742900
node-2059261.447614530
node-2063,2673.562373290
node-2072972.191582783
node-2082,3033.833432830
node-2093,2203.610515660
node-2102,9681.642441435
node-2114,2952.29262820
node-2128993.710646670
node-2133893.270913150
node-2143,0963.656243590
node-2152,5070.8111572335
node-2163,5891.444371197
node-2172,0722.06727710
node-2183,2421.679462120
node-2193,1443.614193206
node-2201,1391.225743619
node-2218413.491812610
node-2222,0652.745231350
node-2233,1851.985434433
node-2242,9531.814268970
node-2254,2502.488871280
node-2263,0491.871643670
node-2272,2533.064253158
node-2288101.718814010
node-2291,4792.650773595
node-2307561.697762490
node-2314,0332.366684090
node-2325122.862812270
node-2332,1352.096281733
node-2342,2670.822123847
node-2352,7703.795343194
node-2364,1911.447812680
node-2374,6033.336224160
node-2386283.266201510
node-2393,3851.3144631990
node-2403,9562.066111019
node-2413,2302.149843365
node-2422,9351.934281040
node-2434,3401.18469950
node-2441,3821.372483900
node-2454,2962.77845610
node-2463,4082.4101391250
node-2472,3953.510935630
node-2484,1742.096481310
node-2491,3573.86538840
node-2503,5840.775382600
node-2519981.46577510
node-2527902.927193310
node-2531,6521.446272531
node-2544,5781.594783310
node-2559613.695702180
node-2561,6461.437502520
How it works

The report

The numbers ride on your stream itself. The report view renders a probe, one value for every tab, and the site's frame writer keeps your stream's probe from then on. Each time it writes a tick's frames, it first asks the probe for a report, which reads the stream at that moment:

report.go
func read(st *sse.Stream, m *demo.Meter, now time.Time) Reading {
	r := Reading{Frames: m.Frames, Coding: st.Encoding(), Last: m.Last, FPS: m.Rate(now, 4*time.Second), Beats: m.Beats}
	r.Tier, r.Heap = st.Tier()
	r.Events, r.Wire = st.Sent()
	if b := st.Budget(); b != nil {
		r.Budget = b.Stats()
	}
	return r
}

demo.Meter is the frame writer's count of the stream's writes: the bytes of each, when they went out, and what the hub sent in between. The report goes out as a patch of $_stream in the same write as the frames, so it describes the stream up to the frame before. On this page every frame carries one; in the landing page's slot, one a second at most.

The coding

sse.Negotiate picks the coding with the highest weight in your browser's Accept-Encoding, zstd before brotli before gzip when they tie. Chrome, Edge and Firefox accept zstd, Safari from 26.3. zstd sends more bytes than brotli for a sixth of its CPU or less: on one core, a 57 KB page with five rows changed per frame took zstd 36 to 41 µs a frame for 405 bytes on the wire, and brotli at quality 4 251 to 265 µs for 121 bytes. The coding reads none when your browser accepted none, or when the budget had no room for a brotli or gzip encoder.

The setting from the budget

The hub shares a memory budget among its streams' encoders: half the process's memory limit. This site runs with GOMEMLIMIT=192MiB, so the encoders share 96 MiB. A stream gets the largest setting that fits both its fair share, the limit divided by the open streams, and what is left of the limit:

Setting Window Reserved
zstd 2^19 512 KiB, with twice that as history 1.66 MiB
zstd 2^19 low memory 512 KiB 1.08 MiB
zstd 2^18 low memory 256 KiB 848 KiB
br q4 2^18 256 KiB 2.14 MiB

The window is how far back the encoder looks for what a frame repeats. Low memory keeps zstd's history at the window, for more CPU per frame. When a new stream finds no room, every zstd stream above the new fair share moves down at its next write, so your setting can change while you watch; when not even 2^18 fits, a stream goes out uncompressed. zstd asleep is a zstd stream that holds no encoder: after a hibernation, or while the budget has no room for it, when its frames go out in raw blocks.

Reserved for your encoder

The heap that the budget reserves for your setting: what lith measured an encoder of that setting to hold after a session of 142 KB frames, rounded up. The budget adds up these reservations; it does not measure the heap.

Events and the wire

Events are the Datastar events as the server formats them: datastar-patch-elements with your table's HTML and datastar-patch-signals with these numbers in JSON. The wire is what your encoder made of them, the bytes the server wrote, before TLS and HTTP framing. The ratio so far divides the one by the other, from the stream's first byte.

The last frame

A frame here is one write: the views that changed in a tick, compressed and flushed together. Its ratio tells how many bytes of events went into each byte on the wire. The large table's frame carries about 28 KB of events; with calm churn eight of its 1,536 cells change, and the frame takes a few hundred bytes. With storm a quarter of the cells change, and the ratio falls to about 7:1.

The window must hold the frame before, or there is nothing to refer back to. In lith's measurements a board of 142 KB a frame takes 1,471 bytes on the wire with a 2^19 window, 2,703 at 2^18 with low memory, and 10,831 at 2^17, where the frame before is out of reach. A stream's first frame has no history at all, and neither has the first frame after a hibernation: those are the spikes in the line.

Frames a second

The table view renders every 250 ms, and a little more than that apart in practice: each render is due 250 ms after the tick that ran the one before. The rate counts the writes of the last four seconds.

Heartbeats

After 20 s without a frame, the hub sends an SSE comment, three bytes of events, so that proxies keep the connection open. The page never sees a comment; the meter finds the heartbeats as the bytes the stream sent between two of its writes. While the table moves there are none. Pick Still, wait half a minute and pick Calm again: the next report counts them.

Hibernation

After a minute without a frame, the hub hibernates the stream (live.Options.Hibernate): the stream ends its zstd frame, gives its encoder to a pool and its reservation back to the budget, and its setting reads zstd asleep. A heartbeat then goes out in a zstd frame of its own, 15 bytes instead of 6. The next frame takes an encoder from the pool and starts without history: lith measured a 37 KB list at 3,790 bytes on the wire after waking.

The site's streams and the budget

Every stream of the hub opens through the budget, which counts them by what they hold: that gives the number of open streams and the list under the meter. The meter is the reserved heap against the limit. With 200 open streams this site measured 110 MB resident, 100 MB of it the encoders' budget, with 115 streams at zstd 2^18 with low memory and the rest uncompressed.

The payload

Your choice of size and churn is your tab's row in site_tabs, written by a command. Still freezes the table at the step it was chosen:

commands.go
func (c SetPayload) Apply(tx *writer.Tx) error {
	var s TabState
	if err := demo.LoadTab(tx.Context(), tx, Slug, c.Viewer, &s); err != nil {
		return err
	}
	if c.Size != "" {
		s.Size = c.Size
	}
	if c.Churn == Still && s.churn() != Still {
		s.Frozen, s.From = StepAt(c.At), s.churn()
	}
	if c.Churn != "" {
		s.Churn = c.Churn
	}
	s.Op = c.Op
	return demo.SaveTab(tx, Slug, c.Viewer, s, c.At)
}

The table at a step is a function of its size, its churn and the step: the cells change in an order that visits every cell before it comes round again, a fixed number of them per step. So the tabs that chose the same share one render of each step, and your tab's view only reads your row and picks that render.

What the inspector costs the server

Measured on 2026-10-13 on an Intel i5-13500, with the site's binary on four cores and GOMEMLIMIT=192MiB as deployed: 100 inspectors with the large table, read by a load test on other cores that does not decompress them, over 60 s each:

Churn CPU Bytes a frame Heap after a collection
Calm 0.08 cores 503 87 MiB
Busy 0.09 cores 1,619 87 MiB
Storm 0.16 cores 4,413 89 MiB

The budget gave all 100 streams zstd 2^18 low memory, 82.8 of its 96 MiB, which is most of that heap. The CPU goes into compressing about 400 frames a second; the table renders once a step for all the tabs that share it. Between collections the heap grows to the goal that the memory limit sets, so the process stayed at 190 to 197 MiB resident, below the unit's MemoryHigh=224M.