From 40218c31291db842f37b44d3d307d8515fd4cb01 Mon Sep 17 00:00:00 2001 From: Brandon Cook Date: Thu, 11 Jun 2026 18:55:38 +1000 Subject: [PATCH 1/2] docs+ci: -gc=conservative is the build profile (leaking GC OOM-trapped long rooms) Co-Authored-By: Claude Fable 5 --- .github/workflows/ci.yml | 8 ++++---- ABI.md | 8 +++++--- CLAUDE.md | 5 +++-- GUIDE.md | 5 +++-- README.md | 11 +++++++---- internal/game/codec.go | 9 +++++---- internal/game/diff_test.go | 2 +- internal/game/run.go | 3 ++- 8 files changed, 30 insertions(+), 21 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b996603..7fbb86f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -121,7 +121,7 @@ jobs: - uses: actions/setup-go@v5 with: { go-version-file: kit/go.mod } - uses: acifani/setup-tinygo@v2 - with: { tinygo-version: "0.41.0" } + with: { tinygo-version: "0.41.1" } - run: sudo apt-get update && sudo apt-get install -y binaryen - name: fetch pinned shellcade-kit (sha256-verified) run: | @@ -149,7 +149,7 @@ jobs: go mod edit -replace github.com/shellcade/kit/v2=../kit go mod tidy go build ./... - tinygo build -opt=1 -no-debug -gc=leaking -o cigame.wasm \ + tinygo build -opt=1 -no-debug -gc=conservative -o cigame.wasm \ -target wasip1 -buildmode=c-shared . ls -la cigame.wasm shellcade-kit check cigame.wasm @@ -232,7 +232,7 @@ jobs: - uses: actions/setup-go@v5 with: { go-version-file: kit/go.mod } - uses: acifani/setup-tinygo@v2 - with: { tinygo-version: "0.41.0" } + with: { tinygo-version: "0.41.1" } - run: sudo apt-get update && sudo apt-get install -y binaryen - name: fetch pinned shellcade-kit (sha256-verified) run: | @@ -253,7 +253,7 @@ jobs: cd cigame go mod edit -replace github.com/shellcade/kit/v2=../kit go mod tidy - tinygo build -opt=2 -no-debug -gc=leaking -o cigame.wasm \ + tinygo build -opt=2 -no-debug -gc=conservative -o cigame.wasm \ -target wasip1 -buildmode=c-shared . ls -la cigame.wasm shellcade-kit check cigame.wasm diff --git a/ABI.md b/ABI.md index af1ee08..57f308c 100644 --- a/ABI.md +++ b/ABI.md @@ -543,11 +543,13 @@ is what keeps the diff a `memcmp` and a from-scratch encoder tiny). - **Frames pass by pointer** in SDKs. A by-value ~46KB frame struct explodes into thousands of wasm locals: ~50× compile time, ~15× artifact size, and optimizer OOMs. -- TinyGo: dev profile `-opt=1 -no-debug -gc=leaking -target wasip1 +- TinyGo: dev profile `-opt=1 -no-debug -gc=conservative -target wasip1 -buildmode=c-shared` (~seconds). Release profile `-opt=2` (CI only). `-opt=0` is unsupported (oversized functions crash wazero's arm64 backend). - `-gc=leaking` is the interim profile while TinyGo's conservative GC fault is - tracked; SDKs must keep the steady state allocation-free (reused encode and + `-gc=conservative` is the profile since 2026-06-11: leaking GC OOM-trapped + long-lived rooms against the host's linear-memory cap, and the TinyGo-0.41 + conservative-GC fault does not reproduce on 0.41.1. SDKs must still keep + the steady state allocation-free (reused encode and baseline buffers, freed Extism allocations). v2's per-consumer baseline table (one packed 24-byte-cell grid per roster slot + a broadcast slot + a keyframe-sized delta scratch) is reused package globals written by index, not diff --git a/CLAUDE.md b/CLAUDE.md index 78a0744..b11d7bc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -20,7 +20,8 @@ and example games. Third-party game authors import this module. - Frames pass by **pointer** in all SDK surfaces (see ABI.md §6) — never by value. - Keep the steady state allocation-free in guest paths (reused encode - buffers, freed Extism memory) while the TinyGo GC issue is open. + buffers, freed Extism memory) — under `-gc=conservative` steady-state + allocations are GC pressure inside the callback deadline. ## Docs live here, and they are the product @@ -53,7 +54,7 @@ go build ./... && go vet ./... # end-to-end author journey (CI mirrors this): scaffold, build, verify. go run ./cmd/shellcade-kit new mygame cd mygame && go mod tidy && go run . # native dev runner -tinygo build -opt=1 -no-debug -gc=leaking \ +tinygo build -opt=1 -no-debug -gc=conservative \ -o mygame.wasm -target wasip1 -buildmode=c-shared . ``` diff --git a/GUIDE.md b/GUIDE.md index f0281ae..d7b801e 100644 --- a/GUIDE.md +++ b/GUIDE.md @@ -472,7 +472,8 @@ nothing until something happens to move. The frame-delta layer already makes CLEAN-but-resent frames cheap on the wire; dirty tracking makes them free in CPU too. Allocation discipline -matters as much as ever under `-gc=leaking`: compose into a reused +matters as much as ever — under `-gc=conservative` steady-state allocations +are GC pressure (pauses inside the callback deadline): compose into a reused `kit.Frame`, write cells directly, avoid per-player-per-wake string building. @@ -546,7 +547,7 @@ The `smoke` package exposes the same machinery as Go API (`smoke.Parse`, | Stage | Command | What it proves | |---|---|---| | iterate | `go run .` (~0.1s) | gameplay, rendering, logic | -| artifact | `tinygo build -opt=1 -no-debug -gc=leaking -target wasip1 -buildmode=c-shared .` (~4s) | the real wasm builds | +| artifact | `tinygo build -opt=1 -no-debug -gc=conservative -target wasip1 -buildmode=c-shared .` (~4s) | the real wasm builds | | verify | `shellcade-kit check game.wasm` | ABI conformance, budgets, determinism — the same gate the arcade runs | | play it | `shellcade-kit play game.wasm --seats 2` | the artifact, on the production engine | diff --git a/README.md b/README.md index 7945f32..3cbb97f 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ Rules of the road: Flags: `-seed N -heartbeat 50ms -config k=v -handle name`. 2. **Artifact check (~4s):** build the real wasm and verify it: - tinygo build -opt=1 -no-debug -gc=leaking -o game.wasm \ + tinygo build -opt=1 -no-debug -gc=conservative -o game.wasm \ -target wasip1 -buildmode=c-shared . 3. **Release:** `-opt=2` in CI (minutes — never in your inner loop). @@ -65,9 +65,12 @@ Rules of the road: `-opt=2`; expect minutes, run it in CI not your inner loop. - `-opt=0` is NOT supported (giant unoptimized functions crash wazero's arm64 compiler). -- `-gc=leaking` is required for now: TinyGo 0.41's conservative GC faults in - this reactor configuration (recorded finding; kit keeps the steady state - allocation-free so the leak rate is negligible for play sessions). +- `-gc=conservative` is the build profile (since 2026-06-11): leaking GC made + every allocation permanent, so long-lived rooms hit the host's 32 MiB cap + and trapped (~52 min of play in production). The previously recorded + TinyGo-0.41 conservative-GC fault does not reproduce on 0.41.1 + (200k-callback soak, flat memory). Keep steady-state paths allocation-free + anyway — it minimizes GC pauses inside the callback deadline. ## Test and play diff --git a/internal/game/codec.go b/internal/game/codec.go index f317748..52eafcc 100644 --- a/internal/game/codec.go +++ b/internal/game/codec.go @@ -20,10 +20,11 @@ type callContext struct { // Roster cache: the host re-sends the full member list in EVERY callback // payload, but rosters change only on join/leave/index-shift. Decoding N // members afresh per callback is O(N) string allocations per input/wake — -// under -gc=leaking (the recommended build profile while the TinyGo GC issue -// is open) those allocations are PERMANENT, so a long-lived large room leaks -// its roster at callback rate (load testing measured ~100KB leaked per -// callback at 1000 players, OOMing the guest within seconds of play). +// GC pressure under -gc=conservative (the recommended build profile), and +// under -gc=leaking builds those allocations are PERMANENT, so a long-lived +// large room leaks its roster at callback rate (load testing measured ~100KB +// leaked per callback at 1000 players, OOMing the guest within seconds of +// play). // // Instead, the raw wire bytes of the member section are compared against the // previous callback's (a ~100B/member memcmp); on a match the previously diff --git a/internal/game/diff_test.go b/internal/game/diff_test.go index 1ae648d..98e7058 100644 --- a/internal/game/diff_test.go +++ b/internal/game/diff_test.go @@ -214,7 +214,7 @@ func TestRosterCache(t *testing.T) { } // Same roster bytes: changed=false and the SAME backing slice is reused - // (zero member allocations — the -gc=leaking lifeline). + // (zero member allocations — the roster-cache lifeline; see codec.go). c2, _, changed := decodeCtx(ctxPayload(ada)) if changed { t.Fatal("identical roster must not report a change") diff --git a/internal/game/run.go b/internal/game/run.go index 6c66866..574436c 100644 --- a/internal/game/run.go +++ b/internal/game/run.go @@ -57,7 +57,8 @@ func ExportMeta() int32 { // the guest's baselines from diffing across a roster renumber. The byte // compare is strictly stronger than the fingerprint hash it replaced, and on // an unchanged roster the callback decodes ZERO member strings (allocation- -// free — load-bearing under -gc=leaking, where per-callback roster decodes +// free — still load-bearing under -gc=conservative (GC pressure), and +// essential under -gc=leaking builds, where per-callback roster decodes // leak at callback rate and OOM long-lived large rooms). func decodeCall() (*room, *wire.Rd) { ctx, r, rosterChanged := decodeCtx(inputBytes()) From ca02292b0ca760a632fbe46845d6256c10258fb0 Mon Sep 17 00:00:00 2001 From: Brandon Cook Date: Thu, 11 Jun 2026 19:23:28 +1000 Subject: [PATCH 2/2] ci: bump shellcade-kit pin to v2.9.0 (kit-pin freshness gate) The kit-pin job fails when the pinned binary release falls behind the newest published one; v2.9.0 shipped 2026-06-10, so any PR now trips the gate. SHA256 is the linux/amd64 entry from v2.9.0's checksums.txt. Co-Authored-By: Claude Fable 5 --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7fbb86f..a14b6f9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,8 +14,8 @@ on: # The kit-pin job below fails CI when this pin falls behind the newest # published binary release, so drift can't accumulate silently again. env: - SHELLCADE_KIT_VERSION: "2.8.0" - SHELLCADE_KIT_SHA256: "e7cd9dde11ff9a8c60815dee056a1c33bc35e6aef7c8131417c52e28b9c4a195" + SHELLCADE_KIT_VERSION: "2.9.0" + SHELLCADE_KIT_SHA256: "9db05e285fb0d9ea41c1f2e772d1658814d9048bc9bee0e18e89d3b2a2be203d" jobs: # Toolchain pin lockstep: the SHELLCADE_KIT_VERSION pin above is a manual