Engineering a Rust optimization quiz
Thanks to my sponsors: Kai Kaufman, Ronen Cohen, Ryan, playest, Matthias Zepper, Matt Jadczak, Daniel Wagner-Hall, Mateusz Wykurz, old.woman.josiah, Jonas Platte, Justin Ossevoort, Sylvie Nightshade, Christopher Valerio, ShikChen, ZacJW, Mark Old, Jack Duvall, Dylan Anthony, Ian McLinden, Ula and 278 more
There are several Rust quizzes online, including one that’s literally called the “Unfair Rust Quiz” at https://this.quiz.is.fckn.gay/, but when I was given the opportunity to record an episode of the Self-Directed Research podcast live on the main stage of EuroRust 2025, I thought I’d come up with something special.
The unfair rust quiz really deserves its name. It is best passed with a knowledgeable friend by your side.
I thought I would test James’s knowledge of Rust, letting him play against an audience of hundreds who would be able to answer quiz questions live from their comfort of their phones.
I quickly discovered that the domain name, https://wat.rs, was available, but I forced myself to go through this project in a very specific order so as not to let it die due to dopamine exhaustion.
Coming up with the questions
The most important part of a quiz is the content.
At first, I reached out on Mastodon and Bluesky, asking for cursed trivia about Rust and Cargo to get me started, and… even though I learned a lot, none of those facts were really suitable for inclusion in a quiz.
I wanted the questions to teach something to the average viewer even if they got everything wrong, so I wanted to start with simple optimizations, like turning divisions:
fn f(x: u64) -> u64 { x / 2 }
…into shifts
fn f(x: u64) -> u64 { x >> 1 }
But while working on that part of the quiz, I accidentally discovered the following:
fn f(x: u64) -> u64 { x / 5 }
// optimizes to:
fn f(x: u64) -> u64 {
((x as u128 * 0xCCCCCCCCCCCCCCCDu128) >> 66) as u64
}
To check what some code optimizes to, I ran it through Compiler Explorer (you can check this particular code on CE), and I always had to translate the assembly back into Rust.
On stage, I wore a Compiler Explorer t-shirt… under my hoodie.
I showed it to the audience in the middle of the recording like a trophy.
I originally wanted to test other backends like GCC and cranelift, but I ran out of time. Also, this would’ve required multiple answer support, which I decided not to include in this version.
I spent days on CE trying to find interesting compiles. I found that the funniest questions were some that I had to build up to: this innocent-looking thing clearly works, so does that one, then we make just one more tiny change and… it breaks everything.
A good example is this series of floating-point arithmetic questions.
x / 1.0
// optimizes to
x
Also,
(x / 1.0) / 0.0
// optimizes to
x / 0.0
However:
(x / 3.0) / 0.0
// ⚠️ does NOT optimize to:
x / 0.0
I wouldn’t find the explanation for this one (or several of the “is this memcpy?” ones) until later playtesting sessions.
I collected all the questions in a big spreadsheet, sorting them into categories like trivia, arithmetic, undefined behavior, dead code elimination, constant folding, loops, etc.
I couldn't get enough UB questions for it to be interesting. Maybe next time?
NIH syndrome.ppt
Once I was convinced I had enough quality questions to fill a 30-minute session, I decided to start building the software I’d need for this.
I remain convinced that there are plenty of quality presentation and even live quizzing software options, but I also knew exactly what I wanted and figured it was a good opportunity to evaluate Rust front-end frameworks.
I’d heard a lot of good about Dioxus and the
developer experience (via their dx tool, clever naming) turned out to be
very smooth, which was all the convincing I needed.
Shiny!
I had a couple of false starts, for example I was very curious about Dioxus LiveView, à la Phoenix, but it turns out that it’s in a state of relative disrepair, and that Dioxus “full-stack” is the way to go instead.
Instead of relying on a stable Dioxus version, I immediately got started with the 0.7.0 release candidate, to take advantage of subsecond.
It took me a week to realize I forgot to turn it on, since it requires passing
--hot-patch to the dx CLI. When enabled, it is indeed very fast, but also
very crashy, just like the release notes warned.
For simple changes, everything works fine! And it's real quick, compared to... everything else in the Rust ecosystem.
This left me in the unpleasant situation of having to batch code changes, wait several seconds for a recompile, then switch back to the browser, click around to restore the application state, and see the result of my changes.
This is a far cry from the experience of building a Svelte 5 app, for example, where Vite is able to compile and hot-reload changes in, well, under a second, preserving state whenever possible.
34,000 lines of Rust ought to be enough for everyone, right?
But what I lost in iteration speed, I gained in compile-time confidence: since I had already built an entire CMS in Rust, I was able to re-use my experience there, like using pulldown-cmark for Markdown parsing or tree-sitter-highlight for syntax highlighting.
My first idea was to hard code all the slides to avoid over engineering, but I quickly discovered how important it was to be able to iterate on the questions and the design of the slides.
So I added a reload button and made up a format for the slides, a single markdown document where each slide is separated by three dashes: a “separator”.
Slides with a question have numbered lists with checkboxes (à la GitHub-flavored markdown TODO lists), and the correct answer is checked.
# Title
---
Question?
1. [x] Yes
1. [ ] Maybe
1. [ ] No
---
(The last slide is replaced by results)
Did you know you needn’t have the numbers in the numbered list be sequential in Markdown? You can have them all be one and it’ll just number things properly.
After getting the very basic rendering working, I did some very brief styling. I was wondering how I would get the text to be big enough on large screens. And my answer was the simple and efficient:
font-size: 3vw;
“vw” stands for “viewport width” and is “a hundredth of the width of the viewport”.
I don’t think anybody should do this, and it makes for weird behavior when zooming in and out in the browser, but it did the trick that one time.
Server-side shenanigans and room codes
Unlike regular presentation software, here all the state is kept on the server. I join as a host and create a room, and then that room starts on a particular slide.
When a player joins a room, votes on a question, or when the host goes to the next slide or previous slide, the entire state is broadcast to everyone in the room — host and players.
Of course, it would be a lot more efficient to only send a diff of the state, to send only what’s changed, but I didn’t have time to make sure this would work reliably, and I figured Rust is efficient enough.
I added very little security, but just enough to feel at peace going into the conference. For example, you can only join as a host if you know the hardcoded password.
Player names cannot be picked manually. They are generated at random from a list of adjectives and animal names, and room identifiers, which are four-letter codes, are filtered for profanity to avoid showing four-letter words on stage.
Didn’t you say “fuck” on stage?
Uhhhhhh, that slipped out
The killer was inside the house all along…
I wanted to be able to go back to the previous slide to discuss it, but that opened the question of when should a slide be open for votes. And the answer I came up with is: if we’re navigating to a slide, and it still has zero votes, then it’s fine. It’s open.
This worked beautifully in all the playtests with up to 50 people in the room. And then on the day of, I navigated forwards, one person managed to sneak in a vote, then I navigated back. And that locked that question for everyone else.
This made multiple people disappointed and it was the main piece of feedback I got after the presentation.
Deploying the beast
When the application started working well enough for me to test it, I wrote a Dockerfile for it. I tried to follow the best practices showcaseed in the Dioxus documentation and all around the web: use multiple stages, use cargo-chef to pre-build dependencies.
As far as I can tell, that advice is no good, especially with Dioxus, which uses its own target directories, so I don’t think it’s benefiting from any of the work Cargo Chef is doing ahead of time.
Also, dx build is wonderful but it insists on reinstalling wasm-opt,
wasm-bindgen etc. even if they’re already in $PATH.
I ended up getting rid of cargo-chef and just using a cache mount:
# Create the final bundle folder. Bundle always executes in release mode with optimizations enabled
RUN --mount=type=cache,target=/app/target \
dx bundle --web --release && \
cp -rfv /app/target/dx/wat/release/web /app/
…which ended up being a lot faster for me when running builds locally.
I already have a Kubernetes cluster running for my website, so I deployed my app there. Deploying applications to it has become sort of mindless, which is actually nice. And somewhat reliable.
The K9S text user interface showing a bunch of deployments included Prometheus and kube-system workloads.
As I deployed it, I also finally grabbed the domain name, wat.rs, via the Istanco registry (it’s less than 25€/year for all your rusty needs), and I was ready to do the first couple of playtests!
Aurora Nockert and Lukas Wirth went first, in one-on-one video calls, and although they didn’t find any major issues with the quiz, I got the chance to make some of the questions clearer and more technically accurate.
This was the last night before leaving for the conference, and I started getting nervous about whether the game would scale to hundreds of players, so I decided to set up a load test.
The problem is I didn’t want to spoil anything for anyone: I needed to come up with a second quiz.
I took a bunch of questions that were left over from the spreadsheet, but it wasn’t enough, so I started reading all documented error messages from the Rust compiler one by one to find interesting questions.
I ended up running that test quiz at 10 p.m. Paris time, on twitch.tv/fasterthanlime, with Luuk Wester as co-host because it’s more fun that way. We got about 40 players consistently, along with a lot of arguments in chat.
D-2: Starting fights at a Paris meetup
The night before EuroRust 2025 in Paris, I joined a Rust meetup. I gave a talk about Dioxus and asked the organizers if I could run the test quiz after dinner.
Waffle agreed to join me to co-host that quiz. Oli walked on stage to explain some const stuff to everyone. It was so popular that it started fights in the audience between different Rust Project contributors. It was a blast!
This also helped me find some bugs when the phones that people were using to vote in the quiz went to sleep and then reconnected. Upon reconnection, phones were assigned a different randomly generated player ID, so players lost their scores.
D-1: Panic mode and missing explanations
On the first day of the conference, I solved the reconnect issue. Most importantly, I made it possible for the host to reconnect. Can you imagine if I accidentally reloaded the page in the middle of the presentation, and then everyone just lost their points?
I also added QR code generation so that people in the audience could join the room before the talk even started, by scanning the screen.
Mara Bos was kind enough to run through another playtest with me and got me the explanation for that one:
fn f(x: f64) -> f64 { (x / 3.0) / 0.0 }
// does NOT optimize to:
fn f(x: f64) -> f64 { x / 0.0 }
The idea here is that 0 divided by 0 is not a number (NaN). And x might be non-zero, but small enough that dividing it by 3 will be rounded down to 0. That’s why we cannot actually optimize out the first division by 3.
Amanieu did the second playtesting session with me and stayed up late thinking about that one:
// this does NOT optimize to memcpy
pub fn naive(src: &[u8], dst: &mut [u8]) {
assert!(src.len() >= dst.len());
for i in 0..dst.len() {
dst[i] = 0;
dst[i] = src[i];
}
}
He came back to me the next morning, victorious: it’s because of LLVM optimization pass order. The “memcpy recognition” pass runs after the “remove useless assignments” pass.
Day of: GitHub OAuth and swipe gestures
I spent the second day of the conference entirely working on either the software or the contents of the quiz.
I wasn’t happy with the generated names. I would have loved to know who actually placed well in the quiz. So I added “Log in with GitHub”, figuring out that people wouldn’t have the time to make a fake GitHub account with profanity in it. And they would just want to use their own GitHub account for glory if they got a good score.
The questions also showed up on phones, to make it easier to vote.
The other slides were only on the big screen.
I came up with the dirtiest OAuth 2.0 implementation you can think of, and stored profile information in client-size, so all of it could be spoofed, but as far as I can tell, nobody did. So good job on being well-behaved, everyone.
I also added some quality of life stuff, like if you don’t use the QR code and you’re just entering the room code, then as soon as you enter the fourth character, it joins automatically.
On the first day, we didn’t have a remote or clicker to be able to advance to the next slide. So I figured I would just use my phone as a remote, which required me to add the ability to join an existing room as a host and to add gesture detection on the client side so that I could swipe on my phone to move between slide.
My main concern at this point was still connection stability or what happens when the players’ phones go to sleep. I noticed that Dioxus 0.7.0-rc.1 had come out a couple days prior, and advertised automatic reconnection of websockets.
Why give me hope like this
So I spent a couple hours porting everything over to the new release candidate.
I don’t know exactly what they meant; it’s possible that it does work, but in my testing it wasn’t enough: at the last minute, I added some logic to reload the page after getting some websocket errors, or if pinging didn’t result in a pong within a few seconds.
Showtime
The last deployment was 40 minutes before James and I has to walk on stage and… I was pretty anxious!
Someone asked me if I always dress like this after the conference, and I think it's probably a wake-up call.
But everything went absolutely fine, everyone was a lot of fun.
I was asked whether I would make the quizzing software open source, for Rust meetups and whatnot. And the answer is why not? I would like to improve it a little bit before open sourcing it though.
I hadn’t presented on stage for a few years and Paris was a positive experience for me. After getting home, I reached out to other conferences to see if they would be interested in having such a quiz, and I’m happy to announce that I will be giving a quiz at RustLab 2025 in Florence, Italy, from November 2 to November 4th.
It’s not too late to buy tickets for it — I will see you there for another test of your Rust skills!
Here's another article just for you:
Engineering a Rust optimization quiz
There are several Rust quizzes online, including one that’s literally called the “Unfair Rust Quiz” at https://this.quiz.is.fckn.gay/, but when I was given the opportunity to record an episode of the Self-Directed Research podcast live on the main stage of EuroRust 2025, I thought I’d come up with something special.
The unfair rust quiz really deserves its name. It is best passed with a knowledgeable friend by your side.