179 lines
11 KiB
JavaScript
179 lines
11 KiB
JavaScript
"use strict";
|
|
|
|
(function () {
|
|
const params = new URLSearchParams(location.search || "");
|
|
const enabled = params.get("debug") === "1" || params.get("debug") === "true";
|
|
if (!enabled) return;
|
|
|
|
const host = document.createElement("div");
|
|
host.id = "debugOverlay";
|
|
host.style.cssText = [
|
|
"position:fixed", "left:8px", "top:76px", "z-index:99999", "width:min(620px, calc(100vw - 16px))", "max-height:calc(100vh - 92px)",
|
|
"overflow:hidden", "font:11px/1.42 ui-monospace,SFMono-Regular,Menlo,monospace", "color:#f7fbff",
|
|
"background:rgba(18,24,30,.86)", "border:1px solid rgba(255,255,255,.18)", "border-radius:10px",
|
|
"pointer-events:auto", "box-shadow:0 8px 24px rgba(0,0,0,.22)"
|
|
].join(";");
|
|
|
|
const bar = document.createElement("div");
|
|
bar.style.cssText = [
|
|
"display:flex", "gap:6px", "align-items:center", "padding:6px 8px", "border-bottom:1px solid rgba(255,255,255,.14)",
|
|
"background:rgba(255,255,255,.06)", "user-select:none", "-webkit-user-select:none"
|
|
].join(";");
|
|
const title = document.createElement("span");
|
|
title.style.cssText = "flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;";
|
|
const copyBtn = document.createElement("button");
|
|
copyBtn.type = "button";
|
|
copyBtn.textContent = "Copy";
|
|
const minBtn = document.createElement("button");
|
|
minBtn.type = "button";
|
|
minBtn.textContent = "Min";
|
|
for (const btn of [copyBtn, minBtn]) {
|
|
btn.style.cssText = [
|
|
"font:11px/1.2 ui-monospace,SFMono-Regular,Menlo,monospace", "color:#f7fbff", "background:rgba(255,255,255,.12)",
|
|
"border:1px solid rgba(255,255,255,.22)", "border-radius:6px", "padding:3px 7px", "cursor:pointer"
|
|
].join(";");
|
|
}
|
|
bar.append(title, copyBtn, minBtn);
|
|
|
|
const body = document.createElement("pre");
|
|
body.style.cssText = [
|
|
"margin:0", "padding:8px 10px", "max-height:calc(100vh - 128px)", "overflow:auto", "white-space:pre-wrap",
|
|
"user-select:text", "-webkit-user-select:text", "cursor:text"
|
|
].join(";");
|
|
body.tabIndex = 0;
|
|
host.append(bar, body);
|
|
document.addEventListener("DOMContentLoaded", () => document.body.appendChild(host));
|
|
|
|
let copiedMessageUntil = 0;
|
|
let minimized = false;
|
|
let lastText = "";
|
|
|
|
function selectBodyText() {
|
|
const range = document.createRange();
|
|
range.selectNodeContents(body);
|
|
const selection = window.getSelection?.();
|
|
if (selection) {
|
|
selection.removeAllRanges();
|
|
selection.addRange(range);
|
|
}
|
|
}
|
|
|
|
copyBtn.addEventListener("click", async (ev) => {
|
|
ev.preventDefault();
|
|
const text = lastText || body.textContent || "";
|
|
try {
|
|
await navigator.clipboard?.writeText?.(text);
|
|
copiedMessageUntil = Date.now() + 1400;
|
|
copyBtn.textContent = "Copied";
|
|
} catch (_) {
|
|
selectBodyText();
|
|
copiedMessageUntil = Date.now() + 1400;
|
|
copyBtn.textContent = "Select";
|
|
}
|
|
});
|
|
|
|
minBtn.addEventListener("click", (ev) => {
|
|
ev.preventDefault();
|
|
minimized = !minimized;
|
|
body.style.display = minimized ? "none" : "block";
|
|
host.style.width = minimized ? "min(360px, calc(100vw - 16px))" : "min(620px, calc(100vw - 16px))";
|
|
minBtn.textContent = minimized ? "Open" : "Min";
|
|
});
|
|
|
|
let lastEvents = [];
|
|
let lastAudio = [];
|
|
window.TarinaiEvents.on("*", ev => { lastEvents.unshift(ev.type); if (lastEvents.length > 10) lastEvents.length = 10; });
|
|
window.TarinaiEvents.on("audio:play", ev => { lastAudio.unshift(`${ev.detail?.id || "?"}:${ev.detail?.category || "?"}`); if (lastAudio.length > 8) lastAudio.length = 8; });
|
|
|
|
function fmtMs(v) {
|
|
const n = Number(v || 0);
|
|
return n >= 10 ? n.toFixed(1) : n.toFixed(2);
|
|
}
|
|
|
|
function shortLabel(label) {
|
|
return String(label || "?")
|
|
.replace(/^update\.phase\./, "phase.")
|
|
.replace(/^update\./, "u.")
|
|
.replace(/^render\./, "r.")
|
|
.replace(/^frame\./, "f.");
|
|
}
|
|
|
|
function topMap(map, limit = 4) {
|
|
return Object.entries(map || {}).sort((a, b) => b[1] - a[1]).slice(0, limit).map(([k, v]) => `${k}:${v}`).join(", ") || "-";
|
|
}
|
|
|
|
function perfLines(perf) {
|
|
const self = (perf.topSelf || perf.entries || []).filter(e => e.label !== "frame.total").slice(0, 10);
|
|
const incl = (perf.topInclusive || perf.entries || []).filter(e => e.label !== "frame.total").slice(0, 6);
|
|
const selfLines = self.map((e, i) => `${String(i + 1).padStart(2, " ")}. ${shortLabel(e.label).padEnd(28, " ")} self ${fmtMs(e.selfAvg)}ms total ${fmtMs(e.avg)}ms ${e.selfPct || 0}%`);
|
|
const inclLine = incl.map(e => `${shortLabel(e.label)}:${fmtMs(e.avg)}`).join(" ");
|
|
return [
|
|
`heavy self avg`,
|
|
...(selfLines.length ? selfLines : [" -"]),
|
|
`heavy inclusive ${inclLine || "-"}`,
|
|
];
|
|
}
|
|
|
|
function diagnosticHints(diag, perf) {
|
|
const hints = [];
|
|
const entries = perf.topSelf || perf.entries || [];
|
|
const top = entries[0];
|
|
if (top && top.selfAvg >= 4) hints.push(`hot:${shortLabel(top.label)} ${fmtMs(top.selfAvg)}ms self`);
|
|
if ((diag.spatial?.rebuildsThisFrame || 0) > 1) hints.push(`spatial rebuild/frame ${diag.spatial.rebuildsThisFrame}`);
|
|
if ((diag.creatures?.smooth || 0) > 0) hints.push(`smooth motion ${diag.creatures.smooth}`);
|
|
if ((diag.scheduler?.activeRealtime || 0) > 120) hints.push(`realtime items ${diag.scheduler.activeRealtime}`);
|
|
if ((diag.scheduler?.heap || 0) > 700) hints.push(`scheduled heap ${diag.scheduler.heap}`);
|
|
if ((diag.items?.bucketRebuildsTotal || 0) > 0 && diag.items?.bucketsDirty) hints.push("item buckets dirty");
|
|
const m = diag.scheduler?.mechanicalStats || diag.items?.mechanicalStats || diag.scheduler?.physics?.mechanical || null;
|
|
if (m?.candidates > 250 || m?.pairs > 180) hints.push(`mechanical candidates ${m.candidates || "?"} pairs ${m.pairs || m.solved || "?"}`);
|
|
const c = diag.scheduler?.constraintStats || null;
|
|
if (c?.totalLinks > 80) hints.push(`constraints links ${c.totalLinks} ran ${c.ran}`);
|
|
return hints.join(" ") || "-";
|
|
}
|
|
|
|
const DEBUG_REFRESH_MS = 750;
|
|
window.setInterval(() => {
|
|
const w = window.world;
|
|
const input = window.TarinaiInputMode.snapshot() || {};
|
|
const diag = w?.lastRuntimeDiagnostics || w?.runtimeDiagnostics?.() || {};
|
|
const behaviorCounts = diag.behavior?.counts || {};
|
|
const topBehaviors = topMap(behaviorCounts, 5);
|
|
const perf = diag.perf || window.TarinaiPerf.snapshot() || {};
|
|
const metrics = perf.metrics || {};
|
|
const visual = perf.visual || {};
|
|
const currentDpr = (typeof uiCache !== "undefined" && uiCache) ? uiCache.canvasDpr : "?";
|
|
const creature = diag.creatures || {};
|
|
const scheduler = diag.scheduler || {};
|
|
const terrain = diag.terrain || {};
|
|
const lines = [
|
|
`tarinai v${window.TARINAI_VERSION || "?"} debug profiler:${perf.enabled ? "on" : "off"}${Date.now() < copiedMessageUntil ? " copied" : " copy:button/select"}`,
|
|
`fps ${window.__tarinaiFps ?? "?"} frame ${fmtMs(perf.frameAvg || 0)}ms visual effects:${visual.effects || "?"} shadows:${visual.shadows || "?"} details:${visual.details || perf.tier || "?"} dprScale:${perf.dprScale || 1}`,
|
|
`live ${diag.runtime?.liveTarinai ?? (w?.tarinai || []).length} items ${(w?.items || []).length} ants ${(w?.ants || []).length} effects ${(w?.effects || []).length}`,
|
|
`visible layered:${metrics["render.visibleLayered"] ?? "?"} back:${metrics["render.visibleBackItems"] ?? "?"} effects:${metrics["render.visibleEffects"] ?? "?"}`,
|
|
`creatures full:${creature.full ?? 0} realtime:${creature.realtime ?? 0} smooth:${creature.smooth ?? 0} skipped:${creature.skipped ?? 0} selected:${creature.selected ?? 0} urgent:${creature.urgent ?? 0} visible:${creature.visible ?? 0} sleepPhys:${creature.sleepPhysical ?? 0} passive:${creature.passivePhysical ?? 0}`,
|
|
`creature lanes ${topMap(creature.lanes, 6)} smoothStates ${topMap(creature.smoothStates, 4)}`,
|
|
`spatial rebuild ${diag.spatial?.rebuildsThisFrame ?? 0}/frame total:${diag.spatial?.rebuildsTotal ?? 0} partial:${diag.spatial?.partialRebuildsTotal ?? 0} dirty:${diag.spatial?.dirtyMarksThisFrame ?? 0} deferred:${diag.spatial?.deferredReads ?? 0} reason:${diag.spatial?.dirtyReason || diag.spatial?.lastRebuildReason || "-"}`,
|
|
`spatial dirtyReasons ${topMap(diag.spatial?.dirtyReasons, 4)} rebuildReasons ${topMap(diag.spatial?.rebuildReasons, 4)}`,
|
|
`nearby obstacleRects hit:${diag.nearby?.obstacleRectHits ?? 0} miss:${diag.nearby?.obstacleRectMisses ?? 0} built:${diag.nearby?.obstacleRectBuilt ?? 0} bypass:${diag.nearby?.obstacleRectBypass ?? 0} constraintDirty mark:${diag.constraintDirty?.marked ?? 0} coal:${diag.constraintDirty?.coalesced ?? 0}`,
|
|
`scheduler heap:${scheduler.heap ?? 0} realtime:${scheduler.activeRealtime ?? scheduler.realtime ?? 0} ran:${scheduler.lastRan ?? 0} itemRt:${scheduler.realtimeRan ?? 0} itemDue:${scheduler.dueRan ?? 0} constraints:${scheduler.constraints ?? 0} postPairs:${scheduler.postConstraintPairs ?? 0} rodStretch:${fmtMs(scheduler.constraintStats?.rodStretchMax || 0)} corr:${scheduler.constraintStats?.rodCorrections ?? 0}`,
|
|
`items active:${scheduler.activeRealtime ?? 0} asleep:${scheduler.sleeping ?? scheduler.heap ?? 0} budgetPairs:${scheduler.physicsBudget?.maxPairs ?? "?"} substeps:${scheduler.physics?.mechanical?.substeps ?? scheduler.mechanicalStats?.substeps ?? "?"} fenceCCD:${Boolean((scheduler.physics?.mechanical || scheduler.mechanicalStats)?.fenceSensitive)}`,
|
|
`terrain dirty raw:${terrain.raw ?? 0} global:${terrain.global ?? 0} chunk:${terrain.chunk ?? 0} coalesced:${terrain.coalesced ?? 0} reasons:${topMap(terrain.reasons, 4)}`,
|
|
`work ai ${diag.work?.stats?.aiRuns ?? 0}/${diag.work?.stats?.aiSkips ?? 0} env ${diag.work?.stats?.envRuns ?? 0}/${diag.work?.stats?.envSkips ?? 0} coll ${diag.work?.stats?.collisionRuns ?? 0}/${diag.work?.stats?.collisionSkips ?? 0}`,
|
|
`hints ${diagnosticHints(diag, perf)}`,
|
|
...perfLines(perf),
|
|
`behavior forced:${diag.behavior?.forced ?? 0} locked:${diag.behavior?.locked ?? 0} top ${topBehaviors || "-"}`,
|
|
`terrain v${w?.terrainVersion ?? 0} spatial v${w?.spatialVersion ?? 0} drawList ${(w?.drawList || []).length} dirty:${Boolean(diag.render?.drawListDirty)}`,
|
|
`item buckets:${diag.items?.bucketTypes ?? 0} dirty:${Boolean(diag.items?.bucketsDirty)} id-map:${diag.items?.idMapSize ?? 0} bucketRebuilds:${diag.items?.bucketRebuildsTotal ?? 0}`,
|
|
`dpr ${currentDpr} cache ${window.TARINAI_APP?.cacheName || "?"} sw ${navigator.serviceWorker?.controller ? "controlled" : "uncontrolled"}`,
|
|
`input ${input.currentMode || "?"} touchFirst:${Boolean(input.touchFirst)} coarse:${Boolean(input.pointerCoarse)} hover:${Boolean(input.hasHover)} compact:${Boolean(input.isCompactViewport)}`,
|
|
`audio ${lastAudio.join(", ") || window.TarinaiAudio.lastPlayedId || "-"}`,
|
|
`events ${lastEvents.join(", ")}`,
|
|
];
|
|
lastText = lines.join("\n");
|
|
title.textContent = minimized
|
|
? `tarinai v${window.TARINAI_VERSION || "?"} fps ${window.__tarinaiFps ?? "?"} frame ${fmtMs(perf.frameAvg || 0)}ms`
|
|
: `tarinai debug fps ${window.__tarinaiFps ?? "?"} frame ${fmtMs(perf.frameAvg || 0)}ms`;
|
|
if (Date.now() >= copiedMessageUntil && copyBtn.textContent !== "Copy") copyBtn.textContent = "Copy";
|
|
if (!minimized) body.textContent = lastText;
|
|
}, DEBUG_REFRESH_MS);
|
|
})();
|