// ==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 加载加速
—
`;
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();
})();