// ==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 `
${L('Von diesem Browser nicht unterstützt (Chrome 123+ nötig).', 'Not supported by this browser (needs Chrome 123+).')}
`; } if (!loaf.count) { return `
${L('Keine langsamen Frames in den letzten 5 Minuten.', 'No slow frames in the last 5 minutes.')}
`; } 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 = `
${L('Langsame Frames (>50 ms)', 'Slow frames (>50 ms)')}: ${loaf.count} | ${L('blockierend', 'blocking')}: ${Math.round(loaf.blocking)} ms
${L('Skript-Ausführung', 'Script execution')}: ${Math.round(loaf.script)} ms (${pct(loaf.script)} %)
${L('Rendering (Style/Layout/Paint)', 'Rendering (style/layout/paint)')}: ${Math.round(loaf.render)} ms (${pct(loaf.render)} %)
${L('davon erzwungenes Layout', 'of which forced layout')}: ${Math.round(loaf.forced)} ms
${hint ? `
${hint}
` : ''}
${L('Top-Verursacher (Skriptzeit):', 'Top culprits (script time):')}
`; for (const s of loaf.sources) { const name = s.fn || s.invoker || L('(anonym)', '(anonymous)'); html += `
• ${esc(s.origin)}/${esc(s.file)} – ${esc(name)}
  ${Math.round(s.total)} ms, ${s.count}×, max ${Math.round(s.max)} ms
`; } 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 `
${title}${bodyHtml}${extra}
`; } 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 = `
⚡ LidelEra ${VERSION} ✕
${L('Installer-ID', 'Installer ID')}: ${installerId}
${L('Leistungssteuerung', 'Performance control')}
${card(L('Performance', 'Performance'), '
')} ${card(L('Blockaden-Analyse (letzte 5 Min.)', 'Blocking analysis (last 5 min)'), '
')} ${card(L('Long Tasks – letzte 30 Sekunden', 'Long tasks – last 30 seconds'), '
')} ${card(L('Interaktionen', 'Interactions'), '
')} ${card(L('System & Ressourcen', 'System & resources'), '
')} ${card(L('Letzte Ereignisse', 'Recent events'), '
')} ${card(L('A/B-Benchmark', 'A/B benchmark'), `
${L('Noch kein Benchmark gestartet.', 'No benchmark started yet.')}
`, ``)}
${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.')}
`; 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'), `
${L('Steuerung', 'Control')}: ${CONFIG.profile.toUpperCase()}
${L('Aktives Profil', 'Active profile')}: ${CONFIG.enabled ? activeProfile().toUpperCase() : 'OFF'}
FPS: ${stats.fps || '—'}
${L('Framezeit', 'Frame time')} P50: ${stats.p50} ms
${L('Framezeit', 'Frame time')} P95: ${stats.p95} ms
${L('Ruckler (>50 ms)', 'Jank (>50 ms)')}: ${stats.jank}
Frame Drops (>33 ms): ${stats.drops}
`); const t = now(); setHtml($('#wl-loaf'), loafHtml(getLoafSummary(t - LOAF_WINDOW, t))); setHtml($('#wl-trace-stats'), `
Long Tasks (30 s): ${stats.longTasks}
${L('Überlappungsbereinigte Dauer', 'Overlap-free duration')}: ${stats.longTaskDuration} ms
${L('Durchschnitt pro Eintrag', 'Average per entry')}: ${Math.round(stats.longTaskAverage)} ms
${L('Längste Aufgabe', 'Longest task')}: ${stats.longTaskMax} ms
50–99 ms: ${stats.task50} | 100–199 ms: ${stats.task100} | ≥200 ms: ${stats.task200}
${L('Long Tasks insgesamt', 'Long tasks total')}: ${stats.totalLongTasks}
`); const breakdown = stats.slowInteractions ? `
${L('Ø bei langsamen Klicks', 'Avg. of slow interactions')}:
${L('Wartezeit', 'Input delay')} ${Math.round(stats.slowInput)} ms | ${L('Handler', 'Handler')} ${Math.round(stats.slowProc)} ms | ${L('Darstellung', 'Presentation')} ${Math.round(stats.slowPres)} ms
` : ''; setHtml($('#wl-interactions'), `
${L('Gemessen (60 s)', 'Measured (60 s)')}: ${stats.interactions}
${L('Langsam (>100 ms)', 'Slow (>100 ms)')}: ${stats.slowInteractions}
${L('Maximal', 'Maximum')}: ${Math.round(stats.maxInteraction)} ms
${breakdown} `); setHtml($('#wl-system'), `
${L('CPU-Kerne', 'CPU cores')}: ${navigator.hardwareConcurrency || na}
${L('RAM-Schätzung', 'RAM estimate')}: ${navigator.deviceMemory ? navigator.deviceMemory + ' GB' : na}
JS-Heap: ${stats.heap ? stats.heap + ' MB' : na} / ${stats.heapLimit ? stats.heapLimit + ' MB' : na}
${L('Ressourcen (>1 s)', 'Resources (>1 s)')}: ${stats.slowResources} / ${stats.resources}
`); 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(); })();