Yesterday I got nerd sniped into missing a workout.
[2:30 PM] I discovered a blog post about vibe coded performance benchmarks for drawing a large number of rectangles on the HTML canvas. It was an interesting read and I was nodding along until I saw that all the Typescript benchmarks were better than the Rust ones. That can’t be right?
[3:00 PM] My hunch was that most of the time was spent crossing the boundary between Rust and Javascript. This is something I’ve been burned by in the past.
[3:30 PM] I decoupled the work scheduling from the sync compute and rendering [1].
Deleted a few lines of code that were creating the render loop and recursively calling it from the request_animation_frame
. And then moved the scheduling to Javascript.
BEFORE
// Rust updates the rectangle positions
// renders to canvas
// updates the fps counter
// recursively schedules next render loop
┌─────────────────┐ ┌───────────────────────────────────┐
│ Javascript │ │ Rust │
│ │ │ │
│ │ │ ┌─────────────────────────────┐ │
│ │ │ │ start │ │
│ │ │ │ │ │
│ │ │ │ ┌──────────────────────┐ │ │
│ │ │ │ │ start_animation_loop │ │ │
│ │ │ │ │ │◀┐ │ │
│ │ wasm_bindgen(start) │ │ └──────┬───────────────┘ │ │ │
│ │ ──────────────────▶ │ │ │ │ │ │
│ │ │ │ ▼ │ │ │
│ │ │ │ ┌──────────────────────┐ │ │ │
│ │ │ │ │ render │ │ │ │
│ │ │ │ │ │ │ │ │
│ │ │ │ └──────┬───────────────┘ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ └─────────────────┘ │ │
│ │ │ └─────────────────────────────┘ │
└─────────────────┘ └───────────────────────────────────┘
AFTER
// JavaScript calls the `frankenpenguin.tick()`
// Rust updates the rectangle positions
// renders to canvas
// JavaScript updates the fps counter
// recursively schedules the ticks
┌────────────────────────────────────┐ ┌────────────────────┐
│ JavaScript │ │ Rust │
│ │ │ │
│ ┌────────────────────────────┐ │ │ │
│ │ render │◀─┐ │ │ │
│ │ │ │ │ │ │
│ │ ┌───────────────────────┐ │ │ │ │ ┌──────────────┐ │
│ │ │ │ │ │ │ │ │ tick │ │
│ │ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ └──┬───────────┘ │
│ │ │ │ │ │ │ │ ▼ │
│ │ │ frankenPenguin.tick() │────────────────────────▶ │ ┌──────────────┐ │
│ │ │ │ │ │ │ │ │ render │ │
│ │ │ requestAnimationFrame │─────┘ │ │ │ │ │
│ │ │ │ │ │ │ └──────────────┘ │
│ │ └───────────────────────┘ │ │ │ │
│ └────────────────────────────┘ │ │ │
└────────────────────────────────────┘ └────────────────────┘
[3:45 PM] I run the benchmarks. Hmmmm, it’s 2x faster, not bad. I was mistakenly comparing the updated Rust + WebGL implementation to the TS + WebGL version.
[4:00 PM] The train is unusually late.
[4:38 PM] I get to the gym, but the person turns me away as I’m too late [2].
[5:21 PM] I push the repository to GitHub.
[This morning] I realize that I was comparing the wrong benchmarks and the diff resulted in a 10x bump in perf.
Aside 1:
To schedule async work in the browser you can pass a callback to the requestAnimationFrame
API.
As Rust doesn’t ship with a runtime, scheduling work requires using the closest one avaiable. In the browser it ends up being… the browser. So you have to use a crate that would provide the bindings to the requestAnimationFrame
API.
Aside 2: I ended with playing Table Tennis afterwards so it’s all good.
Comments via 🦋