Sprache
JavaScript
Größe
59953 Bytes
Erstellt am
01.11.2026, 16:06
Läuft ab am
01.11.2027, 16:06
JavaScript·18 Aufrufe
// ==UserScript==
// @name LidelEra 1.0
// @namespace warera-lite
// @version 1.0.0
// @description LidelEra 1.0 – Performance-Optimierung, Blockaden-Analyse und Benchmark.
// @match https://app.warera.io/*
// @run-at document-start
// @noframes
// @grant GM_getValue
// @grant GM_setValue
// ==/UserScript==
(() => {
'use strict';
if (window.__WARERA_LITE__) return;
window.__WARERA_LITE__ = true;
const VERSION = '1.0.0';
// Gleicher Prefix wie 8.4, damit Einstellungen und Installer-ID erhalten bleiben.
const PREFIX = 'warera_lite_84_';
const DE = (navigator.language || '').toLowerCase().startsWith('de');
const L = (de, en) => (DE ? de : en);
const MAX_TRACE = 200;
const PHASE_MS = 15000; // Länge einer Benchmark-Phase
const SETTLE_MS = 1000; // Einschwingzeit nach jedem Umschalten (wird nicht gemessen)
const BENCH_PHASES = ['A', 'B', 'A', 'B']; // ABAB gleicht Drift (Cache, Spielzustand) aus
const AUTO_CYCLE = 20000; // AUTO misst nur 4 s alle 20 s -> kaum Eigenlast
const AUTO_WINDOW = 4000;
const LOAF_WINDOW = 300000; // Blockaden-Analyse: letzte 5 Minuten
const PROFILES = ['balanced', 'max', 'ultra'];
const RANK = { balanced: 0, max: 1, ultra: 2 };
const read = (key, fallback) => {
try { return GM_getValue(PREFIX + key, fallback); }
catch (_) { return fallback; }
};
const save = (key, value) => {
try { GM_setValue(PREFIX + key, value); }
catch (_) {}
};
const installerId = read('installerId', 'USER-' + Math.random().toString(36).slice(2, 8).toUpperCase());
save('installerId', installerId);
const storedProfile = read('profile', 'auto');
const storedLauncher = read('launcherPosition', null);
const CONFIG = {
enabled: read('enabled', true) !== false,
profile: storedProfile === 'auto' || PROFILES.includes(storedProfile) ? storedProfile : 'auto',
launcherPosition: storedLauncher && Number.isFinite(storedLauncher.x) && Number.isFinite(storedLauncher.y)
? storedLauncher : { x: 16, y: 100 },
dashboardPosition: read('dashboardPosition', null)
};
let effectiveProfile = CONFIG.profile === 'auto' ? 'max' : CONFIG.profile;
let styleElement = null;
let appliedKey = '';
let launcher = null;
let dashboard = null;
let dashboardOpen = false;
let frameHandle = 0;
let lastFrame = 0;
let sampledAuto = false;
let frameSamples = [];
let longTaskSamples = [];
let interactionSamples = [];
let loafSamples = [];
let traceEvents = [];
let longTaskObserver = null;
let eventObserver = null;
let loafObserver = null;
let dashboardTimer = null;
let benchmarkTimer = null;
let benchmark = null;
let lastAutoDecision = 0;
let candidateProfile = null;
let candidateSince = 0;
const stats = {
fps: 0, p50: 0, p95: 0, jank: 0, drops: 0,
longTasks: 0, longTaskDuration: 0, longTaskAverage: 0, longTaskMax: 0, totalLongTasks: 0,
task50: 0, task100: 0, task200: 0,
interactions: 0, slowInteractions: 0, maxInteraction: 0,
slowInput: 0, slowProc: 0, slowPres: 0,
heap: 0, heapLimit: 0, resources: 0, slowResources: 0
};
const now = () => performance.now();
const clamp = (v, min, max) => Math.max(min, Math.min(max, v));
const avg = values => (values.length ? values.reduce((a, b) => a + b, 0) / values.length : 0);
const esc = s => String(s).replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
// Schreibt nur, wenn sich der Inhalt geändert hat -> weniger DOM-Arbeit im Dashboard.
function setHtml(el, html) {
if (el && el._h !== html) {
el._h = html;
el.innerHTML = html;
}
}
function percentile(values, p) {
if (!values.length) return 0;
const sorted = [...values].sort((a, b) => a - b);
return sorted[Math.min(sorted.length - 1, Math.ceil(sorted.length * p) - 1)];
}
function addTrace(type, data = {}) {
traceEvents.push({ time: now(), type, ...data });
if (traceEvents.length > MAX_TRACE) traceEvents.splice(0, traceEvents.length - MAX_TRACE);
}
function activeProfile() {
return CONFIG.profile === 'auto' ? effectiveProfile : CONFIG.profile;
}
// ------------------------------------------------------------
// Optimierung (CSS)
// Stufen sind abgestuft. Statt "animation/transition: none" werden Dauern
// auf ~0 gesetzt, damit animationend/transitionend-Events des Spiels
// weiterhin feuern (sonst können Dialoge/Menüs hängen bleiben).
// ------------------------------------------------------------
function buildCss(profile) {
const rank = RANK[profile] ?? 1;
// BALANCED: teuerster GPU-Effekt (Hintergrund-Blur) + Smooth-Scrolling
let css = `
html { scroll-behavior: auto !important; }
*, *::before, *::after {
backdrop-filter: none !important;
-webkit-backdrop-filter: none !important;
}`;
// MAX: + Textschatten, Übergangsanimationen praktisch sofort
if (rank >= 1) css += `
*, *::before, *::after {
text-shadow: none !important;
transition-duration: 0.001ms !important;
transition-delay: 0s !important;
}`;
// ULTRA: + Schatten, Filter, Dauer-Animationen, will-change, fixe Hintergründe
if (rank >= 2) css += `
*, *::before, *::after {
box-shadow: none !important;
filter: none !important;
animation-duration: 0.001ms !important;
animation-delay: 0s !important;
animation-iteration-count: 1 !important;
animation-fill-mode: forwards !important;
will-change: auto !important;
background-attachment: scroll !important;
}
:focus-visible { outline: 2px solid #6aa9ff !important; outline-offset: 1px; }`;
return css;
}
function applyOptimization() {
const root = document.documentElement;
if (!root) return;
const key = CONFIG.enabled ? activeProfile() : 'off';
if (key === appliedKey && styleElement && styleElement.isConnected) return;
if (!styleElement) {
styleElement = document.createElement('style');
styleElement.id = 'warera-lite-style';
}
if (!styleElement.isConnected) (document.head || root).appendChild(styleElement);
styleElement.textContent = CONFIG.enabled ? buildCss(activeProfile()) : '';
root.dataset.wareraLite = key;
appliedKey = key;
}
// ------------------------------------------------------------
// Long Tasks
// ------------------------------------------------------------
function recordLongTask(entry) {
const start = Number(entry.startTime);
const duration = Number(entry.duration);
if (!Number.isFinite(start) || !Number.isFinite(duration) || duration < 50) return;
const end = start + duration;
// Duplikate nur unter den letzten Einträgen suchen (Einträge kommen sortiert).
for (let i = longTaskSamples.length - 1, n = 0; i >= 0 && n < 5; i--, n++) {
if (longTaskSamples[i].start === start && longTaskSamples[i].end === end) return;
}
longTaskSamples.push({ start, end, duration });
stats.totalLongTasks++;
if (longTaskSamples.length > 1000) {
const cutoff = now() - 120000;
longTaskSamples = longTaskSamples.filter(item => item.end >= cutoff);
}
addTrace('LONG_TASK', { start: Math.round(start), duration: Math.round(duration) });
if (CONFIG.profile === 'auto' && !benchmark?.running) evaluateAutomaticProfile();
}
function setupLongTaskObserver() {
if (!('PerformanceObserver' in window)) return;
const callback = list => { for (const entry of list.getEntries()) recordLongTask(entry); };
try {
longTaskObserver = new PerformanceObserver(callback);
longTaskObserver.observe({ type: 'longtask', buffered: true });
} catch (_) {
try {
longTaskObserver = new PerformanceObserver(callback);
longTaskObserver.observe({ entryTypes: ['longtask'] });
} catch (_) {
longTaskObserver = null;
}
}
}
function unionDuration(intervals, windowStart, windowEnd) {
const clipped = [];
for (const item of intervals) {
const s = Math.max(item.start, windowStart);
const e = Math.min(item.end, windowEnd);
if (e > s) clipped.push([s, e]);
}
if (!clipped.length) return 0;
clipped.sort((a, b) => a[0] - b[0]);
let total = 0;
let s = clipped[0][0];
let e = clipped[0][1];
for (let i = 1; i < clipped.length; i++) {
const next = clipped[i];
if (next[0] <= e) e = Math.max(e, next[1]);
else { total += e - s; s = next[0]; e = next[1]; }
}
return total + e - s;
}
function getLongTaskMetrics(start, end) {
const selected = longTaskSamples.filter(item => item.start < end && item.end > start);
let task50 = 0, task100 = 0, task200 = 0, max = 0;
for (const item of selected) {
if (item.duration >= 200) task200++;
else if (item.duration >= 100) task100++;
else task50++;
if (item.duration > max) max = item.duration;
}
return {
count: selected.length,
duration: Math.round(unionDuration(selected, start, end)),
max: Math.round(max),
task50, task100, task200
};
}
function updateLongTaskStats() {
const t = now();
const result = getLongTaskMetrics(t - 30000, t);
stats.longTasks = result.count;
stats.longTaskDuration = result.duration;
stats.longTaskAverage = result.count ? result.duration / result.count : 0;
stats.longTaskMax = result.max;
stats.task50 = result.task50;
stats.task100 = result.task100;
stats.task200 = result.task200;
}
// ------------------------------------------------------------
// Long Animation Frames (Chrome 123+)
// Zeigt, WELCHES Skript einen langsamen Frame verursacht hat und ob die Zeit
// in Skripten oder im Rendering (Style/Layout/Paint) verloren geht.
// ------------------------------------------------------------
function classifySource(url) {
if (!url) return { origin: L('inline/unbekannt', 'inline/unknown'), file: '' };
try {
const u = new URL(url, location.href);
const file = (u.pathname.split('/').pop() || u.pathname).slice(0, 40);
let origin;
if (u.protocol === 'chrome-extension:' || u.protocol === 'moz-extension:') origin = L('Erweiterung', 'extension');
else if (u.origin === location.origin) origin = L('Spiel', 'game');
else origin = u.hostname;
return { origin, file };
} catch (_) {
return { origin: '?', file: String(url).slice(0, 40) };
}
}
function recordLoaf(entry) {
const start = Number(entry.startTime);
const duration = Number(entry.duration);
if (!Number.isFinite(start) || !(duration >= 50)) return;
const renderStart = Number(entry.renderStart) > 0 ? Number(entry.renderStart) : start + duration;
let scriptTotal = 0;
let forced = 0;
const scripts = [];
for (const s of entry.scripts || []) {
const dur = Number(s.duration) || 0;
scriptTotal += dur;
forced += Number(s.forcedStyleAndLayoutDuration) || 0;
if (dur >= 5) { // nur relevante Skripte merken
const c = classifySource(s.sourceURL);
const fn = String(s.sourceFunctionName || '').slice(0, 40);
const invoker = String(s.invoker || '').slice(0, 50);
scripts.push({
key: `${c.origin}|${c.file}|${fn || invoker}`,
origin: c.origin, file: c.file, fn, invoker, duration: dur
});
}
}
loafSamples.push({
start,
end: start + duration,
blocking: Number(entry.blockingDuration) || 0,
script: scriptTotal,
render: Math.max(0, start + duration - renderStart),
forced,
scripts
});
if (loafSamples.length > 300) loafSamples.splice(0, 100);
}
function setupLoafObserver() {
if (!('PerformanceObserver' in window)) return;
try {
const supported = PerformanceObserver.supportedEntryTypes;
if (supported && !supported.includes('long-animation-frame')) return;
loafObserver = new PerformanceObserver(list => {
for (const entry of list.getEntries()) recordLoaf(entry);
});
loafObserver.observe({ type: 'long-animation-frame', buffered: true });
} catch (_) {
loafObserver = null;
}
}
function getLoafSummary(from, to) {
const items = loafSamples.filter(s => s.end > from && s.start < to);
const sum = { count: items.length, blocking: 0, script: 0, render: 0, forced: 0, sources: [] };
const map = new Map();
for (const s of items) {
sum.blocking += s.blocking;
sum.script += s.script;
sum.render += s.render;
sum.forced += s.forced;
for (const sc of s.scripts) {
let e = map.get(sc.key);
if (!e) {
e = { origin: sc.origin, file: sc.file, fn: sc.fn, invoker: sc.invoker, total: 0, count: 0, max: 0 };
map.set(sc.key, e);
}
e.total += sc.duration;
e.count++;
if (sc.duration > e.max) e.max = sc.duration;
}
}
sum.sources = [...map.values()].sort((a, b) => b.total - a.total).slice(0, 8);
return sum;
}
function loafHtml(loaf) {
if (!loafObserver) {
return `<div style="color:#9ba9bf">${L('Von diesem Browser nicht unterstützt (Chrome 123+ nötig).', 'Not supported by this browser (needs Chrome 123+).')}</div>`;
}
if (!loaf.count) {
return `<div>${L('Keine langsamen Frames in den letzten 5 Minuten.', 'No slow frames in the last 5 minutes.')}</div>`;
}
const total = loaf.script + loaf.render;
const pct = v => (total > 0 ? Math.round(100 * v / total) : 0);
let hint = '';
if (pct(loaf.script) >= 60) {
hint = L('→ Skripte sind der Engpass. CSS-Optimierung hilft hier kaum.', '→ Scripts are the bottleneck. CSS optimization barely helps.');
} else if (pct(loaf.render) >= 40) {
hint = L('→ Rendering ist ein relevanter Anteil. Höhere Profile können helfen.', '→ Rendering is a significant share. Higher profiles may help.');
}
let html = `
<div>${L('Langsame Frames (>50 ms)', 'Slow frames (>50 ms)')}: <b>${loaf.count}</b> | ${L('blockierend', 'blocking')}: <b>${Math.round(loaf.blocking)} ms</b></div>
<div>${L('Skript-Ausführung', 'Script execution')}: <b>${Math.round(loaf.script)} ms</b> (${pct(loaf.script)} %)</div>
<div>${L('Rendering (Style/Layout/Paint)', 'Rendering (style/layout/paint)')}: <b>${Math.round(loaf.render)} ms</b> (${pct(loaf.render)} %)</div>
<div>${L('davon erzwungenes Layout', 'of which forced layout')}: <b>${Math.round(loaf.forced)} ms</b></div>
${hint ? `<div style="color:#f2c96b;margin-top:4px">${hint}</div>` : ''}
<div style="margin-top:6px;color:#9ba9bf">${L('Top-Verursacher (Skriptzeit):', 'Top culprits (script time):')}</div>`;
for (const s of loaf.sources) {
const name = s.fn || s.invoker || L('(anonym)', '(anonymous)');
html += `<div style="font-size:11px;word-break:break-all;margin-top:2px">• <b>${esc(s.origin)}</b>/${esc(s.file)} – ${esc(name)}<br> ${Math.round(s.total)} ms, ${s.count}×, max ${Math.round(s.max)} ms</div>`;
}
return html;
}
// ------------------------------------------------------------
// Frame-Messung
// Dauerhaftes requestAnimationFrame kostet selbst CPU. Im AUTO-Modus
// läuft die Messung deshalb nur in kurzen Fenstern (4 s alle 20 s).
// Dauerhaft nur bei offenem Dashboard oder laufendem Benchmark.
// ------------------------------------------------------------
function continuousTracking() {
return dashboardOpen || Boolean(benchmark?.running);
}
function wantFrames() {
if (document.hidden) return false;
if (continuousTracking()) return true;
return CONFIG.enabled && CONFIG.profile === 'auto' && (now() % AUTO_CYCLE) < AUTO_WINDOW;
}
function startFrameTracking() {
if (!frameHandle && wantFrames()) {
lastFrame = 0;
frameHandle = requestAnimationFrame(frameLoop);
}
}
function stopFrameTracking() {
if (frameHandle) cancelAnimationFrame(frameHandle);
frameHandle = 0;
lastFrame = 0;
}
function frameLoop(timestamp) {
frameHandle = 0;
if (!wantFrames()) {
lastFrame = 0;
if (sampledAuto) { // AUTO-Messfenster zu Ende -> auswerten
sampledAuto = false;
evaluateAutomaticProfile();
}
return;
}
if (lastFrame) {
const delta = timestamp - lastFrame;
if (delta >= 4 && delta <= 1000) {
const sample = { time: timestamp, delta };
frameSamples.push(sample);
if (frameSamples.length > 900) frameSamples.splice(0, 300);
if (!continuousTracking()) sampledAuto = true;
if (benchmark?.running && timestamp >= benchmark.phaseStarted + SETTLE_MS) {
benchmark.acc[benchmark.cond].frames.push(sample);
}
if (delta > 50) addTrace('FRAME_DROP', { duration: Math.round(delta) });
}
}
lastFrame = timestamp;
frameHandle = requestAnimationFrame(frameLoop);
}
function getFrameMetrics(samples) {
if (!samples.length) return { fps: 0, p50: 0, p95: 0, jank: 0, drops: 0 };
const values = samples.map(item => item.delta);
const average = values.reduce((a, b) => a + b, 0) / values.length;
return {
fps: Math.round(1000 / average),
p50: Math.round(percentile(values, 0.50)),
p95: Math.round(percentile(values, 0.95)),
jank: values.filter(v => v > 50).length,
drops: values.filter(v => v > 33.3).length
};
}
function calculateFrameStats() {
// Nur frische Samples (letzte 30 s) – verhindert veraltete Werte.
const cutoff = now() - 30000;
Object.assign(stats, getFrameMetrics(frameSamples.filter(s => s.time >= cutoff)));
}
// ------------------------------------------------------------
// Interaktionen (mit Aufschlüsselung), Speicher, Ressourcen
// ------------------------------------------------------------
function describeTarget(node) {
try {
if (!node || !node.tagName) return '';
let s = node.tagName.toLowerCase();
if (node.id) s += '#' + String(node.id).slice(0, 30);
else if (typeof node.className === 'string' && node.className.trim()) {
s += '.' + node.className.trim().split(/\s+/)[0].slice(0, 30);
}
return s;
} catch (_) {
return '';
}
}
function setupInteractionObserver() {
if (!('PerformanceObserver' in window)) return;
try {
eventObserver = new PerformanceObserver(list => {
for (const entry of list.getEntries()) {
if (!entry.interactionId) continue;
const start = entry.startTime;
const procStart = entry.processingStart || start;
const procEnd = entry.processingEnd || procStart;
const sample = {
time: start,
duration: entry.duration,
name: entry.name,
input: Math.max(0, procStart - start), // Wartezeit: Hauptthread war beschäftigt
proc: Math.max(0, procEnd - procStart), // Handler-Laufzeit
pres: Math.max(0, start + entry.duration - procEnd) // bis das Ergebnis sichtbar ist
};
interactionSamples.push(sample);
if (interactionSamples.length > 300) interactionSamples.splice(0, 100);
if (entry.duration > 100) {
addTrace('SLOW_INTERACTION', {
duration: Math.round(entry.duration),
name: entry.name,
target: describeTarget(entry.target),
input: Math.round(sample.input),
proc: Math.round(sample.proc),
pres: Math.round(sample.pres)
});
}
}
});
eventObserver.observe({ type: 'event', buffered: true, durationThreshold: 40 });
} catch (_) {
eventObserver = null;
}
}
function calculateInteractionStats() {
const cutoff = now() - 60000;
const recent = interactionSamples.filter(item => item.time >= cutoff);
const slow = recent.filter(item => item.duration > 100);
stats.interactions = recent.length;
stats.slowInteractions = slow.length;
stats.maxInteraction = recent.reduce((max, item) => Math.max(max, item.duration), 0);
stats.slowInput = avg(slow.map(i => i.input));
stats.slowProc = avg(slow.map(i => i.proc));
stats.slowPres = avg(slow.map(i => i.pres));
}
function collectMemory() {
if (!performance.memory) return;
stats.heap = Math.round(performance.memory.usedJSHeapSize / 1048576);
stats.heapLimit = Math.round(performance.memory.jsHeapSizeLimit / 1048576);
}
function collectResources() {
const cutoff = now() - 60000;
const entries = performance.getEntriesByType('resource');
let count = 0, slow = 0;
for (let i = entries.length - 1; i >= 0; i--) {
const entry = entries[i];
if (entry.startTime < cutoff) break;
count++;
if (entry.duration > 1000 && entry.responseEnd > 0) slow++;
}
stats.resources = count;
stats.slowResources = slow;
}
function calculateAll() {
calculateFrameStats();
updateLongTaskStats();
calculateInteractionStats();
collectMemory();
collectResources();
}
// ------------------------------------------------------------
// AUTO-Profil mit Hysterese
// - ULTRA nur, wenn das RENDERN sichtbar leidet (P95/FPS oder hoher
// Rendering-Anteil laut LoAF). Reine JavaScript-Blockaden (Long Tasks)
// bessert CSS kaum, dafür würde ULTRA nur Optik opfern -> höchstens MAX.
// - Hochschalten schnell (5 s), Herunterschalten Stufe für Stufe, nur bei
// deutlich besseren Werten und nach 45 s Stabilität.
// ------------------------------------------------------------
function chooseProfile() {
const n = stats.longTasks;
const d = stats.longTaskDuration;
const p95 = stats.p95;
const fps = stats.fps;
const hasFrames = fps > 0;
const t = now();
const loaf = getLoafSummary(t - 30000, t);
const renderBound = loaf.count >= 3 && loaf.render >= 400 &&
loaf.render >= 0.4 * (loaf.script + loaf.render);
const severe = p95 >= 45 || (hasFrames && fps < 25) || renderBound;
const moderate = p95 >= 28 || (hasFrames && fps < 45) || n >= 4 || d >= 500;
const desired = severe ? 'ultra' : moderate ? 'max' : 'balanced';
if (RANK[desired] < RANK[effectiveProfile]) {
const lim = effectiveProfile === 'ultra'
? { n: 7, d: 1100, p95: 34, fps: 33, render: 250 }
: { n: 3, d: 375, p95: 21, fps: 50, render: 400 };
const calm = n < lim.n && d < lim.d && p95 < lim.p95 &&
(!hasFrames || fps >= lim.fps) && loaf.render < lim.render;
if (!calm) return effectiveProfile;
return PROFILES[Math.max(RANK[desired], RANK[effectiveProfile] - 1)];
}
return desired;
}
function evaluateAutomaticProfile() {
if (!CONFIG.enabled || CONFIG.profile !== 'auto' || document.hidden || benchmark?.running) return;
const t = now();
if (t - lastAutoDecision < 5000) return;
lastAutoDecision = t;
calculateFrameStats();
updateLongTaskStats();
const desired = chooseProfile();
if (desired === effectiveProfile) {
candidateProfile = null;
candidateSince = 0;
return;
}
if (candidateProfile !== desired) {
candidateProfile = desired;
candidateSince = t;
return;
}
const worsening = RANK[desired] > RANK[effectiveProfile];
if (t - candidateSince < (worsening ? 5000 : 45000)) return;
effectiveProfile = desired;
candidateProfile = null;
candidateSince = 0;
addTrace('PROFILE', { name: desired });
applyOptimization();
if (dashboardOpen) updateDashboard();
}
function setProfile(profile) {
if (benchmark?.running) return;
if (profile !== 'auto' && !PROFILES.includes(profile)) return;
CONFIG.profile = profile;
save('profile', profile);
candidateProfile = null;
candidateSince = 0;
if (profile !== 'auto') effectiveProfile = profile;
applyOptimization();
startFrameTracking();
if (dashboardOpen) updateDashboard();
}
function setEnabled(value) {
if (benchmark?.running) return;
CONFIG.enabled = Boolean(value);
save('enabled', CONFIG.enabled);
applyOptimization();
startFrameTracking();
if (dashboardOpen) updateDashboard();
}
// ------------------------------------------------------------
// Benchmark: ABAB (4 x 15 s), erste Sekunde jeder Phase wird verworfen
// ------------------------------------------------------------
const newAcc = () => ({
frames: [], ms: 0, count: 0, duration: 0, max: 0, t50: 0, t100: 0, t200: 0,
loafCount: 0, loafBlocking: 0, loafScript: 0, loafRender: 0
});
function beginBenchmarkPhase(index) {
benchmark.index = index;
benchmark.cond = BENCH_PHASES[index];
benchmark.phaseStarted = now();
benchmark.lastSecond = -1;
CONFIG.enabled = benchmark.cond === 'B'; // wird bewusst nicht gespeichert
applyOptimization();
lastFrame = 0;
addTrace('BENCHMARK_PHASE', { phase: benchmark.cond, index: index + 1 });
renderBenchmarkLabel();
}
function finishBenchmarkPhase() {
const end = now();
const from = benchmark.phaseStarted + SETTLE_MS;
const acc = benchmark.acc[benchmark.cond];
const m = getLongTaskMetrics(from, end);
const lf = getLoafSummary(from, end);
acc.ms += Math.max(0, end - from);
acc.count += m.count;
acc.duration += m.duration;
acc.max = Math.max(acc.max, m.max);
acc.t50 += m.task50;
acc.t100 += m.task100;
acc.t200 += m.task200;
acc.loafCount += lf.count;
acc.loafBlocking += lf.blocking;
acc.loafScript += lf.script;
acc.loafRender += lf.render;
}
function summarize(acc) {
const f = getFrameMetrics(acc.frames);
const share = v => (acc.ms ? Math.round(1000 * v / acc.ms) / 10 : 0);
return {
...f,
frames: acc.frames.length,
seconds: Math.round(acc.ms / 100) / 10,
longTasks: acc.count,
longTaskDuration: Math.round(acc.duration),
longTaskShare: share(acc.duration),
longTaskMax: Math.round(acc.max),
loafCount: acc.loafCount,
loafBlocking: Math.round(acc.loafBlocking),
loafBlockingShare: share(acc.loafBlocking),
loafScript: Math.round(acc.loafScript),
loafRender: Math.round(acc.loafRender)
};
}
function renderBenchmarkLabel() {
const el = dashboard?.querySelector('#wl-benchmark');
if (!el || !benchmark?.running) return;
const elapsed = Math.min(PHASE_MS / 1000, Math.floor((now() - benchmark.phaseStarted) / 1000));
const label = benchmark.cond === 'A'
? L('A – Optimierung AUS', 'A – optimization OFF')
: L('B – Optimierung EIN', 'B – optimization ON');
el.textContent = L(
`Benchmark läuft (${benchmark.index + 1}/${BENCH_PHASES.length})\n${label} – ${elapsed}/${PHASE_MS / 1000} s\nBitte währenddessen immer dasselbe tun (z. B. dieselbe Seite offen lassen).`,
`Benchmark running (${benchmark.index + 1}/${BENCH_PHASES.length})\n${label} – ${elapsed}/${PHASE_MS / 1000} s\nPlease keep doing the same thing meanwhile.`
);
}
function setControlsDisabled(disabled) {
if (!dashboard) return;
dashboard.querySelectorAll('#wl-profile, #wl-power, #wl-benchmark-start')
.forEach(el => { el.disabled = disabled; });
}
function restoreAfterBenchmark() {
CONFIG.enabled = benchmark.original.enabled;
CONFIG.profile = benchmark.original.profile;
effectiveProfile = benchmark.original.effective;
save('enabled', CONFIG.enabled);
save('profile', CONFIG.profile);
applyOptimization();
setControlsDisabled(false);
}
function startBenchmark() {
if (benchmark?.running || !dashboard) return;
benchmark = {
running: true,
index: 0,
cond: 'A',
phaseStarted: 0,
lastSecond: -1,
testProfile: activeProfile(),
acc: { A: newAcc(), B: newAcc() },
original: { enabled: CONFIG.enabled, profile: CONFIG.profile, effective: effectiveProfile }
};
// AUTO ist während des Tests eingefroren (evaluateAutomaticProfile bricht ab).
// Getestet wird das aktuell aktive Profil.
setControlsDisabled(true);
beginBenchmarkPhase(0);
startFrameTracking();
if (benchmarkTimer) clearInterval(benchmarkTimer);
benchmarkTimer = setInterval(() => {
if (!benchmark?.running) return;
const elapsed = now() - benchmark.phaseStarted;
if (elapsed >= PHASE_MS) {
finishBenchmarkPhase();
if (benchmark.index + 1 < BENCH_PHASES.length) beginBenchmarkPhase(benchmark.index + 1);
else finishBenchmark();
} else {
const sec = Math.floor(elapsed / 1000);
if (sec !== benchmark.lastSecond) { // Label nur 1x pro Sekunde, kein Voll-Rerender
benchmark.lastSecond = sec;
renderBenchmarkLabel();
}
}
}, 250);
}
function fmtResult(label, r) {
const lines = [
label,
`FPS: ${r.fps} | P50: ${r.p50} ms | P95: ${r.p95} ms`,
`Ruckler (>50 ms): ${r.jank} | Frame Drops (>33 ms): ${r.drops}`,
`Long Tasks: ${r.longTasks} | Dauer: ${r.longTaskDuration} ms (${r.longTaskShare} % der Zeit)`,
`Längste Task: ${r.longTaskMax} ms`
];
if (loafObserver) {
lines.push(`Blockierend (LoAF): ${r.loafBlocking} ms (${r.loafBlockingShare} %) | Skript ${r.loafScript} ms | Rendering ${r.loafRender} ms`);
}
lines.push(`Messzeit: ${r.seconds} s | Frames: ${r.frames}`);
return lines.join('\n');
}
function buildVerdict(a, b) {
const rel = (before, after) => (before > 0 ? (before - after) / before : 0);
const better = [];
const worse = [];
const checks = [
['P95', rel(a.p95, b.p95), 0.10],
['FPS', a.fps > 0 ? (b.fps - a.fps) / a.fps : 0, 0.10],
['Long-Task-Anteil', a.longTaskShare >= 1 ? rel(a.longTaskShare, b.longTaskShare) : 0, 0.20]
];
if (loafObserver) {
checks.push(['Blockierzeit (LoAF)', a.loafBlockingShare >= 1 ? rel(a.loafBlockingShare, b.loafBlockingShare) : 0, 0.20]);
checks.push(['Rendering-Zeit', a.loafRender >= 200 ? rel(a.loafRender, b.loafRender) : 0, 0.20]);
}
for (const [name, change, threshold] of checks) {
if (change >= threshold) better.push(name);
else if (change <= -threshold) worse.push(name);
}
if (better.length && !worse.length) return `Ergebnis: B (EIN) ist messbar besser (${better.join(', ')}).`;
if (worse.length && !better.length) return `Ergebnis: B (EIN) ist schlechter (${worse.join(', ')}). Ein leichteres Profil testen.`;
if (better.length && worse.length) return `Ergebnis: gemischt – besser: ${better.join(', ')}; schlechter: ${worse.join(', ')}.`;
return 'Ergebnis: kein klarer Unterschied (liegt im Messrauschen). Dann ist eher JavaScript/Netzwerk der Engpass, nicht die Grafik.';
}
function finishBenchmark() {
if (!benchmark?.running) return;
benchmark.running = false;
if (benchmarkTimer) clearInterval(benchmarkTimer);
benchmarkTimer = null;
restoreAfterBenchmark();
startFrameTracking();
const a = summarize(benchmark.acc.A);
const b = summarize(benchmark.acc.B);
benchmark.resultA = a;
benchmark.resultB = b;
const lines = [
'A/B-Benchmark abgeschlossen (ABAB)',
`Getestetes Profil: ${benchmark.testProfile.toUpperCase()}`,
'',
fmtResult('A – Optimierung AUS', a),
'',
fmtResult('B – Optimierung EIN', b),
''
];
if (a.frames < a.seconds * 20 || b.frames < b.seconds * 20) {
lines.push('Warnung: auffallend wenige Frames – war der Tab im Hintergrund? Test wiederholen.', '');
}
lines.push(buildVerdict(a, b), '', 'Ursprüngliche Einstellungen wiederhergestellt.');
benchmark.report = lines.join('\n');
benchmark.finishedAt = new Date().toISOString();
addTrace('BENCHMARK_COMPLETE', { fpsA: a.fps, fpsB: b.fps, p95A: a.p95, p95B: b.p95 });
if (dashboard) {
updateDashboard();
const el = dashboard.querySelector('#wl-benchmark');
if (el) el.textContent = benchmark.report;
}
}
function abortBenchmark() {
if (!benchmark?.running) return;
benchmark.running = false;
if (benchmarkTimer) clearInterval(benchmarkTimer);
benchmarkTimer = null;
restoreAfterBenchmark();
benchmark.report = null;
benchmark.resultA = null;
benchmark.resultB = null;
}
// ------------------------------------------------------------
// Launcher und Dashboard
// ------------------------------------------------------------
function clampPosition(x, y, element) {
return {
x: clamp(x, 0, Math.max(0, innerWidth - element.offsetWidth)),
y: clamp(y, 0, Math.max(0, innerHeight - element.offsetHeight))
};
}
function createLauncher() {
if (!document.body || launcher) return;
launcher = document.createElement('button');
launcher.id = 'warera-lite-launcher';
launcher.type = 'button';
launcher.textContent = '⚡';
launcher.title = 'LidelEra 1.0';
Object.assign(launcher.style, {
position: 'fixed',
left: CONFIG.launcherPosition.x + 'px',
top: CONFIG.launcherPosition.y + 'px',
zIndex: '2147483647',
width: '38px',
height: '34px',
borderRadius: '11px',
border: '1px solid #3b82a8',
background: 'linear-gradient(145deg,#172b40,#101923)',
color: '#d9f3ff',
fontSize: '17px',
cursor: 'grab',
touchAction: 'none',
userSelect: 'none'
});
let drag = null;
launcher.addEventListener('pointerdown', event => {
if (event.pointerType === 'mouse' && event.button !== 0) return;
const rect = launcher.getBoundingClientRect();
drag = { x: event.clientX, y: event.clientY, left: rect.left, top: rect.top, moved: false };
try { launcher.setPointerCapture(event.pointerId); } catch (_) {}
});
launcher.addEventListener('pointermove', event => {
if (!drag) return;
const dx = event.clientX - drag.x;
const dy = event.clientY - drag.y;
if (Math.abs(dx) + Math.abs(dy) > 6) drag.moved = true;
if (!drag.moved) return;
const pos = clampPosition(drag.left + dx, drag.top + dy, launcher);
launcher.style.left = pos.x + 'px';
launcher.style.top = pos.y + 'px';
});
launcher.addEventListener('pointerup', () => {
if (!drag) return;
if (drag.moved) {
const rect = launcher.getBoundingClientRect();
CONFIG.launcherPosition = { x: Math.round(rect.left), y: Math.round(rect.top) };
save('launcherPosition', CONFIG.launcherPosition);
} else {
dashboardOpen ? closeDashboard() : openDashboard();
}
drag = null;
});
launcher.addEventListener('pointercancel', () => { drag = null; });
document.body.appendChild(launcher);
const pos = clampPosition(CONFIG.launcherPosition.x, CONFIG.launcherPosition.y, launcher);
launcher.style.left = pos.x + 'px';
launcher.style.top = pos.y + 'px';
}
function buttonStyle() {
return 'padding:6px 8px;border:1px solid #35536d;border-radius:8px;background:#1b3044;color:#eaf5ff;cursor:pointer;font:600 11px/1.25 system-ui,sans-serif;min-height:30px;';
}
function makeDraggable(element, handle) {
let drag = null;
handle.addEventListener('pointerdown', event => {
if (event.target.id === 'wl-close') return;
if (event.pointerType === 'mouse' && event.button !== 0) return;
const rect = element.getBoundingClientRect();
drag = { x: event.clientX - rect.left, y: event.clientY - rect.top };
try { handle.setPointerCapture(event.pointerId); } catch (_) {}
});
handle.addEventListener('pointermove', event => {
if (!drag) return;
const pos = clampPosition(event.clientX - drag.x, event.clientY - drag.y, element);
element.style.left = pos.x + 'px';
element.style.top = pos.y + 'px';
});
const finish = () => {
if (!drag) return;
const rect = element.getBoundingClientRect();
CONFIG.dashboardPosition = { x: Math.round(rect.left), y: Math.round(rect.top) };
save('dashboardPosition', CONFIG.dashboardPosition);
drag = null;
};
handle.addEventListener('pointerup', finish);
handle.addEventListener('pointercancel', () => { drag = null; });
}
function card(title, bodyHtml, extra = '') {
return `
<div style="padding:9px 10px;background:rgba(30,48,67,.72);border:1px solid rgba(93,133,164,.18);border-radius:10px;margin-top:8px">
<b>${title}</b>${bodyHtml}${extra}
</div>`;
}
function openDashboard() {
if (dashboard || !document.body) return;
dashboardOpen = true;
dashboard = document.createElement('section');
dashboard.id = 'warera-lite-dashboard';
Object.assign(dashboard.style, {
position: 'fixed',
left: '62px',
top: '100px',
width: '350px',
maxWidth: 'calc(100vw - 16px)',
maxHeight: '85vh',
overflowY: 'auto',
zIndex: '2147483647',
background: 'linear-gradient(160deg,#111d2b 0%,#0b121d 100%)',
color: '#e8f1fb',
border: '1px solid #31516b',
borderRadius: '14px',
padding: '12px',
boxSizing: 'border-box',
font: '12px/1.45 system-ui,sans-serif'
});
dashboard.innerHTML = `
<style>
#warera-lite-dashboard { color-scheme: dark; }
#warera-lite-dashboard #wl-drag {
padding: 2px 1px 9px; border-bottom: 1px solid rgba(117,164,196,.22);
letter-spacing: .2px;
}
#warera-lite-dashboard button, #warera-lite-dashboard select {
box-sizing: border-box; transition: background .15s ease, border-color .15s ease, transform .15s ease;
}
#warera-lite-dashboard button:hover:not(:disabled) {
border-color: #65b8df !important; filter: brightness(1.12);
}
#warera-lite-dashboard button:active:not(:disabled) { transform: translateY(1px); }
#warera-lite-dashboard button:disabled { opacity: .5; cursor: not-allowed; }
#warera-lite-dashboard select:focus-visible,
#warera-lite-dashboard button:focus-visible {
outline: 2px solid #63b8e5; outline-offset: 2px;
}
#warera-lite-dashboard b { color: #f1f7ff; }
#warera-lite-dashboard #wl-events {
padding: 7px; background: rgba(7,14,23,.45); border-radius: 7px;
}
</style>
<header id="wl-drag" style="font-weight:700;font-size:16px;cursor:move;margin-bottom:12px;touch-action:none;user-select:none;display:flex;justify-content:space-between;align-items:center">
<span>⚡ LidelEra <span style="color:#9ba9bf;font-size:10px">${VERSION}</span></span>
<span id="wl-close" style="cursor:pointer;padding:0 4px">✕</span>
</header>
<div style="font-size:11px;color:#9ba9bf;margin-bottom:8px">
${L('Installer-ID', 'Installer ID')}: <span style="color:#e7edf7">${installerId}</span>
</div>
<div style="margin-bottom:10px">
<div style="margin-bottom:5px">${L('Leistungssteuerung', 'Performance control')}</div>
<select id="wl-profile" style="width:100%;padding:7px 9px;background:#17283a;color:#edf6ff;border:1px solid #35536d;border-radius:8px">
<option value="auto">${L('AUTO – automatisch', 'AUTO – automatic')}</option>
<option value="balanced">${L('BALANCED – Ausgewogen', 'BALANCED')}</option>
<option value="max">${L('MAX – Stark optimiert', 'MAX – highly optimized')}</option>
<option value="ultra">${L('ULTRA – Maximale Leistung', 'ULTRA – maximum performance')}</option>
</select>
</div>
<button id="wl-power" style="width:100%;padding:7px 9px;margin-bottom:4px;border:1px solid #2d9b78;border-radius:8px;background:linear-gradient(135deg,#167c55,#116246);color:white;font:600 11px/1.3 system-ui,sans-serif;cursor:pointer"></button>
${card(L('Performance', 'Performance'), '<div id="wl-performance"></div>')}
${card(L('Blockaden-Analyse (letzte 5 Min.)', 'Blocking analysis (last 5 min)'), '<div id="wl-loaf"></div>')}
${card(L('Long Tasks – letzte 30 Sekunden', 'Long tasks – last 30 seconds'), '<div id="wl-trace-stats"></div>')}
${card(L('Interaktionen', 'Interactions'), '<div id="wl-interactions"></div>')}
${card(L('System & Ressourcen', 'System & resources'), '<div id="wl-system"></div>')}
${card(L('Letzte Ereignisse', 'Recent events'),
'<div id="wl-events" style="max-height:120px;overflow:auto;white-space:pre-wrap;font-size:11px;margin-top:5px"></div>')}
${card(L('A/B-Benchmark', 'A/B benchmark'),
`<div id="wl-benchmark" style="white-space:pre-line;margin-top:5px;font-size:12px">${L('Noch kein Benchmark gestartet.', 'No benchmark started yet.')}</div>`,
`<button id="wl-benchmark-start" style="${buttonStyle()}width:100%;margin-top:7px">${L('60-Sekunden-ABAB-Test starten', 'Start 60-second ABAB test')}</button>`)}
<div style="display:grid;grid-template-columns:1fr 1fr;gap:7px;margin-top:10px">
<button id="wl-copy" style="${buttonStyle()}">${L('Diagnose kopieren', 'Copy diagnostics')}</button>
<button id="wl-reset" style="${buttonStyle()}">${L('Zurücksetzen', 'Reset')}</button>
<button id="wl-refresh" style="${buttonStyle()}">${L('Aktualisieren', 'Refresh')}</button>
<button id="wl-close2" style="${buttonStyle()}">${L('Schließen', 'Close')}</button>
</div>
<div style="color:#93a0b4;font-size:10px;margin-top:10px;line-height:1.4">
${L('Long Tasks sind blockierende Hauptthread-Aufgaben ab 50 ms. Die Blockaden-Analyse (Long Animation Frames) zeigt, welches Skript sie verursacht.',
'Long tasks are blocking main-thread tasks >= 50 ms. The blocking analysis (long animation frames) shows which script caused them.')}
</div>
`;
document.body.appendChild(dashboard);
const $ = sel => dashboard.querySelector(sel);
$('#wl-profile').value = CONFIG.profile;
$('#wl-profile').addEventListener('change', event => setProfile(event.target.value));
$('#wl-power').addEventListener('click', () => setEnabled(!CONFIG.enabled));
$('#wl-close').addEventListener('click', closeDashboard);
$('#wl-close2').addEventListener('click', closeDashboard);
$('#wl-copy').addEventListener('click', event => copyDiagnostics(event.currentTarget));
$('#wl-refresh').addEventListener('click', updateDashboard);
$('#wl-benchmark-start').addEventListener('click', startBenchmark);
$('#wl-reset').addEventListener('click', () => {
frameSamples = [];
longTaskSamples = [];
interactionSamples = [];
loafSamples = [];
traceEvents = [];
stats.totalLongTasks = 0;
updateDashboard();
});
if (benchmark?.report && !benchmark.running) $('#wl-benchmark').textContent = benchmark.report;
makeDraggable(dashboard, $('#wl-drag'));
if (CONFIG.dashboardPosition) {
dashboard.style.left = CONFIG.dashboardPosition.x + 'px';
dashboard.style.top = CONFIG.dashboardPosition.y + 'px';
} else if (launcher) {
const rect = launcher.getBoundingClientRect();
dashboard.style.left = (rect.left + 52) + 'px';
dashboard.style.top = rect.top + 'px';
}
const rect = dashboard.getBoundingClientRect();
const pos = clampPosition(rect.left, rect.top, dashboard);
dashboard.style.left = pos.x + 'px';
dashboard.style.top = pos.y + 'px';
startFrameTracking();
updateDashboard();
dashboardTimer = setInterval(() => {
if (dashboardOpen && !document.hidden) updateDashboard();
}, 2000);
}
function closeDashboard() {
// Ein abgebrochener Test wird nicht als gültiges Ergebnis ausgegeben.
abortBenchmark();
dashboardOpen = false;
if (dashboardTimer) clearInterval(dashboardTimer);
dashboardTimer = null;
dashboard?.remove();
dashboard = null;
if (!wantFrames()) stopFrameTracking();
}
function updateDashboard() {
if (!dashboard) return;
// Während des Benchmarks nichts Schweres rendern, um die Messung nicht zu stören.
if (benchmark?.running) {
renderBenchmarkLabel();
return;
}
calculateAll();
const $ = sel => dashboard.querySelector(sel);
const power = $('#wl-power');
power.textContent = CONFIG.enabled ? L('Optimierung aktiv', 'Optimization active') : L('Optimierung deaktiviert', 'Optimization disabled');
power.style.background = CONFIG.enabled ? '#167c55' : '#8b3434';
const na = L('n.v.', 'n/a');
setHtml($('#wl-performance'), `
<div>${L('Steuerung', 'Control')}: <b>${CONFIG.profile.toUpperCase()}</b></div>
<div>${L('Aktives Profil', 'Active profile')}: <b>${CONFIG.enabled ? activeProfile().toUpperCase() : 'OFF'}</b></div>
<div>FPS: <b>${stats.fps || '—'}</b></div>
<div>${L('Framezeit', 'Frame time')} P50: <b>${stats.p50} ms</b></div>
<div>${L('Framezeit', 'Frame time')} P95: <b>${stats.p95} ms</b></div>
<div>${L('Ruckler (>50 ms)', 'Jank (>50 ms)')}: <b>${stats.jank}</b></div>
<div>Frame Drops (>33 ms): <b>${stats.drops}</b></div>
`);
const t = now();
setHtml($('#wl-loaf'), loafHtml(getLoafSummary(t - LOAF_WINDOW, t)));
setHtml($('#wl-trace-stats'), `
<div>Long Tasks (30 s): <b>${stats.longTasks}</b></div>
<div>${L('Überlappungsbereinigte Dauer', 'Overlap-free duration')}: <b>${stats.longTaskDuration} ms</b></div>
<div>${L('Durchschnitt pro Eintrag', 'Average per entry')}: <b>${Math.round(stats.longTaskAverage)} ms</b></div>
<div>${L('Längste Aufgabe', 'Longest task')}: <b>${stats.longTaskMax} ms</b></div>
<div>50–99 ms: <b>${stats.task50}</b> | 100–199 ms: <b>${stats.task100}</b> | ≥200 ms: <b>${stats.task200}</b></div>
<div>${L('Long Tasks insgesamt', 'Long tasks total')}: <b>${stats.totalLongTasks}</b></div>
`);
const breakdown = stats.slowInteractions
? `<div style="margin-top:4px">${L('Ø bei langsamen Klicks', 'Avg. of slow interactions')}:<br>
${L('Wartezeit', 'Input delay')} <b>${Math.round(stats.slowInput)} ms</b> |
${L('Handler', 'Handler')} <b>${Math.round(stats.slowProc)} ms</b> |
${L('Darstellung', 'Presentation')} <b>${Math.round(stats.slowPres)} ms</b></div>`
: '';
setHtml($('#wl-interactions'), `
<div>${L('Gemessen (60 s)', 'Measured (60 s)')}: <b>${stats.interactions}</b></div>
<div>${L('Langsam (>100 ms)', 'Slow (>100 ms)')}: <b>${stats.slowInteractions}</b></div>
<div>${L('Maximal', 'Maximum')}: <b>${Math.round(stats.maxInteraction)} ms</b></div>
${breakdown}
`);
setHtml($('#wl-system'), `
<div>${L('CPU-Kerne', 'CPU cores')}: <b>${navigator.hardwareConcurrency || na}</b></div>
<div>${L('RAM-Schätzung', 'RAM estimate')}: <b>${navigator.deviceMemory ? navigator.deviceMemory + ' GB' : na}</b></div>
<div>JS-Heap: <b>${stats.heap ? stats.heap + ' MB' : na}</b> / ${stats.heapLimit ? stats.heapLimit + ' MB' : na}</div>
<div>${L('Ressourcen (>1 s)', 'Resources (>1 s)')}: <b>${stats.slowResources} / ${stats.resources}</b></div>
`);
const recent = traceEvents.slice(-10).reverse();
const eventsEl = $('#wl-events');
const eventsText = recent.length
? recent.map(item => {
const what = item.duration != null ? `${item.duration} ms` : (item.name ?? '—');
const extra = item.type === 'SLOW_INTERACTION'
? ` ${item.name}${item.target ? ' ' + item.target : ''} (${item.input}/${item.proc}/${item.pres})`
: '';
return `${(item.time / 1000).toFixed(1)} s | ${item.type} | ${what}${extra}`;
}).join('\n')
: L('Keine auffälligen Ereignisse.', 'No notable events.');
if (eventsEl._t !== eventsText) {
eventsEl._t = eventsText;
eventsEl.textContent = eventsText;
}
}
// ------------------------------------------------------------
// Diagnose exportieren
// ------------------------------------------------------------
function getDiagnostics() {
calculateAll();
const t = now();
const loaf = getLoafSummary(t - LOAF_WINDOW, t);
const lines = [
`LIDELERA ${VERSION} DIAGNOSE`,
`Installer ID: ${installerId}`,
`Zeit: ${new Date().toISOString()}`,
`Browser: ${navigator.userAgent}`,
`CPU-Kerne: ${navigator.hardwareConcurrency || '?'}`,
`RAM-Schätzung: ${navigator.deviceMemory || '?'} GB`,
`Steuerung: ${CONFIG.profile}`,
`Aktives Profil: ${activeProfile()}`,
`Optimierung: ${CONFIG.enabled ? 'aktiv' : 'aus'}`,
`FPS: ${stats.fps}`,
`Framezeit P50: ${stats.p50} ms`,
`Framezeit P95: ${stats.p95} ms`,
`Ruckler >50 ms: ${stats.jank}`,
`Frame Drops >33 ms: ${stats.drops}`,
`Long Tasks letzte 30 s: ${stats.longTasks}`,
`Long Task Dauer (überlappungsbereinigt): ${stats.longTaskDuration} ms`,
`Längste Task: ${stats.longTaskMax} ms`,
`Tasks 50–99 ms: ${stats.task50}`,
`Tasks 100–199 ms: ${stats.task100}`,
`Tasks >=200 ms: ${stats.task200}`,
`Long Tasks insgesamt: ${stats.totalLongTasks}`,
`Interaktionen letzte 60 s: ${stats.interactions}`,
`Interaktionen >100 ms: ${stats.slowInteractions}`,
`Maximale Interaktionsdauer: ${Math.round(stats.maxInteraction)} ms`,
`Langsame Interaktionen Ø: Wartezeit ${Math.round(stats.slowInput)} ms | Handler ${Math.round(stats.slowProc)} ms | Darstellung ${Math.round(stats.slowPres)} ms`,
`JS-Speicher: ${stats.heap} MB`,
`JS-Heap-Limit: ${stats.heapLimit} MB`,
`Long Task API: ${Boolean(longTaskObserver)}`,
`Event Timing API: ${Boolean(eventObserver)}`,
`Long Animation Frames API: ${Boolean(loafObserver)}`
];
if (loafObserver) {
const total = loaf.script + loaf.render;
const pct = v => (total > 0 ? Math.round(100 * v / total) : 0);
lines.push(
'',
'BLOCKADEN-ANALYSE (Long Animation Frames, letzte 5 Min.)',
`Langsame Frames >50 ms: ${loaf.count}`,
`Blockierend gesamt: ${Math.round(loaf.blocking)} ms`,
`Skript-Ausführung: ${Math.round(loaf.script)} ms (${pct(loaf.script)} %)`,
`Rendering (Style/Layout/Paint): ${Math.round(loaf.render)} ms (${pct(loaf.render)} %)`,
`Davon erzwungenes Layout: ${Math.round(loaf.forced)} ms`,
'Top-Verursacher:'
);
for (const s of loaf.sources) {
lines.push(` ${s.origin}/${s.file} | ${s.fn || s.invoker || '(anonym)'} | ${Math.round(s.total)} ms, ${s.count}x, max ${Math.round(s.max)} ms`);
}
}
if (benchmark?.report) lines.push('', benchmark.report);
else if (benchmark?.running) lines.push('', `A/B BENCHMARK läuft: Phase ${benchmark.index + 1}/${BENCH_PHASES.length}`);
lines.push('', 'LETZTE TRACE-EREIGNISSE');
for (const item of traceEvents.slice(-30)) {
lines.push(`${item.time.toFixed(1)} ms | ${item.type} | ${JSON.stringify(item)}`);
}
return lines.join('\n');
}
async function copyDiagnostics(button) {
const text = getDiagnostics();
let ok = false;
try {
await navigator.clipboard.writeText(text);
ok = true;
} catch (_) {
const area = document.createElement('textarea');
area.value = text;
area.style.cssText = 'position:fixed;left:-9999px;';
document.body.appendChild(area);
area.select();
try { ok = document.execCommand('copy'); } catch (_) {}
area.remove();
}
// Kein alert(): der blockiert den Hauptthread und friert das Spiel kurz ein.
if (button) {
const original = button.textContent;
button.textContent = ok ? L('Kopiert ✓', 'Copied ✓') : L('Fehlgeschlagen', 'Failed');
setTimeout(() => { if (button.isConnected) button.textContent = original; }, 1500);
}
}
// ------------------------------------------------------------
// Fenstergröße und Initialisierung
// ------------------------------------------------------------
function onResize() {
if (launcher) {
const rect = launcher.getBoundingClientRect();
const pos = clampPosition(rect.left, rect.top, launcher);
launcher.style.left = pos.x + 'px';
launcher.style.top = pos.y + 'px';
CONFIG.launcherPosition = pos;
save('launcherPosition', pos);
}
if (dashboard) {
const rect = dashboard.getBoundingClientRect();
const pos = clampPosition(rect.left, rect.top, dashboard);
dashboard.style.left = pos.x + 'px';
dashboard.style.top = pos.y + 'px';
CONFIG.dashboardPosition = pos;
save('dashboardPosition', pos);
}
}
// Stellt UI/Style wieder her, falls die SPA sie aus dem DOM entfernt hat.
function ensureUi() {
if (styleElement && !styleElement.isConnected) {
appliedKey = '';
applyOptimization();
}
if (launcher && document.body && !launcher.isConnected) document.body.appendChild(launcher);
}
function init() {
if (document.documentElement) applyOptimization();
else document.addEventListener('DOMContentLoaded', applyOptimization, { once: true });
setupLongTaskObserver();
setupLoafObserver();
setupInteractionObserver();
if (document.body) createLauncher();
else document.addEventListener('DOMContentLoaded', createLauncher, { once: true });
document.addEventListener('visibilitychange', () => {
if (document.hidden) stopFrameTracking();
else startFrameTracking();
});
window.addEventListener('resize', onResize, { passive: true });
// Ein einziger, billiger Takt: AUTO-Messfenster starten, Profil prüfen, UI absichern.
setInterval(() => {
if (document.hidden) return;
ensureUi();
if (wantFrames()) startFrameTracking();
if (CONFIG.profile === 'auto' && !benchmark?.running) evaluateAutomaticProfile();
}, 1000);
}
init();
})();