Commit Graph
43 Commits
Author SHA1 Message Date
Mark Greene c5fea49f5d Fix: Faster tank and legendary camouflage model builds
## Why

In CI, `models_legendary` was the slowest chunk of the website build (65 min on 2026-11-08), slower than `models_tanks` (49 min) even though there are far fewer camouflages (443) than tanks (738). In the CI log, each camouflage takes longer than the last, from about 8 s at the start to about 50 s near the end, which looked like a leak.

It isn't a leak:

- Nothing is kept between routes (no module-level caches) and the file reader caches nothing.
- The camouflages later in the list are heavier: newer tanks with more parts and textures.
- All the work is JavaScript on one thread, so Astro's 4 routes at a time take turns on one core. Each route's time also includes waiting for the other three. The build only kept about 1 core busy.

A CPU profile of one heavy camouflage (1224, 52 s) showed where the time goes:

- **About 60% in `readTexture`:** decoding DDS textures in our own decoder (`core/src/streams/dds.ts`).
- **About 16% in `crc32`:** checksumming every `.dvpl` game file with the pure-JavaScript `crc` package.
- **Repeated work:** that camouflage decoded 32 textures, but only 8 different files. Every part decodes the camouflage's shared atlas again (4 textures × 7 parts).

## What changed

### DDS decoder (`packages/core/src/streams/dds.ts`)

The old decoder allocated at almost every step:

- for every pixel, a new 4-value array (`[...color, alpha]`), then `forEach` + `Math.round`;
- for every block, a closure plus palette objects built with `.map()` and spread;
- for every small read (2–8 bytes), a byte copy through `consumeUint8Array`;
- every loop through lodash `times()`, adding a function call per block and per pixel.

A 2048×2048 texture has 4 million pixels in 262,000 blocks, so that meant tens of millions of allocations and calls per texture, all on the single JavaScript thread.

`decodeBlocks` now:

- reads the file's bytes in place;
- works out each block's palette once into two reused 16-byte buffers;
- writes the pixels straight into the output with plain loops.

The output is unchanged. Palette values use the same formulas in the same order (for example `(2 * c0 + c1) / 3`) and are rounded with the same `Math.round(value * 255)`. Rounding once per palette entry gives the same bytes as rounding once per pixel. Two old quirks are kept on purpose:

- BC1 is treated as transparent when `color0 < color1` (the standard says `<=`).
- BC3's 6-alpha mode stores `255` for index 7, which wraps to `1` as a byte (the standard value is opaque). This is marked in a comment and left for a separate fix, because fixing it changes some textures.

### Texture reuse (`[...chunk_models_tanks]/[id]/model.glb.ts`)

`cachedImage` keeps the last 64 finished textures (encoded images), keyed by reader and file path, and wraps `readBaseColor`, `readOcclusion`, `readRoughnessMetallic` and `readNormal`. Repeat requests reuse the same image instead of decoding and encoding again. The cap keeps memory from growing over the build. The logic inside the four readers is unchanged; they're only re-indented.

### DVPL checksum (`packages/core/src/blitz/readDVPL.ts`)

`Bun.hash.crc32` replaces the `crc` package and gives the same values. `readDVPL` only runs under Bun (every caller reads through `LocalVFS`, which already uses `Bun.file`). `crc` is removed from core's dependencies; the scripts package still uses it.

## Timings

All local timings are on the same Mac, one after another, with nothing else heavy running.

### Full builds

"Before" for a local build is CI time ÷ 1.5, which matches the real ~40-minute local camouflage build of main.

| Build | CI on main (2026-11-08) | Before, local (CI ÷ 1.5) | After, local | Speed-up |
|---|---|---|---|---|
| models_tanks | 49 min | ~33 min | 6 min 37 s | ~4.9× |
| models_legendary | 65 min | ~43 min | 4 min 52 s | ~8.9× |

