222 lines
6.7 KiB
JavaScript
222 lines
6.7 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", "high"]),
|
|
shadows: new Set(["off", "high"]),
|
|
details: new Set(["low", "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" || raw === "low" || raw === "mid" || raw === "high") return "high";
|
|
}
|
|
if (key === "details") {
|
|
if (raw === "low") return "low";
|
|
if (raw === "mid" || raw === "high") return "high";
|
|
}
|
|
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;
|
|
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);
|