// ==UserScript== // @name X66 题包加载加速器 // @namespace https://x66.local/accelerator // @version 1.1.0 // @description 自动并发预取帧数据(含点云),写入本地缓存,大幅加速 X66 平台新包首帧加载 // @author Bro // @match http://data-label-lan.gacrnd.com/* // @match https://data-label-lan.gacrnd.com/* // @grant none // @run-at document-start // @noframes // ==/UserScript== (function () { 'use strict'; // ========== 配置(基于实测调优)========== const CFG = { concurrency: 6, // 实测甜点:6 最优,>8 打爆 VPN 带宽 maxConcurrency: 8, // 上限 firstBatch: 20, // 优先预热前 20 帧(你要先看的) neighborRange: 10, // 当前帧前后各 10 帧为"近邻优先级" waitEditorMs: 1500, // 等待 editor 就绪轮询间隔 maxWaitMs: 180000, // 最多等 3 分钟 autoDelayMs: 2500, // 检测到新包后延迟启动(让平台先加载当前帧) autoStart: true, // 自动预热 retryFailed: true, // 失败重试一次 }; const TAG = '%c[X66加速]'; const TAGCSS = 'color:#0a9;font-weight:bold'; const LOG = (...a) => console.log(TAG, TAGCSS, ...a); // ========== 状态 ========== const ST = { running: false, enabled: true, editor: null, OCe: null, total: 0, done: 0, failed: 0, startTime: 0, lastPackageKey: '', netProbed: false, netMBps: 0, concurrency: CFG.concurrency, }; // ========== UI ========== let ui = null; function buildUI() { if (ui) return ui; const el = document.createElement('div'); el.id = 'x66-accel-panel'; el.style.cssText = [ 'position:fixed', 'right:16px', 'bottom:16px', 'z-index:2147483646', 'width:258px', 'background:#fff', 'color:#1a1a1a', 'border:1px solid #d5d5d5', 'border-radius:10px', 'box-shadow:0 4px 18px rgba(0,0,0,.13)', 'font:12px/1.5 -apple-system,"Segoe UI",sans-serif', 'padding:10px 12px' ].join(';'); el.innerHTML = `
X66 加载加速 —
待机
0 / 0
`; document.body.appendChild(el); ui = el; // 拖拽 (function drag() { const head = el.querySelector('#x66a-head'); let sx = 0, sy = 0, ox = 0, oy = 0, moving = false; head.addEventListener('mousedown', e => { moving = true; sx = e.clientX; sy = e.clientY; const r = el.getBoundingClientRect(); ox = r.left; oy = r.top; e.preventDefault(); }); document.addEventListener('mousemove', e => { if (!moving) return; el.style.left = (ox + e.clientX - sx) + 'px'; el.style.top = (oy + e.clientY - sy) + 'px'; el.style.right = 'auto'; el.style.bottom = 'auto'; }); document.addEventListener('mouseup', () => { moving = false; }); })(); el.querySelector('#x66a-fold').addEventListener('click', function () { const b = el.querySelector('#x66a-body'); const hidden = b.style.display === 'none'; b.style.display = hidden ? 'block' : 'none'; this.textContent = hidden ? '—' : '+'; }); el.querySelector('#x66a-enable').addEventListener('change', e => { ST.enabled = e.target.checked; setStatus(ST.enabled ? '待机' : '已禁用'); if (!ST.enabled) stop(); }); el.querySelector('#x66a-go').addEventListener('click', () => { if (!ST.enabled) { setStatus('请先启用'); return; } if (ST.running) { setStatus('正在跑'); return; } const e = window.editor; if (!e || !e.state || !e.state.frames) { setStatus('页面未就绪'); return; } ST.editor = e; preheat(e).catch(err => { LOG('异常', err); setStatus('出错'); }); }); el.querySelector('#x66a-stop').addEventListener('click', () => { stop(); setStatus('已停止'); }); return el; } function setStatus(t) { const e = document.getElementById('x66a-status'); if (e) e.textContent = t; } function setTip(t) { const e = document.getElementById('x66a-tip'); if (e) e.innerHTML = t; } function setProgress() { const bar = document.getElementById('x66a-bar'); const cnt = document.getElementById('x66a-count'); const spd = document.getElementById('x66a-speed'); if (!bar) return; bar.style.width = (ST.total ? Math.round(ST.done / ST.total * 100) : 0) + '%'; cnt.textContent = ST.done + ' / ' + ST.total + (ST.failed ? '(失败 ' + ST.failed + ')' : ''); if (ST.startTime && ST.done > 0) { const sec = (Date.now() - ST.startTime) / 1000; spd.textContent = (ST.done / sec).toFixed(2) + ' 帧/秒'; } } // ========== 获取 OCe 构造器(隔离,不污染平台 loaders)========== function acquireLoaderCtor(editor) { if (ST.OCe) return ST.OCe; const dr = editor.dataResource; const st = editor.state; const f = st.frames[0]; const before = dr.loaders.length; let L = null; try { L = dr.loadNext(f, true); } catch (e) { } // 立即还原 loaders,避免污染平台调度 if (dr.loaders.length > before) dr.loaders.length = before; if (L && L.constructor && typeof L.constructor === 'function') { ST.OCe = L.constructor; LOG('loader 构造器已就绪'); return ST.OCe; } return null; } // ========== 等待 editor 就绪 ========== function waitForEditor() { return new Promise((resolve, reject) => { const t0 = Date.now(); const timer = setInterval(() => { const e = window.editor; if (e && e.dataResource && e.state && Array.isArray(e.state.frames) && e.state.frames.length) { clearInterval(timer); resolve(e); } else if (Date.now() - t0 > CFG.maxWaitMs) { clearInterval(timer); reject(new Error('等待 editor 超时')); } }, CFG.waitEditorMs); }); } // ========== 带宽探测(决定并发)========== async function probeNetwork(editor) { if (ST.netProbed) return ST.concurrency; const dm = editor.dataResource.dataMap; const ks = Object.keys(dm).filter(k => dm[k] && dm[k].pointsUrl); if (ks.length < 2) { ST.netProbed = true; return ST.concurrency; } try { const urls = ks.slice(0, 2).map(k => dm[k].pointsUrl); const t0 = performance.now(); const bufs = await Promise.all(urls.map(u => fetch(u).then(r => r.arrayBuffer()))); const ms = Math.max(performance.now() - t0, 1); const bytes = bufs.reduce((a, b) => a + b.byteLength, 0); const mbps = (bytes * 8 / 1048576) / (ms / 1000); ST.netMBps = (bytes / 1048576) / (ms / 1000); // 实测:并发 6 最优;带宽很好时最多到 8 if (mbps > 600) ST.concurrency = Math.min(8, CFG.maxConcurrency); else if (mbps > 200) ST.concurrency = 6; else if (mbps > 80) ST.concurrency = 4; else ST.concurrency = 3; ST.netProbed = true; LOG('带宽 ' + mbps.toFixed(0) + ' Mbps → 并发 ' + ST.concurrency); return ST.concurrency; } catch (e) { ST.netProbed = true; return ST.concurrency; } } // ========== 预热核心 ========== async function preheat(editor) { if (ST.running) { LOG('已在运行中'); return; } const dr = editor.dataResource; const st = editor.state; const OCe = acquireLoaderCtor(editor); if (!OCe) { LOG('无法获取 loader 构造器'); setStatus('不支持'); return; } const conc = await probeNetwork(editor); // 目标:所有未加载帧 const unloaded = st.frames.filter(f => !dr.dataMap[f.id]); if (!unloaded.length) { LOG('已全部加载完成'); setStatus('已完整'); setProgress(); setTip('本包帧数据已全部就绪'); return; } // 优先级排序:当前帧近邻 → 前 N 帧 → 其余 const cur = st.frameIndex || 0; const frames = st.frames; const idxOf = new Map(frames.map((f, i) => [f.id, i])); const near = [], firstN = [], rest = []; for (const f of unloaded) { const i = idxOf.get(f.id); if (Math.abs(i - cur) <= CFG.neighborRange) near.push(f); else if (i < CFG.firstBatch) firstN.push(f); else rest.push(f); } // 近邻按距离排序 near.sort((a, b) => Math.abs(idxOf.get(a.id) - cur) - Math.abs(idxOf.get(b.id) - cur)); firstN.sort((a, b) => idxOf.get(a.id) - idxOf.get(b.id)); rest.sort((a, b) => idxOf.get(a.id) - idxOf.get(b.id)); const targets = [...near, ...firstN, ...rest]; ST.running = true; ST.total = targets.length; ST.done = 0; ST.failed = 0; ST.startTime = Date.now(); setStatus('预热中'); setProgress(); LOG('开始预热 ' + targets.length + ' 帧,并发 ' + conc); let idx = 0; const worker = async () => { while (ST.running) { const i = idx++; if (i >= targets.length) break; const f = targets[i]; if (dr.dataMap[f.id]) { ST.done++; setProgress(); continue; } try { const L = new OCe(dr, f); // 隔离构造,不入 dr.loaders L.load(); await L.get(); ST.done++; } catch (e) { if (CFG.retryFailed) { try { const L2 = new OCe(dr, f); L2.load(); await L2.get(); ST.done++; } catch (e2) { ST.failed++; LOG('帧失败', f.id); } } else { ST.failed++; } } setProgress(); } }; await Promise.all(Array.from({ length: conc }, () => worker())); ST.running = false; const sec = ((Date.now() - ST.startTime) / 1000).toFixed(1); LOG('预热完成 ' + ST.done + ' 帧,失败 ' + ST.failed + ',耗时 ' + sec + 's'); setStatus('完成'); setTip('预热完成:' + ST.done + ' 帧 / ' + sec + 's(' + (ST.done / sec).toFixed(2) + ' 帧/秒)'); } function stop() { ST.running = false; try { const dr = ST.editor && ST.editor.dataResource; if (dr && dr.loaders) dr.loaders.length = 0; } catch (e) { } } // ========== 自动检测新题包 ========== function packageKey(editor) { try { const st = editor.state; const f0 = st.frames[0]; return (f0 && f0.datasetId || '') + '|' + st.frames.length + '|' + (f0 && f0.id); } catch (e) { return ''; } } async function autoLoop() { if (!/\/tool\/pc/.test(location.pathname + location.search)) return; // 只在标注编辑器页生效 buildUI(); setStatus('等待题包'); setTip('打开/领取题包后将自动预热'); setInterval(async () => { if (!ST.enabled || ST.running) return; const e = window.editor; if (!e || !e.state || !Array.isArray(e.state.frames) || !e.state.frames.length) return; ST.editor = e; const key = packageKey(e); if (!key || key === ST.lastPackageKey) return; ST.lastPackageKey = key; ST.netProbed = false; const dr = e.dataResource; const loaded = Object.keys(dr.dataMap).length; const total = e.state.frames.length; LOG('检测到题包 ' + key + ',已加载 ' + loaded + '/' + total); if (loaded < total) { setStatus('检测到新题包'); setTip('将在 ' + (CFG.autoDelayMs / 1000) + 's 后开始后台预热…'); setTimeout(() => { if (ST.enabled && !ST.running) { preheat(e).catch(err => LOG('预热异常', err)); } }, CFG.autoDelayMs); } else { setStatus('已完整'); } }, CFG.waitEditorMs * 2); } // ========== 启动 ========== function boot() { LOG('v1.1.0 已注入'); const start = () => { waitForEditor().then(() => autoLoop()).catch(() => autoLoop()); }; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', start); } else { start(); } } boot(); })();