Threads Playground

A page to write programs in, and see what they compile to and how they run.

The threads playground is a page where you write threads programs, see what they compile to, and watch them run. It's the playground of @arv/threads, which brings threads to JavaScript in plain TypeScript, on Web Workers and a SharedArrayBuffer. You'll find the model, the API and the limits in these docs.

Disclaimer

Written iterating with Claude Code (Claude Opus 5.5).

You can try it at arv.github.io/threads/playground. It compiles everything right in your browser.

Using the Playground

Pick a program from the Program menu and edit it in Monaco. You get TypeScript's hover types, completions and errors from the threads API. As you type, the page compiles your program and shows you:

  • The compiled JavaScript. This is what the fork of TypeScript compiles your program to. Everything the compiler added is marked.
  • The JavaScript with its types erased. This is the same program with its types only erased, and it still runs.
  • The ownership checker's errors. They show up as squiggles in the editor and in a list. On the dev server they come from threads-check. In the browser they come from the fork's own check.
  • The output. Run runs the program on a pool of Web Workers.

The Compiled and Types erased switch picks which of the two JavaScript versions you see, and which one runs.

The page remembers your edits to each program in this browser. If you want the original back, click Reset.

Running a Program

Run calls the program's main with the thread counts in the Threads field, which starts out as 1, 2, 4 and 8.

main runs on a worker too. It's started like any other thread, so it may block, for example on join, recv or a barrier. The page itself never blocks. It only awaits main's result.

Stop ends a run that doesn't end on its own. The pool stops, along with every thread on it, and a fresh pool starts.

When Compiling Fails

If compiling goes wrong, the status in the page's header tells you. On the dev server, the page says when it can't reach Vite's dev server. When the page compiles in the browser, it says if the compiler failed, or if the compiler didn't load at all.

Run It Locally

What You Need

  • Node 22.18 or later (24.11 or later if you're on Node 24).
  • A recent Chrome, or another browser with SharedArrayBuffer.
  • The TypeScript fork, for the compiled JavaScript. This is a fork of TypeScript 7.0.2, arv/threads-checker. Build it in a clone of it with go build -o built/local/tsgo ./cmd/tsgo.

The ownership checker doesn't need the fork. It's src/checker/, and it runs on stock TypeScript 7.1.

The repository is a pnpm workspace on Vite+. Its Development section has the details.

Starting the Dev Server

Install the dependencies, then start the dev server with TSGO pointing at the fork:

vp install                                                 # at the repository's root
TSGO=/path/to/threads-checker/built/local/tsgo vp dev      # here, or at the root

Open the address Vite prints, and you're ready to go.

Checking the Programs

vp run typecheck checks the programs' types, and vp run own-check runs the ownership checker on them. Run both before you send a change to a program.

Static Builds

Without a dev server, the page compiles programs itself. It uses the fork compiled to WebAssembly (arv/threads-checker's cmd/wasm), running in a worker. You get the same JavaScript, tsgo own check's ownership errors, and tsgo erasing the types.

The catch is size. The compiler is 34 MB (about 8 MB compressed), because Go's WebAssembly is huge. The page shows its download as it goes.

vp run build-wasm                                          # the compiler, into public/compiler/ (needs Go)
TSGO=/path/to/threads-checker/built/local/tsgo vp build    # into dist/, to be served from /playground/
vp preview                                                 # serves dist/, at /playground/

vp run build-wasm builds from the clone that TSGO_FORK names. By default, that's ~/src/arv/threads-checker.

The build is served from /playground/. To serve it from another path, build it with BASE=/elsewhere/ vp build.

You can also compile in the browser on the dev server. Just add ?wasm to the address.

Deploying to GitHub Pages

On every push to main, the Pages workflow builds it all and deploys it to GitHub Pages, at arv.github.io/threads/playground/.

Cross-Origin Isolation

SharedArrayBuffer only works on a cross-origin isolated page. That takes the COOP and COEP headers, which the dev and preview servers send.

Some hosts, such as GitHub Pages, can't send these headers. There, a service worker (public/coi-sw.js) provides them. The first time you visit, the page loads once more, under the service worker.

Programs

The programs you can pick from live in programs/. Each one exports main, which the page calls with the thread counts to run with.

The basics come first, with one idea each:

  • Hello, threads
  • Moving a value to a thread
  • Sharing a value
  • A shared struct
  • A SharedArray of booleans (a sieve)
  • Strings in shared memory
  • Atomics (claiming work, and counting together)
  • A channel pipeline
  • A pool of workers on channels
  • Ten thousand async goroutines waiting for a database replica
  • A SharedMap and a SharedSet counting words

Then come the bigger examples:

  • N-body four ways: shared structs with a barrier, shared structs with channels, moving and sharing objects, and channels copying objects
  • Mandelbrot on channels
  • JSON parsed on threads
  • The nearest-centers example from the tutorial
  • What channels cost
  • Reference counting under a race
  • A growable SharedVector, written in user space on the API
  • The mistakes the checker catches

How Fast They Run

Here's how two of them do, compiled and with the types only erased, on an Apple M5 Pro. Times are in milliseconds, on 1, 2, 4 and 8 threads:

Compiled Types erased
N-body, shared structs (plain V8, 1 thread: 25) 29 / 15 / 8 / 6 1820 / 971 / 498 / 301
Mandelbrot, channels 712 / 341 / 180 / 97 713 / 349 / 180 / 99

Layout

Path What It Is
index.html, main.ts The page, built on Monaco
boot.ts The page's entry: it isolates the page, then loads main.ts
programs/ The page's programs
vite.config.ts The dev server, with the runtime's Vite plugin, which compiles with the fork and runs the checker
wasm.ts, compiler.ts Compiling in the browser: the page's side, and the worker that runs the compiler
build-wasm.ts Builds the compiler, in WebAssembly, into public/compiler/
public/coi-sw.js The service worker that isolates the page where the host can't
assets.d.ts The types of what Vite imports besides modules

The runtime and the checker are in src/.

Docs