With the 2026-11-10 production run (58 and 57 min), it's ~5.8× for tanks and ~7.8× for camouflages.

The camouflage build after each step, locally:

| Stage | models_legendary |
|---|---|
| main (real local build) | ~40 min |
| Texture reuse + checksum | 32 min 24 s |
| + DDS decoder | 4 min 52 s |

### Samples (main against this change, same machine)

| Sample | main | After | Speed-up | Identical |
|---|---|---|---|---|
| 30 DDS textures (BC1/BC2/BC3, incl. sRGB) | 6.1 s | 0.3 s | ~20× | 30/30 |
| 5 heavy late camouflages from CI | 124.6 s | 13.4 s | ~9.3× | 5/5 |
| 45 camouflages (every 10th in build order) | 258.5 s | 77.2 s | ~3.35× | 45/45 |
| 29 tanks (15 slowest in CI + 14 spread) | 105.3 s | 43.6 s | ~2.4× | 29/29 |

The 5 heavy camouflages after each step:

| Camouflage | main | Texture reuse | + checksum | + DDS decoder |
|---|---|---|---|---|
| 1224 | 49.7 s | 6.2 s | 6.4 s | 3.2 s |
| 2146 | 17.2 s | 6.3 s | 6.4 s | 2.9 s |
| 2122 | 31.4 s | 7.5 s | 5.9 s | 2.6 s |
| 2143 | 16.9 s | 10.1 s | 6.1 s | 2.8 s |
| 2059 | 9.6 s | 5.8 s | 4.1 s | 1.9 s |
| **Total** | **124.6 s** | **36.0 s** | **28.9 s** | **13.4 s** |

The single-camouflage speed-up (3.35× on the 45-camouflage sample) is smaller than the full-build one (~8×). With decoding no longer holding the JavaScript thread, Astro's 4 routes at a time overlap instead of queuing, and sharp's threads run alongside them.

### Full site build (`CHUNK=*`, all chunks one after another)

36 min 36 s, 0 errors, 21,613 routes, peak memory 4.6 GB.

| Chunk | Time |
|---|---|
| api | ~4.3 min |
| models_tanks | ~6.1 min |
| models_legendary | ~4.6 min |
| default (pages, 9 locales) | ~21 min |

### CPU and memory

- The tank build used 1,183 s of CPU in 397 s, so about 3 cores busy on average; before, it was about 1.
- Peak memory was 3.5 GB for the tank build and 4.6 GB for the full site.
- CI runners have 4 cores and 16 GB, so this fits. No threads or processes were added: Astro's concurrency (4) and sharp's threading are unchanged.

## What this means for CI

The chunks run as parallel jobs, so the website build takes as long as its slowest chunk. Before, that was `models_legendary` at about 65 min. Using CI ≈ 1.5× local, tank models should take about 10 min and camouflages about 7–8 min. `default` (about 36 min in CI) becomes the slowest chunk, so the whole build should drop from about 65 min to about 36. This is an estimate until the next CI run.

## Checked

