Tilemap measurements

Measured runtime: 124b3f76f4e5dda1204e04f100bfc6078ed897cc. 2026-10-28T19:27:41.840Z, Node v24.22.1, Chromium 156.0.8078.4, Linux / AMD EPYC 9V74 80-Core Processor, WebGL/SwiftShader and Canvas2D. No other local tests/builds ran during collection. Raw samples and scripts/benchmark-tilemap.mjs preserve all 72 final runs. The earlier pre-cleanup-guard series is preserved in commit 3f1171f; this final series measures the completed runtime. Host variation between series is not evidence of a speedup/regression.

Three repetitions per size × visibility × update mode × backend. Each run warms five draws and records 20 draws. Logical view 400×300, browser 1440×900/DPR1, 16×16 world-unit cells, one map placement. All-visible centers/zooms the camera to contain the complete map; partial view covers 25×19 cells (475 Sprites). Construction includes input Grid allocation plus validated copy/counts; preparation awaits the image and includes initial visible Sprite creation and CPU first-render submission. CPU update/draw includes eight real visible cell changes in dynamic mode. Per-run p50 is the ordinary median of the 20 raw samples; p95 uses nearest rank (index 18). Both derived statistics are computed from the retained raw timings. Disposal measures map disposal, not Game/context teardown. Timings are medians of the three per-run medians (p95 is median of per-run nearest-rank 95th percentiles). No GPU completion fence, FPS, native/GPU memory measurement or engine/library comparison is implied.

webgl

Size View Changes/step Sprites Construct ms Prepare ms Update/draw p50 ms p95 ms Dispose ms
32×32 all 0 1024 0.40 9.80 0.00 0.20 1.50
32×32 all 8 1024 0.10 5.60 0.10 0.20 1.20
32×32 partial 0 475 0.10 4.30 0.00 0.10 0.50
32×32 partial 8 475 0.10 5.00 0.10 0.30 0.50
100×100 all 0 10000 0.50 24.30 0.00 0.10 7.90
100×100 all 8 10000 0.40 13.50 0.40 0.50 4.60
100×100 partial 0 475 0.50 1.80 0.00 0.10 0.30
100×100 partial 8 475 0.30 1.90 0.00 0.10 0.30
256×256 all 0 65536 4.50 154.30 0.00 0.10 48.60
256×256 all 8 65536 2.40 146.20 2.80 6.50 60.20
256×256 partial 0 475 2.30 2.20 0.00 0.10 0.20
256×256 partial 8 475 3.20 1.90 0.10 0.10 0.40

canvas

Size View Changes/step Sprites Construct ms Prepare ms Update/draw p50 ms p95 ms Dispose ms
32×32 all 0 1024 0.80 13.70 3.80 4.60 1.50
32×32 all 8 1024 0.20 7.50 3.80 4.10 0.50
32×32 partial 0 475 0.10 3.00 1.95 3.20 0.40
32×32 partial 8 475 0.10 3.50 2.00 3.10 0.30
100×100 all 0 10000 0.40 82.50 55.85 116.90 5.30
100×100 all 8 10000 0.70 104.20 73.60 133.30 7.60
100×100 partial 0 475 0.50 3.50 2.70 2.90 0.30
100×100 partial 8 475 0.40 3.10 1.70 2.20 0.40
256×256 all 0 65536 4.10 674.50 610.05 710.90 47.70
256×256 all 8 65536 2.30 707.00 578.75 688.50 61.30
256×256 partial 0 475 3.40 3.80 2.60 2.70 0.40
256×256 partial 8 475 3.50 3.90 2.70 2.80 0.40

Work and resource counts

  • All runs: exactly one internal Actor per map, one base image/TextureSource, one image lease. Visible frame views share that source; static maps display one crop Texture, dynamic maps two. Final map release returns base Texture/image-reference counts to zero. Scene/browser fixtures additionally assert Game and presentation cleanup.
  • Static stable view: zero cell visits/updates over the 20 measured draws. Dynamic: 160 visits/updates total (eight per draw), for every size and backend. These are tile-content reconciliation counts, not GPU vertex uploads or Pixi internal traversal counts.
  • Offscreen edits produce no visible cell visits; they are realized when entering view. Used-frame validation runs over unique current IDs on a content revision and caches decoded rectangles.
  • A changed visible tile rectangle scans the visible range and removes outgoing Sprites. This implementation does not yet optimize camera scrolling to incoming strips or reuse a Sprite pool. Full-map initialization/data storage remains O(width×height); fill scans data and invalidates the visible range.
  • Browser performance.now resolution is approximately 0.1ms here. WebGL p50 values printed as 0.00 mean below clock resolution, not zero work. Consecutive CPU submissions can queue GPU work; these timings do not establish real-time frame rate.
  • Canvas2D all-visible cost grows sharply: 65,536 independent visible Sprite draws are unsuitable for smooth real-time use on this software environment. The partial-view workload bounds object count regardless of map extent, but still draws all visible Sprites each frame. GPU geometry/chunk caches or a measured external tile renderer are future candidates for large all-visible scenes.
  • A cached crop remains until its base image loses its final lease, matching SpriteSheet semantics. Dynamically creating unbounded new rectangles/IDs on a permanently referenced sheet can retain crop views. Hidden/disabled/empty maps retain their declared sheet lease.

