DEV Community

Tony Goodchild
Tony Goodchild

Posted on

Every data grid claims to handle live data.

Every data grid claims to handle live data. We wanted a workload that would not let us hand-wave, so we compiled Doom to WebAssembly and made a grid its screen. You can play it here: https://toclocoinc.github.io/lattice-grid-demo-doom/ (arrows move, F or Shift fires, Space uses). The code is on GitHub: https://github.com/toclocoinc/lattice-grid-demo-doom

This is the story of how it works, what we measured and what changed in the grid because of it.

The setup

The engine is doomgeneric, a port of the original Doom source whose only job is to hand each rendered frame to the host as a raw pixel buffer. It is compiled with Emscripten and runs Freedoom's level data, which is freely redistributable. There is no SDL, no audio and no canvas in the page.

Each frame arrives as 320 by 200 pixels. The page keeps the previous frame, diffs the new one against it, and works out which rows changed. Then it pushes those rows into a Lattice Grid through the ordinary row-update call, grid.rows.apply, the same call an application uses when a price ticks or a status flips. The grid does not know it is drawing a game.

Pixels as cells, then rows as gradients

The first version was the purest one: one grid cell per pixel, each cell's background colour coming from its value through a formatting rule. At 80 by 50 that is 4,000 cells, and it plays at around 33 frames a second. It is also the version that made us measure properly.

Timing a rows.apply call against the next animation frame reported about 17 milliseconds whatever we threw at it. That number was the cost of enqueueing the update. The grid, like any serious renderer, defers its DOM writes to its own frame tick, so the honest measurement is the time from the call until the changed cell's computed background is actually on screen. We built a small harness for exactly that, and it became the yardstick for everything that followed.

At 160 by 100 and above, the limit is the browser itself: restyling and painting ten thousand changed elements a frame is more than any DOM can do at game speed. So the shipped demo takes a different shape. The grid has one column and 200 rows. Each row's cell carries its line of pixels as a single hard-stop CSS gradient, with runs of the same colour merged, still applied through the formatting rule, still delivered through rows.apply. A frame that changes 35,000 pixels becomes about 170 row writes. Native 320 by 200 runs at 30 to 60 frames a second on a desktop Chrome, and the engine's own 35 frame cap is now the limiter.

The page keeps both ideas visible. The resolution switch offers 160 by 100 and 80 by 50, and an automatic fallback drops to 160 by 100 if a machine cannot hold 30 frames a second during the first seconds of play.

What changed in the grid

A demo like this is only worth doing if the product gets better. Two things did, and both shipped this week to every user.

Conditional formatting rules now cost the same as unstyled cells on a bulk update. Put a rule on every cell of a live board and 4,000 styled cells still update in one frame.

A partial update now costs only the rows and cells it touched. Attributes, classes, text nodes and cell positions are written only when their value actually moves, and the overflow-title measurement runs only for a cell whose text or width changed. On an 80 by 50 grid, 4,000 changed styled cells reach the screen in about 49 milliseconds, and 900 in about 33, measured by the harness above.

Neither change needs an option. A grid fed by a websocket, a polling loop or a trading feed simply does less work per tick.

Try it

Play it in the browser: https://toclocoinc.github.io/lattice-grid-demo-doom/

Read the pipeline, it is under 200 lines: https://github.com/toclocoinc/lattice-grid-demo-doom

Top comments (0)