eieio.games

by nolen royalty

One Million Checkboxes

One million checkboxes that anyone can check

Jun 25, 2024

YOU PROBABLY WANT ONE OF THESE LINKS

You may also be interested in the New York Times article about OMCB or its Wikipedia page.

But! If you want to read what I wrote before releasing OMCB - before hundreds of thousands of people checked hundreds of millions of boxes - read on :)


I made a website. It’s called One Million Checkboxes. It has one million checkboxes on it. Checking a box checks that box for everyone (and makes some numbers go up).

You can find it at onemillioncheckboxes.com.

checking some boxes

Why

I don’t really know. The idea came up in a conversation with my friend Neal last Friday and I felt compelled to make it.

How

There are a few fun tricks I used here.

  • To efficiently store state I use a bit array. Checking box 0 just flips the first bit in that array.
  • I store my state in redis since redis can easily flip individual bits of a value.
  • I broadcast individual “toggle” updates via websockets and push out a full state snapshot every 30 seconds or so to make sure clients stay synched.
  • I use react-window to avoid rendering checkboxes that aren’t in view.

Is there anything else you’d like to tell us

Not much! This one was fun and fast. I did run into one bug that was baffling - I’ll tell you about it really quick.

Endianness

When we toggle a checkbox, the server does something like this:

def set_bit(index, value):
    state['bitset'][index] = value

Pretty simple! We convert that state to a big array of bytes and ship it to the client 1. The client looks at the state to figure out which toggles are set. My original implementation looked like this:

1

I originally RLE encoded this but later decided that just shipping 125kb was fine since it saves my server some work.

class BitSet {
  constructor(size) {
    this.size = size;
    this.bits = new Uint32Array(Math.ceil(size / 32));
  }

  get(index) {
    const arrayIndex = Math.floor(index / 32);
    const bitIndex = index % 32;
    return (this.bits[arrayIndex] & (1 << bitIndex)) !== 0;
  }

  set(index) {
    const arrayIndex = Math.floor(index / 32);
    const bitIndex = index % 32;
    const mask = 1 << bitIndex;
    this.bits[arrayIndex] |= mask;
  }
}

Does this work?

No! Look at what we get from each of these implementations when we set the first bit:

def set_bit(index, value):
    state['bitset'][index] = value
state = { "bitset": [0 for _ in range(32)] }
set_bit(0, 1)
print("".join(str(x) for x in state["bitset"]))
# '10000000000000000000000000000000'
int("".join(str(x) for x in state["bitset"]), 2)
# 2147483648
> bitset = new BitSet(32)
> bitset.set(0)
> bitset
BitSet { size: 32, bits: Uint32Array(1) [ 1 ] }

Our python implementation treats bit 0 as the leftmost bit of the leftmost byte. In javascript we’re grabbing the rightmost bit of the leftmost byte!

This isn’t quite an endianness problem - really we’re reversing the order of bits in a byte, instead of reversing the order of bytes in a word. But it certainly feels like an endianness bug.

Fixing this meant deciding whether I wanted to model my data as “one million bits” or “125,000 bytes” - and of course realizing that those were two different things. The bug appeared mid-refactor - my original update code hid the error in my data model until you refreshed the page - and so it took me ages to suspect the bit twiddling. In retrospect this was a mistake. Always suspect the bit twiddling.

Wrapping Up

This site was fun to make. It got me thinking about a new space of collaborative experiences that I want to explore.

It was also really nice to make and ship something in two days! I’ve got a few ongoing projects that I’ve been working on for too long and this was a welcome break (maybe I should just ship those projects too).

Anyway. I hope you enjoy the site, and I’ll be back soon with some larger webcam-based projects :)

Thanks for reading!

Keep up with me on my socials 👆

Or sub to my newsletter here! 👇