carlos / todomvc-bakeoff Public

Clone
git clone https://amadan.net/carlos/todomvc-bakeoff

Plain git — no account needed to clone.

Download

Download this file

1# todo-spinel
2
3TodoMVC for the CARLOS bakeoff, written in Ruby and compiled ahead of time
4by [Spinel](https://github.com/matz/spinel), matz's Ruby-to-C compiler. The
5result is one static linux-arm64 binary with SQLite inside it and no Ruby at
6runtime. It's live at
7[https://todo-spinel.bfj.oncarlos.com](https://todo-spinel.bfj.oncarlos.com).
8
9## The stack
10
11**[tep](https://github.com/OriPekelman/tep)** is the web layer. You write a
12Sinatra-style `app.rb`; tep translates it into plain Ruby that Spinel can
13compile, inlines its own HTTP server, and drives Spinel to a binary. It's the
14only web framework that targets Spinel, and it already powers a CARLOS app
15(lpay), so it was the obvious pick. Spinel compiles a subset of Ruby, which is
16why `app.rb` reads a little old-fashioned: `while` loops, string
17concatenation, small top-level functions, no metaprogramming.
18
19**SQLite** comes from the official amalgamation, compiled by the Makefile and
20linked statically. tep's own SQLite shim talks to it. Lists live in one
21`todos` table keyed by the visitor's cookie, and a `visitors` table records
22every token the server has issued, so a cookie it didn't issue gets a fresh
23one. Migrations are numbered by
24`PRAGMA user_version` and only ever add.
25
26**HTML** is built in Ruby functions in `app.rb`, escaped by hand. tep has
27build-time ERB, but its templates can only be called from a route block and
28every value has to go through a per-request string bag, which made the
29item and fragment renderers clumsier than plain functions.
30
31**JavaScript** is one ES module, `assets/static/app.js`, with no framework.
32The page works fully without it: every action is a real form POST that
33answers 303. With JS on, the script sends the same forms with `fetch` and an
34`X-Fragment: 1` header, and swaps in the `section.main` and `footer.footer`
35the server sends back. The no-JS controls sit invisibly over their TodoMVC
36counterparts and disappear when the script adds the `js` class.
37
38tep is copied into `.cache/` and patched rather than used as-is.
39`patches/1-tep-carlos.patch` is lpay's: the `--socket` contract and a
40`Content-Length` that counts bytes. `patches/2-tep-bakeoff.patch` adds
41`--listen 127.0.0.1:<port>`, which refuses anything but a loopback address,
42and removes tep's fallback to listening on every interface: with neither
43`--socket` nor `--listen` the binary exits. It gives each request a
4410-second deadline for the whole exchange, so a slow or idle client can't
45hold the single worker. It reads bodies whole up to 256 KiB (tep stopped at
4664 KiB and carried on with a truncated title), answers 413 above that, and
47answers 400 without running any handler when a body is short of its
48`Content-Length` or chunked. Smaller changes: the
49`303 See Other` reason phrase, and a return-type fix in `Tep::Assets._add`
50that otherwise breaks the C compile. Last, it replaces tep's form decoding.
51spinelkit's `Url.parse_query` keeps a decoded key in an unrooted temporary
52while it decodes the value, so a garbage collection in between loses the
53key. On arm64 that dropped any todo title over a thousand or so
54characters; on x86-64 it only shows under `SPINEL_GC_STRESS=1`. The patch
55decodes in C and keeps every intermediate in a local, and `make test` runs
56a second pass under GC stress to catch anything like it. Edit
57`.cache/x86/tep` and run `scripts/regen-patch` to change the second patch.
58
59## Build, run, test
60
61```
62source ../../scripts/env.sh
63make build # dist/app, for this machine
64dist/app --listen 127.0.0.1:8080 --db $TMPDIR/todo.db
65make test # test/run.sh: curl checks of the contract
66../../scripts/test-app.sh http://127.0.0.1:8080 # the shared browser suite
67```
68
69`make build` takes about a minute and a half, nearly all of it Spinel.
70
71`make build-arm64` makes `dist/app-linux-arm64`. The host Spinel turns Ruby
72into C natively, then `scripts/arm64-cc` compiles that C inside the emulated
73arm64 root (`scripts/arm64.sh`) and links it statically against the root's
74glibc, OpenSSL (tep's HTTP layer links it) and our SQLite. Running all of
75Spinel under emulation also works but takes three times as long. Expect five
76minutes warm, and another four the first time while SQLite compiles.
77
78`make clean-deps` removes the compiled SQLite and tep copies; the downloaded
79SQLite zip stays in `.cache/src`.
80
81## Deploy
82
83```
84make deploy MESSAGE="what changed"
85```
86
87This ships `dist/app-linux-arm64` to the `todo-spinel` app on account `bfj`.
88The version is the commit's short SHA, so commit before you build. Before a
89first deploy, run `../../scripts/boot-check.sh dist/app-linux-arm64`.
90