tarinai/js/perf_profiler.js
2026-07-01 22:59:49 +09:00

224 lines
6.8 KiB
JavaScript

"use strict";
(function (global) {
const nowMs = () => (global.performance?.now ? global.performance.now() : Date.now());
const debugEnabled = (() => {
try {
const params = new URLSearchParams(global.location?.search || "");
return params.get("debug") === "1" || params.get("debug") === "true" || params.get("perf") === "1";
} catch (_) {
return false;
}
})();
const buckets = new Map();
const stack = [];
const metrics = new Map();
let frameOpen = null;
let frameCount = 0;
let lastSmoothAt = 0;
const storageKey = "tarinai_visual_settings_v1";
const allowed = {
effects: new Set(["off", "low", "mid", "high"]),
shadows: new Set(["off", "low", "mid", "high"]),
details: new Set(["low", "mid", "high"]),
};
const defaults = Object.freeze({ effects: "high", shadows: "high", details: "high" });
let visualSettings = loadVisualSettings();
let resizeRequested = false;
function bucketFor(label) {
let b = buckets.get(label);
if (!b) {
b = { label, ms: 0, selfMs: 0, avg: 0, selfAvg: 0, max: 0, calls: 0, last: 0, lastSelf: 0 };
buckets.set(label, b);
}
return b;
}
function begin(label) {
if (!debugEnabled) return null;
const frame = { label: String(label || "unknown"), start: nowMs(), childMs: 0 };
stack.push(frame);
return () => {
const top = stack.pop();
const active = top === frame ? frame : top || frame;
// If calls ended out of order, keep the profiler alive rather than throwing.
if (top !== frame) {
const idx = stack.lastIndexOf(frame);
if (idx >= 0) stack.splice(idx, 1);
}
const elapsed = Math.max(0, nowMs() - active.start);
const selfElapsed = Math.max(0, elapsed - (active.childMs || 0));
if (stack.length) stack[stack.length - 1].childMs += elapsed;
const b = bucketFor(active.label);
b.ms += elapsed;
b.selfMs += selfElapsed;
b.calls += 1;
b.last = elapsed;
b.lastSelf = selfElapsed;
if (elapsed > b.max) b.max = elapsed;
return elapsed;
};
}
function beginFrame() {
frameOpen = nowMs();
if (debugEnabled) stack.length = 0;
return frameOpen;
}
function smoothBuckets() {
if (!debugEnabled) return;
for (const b of buckets.values()) {
b.avg = b.avg ? b.avg * 0.82 + b.ms * 0.18 : b.ms;
b.selfAvg = b.selfAvg ? b.selfAvg * 0.82 + b.selfMs * 0.18 : b.selfMs;
b.ms = 0;
b.selfMs = 0;
b.calls = 0;
}
}
function normalizeVisualSetting(key, value) {
const raw = String(value || "");
if (key === "effects" || key === "shadows") {
if (raw === "off") return "off";
if (raw === "on") return "high";
if (raw === "low" || raw === "mid" || raw === "high") return raw;
}
if (key === "details") {
if (raw === "low") return "low";
if (raw === "mid" || raw === "high") return raw;
}
return defaults[key];
}
function sanitizeVisualSettings(value) {
const src = value && typeof value === "object" ? value : {};
const next = { ...defaults };
for (const key of Object.keys(defaults)) {
const normalized = normalizeVisualSetting(key, src[key]);
if (allowed[key]?.has(normalized)) next[key] = normalized;
}
return next;
}
function loadVisualSettings() {
try {
const raw = global.localStorage?.getItem(storageKey);
return sanitizeVisualSettings(raw ? JSON.parse(raw) : null);
} catch (_) {
return { ...defaults };
}
}
function saveVisualSettings() {
try {
global.localStorage?.setItem(storageKey, JSON.stringify(visualSettings));
} catch (_) {}
}
function dprScaleForDetails(details) {
if (details === "low") return 0.72;
if (details === "mid") return 0.86;
return 1;
}
function endFrame() {
frameCount += 1;
if (frameOpen != null && debugEnabled) {
const elapsed = Math.max(0, nowMs() - frameOpen);
const b = bucketFor("frame.total");
b.ms += elapsed;
b.selfMs += Math.max(0, elapsed - stack.reduce((sum, f) => sum + (f.childMs || 0), 0));
b.calls += 1;
b.last = elapsed;
b.lastSelf = b.last;
if (elapsed > b.max) b.max = elapsed;
}
frameOpen = null;
if (debugEnabled) stack.length = 0;
const t = nowMs();
if (t - lastSmoothAt >= 750) {
lastSmoothAt = t;
smoothBuckets();
}
}
function visualLevel(key, fallback = "high") {
return visualSettings[key] || fallback;
}
function visualSettingsValue() {
return { ...visualSettings };
}
function setVisualSetting(key, value) {
if (!Object.prototype.hasOwnProperty.call(defaults, key)) return false;
const nextValue = normalizeVisualSetting(key, value);
if (!allowed[key]?.has(nextValue)) return false;
if (visualSettings[key] === nextValue) return true;
visualSettings = { ...visualSettings, [key]: nextValue };
saveVisualSettings();
if (key === "details") resizeRequested = true;
return true;
}
function renderQualityTier() { return visualSettings.details; }
function shadowQualityTier() { return visualSettings.shadows; }
function dprScaleValue() { return dprScaleForDetails(visualSettings.details); }
function consumeResizeRequest() {
const v = resizeRequested;
resizeRequested = false;
return v;
}
function setMetric(name, value) {
if (!debugEnabled) return;
metrics.set(String(name || "metric"), value);
}
function snapshot() {
const frameAvg = buckets.get("frame.total")?.avg || 0;
const entries = [...buckets.values()].map(b => ({
label: b.label,
avg: Number((b.avg || 0).toFixed(2)),
selfAvg: Number((b.selfAvg || 0).toFixed(2)),
max: Number((b.max || 0).toFixed(2)),
last: Number((b.last || 0).toFixed(2)),
lastSelf: Number((b.lastSelf || 0).toFixed(2)),
calls: b.calls || 0,
pct: frameAvg > 0 ? Number((((b.avg || 0) / frameAvg) * 100).toFixed(0)) : 0,
selfPct: frameAvg > 0 ? Number((((b.selfAvg || 0) / frameAvg) * 100).toFixed(0)) : 0,
}));
const topInclusive = entries.slice().sort((a, b) => b.avg - a.avg).slice(0, 12);
const topSelf = entries.slice().sort((a, b) => b.selfAvg - a.selfAvg).slice(0, 12);
return {
enabled: debugEnabled,
tier: visualSettings.details,
visual: visualSettingsValue(),
dprScale: dprScaleValue(),
frameCount,
frameAvg: Number(frameAvg.toFixed(2)),
entries: topInclusive,
topInclusive,
topSelf,
metrics: Object.fromEntries(metrics.entries()),
};
}
global.TarinaiPerf = Object.freeze({
begin,
beginFrame,
endFrame,
renderQualityTier,
shadowQualityTier,
visualLevel,
visualSettings: visualSettingsValue,
setVisualSetting,
dprScale: dprScaleValue,
consumeResizeRequest,
setMetric,
snapshot,
});
})(typeof window !== "undefined" ? window : globalThis);