Reproduce

CHROMIUM_PATH=/path/to/chrome-headless-shell node scripts/benchmark-tilemap.mjs benchmarks/tilemap/results.json

Run alone. The script starts its own Vite server on port 4173, validates expected Sprite/visit/resource counts, and fails on fixture/page errors. It does not change runtime dependencies.

CPU-only measurements on Node v24.22.1, Linux / AMD EPYC 9V74. No renderer/browser, FPS or GPU measurements are implied. The same harness measured baseline Core d17cdf9b6ef7e33e61576c4ed24cbfcc837c38eb and current Core 9132f79a78c23ed6cd6ad8efdc881dc9282c7749. No other local tests/builds ran during collection. Baseline raw data and current raw data include every sample, construction/cold-step times, update samples, diagnostics and forced-GC heap deltas.

Each case warms 15 steps and records 40 steps at 1/60s, then 40 alternating single-cell edits plus zero-second collision steps. Cell dimensions 24×16. Small-wall uses 32 dynamic circles of radius 10 beside vertical walls; wide-sparse uses eight rotated 384×12 Trigger boxes across wall columns every 16 cells; filtered-dense uses eight radius-160 circles whose masks reject a completely filled map; dense-trigger uses eight radius-80 Trigger circles inside filled terrain. Actor masks reject Actor/Actor pairs. P50/p95 use nearest rank. All times below are p50 milliseconds per Engine.step.

Map Workload Baseline cells Current cells Current rectangles Cached cells → rectangles
32×32 small-wall 0.2255 0.2462 0.2826 32 → 2
32×32 wide-sparse 0.0791 0.0819 0.0492 32 → 2
32×32 filtered-dense 0.1364 0.1788 0.0277 512 → 2
32×32 dense-trigger 0.2769 0.2995 0.3194 512 → 2
128×128 small-wall 0.1430 0.1522 0.1902 544 → 34
128×128 wide-sparse 0.0362 0.0454 0.0649 224 → 14
128×128 filtered-dense 0.1259 0.1352 0.0127 4096 → 16
128×128 dense-trigger 0.2319 0.3511 0.3147 2816 → 11
512×512 small-wall 0.1114 0.1359 0.2515 544 → 34
512×512 wide-sparse 0.0360 0.0451 0.0634 224 → 14
512×512 filtered-dense 0.1238 0.1613 0.0138 4352 → 17
512×512 dense-trigger 0.2010 0.3779 0.3205 2816 → 11

The favorable filtered-dense case eliminates repeated per-cell policy work by rejecting whole equal-policy rectangles. Wide-sparse results vary by footprint and size. Small-wall index overhead is consistently unfavorable here; dense touching Triggers still expand cells to preserve exact contact payloads. Therefore tileSearch:"cells" remains the default. These short CPU series show environment/JIT variation as well as implementation effects; baseline/current cell timings are not identical, and they do not establish a universal regression or speedup. Changing just map extent does not imply more collision work when query footprints remain local.

Single-cell edits rebuilt at most one 256-cell chunk in every measured indexed case. Cached cells/regions describe only queried chunks, not total map coverage. A full 512×512 map has 262,144 cells; the filtered-dense run cached 4,352 collidable cells as 17 rectangles. No per-cell Actor/Collider existed before or after this change; region reduction is candidate-index geometry reduction, not a claim of 256× fewer narrow phases, response corrections or events. The index is additive memory: raw signed forced-GC heap deltas are noisy, and fixtures still retain disposed map/Actor references, so they are not a leak proof or total process/GPU memory measurement. Exact cache/contact/image-reference cleanup is tested separately.

Reproduce on the committed source, with no concurrent tests/builds:

node --expose-gc scripts/benchmark-tile-index.mjs --out /tmp/tile-index-current.json
git worktree add --detach ../Simpludify-baseline d17cdf9b6ef7e33e61576c4ed24cbfcc837c38eb
node --expose-gc scripts/benchmark-tile-index.mjs --core ../Simpludify-baseline/packages/core/src/index.ts --baseline --out /tmp/tile-index-baseline.json

The harness loads each Core directly using Vite SSR; no browser or image decode is involved. It asserts cache/contact release after disposal and retains the exact measured Core SHA. Re-run multiple series on deployment hardware before selecting a search mode; wall-clock microbenchmarks are not CI performance thresholds.