- Byte-identical GLBs (SHA-1) for 45 camouflages and 29 tanks against main, and decoded pixels for 30 DDS textures.
- A full site build finishes with no errors.
- Animations and particles are unaffected: the camouflage GLBs, including their animation data, are byte-identical.
2026-11-13 17:06:17 +13:00
tresabhi ad0c8b3431 open source WOTB_GLOSSARY 2026-11-08 18:38:46 -06:00
tresabhi b26c5137f3 aliases 2026-08-06 20:01:50 -05:00
tresabhi bc2b9f1267 glossary 2026-08-05 13:01:51 -05:00
TrèsAbhi e2f50871ec fix: update all 2026-03-31 14:11:45 -05:00
TrèsAbhi 9ffc040723 feat: implement all methods 2025-11-29 15:16:09 -06:00
TrèsAbhi 9cb1d0cb3f fix: read dvpl files 2025-11-26 13:25:46 -06:00
TrèsAbhi 9ca329e53f feat: start vfs 2025-11-24 11:02:18 -06:00
TrèsAbhi 26304fc281 feat: some more type prefixes 2025-10-13 23:20:26 -05:00
TrèsAbhi 0d8cb5c1ae feat: some type prefixes 2025-10-13 23:15:32 -05:00
TrèsAbhi 03a51c47bb fix: self core imports 2025-10-12 15:05:58 +00:00
tresabhi c5e6f5e2eb feat: glossary cron 2025-04-19 00:47:28 -05:00
tresabhi 73cb3873d9 feat: search min and max lengths 2024-12-14 13:27:45 -06:00
tresabhi 2f7f2362bb feat: auction command 2024-12-13 10:58:16 -06:00
tresabhi c0d16b51de fix: use import.meta.env. 2024-11-29 12:11:51 -06:00
tresabhi 5bf6a14cff fix: some rating functionality 2024-11-20 10:50:09 -06:00
tresabhi fcb848df5b feat: change tier back to number 2024-11-20 10:26:55 -06:00
tresabhi 5ae6961a13 feat: migrate 2024-11-18 11:04:35 -06:00
tresabhi 02aed3e3a5 feat: some migration 2024-11-18 10:56:24 -06:00
TrèsAbhi 1e64cb9e7d feat: logins 2024-10-09 19:40:55 -05:00
TrèsAbhi 5fcc42e065 fix: casing 2 2024-10-07 18:48:16 -05:00
TrèsAbhi 95bb67bcf1 fix: some linting for new proto 6 2024-10-06 00:02:50 -05:00
TrèsAbhi 96310a9d63 fix: some linting for new proto 5 2024-10-05 23:20:04 -05:00
TrèsAbhi f14ca394b5 fix: some linting for new proto 3 2024-10-05 16:44:07 -05:00
TrèsAbhi 79bc259565 fix: some linting for new proto 2 2024-10-05 15:57:34 -05:00
TrèsAbhi 78dce028d9 idk bro 2024-10-02 10:21:16 -05:00
TrèsAbhi 57170469f2 feat: some work on functional pro ts 2024-09-29 22:24:35 -05:00
TrèsAbhi 85491c7f22 fix: logsin 2024-09-15 16:20:11 -05:00
TrèsAbhi 502dcf4e54 feat: more migration 2024-09-14 19:43:14 -05:00
TrèsAbhi 8f44cdedd4 fix: port to core 7 2024-09-08 10:53:54 -05:00
TrèsAbhi 3d23ea1497 fix: port to core 4 2024-09-08 10:42:34 -05:00
TrèsAbhi de6a4d5675 fix: port to core 3 2024-09-08 10:28:54 -05:00
TrèsAbhi 5e989a16bc fix: port to core 1 2024-09-08 10:24:48 -05:00
TrèsAbhi fd2e2f9c7b fix: more imports 4 2024-09-08 10:21:08 -05:00
TrèsAbhi 843992e631 fix: more imports 3 2024-09-08 10:17:40 -05:00
TrèsAbhi c51ddbc725 fix: more imports 2 2024-09-08 10:15:42 -05:00
TrèsAbhi 4f24db0956 fix: more imports 2024-09-08 10:11:24 -05:00
TrèsAbhi 7c6f381bbe fix: no cross-bot imports 2024-09-07 23:00:56 -05:00
TrèsAbhi 3c80a43435 fix: types 2024-09-07 21:45:00 -05:00
TrèsAbhi 4055988478 feat: migration 3 2024-09-06 23:26:35 -05:00
TrèsAbhi 6285f33989 feat: migration 2 2024-09-06 23:17:53 -05:00
TrèsAbhi e1c8194beb feat: migrated tankopedia search 2024-09-06 22:05:06 -05:00
TrèsAbhi 73c7bdf0c0 feat: start of monorepo 2024-09-06 21:45:58 -05:00