zaqaru an x86-64 container in a wasm module, with a time-travel debugger

Behind the button below is an ordinary Debian image — nginx, gunicorn and Django, five processes, nothing rebuilt — running as a WebAssembly module in this browser tab, on an x86-64 interpreter written in Rust and a small Linux-personality kernel compiled into the same module. It was booted once, in CI, and written to a file; the page continues it from that instant, already listening on port 80.

The page is the server's only client. It sends the container an HTTP request, nginx proxies it to gunicorn, Django answers, and the response comes back to the page. Then the slider goes backwards: every instruction the request cost is an instant the machine can be stood on again — its processes, their files and descriptors and sockets as they were, the bytes that crossed between the container and the page, and underneath, the registers, the instructions at rip, the stack, the memory map — because a container's run is a pure function of what the host answered it, and between two instructions the module's memory is the whole machine.

The container is a StructFS store, and the page is a browser of it with a time axis: every panel is a read of one of the store's paths at the chosen instant, titled by the path, the raw value a click away, and the panels themselves are what the store's meta lens declares. The timeline is the store's traffic — the container's reads and writes to the host, the clock, the request's bytes — beside its syscalls. Pin an instant and every panel shows what changed since.

open the demo →
It downloads about 105 MB — the module is 74 MB, the booted snapshot 29 MB — and the tab needs about 1.5 GB of memory. A desktop browser, then; it has been run in Chrome and Firefox.

what to do

  1. Press send. The request is GET / HTTP/1.0; the answer arrives in a fraction of a second, and the page stands the container on the instant the request came in.
  2. Click rows of the timeline — the request's bytes arriving from the page, accept4, recvfrom, sendto, the response going out — or drag the slider inside the request's span. The container restores the nearest checkpoint and re-executes to that exact instruction; the panels show the machine as it was. Paths and descriptors in a row are links to what they name.
  3. Browse the files panel — /etc/nginx/nginx.conf, nginx's logs under /var/log — as they stood at that instant; click a process to view its paths; type any path of the store into the bar at the top and read it.
  4. Press pin, move, and every panel says what changed since. Step with −1 and +1: every instruction is an addressable instant.

what it costs

Django's boot, done once in CI3.29 G instructions, about half a minute
standing the container up from the snapshot, files decompressed againabout 1 s
a request through nginx, gunicorn and Djangoabout 4 M instructions, a fraction of a second
a seek into the middle of the requesta fraction of a second

how

Source, design and measurements: github.com/mgyenik/zaqaru — docs/time-travel-debugger.md for the design, web/ for the page and the JavaScript host. The debugger asks the container questions through the isotope Server Protocol: the container is a StructFS store, and every panel is a read of one of its paths.