From adc933304f401b9b3ed805ecd93b047afe5cae6d Mon Sep 17 00:00:00 2001 From: "@permadeath.com" Date: Mon, 10 Aug 2026 19:40:57 -0400 Subject: [PATCH] docs: catch counts, paths and claims up with the tree Nine MegaMek patches and three Suramadu patches, not eight and two; test.sh and run.sh live under scripts/; every archive digest is pinned now; patch 0003 shipped and the DirectDraw cache is 16384; prune-data removes ~420MB; client paths are /, not /megamek or /play/. --- DEVELOPING.md | 15 ++++++------ LICENSING.md | 11 +++++---- PERFORMANCE.md | 4 ++-- README.md | 23 +++++++++++-------- TODO.md | 22 ++++++++++-------- .../patches/archive/multitenancy/README.md | 5 ++-- tests/README.md | 2 +- tests/run.sh | 2 +- 8 files changed, 48 insertions(+), 36 deletions(-) diff --git a/DEVELOPING.md b/DEVELOPING.md index 9d937ff..fa30a3a 100644 --- a/DEVELOPING.md +++ b/DEVELOPING.md @@ -6,7 +6,8 @@ Docker is the only requirement. There is no host build. ./scripts/build.sh build the image ./scripts/test.sh lint, syntax, unit and compile checks - ./scripts/run.sh play a match; http://localhost:8080/megamek/ + ./scripts/run.sh play a match; http://localhost:8080/ lists the + per-seat clients ./scripts/push.sh build and publish; REGISTRY overrides the default ./scripts/deploy.sh push, then point infra at what was pushed @@ -21,7 +22,7 @@ something that has to start it, wait for it, measure something and stop it. ./scripts/arena.sh logs -f ./scripts/arena.sh down # safe when nothing is running -`up` returns when `http://localhost:8080/megamek/` answers, not when docker +`up` returns when `http://localhost:8080/` answers, not when docker accepted the run, and when it never answers it fails with the container's last log lines rather than hanging. Exit codes say why: **2** a match is already running, **3** it never served, **4** nothing is running. @@ -162,8 +163,8 @@ says which mode you are in. ### Working on one stage -Stages are `jre`, `megamek`, `suramadu`, `arena`, `test`, `runtime`. Build just -one, then poke around inside it: +Stages are `base`, `jre`, `megamek`, `suramadu`, `arena`, `test`, `profiler`, +`runtime`. Build just one, then poke around inside it: docker build --target megamek -t arena-mm . docker run --rm -it --entrypoint /bin/bash arena-mm @@ -201,6 +202,6 @@ Downloads are fetched to `.part`, verified, and only then renamed, so an interrupted transfer never becomes a cache entry. A cached file that fails its check is deleted and refetched. **Pruning is not the fix** — just build again. -Only the JDK has a pinned digest. MegaMek and Suramadu have no published -checksum, so their check is "does it decompress", which catches truncation but -not substitution. See [TODO.md](TODO.md). +Every archive is pinned by SHA-256 in `versions.env` and verified before the +cache entry is created — the JDK against the vendor's published digest, MegaMek +and Suramadu against digests recorded from a verified fetch. diff --git a/LICENSING.md b/LICENSING.md index 9368445..85bf9ce 100644 --- a/LICENSING.md +++ b/LICENSING.md @@ -106,20 +106,21 @@ is no conflict, only the network clause to comply with. ## Our patches -Eight, in [megamek/patches/patchset/](megamek/patches/patchset/), stored as +Nine, in [megamek/patches/patchset/](megamek/patches/patchset/), stored as unified diffs rather than copied source files. A copied file would silently discard upstream changes on the next release; a diff fails to apply instead, which is the behaviour we want. Each one fixes a MegaMek bug rather than adapting MegaMek to our environment, so -all five should go upstream. An accepted PR costs nothing to maintain; a carried +all nine should go upstream. An accepted PR costs nothing to maintain; a carried patch costs something every release. `megamek/verify-patches.sh` runs on every build and checks that the patched jar differs from stock in exactly those classes and no others. -One more, in [suramadu/patches/](suramadu/patches/), applied to the frontend -bundle inside the Suramadu war: keyboard input is sent immediately instead -of waiting on timers. Suramadu is AGPL-3.0, so the same rule applies with +Three more, in [suramadu/patches/](suramadu/patches/), applied to the frontend +bundle inside the Suramadu war: keyboard input sent immediately, mouse input +flushed faster, and frames acknowledged on receive rather than after paint. +Suramadu is AGPL-3.0, so the same rule applies with more force — we serve the modified code over a network. The patches ship inside the image at `/opt/arena/patches` (Suramadu's under diff --git a/PERFORMANCE.md b/PERFORMANCE.md index a0ab299..7c729c5 100644 --- a/PERFORMANCE.md +++ b/PERFORMANCE.md @@ -24,7 +24,7 @@ down from ~470ms to ~330ms under heavier play. | Send keyboard input immediately (suramadu patch 0001) | `1365089` | The client held keys behind a 100ms timer. keydown-to-socket 33-134ms down to ~21ms. | | EntityImage headless fix (megamek patch 0005) | `21c80d5` | The class died at static init in headless JVMs, breaking camo and re-running image work per entity sync. | | Flush coalesced mouse input every 20ms (suramadu patch 0002) | `eef0e92` | Hover/drag paid up to 100ms before the server saw the mouse. Input-to-socket p90 is now under 5ms. | -| DirectDraw image cache 128 to 4096 entries | `bab83d6` | MegaMek's hex/sprite working set far exceeds 128; evicted images re-send as full PNGs. | +| DirectDraw image cache 128 to 16384 entries | `bab83d6` `8de5814` | MegaMek's hex/sprite working set far exceeds 128; evicted images re-send as full PNGs. 4096 first, then 16384: -17%/-23% bytes. | | Paint dispatcher tick 33ms to 16ms | `e6ba5cc` | A repaint costs ~1.5 ticks of latency; halving the tick took ~25ms off the median frame. | | lance.blue skin: 1008px background tile, no board border | `0d96fa1` `63898c3` `e61fc91` | Branding, plus a cache-friendly large tile and a reclaimed 45px border band. | | Skip pointless FoV hex-cache clears (megamek patch 0006) | `207dec8` | Selecting a unit cleared the whole hex composite cache, recomposited the board, stalled the EDT, and tripped Suramadu's cache-reset timeout. Biggest tail win: total p90 ~440ms to ~330ms. | @@ -32,13 +32,13 @@ down from ~470ms to ~330ms under heavier play. | Disable move animation and chat auto-slide | `207dec8` `400e4ac` | Repaint churn: hex-by-hex movement and a 50Hz slide animation each streamed frames for nothing. | | Headless accelerated-image fallback (megamek patch 0008) | `351eb0b` | `createAcceleratedImage(Image)` returned null headless, erasing base images mid-build and aborting `SENDING_ENTITIES` — silent bot force-ID divergence. Correctness fix; negligible CPU. Not yet confirmed in a container. | | Turn FoV darkening off | `33518d6` | The shading is baked into the hex composites, so what patch 0006 kept was a full recomposite of the board on every real selection change, plus a `LosEffects` per hex out to 60. Off, `checkFoVHexImageCacheClear` returns immediately. Unmeasured, but it is the same work 0006 measured, on the path 0006 left in place. | +| Client ack-on-receive (suramadu patch 0003) | `1a1e5c7` | The client acked a frame only after painting it, keeping the render inside the server's send loop; acking on receive lets the next frame travel while this one paints. | ## Investigated, not shipped | Idea | Outcome | |---|---| | Raise `ddMaxConstCacheSize` | Wrong knob: it is an id-space boundary and per-frame sanity cap, not a memory limit. | -| Client ack-on-receive (frame pipelining) | Designed (~15 lines, stats-safe). Parked: the stream is now content-limited, not transport-limited. | | Server-side two frames in flight | Rejected: requires a jar-patch build pipeline for one Suramadu class. Revisit only if transport-bound again. | | Ship fonts to the browser (`#@@` fontconfig entries) | Named physical fonts (Noto Sans, symbols) rasterize server-side; shippable but a polish item, not latency. | | Repaint the board by hex instead of in full | Measured and rejected — see below. Do not retry without a new argument. | diff --git a/README.md b/README.md index f996b9a..c76ecee 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,8 @@ browser. This repo packages that into a container image. Docker is the only requirement. There is no host build. ./scripts/build.sh # build the image - ./scripts/run.sh # play a match; open http://localhost:8080/megamek/ + ./scripts/run.sh # play a match; open http://localhost:8080/ and + # pick the seat — one client app per seat, at / ./scripts/test.sh # lint, syntax, unit and compile checks ./scripts/push.sh # build and publish; REGISTRY overrides the default @@ -52,12 +53,12 @@ a rendering bug, everything MegaMek-shaped is in one directory. | | | |---|---| -| `megamek/` | configuring and patching MegaMek: the eight patches, client settings, logging config, `units.cache` priming, `prune-data.sh` — one documented line per thing removed from the shipped data, and why a match cannot reach it — and `skin/` — `lanceBlueSkin.xml`, the lance.blue board backgrounds and window icons, and the install script that checks every image the skin references exists | +| `megamek/` | configuring and patching MegaMek: the nine patches, client settings, logging config, `units.cache` priming, `prune-data.sh` — one documented line per thing removed from the shipped data, and why a match cannot reach it — and `skin/` — `lanceBlueSkin.xml`, the lance.blue board backgrounds and window icons, and the install script that checks every image the skin references exists | | `suramadu/` | configuring Suramadu: its server config, Jetty properties, the fontconfig generator its toolkit requires, `web/` — the themed loading page served in place of the war's — and `patches/`, applied to the frontend bundle inside the war at build time | | `src/` | our own Java — `MatchHost`, `MatchWatcher`, `TextRenderer` — and the script that jars it | | `container/` | process management inside a running container: `entrypoint.sh`, `init/`, `watch/`, `exit/`, `lib/` | | `tests/` | the suite, plus the benchmark and screenshot helpers that are not part of it | -| `run.sh` `test.sh`, `scripts/` | what you type | +| `scripts/` | what you type | | `Dockerfile` | assembles the above; also where the downloads happen | | `versions.env` | every pinned version and checksum | @@ -72,8 +73,9 @@ build in parallel. Headquarters launches a task with one input: a presigned URL for a **launch manifest**, which names the scenario, the player slots, their ATProto DIDs and -where to upload results. Nothing else is read from the environment. The contract -is `docs/launch-manifest.md` in the headquarters repo. +where to upload results. Nothing else about the match is read from the +environment; `ARENA_PROFILE` and its two companions are the one diagnostic +toggle. The contract is `docs/launch-manifest.md` in the headquarters repo. entrypoint.sh ├── init/ manifest → identity → assets → rendered config @@ -81,7 +83,7 @@ is `docs/launch-manifest.md` in the headquarters repo. ├── host arena.MatchHost — the MegaMek server, Princess, one game ├── watcher arena.MatchWatcher — observer client spooling turn reports ├── watch/ uploads reports while the match is still being played, and - │ times out a lobby nobody joins + │ times out a match nobody is playing └── exit/ result, images and any deferred uploads, on victory *or* signal The container exits when the host exits. Suramadu and the watcher are @@ -106,7 +108,7 @@ manifest that omits the limit, or sets it to 0, gets no timeout. ## Patches -Eight, in `megamek/patches/patchset/`. Each is a MegaMek bug rather than an +Nine, in `megamek/patches/patchset/`. Each is a MegaMek bug rather than an adaptation to our environment, so each should go upstream. `megamek/verify-patches.sh` runs on every build and checks that the patched jar differs from stock in exactly these classes and no others. @@ -153,10 +155,10 @@ differs from stock in exactly these classes and no others. the overlay-off path uses, and reads with `ImageIO` instead of `ImageIcon` plus `PixelGrabber` so the load cannot abort or take an interrupt. -Two more in `suramadu/patches/`, applied by `suramadu/patch-war.sh` to the +Three more in `suramadu/patches/`, applied by `suramadu/patch-war.sh` to the frontend bundle inside the Suramadu war. `suramadu/verify-embed.sh` runs on every build and checks the served bundle — including its precompressed -copies — actually carries both: +copies — actually carries all three: - **0001 keyboard latency** — the stock frontend parks keydown events behind a 100ms timer and only flushes its input queue on keyup or a 100ms interval, @@ -166,6 +168,9 @@ copies — actually carries both: flush interval, so hover feedback and drags paid it twice over. The patch drops the interval to 20ms in both mouse and touch mode. Measured: input-to-socket p90 under 5ms. +- **0003 paint ack** — the stock frontend acknowledged a frame only after + painting it, so the render sat inside the server's send loop. The patch + acks on receive, letting the next frame travel while this one paints. ## Constraints diff --git a/TODO.md b/TODO.md index 8798911..2ad641a 100644 --- a/TODO.md +++ b/TODO.md @@ -9,7 +9,7 @@ the finish is supposed to leave behind. ## Verify the port -- [x] **`./test.sh` and `./scripts/build.sh`** — both run clean. +- [x] **`./scripts/test.sh` and `./scripts/build.sh`** — both run clean. - [x] **Suramadu under the jlink runtime.** `jdk.net` was the missing module: without it Jetty bound the port and then threw on every accept(), which presents as a network fault. `tests/module-scan.py` reports what is @@ -85,14 +85,14 @@ the finish is supposed to leave behind. server-side from the observer's state. - [ ] **Multi-human matches** are expressible in the manifest and handled by `MatchHost`, and every human seat now renders its own Suramadu app at - `/play/` — cloned at render time from the template app with only + `/` — cloned at render time from the template app with only the path, name and login changed, then the template is deleted, so the per-identity paths are the only clients the container serves. Keyed by DID because a DID is public and unique, and because it points where this wants to go: the container checking the visitor's identity itself — a proof bound to the DID the path names — rather than trusting whatever the proxy let through. Spectator manifests render - `/play/ArenaSpectator`. + `/ArenaSpectator`. What is left: headquarters' proxy matching caller DID to path DID (a string compare on its side), the container-side identity check @@ -336,8 +336,10 @@ and the p90 tail fell from ~470ms to ~330ms. The proxy was ruled out early - [x] **The local match lifecycle now runs.** `up`, `status`, `logs`, `wait-ready` and `down` were exercised against the real daemon on - 2026-08-07. What the stubbed tests could not answer, answered: `/megamek/` - returns 200, so the readiness probe tests the right URL; a match serves + 2026-08-07. What the stubbed tests could not answer, answered: the + client URL returned 200, so the readiness probe tested the right URL — + `/megamek/` then; the per-DID clients replaced it and the probe checks + the container root now; a match serves **7s** after `up`, nowhere near the 180s timeout; the caps docker reports back match what was asked for; `down` leaves nothing behind; and `up -i newest` resolves through `scripts/image.sh`. Two bugs the stubs @@ -407,8 +409,10 @@ and the p90 tail fell from ~470ms to ~330ms. The proxy was ruled out early headquarters' `config.rs` describe the format and were updated with it. - [ ] **The image pull adds ~16s to every match start.** The image is on the order of a gigabyte, and Fargate pulls it cold for each task. - `megamek/prune-data.sh` now removes ~304MB of it - the crew portrait - packs, two hex sets nothing loads, and MekHQ/MegaMekLab art - against a + `megamek/prune-data.sh` now removes ~420MB of it - the crew portrait + packs, two hex sets nothing loads, MekHQ/MegaMekLab art, an 89MB + leftover `border.psd`, the map-pack `.docx`, a duplicate + `lib/MegaMek.jar` and the planetary-systems data - against a MegaMek release tarball of 609MiB gzipped. What that is worth in seconds is still unmeasured: it needs a build and a push, because pull time follows compressed image size and nothing has yet reported one. Read it @@ -445,9 +449,9 @@ and the p90 tail fell from ~470ms to ~330ms. The proxy was ruled out early Unmeasured on Fargate: the overlap is real but the two JVMs now contend, and only a live task says what the net is. The phase marks above are how to read it - compare `host-ready` against the old serial sum. -- [ ] **Upstream all eight MegaMek patches.** Each is a latent MegaMek bug. An +- [ ] **Upstream all nine MegaMek patches.** Each is a latent MegaMek bug. An accepted PR costs nothing to maintain; a carried patch costs something - every release. The two Suramadu frontend patches are the same + every release. The three Suramadu frontend patches are the same argument against a different upstream. - [ ] **x86_64 only.** `JDK_SHA256` in `versions.env` pins one architecture, and the Dockerfile's JDK URL is x64. Needs a per-arch checksum table before an diff --git a/megamek/patches/archive/multitenancy/README.md b/megamek/patches/archive/multitenancy/README.md index a85b96d..52809b7 100644 --- a/megamek/patches/archive/multitenancy/README.md +++ b/megamek/patches/archive/multitenancy/README.md @@ -17,5 +17,6 @@ Dropped because: 2-game run silently wedging). Other `getServerInstance()` callers remain in `Compute`, `WeaponHandler`, `ACWeapon` and `TWGameManager`. -To reproduce the density experiment, restore these to `patches/src/` and add -them back to `megamek/apply-patches.sh`. +To reproduce the density experiment, copy `multitenancy.patch` into +`megamek/patches/patchset/`; `megamek/apply-patches.sh` applies everything in +that directory. diff --git a/tests/README.md b/tests/README.md index 89b8c16..052067a 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,7 +1,7 @@ # tests ```sh -./test.sh # from the repo root - the supported way +./scripts/test.sh # from the repo root - the supported way ``` Everything runs in the image's `test` stage, so the only requirement on your diff --git a/tests/run.sh b/tests/run.sh index 085c531..d6a4b29 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -9,7 +9,7 @@ # PATCHED_JAR the jar megamek/apply-patches.sh produced # JAVA_HOME a JDK # -# ./test.sh from the repo root, the supported way +# ./scripts/test.sh from the repo root, the supported way # ./tests/run.sh lint one group, if you are already inside the stage # # Groups, fastest first, so a break surfaces early: -- 2.51.2