gled 2.30.2

gled is an application for creating animations and effects on artnet or dmx installations
# Performance

gled renders the LED output on its own render thread, independent of any
window or client. Splitting gled into a server and clients that connect over
the network must not slow that thread down. This page records the measured
render frame rate before and after each step of that split, on the developer
laptop and on a Raspberry Pi 5.

## How it is measured

- **Render frame rate.** The render thread logs a `frame_stats` line once per
  second with the average frame rate and the longest gap between two frames.
  The frame limiter is raised to 5000 fps with `GLED_FPS_LIMIT`, so the number
  shows the headroom, not the configured limit.
- **Before the split** the measurement was `tests/fps.rs` (removed since; it
  is in the git history before the split), which started the desktop app and
  parsed the log.
- **After the split** the measurement is `crates/gled-server/tests/perf.rs`. It
  starts the engine headless on the GPU with a copy of the local asset
  repository, serves it over WebSocket, and connects 0, 1, 5 and 10 clients.
  Each client changes a scene's opacity 20 times a second and receives every
  update. A separate monitoring client reads the render statistics the
  engine publishes. The test also reports how long a change takes to come
  back to its client, and how much each client receives:
  ```sh
  cargo test --release -p gled-server --test perf -- --ignored --nocapture
  ```
  It renders without a frame limit, which is harder for the render thread
  than the desktop measurement with its 5000 fps limit and a window to draw.
  The two kinds of numbers are therefore not comparable with each other.

## Acceptance thresholds

| Situation | Requirement |
|---|---|
| No client connected | median frame rate at least 95 % of the baseline |
| 5 or 10 clients connected | median frame rate at least 85 % of the baseline |
| Any | longest frame gap at most twice the baseline |

## Results

Laptop: AMD Ryzen AI 9 HX 370 with Radeon 890M, Fedora 44, Wayland with
XWayland. Project "2025 / Proseccival" (14 scene instances, about 50 KB of
project JSON).

| Version | Clients | Median fps | Best fps | Longest frame gap |
|---|---|---|---|---|
| 2.29.0 desktop, baseline, run 1 | 0 | 3914 | 4020 | 4.1 ms |
| 2.29.0 desktop, baseline, run 2 | 0 | 3876 | 3935 | 2.9 ms |
| 2.29.0 desktop, baseline, run 3 | 0 | 3925 | 3969 | 4.7 ms |
| A1 in progress: model decoupled from egui and globals, run 1 | 0 | 4153 | 4184 | 2.9 ms |
| A1 in progress: model decoupled from egui and globals, run 2 | 0 | 4098 | 4148 | 4.2 ms |
| A1 done: model in its own crate, run 1 | 0 | 3812 | 3990 | 5.0 ms |
| A1 done: model in its own crate, run 2 | 0 | 3789 | 4125 | 4.6 ms |
| 2.29.0 baseline, rerun interleaved with A1, run 1 | 0 | 3952 | 4940 | 4.6 ms |
| 2.29.0 baseline, rerun interleaved with A1, run 2 | 0 | 3800 | 4470 | 5.3 ms |
| A2 done: GPU code in its own crate, run 1 | 0 | 3844 | 4943 | 5.0 ms |
| A2 done: GPU code in its own crate, run 2 | 0 | 4888 | 4923 | 2.5 ms |
| 2.29.0 baseline, interleaved with A2 | 0 | 4925 | 4939 | 3.2 ms |
| A3: engine thread, desktop app as its client, run 1 | 0 | 4868 | 4875 | 1.8 ms |
| A3: engine thread, desktop app as its client, run 2 | 0 | 4724 | 4753 | 3.9 ms |
| 2.29.0 baseline, interleaved with A3, run 1 | 0 | 4691 | 4781 | 3.3 ms |
| 2.29.0 baseline, interleaved with A3, run 2 | 0 | 4913 | 4920 | 2.2 ms |

Runs taken while other builds compete for the CPU show worst frame gaps of
15 to 40 ms for both versions; only measurements on an otherwise idle machine
are comparable.

### Headless engine with remote clients

Same laptop and project, A5: the engine runs headless in the test process,
without a frame limit, and serves the clients over WebSocket. The round trip
includes up to 33 ms of waiting, because a client gets at most 30 updates a
second.

| Clients | Median fps | Longest frame gap | Round trip p50 | Round trip p99 | Received per client |
|---|---|---|---|---|---|
| 0 | 20056 | 1.5 ms | - | - | - |
| 1 | 20013 | 1.2 ms | 20 ms | 44 ms | 89 KB/s |
| 5 | 19931 | 1.8 ms | 24 ms | 48 ms | 81 KB/s |
| 10 | 19971 | 2.1 ms | 20 ms | 45 ms | 84 KB/s |

Ten editing clients cost less than 1 % of the frame rate. The first version
sent the whole project, about 40 KB, with every change and needed 809 KB/s per
client; sending only the changed scenes brought that down tenfold. Without
edits a client receives only the beat timing, about 30 small messages a
second.

Rerun with the full browser interface's protocol (assets, status, sound,
MIDI and input events), same laptop, 2026-10-14:

| Clients | Median fps | Longest frame gap | Round trip p50 | Round trip p99 | Received per client |
|---|---|---|---|---|---|
| 0 | 19808 | 2.7 ms | - | - | - |
| 1 | 19404 | 1.8 ms | 18 ms | 42 ms | 103 KB/s |
| 5 | 19293 | 1.4 ms | 23 ms | 49 ms | 99 KB/s |
| 10 | 18330 | 3.4 ms | 23 ms | 47 ms | 100 KB/s |

Ten editing clients now cost 7.5 % of the frame rate at about 20000 frames a
second, within the 15 % limit; each client receives about 100 KB/s, a fifth
more than before because of the status the interface shows.

All numbers above were taken in blackout: the engine starts in it and the
test did not switch it off, so no effect was rendered. The test now switches
blackout off first.

## GPU time of the LED output

The headless numbers above are limited by the laptop's CPU; they hide what the
LED output costs the GPU, which is what limits a Raspberry Pi 5. A benchmark
records nothing but output frames, ten effects each, and times the GPU on its
own:

```sh
cargo test -p gled-gpu gpu_time -- --ignored --nocapture
```

Each effect used to be three kinds of compute pass (a clear per frame, then
per effect a pass computing its lamps and one mixing them into the output),
dispatched as single-thread workgroups over all 128 universes whatever the
effect's lamps were: about 300 000 workgroups for 15 effects. Now each effect
is one pass of 64-thread workgroups over only the quads of four lamps it has
lamps in, adding into the shared output directly.

Laptop as above, Radeon 890M, debug build, two runs each:

| Lamps per effect | Before | After |
|---:|---:|---:|
| 170 | 0.56 ms | 0.08 ms |
| 2 720 | 0.57 to 0.59 ms | 0.08 to 0.09 ms |
| 21 760 | 0.57 to 0.63 ms | 0.08 to 0.09 ms |

What is left on the Radeon is the fixed cost of the ten passes.

Raspberry Pi 5, release image, frame rate as the interface shows it:

| Version | Active scenes | Frame rate |
|---|---|---|
| Before | 5 | 56 fps |
| Before | 15 | 25 fps, at 17 % CPU |
| After | 13 | 230 fps |