mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
237 lines
6.8 KiB
JavaScript
237 lines
6.8 KiB
JavaScript
// Local-only performance measurement harness.
|
|
//
|
|
// This intentionally observes rather than changes application behavior. It is
|
|
// available only on a loopback host *and* after an explicit localStorage opt-in,
|
|
// so a production visitor cannot accidentally enable it.
|
|
|
|
const STORAGE_KEY = "__bitvidPerformanceHarness__";
|
|
const GLOBAL_KEY = "__bitvidPerformance";
|
|
const LOCAL_HOSTS = new Set(["localhost", "127.0.0.1", "::1"]);
|
|
const MAX_LONG_TASKS = 200;
|
|
const MAX_MARKS = 200;
|
|
|
|
function readStorage(storage, key) {
|
|
try {
|
|
return storage?.getItem?.(key) ?? null;
|
|
} catch (_) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function finiteNumber(value, fallback = 0) {
|
|
return Number.isFinite(value) ? Number(value) : fallback;
|
|
}
|
|
|
|
function normalizeMarkName(name) {
|
|
return typeof name === "string" ? name.trim().slice(0, 120) : "";
|
|
}
|
|
|
|
export function isLocalPerformanceHarnessEnabled({
|
|
locationRef = typeof location !== "undefined" ? location : null,
|
|
storage = typeof localStorage !== "undefined" ? localStorage : null,
|
|
} = {}) {
|
|
const hostname = typeof locationRef?.hostname === "string"
|
|
? locationRef.hostname.toLowerCase()
|
|
: "";
|
|
return LOCAL_HOSTS.has(hostname) && readStorage(storage, STORAGE_KEY) === "1";
|
|
}
|
|
|
|
export class PerformanceHarness {
|
|
constructor({ scope, performanceRef, PerformanceObserverClass } = {}) {
|
|
this.scope = scope || null;
|
|
this.performance = performanceRef || this.scope?.performance || null;
|
|
this.PerformanceObserverClass =
|
|
PerformanceObserverClass || this.scope?.PerformanceObserver || null;
|
|
this.startedAt = 0;
|
|
this.longTasks = [];
|
|
this.marks = [];
|
|
this.gauges = new Map();
|
|
this.longTaskObserver = null;
|
|
this.started = false;
|
|
}
|
|
|
|
start() {
|
|
if (this.started) {
|
|
return this;
|
|
}
|
|
this.started = true;
|
|
this.startedAt = this.now();
|
|
this.observeLongTasks();
|
|
this.mark("harness-start");
|
|
if (this.scope) {
|
|
this.scope[GLOBAL_KEY] = this.publicApi();
|
|
}
|
|
return this;
|
|
}
|
|
|
|
stop() {
|
|
try {
|
|
this.longTaskObserver?.disconnect?.();
|
|
} catch (_) {
|
|
// Diagnostics must never affect application teardown.
|
|
}
|
|
this.longTaskObserver = null;
|
|
this.started = false;
|
|
if (this.scope?.[GLOBAL_KEY]) {
|
|
delete this.scope[GLOBAL_KEY];
|
|
}
|
|
}
|
|
|
|
now() {
|
|
return finiteNumber(this.performance?.now?.(), Date.now());
|
|
}
|
|
|
|
observeLongTasks() {
|
|
if (typeof this.PerformanceObserverClass !== "function") {
|
|
return;
|
|
}
|
|
try {
|
|
const observer = new this.PerformanceObserverClass((list) => {
|
|
const entries = list?.getEntries?.() || [];
|
|
for (const entry of entries) {
|
|
this.recordLongTask(entry);
|
|
}
|
|
});
|
|
observer.observe({ type: "longtask", buffered: true });
|
|
this.longTaskObserver = observer;
|
|
} catch (_) {
|
|
// Long Task timing is optional and unsupported in several browsers.
|
|
}
|
|
}
|
|
|
|
recordLongTask(entry) {
|
|
const task = {
|
|
startTime: finiteNumber(entry?.startTime),
|
|
duration: finiteNumber(entry?.duration),
|
|
name: typeof entry?.name === "string" ? entry.name : "longtask",
|
|
};
|
|
this.longTasks.push(task);
|
|
if (this.longTasks.length > MAX_LONG_TASKS) {
|
|
this.longTasks.splice(0, this.longTasks.length - MAX_LONG_TASKS);
|
|
}
|
|
}
|
|
|
|
mark(name, detail = {}) {
|
|
const normalizedName = normalizeMarkName(name);
|
|
if (!normalizedName) {
|
|
return false;
|
|
}
|
|
const entry = {
|
|
name: normalizedName,
|
|
at: this.now(),
|
|
detail: detail && typeof detail === "object" ? { ...detail } : {},
|
|
};
|
|
this.marks.push(entry);
|
|
if (this.marks.length > MAX_MARKS) {
|
|
this.marks.splice(0, this.marks.length - MAX_MARKS);
|
|
}
|
|
try {
|
|
this.performance?.mark?.(`bitvid:${normalizedName}`);
|
|
} catch (_) {
|
|
// User Timing is also optional; retain the local mark either way.
|
|
}
|
|
return true;
|
|
}
|
|
|
|
setGauge(name, value) {
|
|
const normalizedName = normalizeMarkName(name);
|
|
if (!normalizedName || !Number.isFinite(value)) {
|
|
return false;
|
|
}
|
|
this.gauges.set(normalizedName, Number(value));
|
|
return true;
|
|
}
|
|
|
|
increment(name, amount = 1) {
|
|
const normalizedName = normalizeMarkName(name);
|
|
if (!normalizedName || !Number.isFinite(amount)) {
|
|
return false;
|
|
}
|
|
return this.setGauge(normalizedName, (this.gauges.get(normalizedName) || 0) + amount);
|
|
}
|
|
|
|
snapshot() {
|
|
const resources = this.performance?.getEntriesByType?.("resource") || [];
|
|
const navigation = this.performance?.getEntriesByType?.("navigation")?.[0] || null;
|
|
const memory = this.performance?.memory;
|
|
const longTaskDurationMs = this.longTasks.reduce(
|
|
(total, task) => total + finiteNumber(task.duration),
|
|
0,
|
|
);
|
|
return {
|
|
enabled: this.started,
|
|
startedAt: this.startedAt,
|
|
elapsedMs: Math.max(0, this.now() - this.startedAt),
|
|
longTasks: {
|
|
count: this.longTasks.length,
|
|
totalDurationMs: longTaskDurationMs,
|
|
recent: this.longTasks.slice(-20),
|
|
},
|
|
marks: this.marks.slice(-50),
|
|
gauges: Object.fromEntries(this.gauges.entries()),
|
|
resources: {
|
|
count: resources.length,
|
|
transferSize: resources.reduce(
|
|
(total, entry) => total + finiteNumber(entry?.transferSize),
|
|
0,
|
|
),
|
|
},
|
|
navigation: navigation
|
|
? {
|
|
domContentLoadedMs: finiteNumber(navigation.domContentLoadedEventEnd),
|
|
loadEventMs: finiteNumber(navigation.loadEventEnd),
|
|
}
|
|
: null,
|
|
memory: memory
|
|
? {
|
|
usedJsHeapSize: finiteNumber(memory.usedJSHeapSize),
|
|
totalJsHeapSize: finiteNumber(memory.totalJSHeapSize),
|
|
jsHeapSizeLimit: finiteNumber(memory.jsHeapSizeLimit),
|
|
}
|
|
: null,
|
|
};
|
|
}
|
|
|
|
reset() {
|
|
this.longTasks.length = 0;
|
|
this.marks.length = 0;
|
|
this.gauges.clear();
|
|
this.mark("harness-reset");
|
|
}
|
|
|
|
publicApi() {
|
|
return Object.freeze({
|
|
snapshot: () => this.snapshot(),
|
|
mark: (name, detail) => this.mark(name, detail),
|
|
setGauge: (name, value) => this.setGauge(name, value),
|
|
increment: (name, amount) => this.increment(name, amount),
|
|
reset: () => this.reset(),
|
|
stop: () => this.stop(),
|
|
});
|
|
}
|
|
}
|
|
|
|
export function initPerformanceHarness({
|
|
scope = typeof window !== "undefined" ? window : null,
|
|
locationRef = scope?.location,
|
|
storage = scope?.localStorage,
|
|
performanceRef = scope?.performance,
|
|
PerformanceObserverClass = scope?.PerformanceObserver,
|
|
} = {}) {
|
|
if (!scope || !isLocalPerformanceHarnessEnabled({ locationRef, storage })) {
|
|
return null;
|
|
}
|
|
if (scope[GLOBAL_KEY]) {
|
|
return scope[GLOBAL_KEY];
|
|
}
|
|
const harness = new PerformanceHarness({
|
|
scope,
|
|
performanceRef,
|
|
PerformanceObserverClass,
|
|
});
|
|
harness.start();
|
|
return scope[GLOBAL_KEY];
|
|
}
|
|
|
|
export const PERFORMANCE_HARNESS_STORAGE_KEY = STORAGE_KEY;
|