sbuffkin_hexmaker/dev/bg-hex-recalc-bench.html
isaprettycoolguy@protonmail.com b69131f16d perf: cut redundant per-item work on map + table load
Map grid: the per-hex render loop called getTerrainFromFile +
getIconOverrideFromFile + getGmIconsFromFile, each re-running
getAbstractFileByPath + getFileCache — 3 identical cache lookups per hex
(~11.5k redundant ops on a 3843-hex map). Fetch frontmatter once and feed
three new pure extractors (terrainFromFm/iconOverrideFromFm/gmIconsFromFm).
Reuse the bg-image layer across renders (bgLayerEl/bgLayerSrc) so
renderGrid()'s viewportEl.empty() no longer re-decodes the image each time.
Tag painted hexes with .duckmage-hex-painted (setHexColor) and switch the
bg-image empty-hex rule to a class selector instead of a slower
:not([style*="background-color"]) attribute-substring match.

Table view: memoise the name→entry palette Map per region instead of
rebuilding it per row, and re-filter only the rows just filled in each batch
(applyFilters(rows?)) instead of re-scanning all rows after every batch.

Adds dev/bg-hex-recalc-bench + unit tests for the new pure extractors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 07:03:23 -04:00

125 lines
4.9 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<style>
body { margin: 0; font-family: monospace; }
.viewport { position: relative; will-change: transform; }
.bg-layer {
position: absolute; top: 0; left: 0; transform-origin: 0 0;
transform: scale(4.84); z-index: 0; pointer-events: none;
}
.bg-layer img { display: block; max-width: none; max-height: none; }
.grid { position: relative; display: flex; flex-direction: row; align-items: flex-start; }
.col { display: flex; flex-direction: column; margin-right: -1.1em; }
.col-offset { margin-top: 1.9em; }
.hex {
width: 4.4em; height: 3.81em; font-size: 8px;
clip-path: polygon(25% 0%, 75% 0%, 100% 50%, 75% 100%, 25% 100%, 0% 50%);
background-color: #2a4d3a;
position: relative;
}
.hex:hover::after { content: ""; position: absolute; inset: 0; background: rgba(255,255,255,.2); }
/* ── The two competing rules — only ONE is active at a time ── */
/* A: attribute-substring selector (current code) */
.mode-attr.has-bg-image .hex:not([style*="background-color"]) { background-color: transparent; }
/* B: class selector (proposed) */
.mode-class.has-bg-image .hex:not(.has-terrain) { background-color: transparent; }
</style>
</head>
<body>
<div id="root"></div>
<script>
const COLS = 63, ROWS = 61; // chult map dimensions
const TERRAIN_FRACTION = 0.55; // ~half painted, rest show the image
const HEX_COUNT = COLS * ROWS;
const root = document.getElementById("root");
// A 1107×1499 image analogue (canvas data URL — comparable paint cost, no fetch).
const c = document.createElement("canvas");
c.width = 1107; c.height = 1499;
const ctx = c.getContext("2d");
const grd = ctx.createLinearGradient(0, 0, 1107, 1499);
grd.addColorStop(0, "#3b5"); grd.addColorStop(1, "#247");
ctx.fillStyle = grd; ctx.fillRect(0, 0, 1107, 1499);
const IMG_URL = c.toDataURL("image/png");
let painted = 0;
// Build the full map subtree once, fresh — this is what renderGrid() does on
// open: empty() the viewport then recreate everything. `config`:
// "none" → no bg image, no has-bg-image class (the "fast" baseline)
// "attr" → bg image + has-bg-image + the current [style*=…] empty-hex rule
// "class" → bg image + has-bg-image + the proposed .has-terrain rule
function build(config) {
root.className = "";
if (config !== "none") root.classList.add("has-bg-image", config === "attr" ? "mode-attr" : "mode-class");
root.replaceChildren();
const viewport = document.createElement("div");
viewport.className = "viewport";
if (config !== "none") {
const bg = document.createElement("div");
bg.className = "bg-layer";
const img = document.createElement("img");
img.src = IMG_URL;
bg.appendChild(img);
viewport.appendChild(bg);
}
const grid = document.createElement("div");
grid.className = "grid";
painted = 0;
for (let x = 0; x < COLS; x++) {
const col = document.createElement("div");
col.className = "col" + (x % 2 ? " col-offset" : "");
for (let y = 0; y < ROWS; y++) {
const hex = document.createElement("div");
hex.className = "hex";
if (((x * 31 + y * 17) % 100) / 100 < TERRAIN_FRACTION) {
hex.style.backgroundColor = "#2a4d3a"; // inline → matches [style*=…]
hex.classList.add("has-terrain"); // class → matches :not(.has-terrain)
painted++;
}
col.appendChild(hex);
}
grid.appendChild(col);
}
viewport.appendChild(grid);
root.appendChild(viewport);
}
// Time a full rebuild + forced style/layout flush — the open-time cost.
function bench(config, iters) {
for (let i = 0; i < 3; i++) { build(config); void document.body.offsetHeight; } // warm
const samples = [];
for (let i = 0; i < iters; i++) {
const t0 = performance.now();
build(config);
void document.body.offsetHeight; // force recalc + layout
samples.push(performance.now() - t0);
}
samples.sort((a, b) => a - b);
return {
median: samples[Math.floor(samples.length / 2)],
mean: samples.reduce((s, v) => s + v, 0) / samples.length,
p95: samples[Math.floor(samples.length * 0.95)],
};
}
window.__runBench = (iters = 40) => {
const none1 = bench("none", iters);
const attr1 = bench("attr", iters);
const cls1 = bench("class", iters);
const none2 = bench("none", iters);
const attr2 = bench("attr", iters);
const cls2 = bench("class", iters);
const avg = (a, b, k) => (a[k] + b[k]) / 2;
const pack = (a, b) => ({ median: avg(a, b, "median"), mean: avg(a, b, "mean"), p95: avg(a, b, "p95") });
return {
hexCount: HEX_COUNT, painted, empty: HEX_COUNT - painted, iters,
none: pack(none1, none2), attr: pack(attr1, attr2), cls: pack(cls1, cls2),
};
};
document.title = "ready";
</script>
</body>
</html>