mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2026-03-23 07:37:24 +01:00
- Mark Epic 2 as done and update related stories to reflect completion. - Add Epic 2 retrospective document detailing team performance, metrics, and insights. - Enhance draw-relief-icons.ts to include parentEl parameter in drawRelief function. - Introduce performance measurement scripts for WebGL and SVG rendering comparisons. - Add benchmarks for geometry building in draw-relief-icons.
155 lines
6.1 KiB
JavaScript
155 lines
6.1 KiB
JavaScript
// Performance measurement script for Story 3.1 — v2 (calls init() explicitly)
|
|
// Measures NFR-P1 through NFR-P6 using Playwright in a real Chromium browser.
|
|
import {chromium} from "playwright";
|
|
|
|
async function measure() {
|
|
const browser = await chromium.launch({headless: true});
|
|
const page = await browser.newPage();
|
|
|
|
console.log("Loading app...");
|
|
await page.goto("http://localhost:5173/Fantasy-Map-Generator/?seed=test-seed&width=1280&height=720");
|
|
|
|
console.log("Waiting for map generation...");
|
|
await page.waitForFunction(() => window.mapId !== undefined, {timeout: 90000});
|
|
await page.waitForTimeout(2000);
|
|
|
|
console.log("Running measurements...");
|
|
|
|
// Check framework availability
|
|
const frameworkState = await page.evaluate(() => ({
|
|
available: typeof window.WebGL2LayerFramework !== "undefined",
|
|
hasFallback: window.WebGL2LayerFramework?.hasFallback,
|
|
reliefCount: window.pack?.relief?.length ?? 0
|
|
}));
|
|
|
|
console.log("Framework state:", frameworkState);
|
|
|
|
// Generate relief icons if not present
|
|
await page.evaluate(() => {
|
|
if (!window.pack?.relief?.length && typeof window.generateReliefIcons === "function") {
|
|
window.generateReliefIcons();
|
|
}
|
|
});
|
|
|
|
const reliefCount = await page.evaluate(() => window.pack?.relief?.length ?? 0);
|
|
console.log("Relief icons:", reliefCount);
|
|
|
|
// --- NFR-P3: setVisible toggle time (pure JS, O(1) operation) ---
|
|
const nfrP3 = await page.evaluate(() => {
|
|
const timings = [];
|
|
for (let i = 0; i < 10; i++) {
|
|
const t0 = performance.now();
|
|
window.WebGL2LayerFramework.setVisible("terrain", false);
|
|
timings.push(performance.now() - t0);
|
|
window.WebGL2LayerFramework.setVisible("terrain", true);
|
|
}
|
|
const avg = timings.reduce((a, b) => a + b, 0) / timings.length;
|
|
const min = Math.min(...timings);
|
|
const max = Math.max(...timings);
|
|
return {avg: avg.toFixed(4), min: min.toFixed(4), max: max.toFixed(4), timings};
|
|
});
|
|
console.log("NFR-P3 setVisible toggle (10 samples):", nfrP3);
|
|
|
|
// --- NFR-P5: init() timing (re-init after cleanup) ---
|
|
// The framework singleton was already init'd at startup. We time it via Navigation Timing.
|
|
const nfrP5 = await page.evaluate(() => {
|
|
// Use Navigation Timing to estimate total startup including WebGL init
|
|
const navEntry = performance.getEntriesByType("navigation")[0];
|
|
// Also time a requestRender cycle (RAF-based)
|
|
const t0 = performance.now();
|
|
window.WebGL2LayerFramework.requestRender();
|
|
const scheduleTime = performance.now() - t0;
|
|
return {
|
|
pageLoadMs: navEntry ? navEntry.loadEventEnd.toFixed(1) : "N/A",
|
|
requestRenderScheduleMs: scheduleTime.toFixed(4),
|
|
note: "init() called synchronously at module load; timing via page load metrics"
|
|
};
|
|
});
|
|
console.log("NFR-P5 (init proxy):", nfrP5);
|
|
|
|
// --- NFR-P1/P2: drawRelief timing ---
|
|
// Requires terrain element and relief icons to be available
|
|
const terrainExists = await page.evaluate(() => !!document.getElementById("terrain"));
|
|
console.log("Terrain element exists:", terrainExists);
|
|
|
|
if (terrainExists && reliefCount > 0) {
|
|
// NFR-P1: 1k icons — slice pack.relief to 1000
|
|
const nfrP1 = await page.evaluate(
|
|
() =>
|
|
new Promise(resolve => {
|
|
const fullRelief = window.pack.relief;
|
|
window.pack.relief = fullRelief.slice(0, 1000);
|
|
const terrain = document.getElementById("terrain");
|
|
const t0 = performance.now();
|
|
window.drawRelief("webGL", terrain);
|
|
requestAnimationFrame(() => {
|
|
const elapsed = performance.now() - t0;
|
|
window.pack.relief = fullRelief; // restore
|
|
resolve({icons: 1000, elapsedMs: elapsed.toFixed(2)});
|
|
});
|
|
})
|
|
);
|
|
console.log("NFR-P1 drawRelief 1k:", nfrP1);
|
|
|
|
// NFR-P2: 10k icons — slice to 10000 (or use all if fewer)
|
|
const nfrP2 = await page.evaluate(
|
|
() =>
|
|
new Promise(resolve => {
|
|
const fullRelief = window.pack.relief;
|
|
const count = Math.min(10000, fullRelief.length);
|
|
window.pack.relief = fullRelief.slice(0, count);
|
|
const terrain = document.getElementById("terrain");
|
|
const t0 = performance.now();
|
|
window.drawRelief("webGL", terrain);
|
|
requestAnimationFrame(() => {
|
|
const elapsed = performance.now() - t0;
|
|
window.pack.relief = fullRelief;
|
|
resolve({icons: count, elapsedMs: elapsed.toFixed(2)});
|
|
});
|
|
})
|
|
);
|
|
console.log("NFR-P2 drawRelief 10k:", nfrP2);
|
|
} else {
|
|
console.log("NFR-P1/P2: terrain or relief icons not available — skipping");
|
|
}
|
|
|
|
// --- NFR-P4: Zoom latency proxy ---
|
|
// Measure time from synthetic zoom event dispatch to requestRender scheduling
|
|
const nfrP4 = await page.evaluate(() => {
|
|
const timings = [];
|
|
for (let i = 0; i < 5; i++) {
|
|
const t0 = performance.now();
|
|
// Simulate the zoom handler: requestRender() is what the zoom path calls
|
|
window.WebGL2LayerFramework.requestRender();
|
|
timings.push(performance.now() - t0);
|
|
}
|
|
const avg = timings.reduce((a, b) => a + b, 0) / timings.length;
|
|
return {
|
|
avgMs: avg.toFixed(4),
|
|
note: "JS scheduling proxy — actual GPU draw happens in RAF callback, measured separately"
|
|
};
|
|
});
|
|
console.log("NFR-P4 zoom requestRender proxy:", nfrP4);
|
|
|
|
// --- NFR-P6: GPU state preservation structural check ---
|
|
const nfrP6 = await page.evaluate(() => {
|
|
// Inspect setVisible source to confirm clearLayer is NOT called
|
|
const setVisibleSrc = window.WebGL2LayerFramework.setVisible.toString();
|
|
const callsClearLayer = setVisibleSrc.includes("clearLayer");
|
|
const callsDispose = setVisibleSrc.includes("dispose");
|
|
return {
|
|
setVisibleCallsClearLayer: callsClearLayer,
|
|
setVisibleCallsDispose: callsDispose,
|
|
verdict: !callsClearLayer && !callsDispose ? "PASS — GPU resources preserved on hide" : "FAIL"
|
|
};
|
|
});
|
|
console.log("NFR-P6 structural check:", nfrP6);
|
|
|
|
await browser.close();
|
|
console.log("\nMeasurement complete.");
|
|
}
|
|
|
|
measure().catch(e => {
|
|
console.error("Error:", e.message);
|
|
process.exit(1);
|
|
});
|