// ==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('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('Ø 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