map/tests/additional-generation-max-worker.mjs
2026-08-10 13:59:33 +09:00

134 lines
5 KiB
JavaScript

import { Worker } from "node:worker_threads";
import { performance } from "node:perf_hooks";
import { generateMap } from "../src/mapPipeline.js";
import { createWorldMap } from "../src/worldMap.js";
const budgetMs = Math.max(1, Number(process.env.PATCH_MAX_WORKER_BUDGET_MS) || 60_000);
const timeoutMs = Math.max(90_000, budgetMs + 30_000);
const worldSeed = Number(process.env.PATCH_TEST_WORLD_SEED ?? 114514) >>> 0;
const candidateVariant = Number(process.env.PATCH_TEST_VARIANT ?? 0) >>> 0;
function deriveSeed(worldSeed, terrainType, variant) {
let h = (worldSeed >>> 0) ^ 0x9e3779b9;
h = Math.imul(h ^ (variant >>> 0), 668265263) >>> 0;
for (const ch of String(terrainType || "auto")) h = Math.imul(h ^ ch.charCodeAt(0), 16777619) >>> 0;
return h >>> 0;
}
const initialStartedAt = performance.now();
const initial = generateMap(worldSeed);
const initialGenerationMs = Math.round(performance.now() - initialStartedAt);
const world = createWorldMap(initial);
const width = 470;
const height = 333;
const x0 = Math.max(0, Math.min(world.width - width, world.originX + 238));
const y0 = Math.max(0, Math.min(world.height - height, world.originY));
const rect = { x0, y0, x1: x0 + width, y1: y0 + height, kind: "lasso" };
const insetX = Math.max(4, Math.floor(width * 0.16));
const insetY = Math.max(4, Math.floor(height * 0.16));
rect.polygon = [
{ x: rect.x0 + insetX, y: rect.y0 },
{ x: rect.x1 - 1, y: rect.y0 + insetY },
{ x: rect.x1 - insetX, y: rect.y1 - 1 },
{ x: rect.x0, y: rect.y1 - insetY },
];
const terrainType = initial.terrainTemplate?.terrainType || initial.terrainDebug?.terrainType || "auto";
const seed = deriveSeed(world.seed, terrainType, candidateVariant);
const candidatePlan = [{ candidateId: `max-worker:${candidateVariant}`, candidateOrdinal: 1, variant: candidateVariant, seed }];
const worker = new Worker(new URL("./browser-worker-node-shim.mjs", import.meta.url), { type: "module" });
const startedAt = performance.now();
let progressEvents = 0;
let maxRssBytes = process.memoryUsage().rss;
let maxHeapBytes = process.memoryUsage().heapUsed;
let lastLabel = "";
const memoryTimer = setInterval(() => {
const memory = process.memoryUsage();
maxRssBytes = Math.max(maxRssBytes, memory.rss);
maxHeapBytes = Math.max(maxHeapBytes, memory.heapUsed);
}, 100);
async function finish(exitCode, payload) {
clearInterval(memoryTimer);
clearTimeout(timeoutTimer);
try { await worker.terminate(); } catch {}
const elapsedMs = Math.round(performance.now() - startedAt);
const finalMerge = payload?.candidateQuality?.finalMerge || null;
const qualityPass = payload?.candidateQuality?.hardPass === true && finalMerge?.hardPass === true;
const seamPass = payload?.seamDiagnostics?.hardPass === true;
const withinBudget = elapsedMs < budgetMs;
const ok = payload?.ok === true && payload?.searchStatus === "succeeded" && qualityPass && seamPass && withinBudget;
console.log(JSON.stringify({
ok,
worldSeed,
candidateVariant,
workerResultOk: payload?.ok === true,
initialGenerationMs,
elapsedMs,
budgetMs,
withinBudget,
maxRssBytes,
maxHeapBytes,
progressEvents,
lastLabel,
searchStatus: payload?.searchStatus || null,
terminalCode: payload?.terminalCode || payload?.code || null,
qualityPass,
seamPass,
finalMerge,
humanTerrainReconciliation: payload?.seamDiagnostics?.humanTerrainReconciliation || null,
error: payload?.error || null,
reason: payload?.reason || null,
}, null, 2));
process.exit(ok ? 0 : exitCode || 1);
}
const timeoutTimer = setTimeout(() => finish(2, { ok: false, error: `Timed out after ${timeoutMs} ms.` }), timeoutMs);
worker.on("message", (message) => {
if (message.id !== 1) return;
if (message.type === "progress") {
progressEvents++;
const progress = message.progress || {};
const label = String(progress.label || "");
if (label && label !== lastLabel && (/Large expansion tile|finalization complete/i.test(label))) {
lastLabel = label;
console.error(`[max-worker] ${Math.round(performance.now() - startedAt)} ms: ${label}`);
}
return;
}
finish(message.ok ? 1 : 3, {
ok: message.ok,
code: message.code,
error: message.error,
searchStatus: message.result?.searchStatus,
terminalCode: message.result?.code,
reason: message.result?.reason,
candidateQuality: message.result?.candidateQuality,
seamDiagnostics: message.result?.seamDiagnostics,
});
});
worker.on("error", (error) => finish(3, { ok: false, error: error?.stack || String(error) }));
worker.postMessage({
id: 1,
world,
rect,
options: {
patchMode: "expansion",
terrainType,
variant: candidateVariant,
seed,
maxQualityRetries: 0,
qualityTerrainAttempts: 1,
acceptBestAvailableQuality: false,
includeSeamVisualization: false,
},
search: {
searchId: "max-worker-benchmark",
operationId: "max-worker-benchmark",
committedRevision: 1,
workerEpoch: 1,
executionAttempt: 1,
totalCandidateCount: 1,
candidatePlan,
},
});