131 lines
8.5 KiB
JavaScript
131 lines
8.5 KiB
JavaScript
/**
|
||
* 浏览器实测:ECAT 刷新速度下限档 6ms(不限速档)。
|
||
* 单测只验了纯逻辑;这里验真机行为:
|
||
* - 输入框 min/step 随链路变(否则浏览器把合法的 6ms 标成 :invalid)
|
||
* - 6ms 真的比 50ms 快,且拿到实测频率而不是理论 166Hz
|
||
* - 存量缓存里的旧 1ms 会被抬到 6ms(绘制上限之上没有消费者)
|
||
* - 不限速档**不**亮卡顿黄标(每拍撞上在途请求是预期行为)
|
||
* - 切回 WiFi 时 6ms 不会被带过去(wifi 下限 50ms)
|
||
*/
|
||
const CDP = "http://127.0.0.1:9333";
|
||
const list = await (await fetch(CDP + "/json/list")).json();
|
||
const page = list.find((t) => t.type === "page");
|
||
const sock = new WebSocket(page.webSocketDebuggerUrl);
|
||
let id = 0; const pending = new Map();
|
||
await new Promise((r, j) => { sock.onopen = r; sock.onerror = j; });
|
||
sock.onmessage = (e) => { const m = JSON.parse(e.data); if (m.id && pending.has(m.id)) { pending.get(m.id)(m); pending.delete(m.id); } };
|
||
const send = (method, params = {}) => { const myId = ++id; return new Promise((res) => { pending.set(myId, res); sock.send(JSON.stringify({ id: myId, method, params })); }); };
|
||
async function ev(expr) {
|
||
const r = await send("Runtime.evaluate", { expression: expr, awaitPromise: true, returnByValue: true });
|
||
return r.result && r.result.result && r.result.result.value;
|
||
}
|
||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||
let pass = 0, fail = 0; const out = [];
|
||
const check = (n, c, d) => { if (c) { pass++; out.push(" \u2713 " + n); } else { fail++; out.push(" \u2717 " + n + (d ? " \u2014 " + d : "")); } };
|
||
|
||
await send("Runtime.enable"); await send("Page.enable");
|
||
/* 页面服务端口可传参:node scripts/xxx.mjs [port](默认 2625) */
|
||
const PORT = Number(process.argv[2] || 2625);
|
||
const PAGE = `http://127.0.0.1:${PORT}/pages/motor.html`;
|
||
|
||
/* 干净起点,直接进 ECAT */
|
||
await send("Page.navigate", { url: PAGE }); await sleep(2000);
|
||
await ev(`localStorage.setItem("dcu_motor_transport","ecat"); localStorage.removeItem("dcu_motor_ui_v2"); "ok"`);
|
||
await send("Page.navigate", { url: PAGE }); await sleep(3500);
|
||
|
||
/* 请求计数探针 */
|
||
await ev(`(() => {
|
||
window.__cnt = 0;
|
||
if (!window.__origFetch) window.__origFetch = window.fetch;
|
||
window.fetch = function (u) { if (String(u).includes("motor/state")) window.__cnt++; return window.__origFetch.apply(this, arguments); };
|
||
return "ok";
|
||
})()`);
|
||
async function measure(sec) {
|
||
await ev(`window.__cnt = 0; "ok"`);
|
||
await sleep(sec * 1000);
|
||
return await ev(`window.__cnt`) / sec;
|
||
}
|
||
|
||
const pre = JSON.parse(await ev(`JSON.stringify({tp: motorDbg.state.tpId, conn: motorDbg.state.connected, eff: motorDbg.state.refreshIntervalMs})`));
|
||
out.push("\n[前置] " + JSON.stringify(pre));
|
||
check("已在 ecat 链路且已连接", pre.tp === "ecat" && pre.conn === true, JSON.stringify(pre));
|
||
|
||
out.push("\n[A] ECAT 下输入框的 min/step 应放开到 6/1");
|
||
let dom = JSON.parse(await ev(`JSON.stringify({min: document.querySelector("#refreshRate").min, step: document.querySelector("#refreshRate").step})`));
|
||
check("min=6(否则浏览器校验会把 6ms 判成非法)", dom.min === "6", JSON.stringify(dom));
|
||
check("step=1(下限档要能逐 ms 微调)", dom.step === "1", JSON.stringify(dom));
|
||
|
||
out.push("\n[B] 基线:ECAT 默认 50ms");
|
||
const hz50 = await measure(3);
|
||
out.push(` 实测 ${hz50.toFixed(1)} Hz`);
|
||
check("50ms 档约 20Hz", hz50 > 12 && hz50 < 30, `实测 ${hz50.toFixed(1)}`);
|
||
|
||
out.push("\n[C] 设成 6ms —— 应被接受且明显更快");
|
||
await ev(`(() => { const i=document.querySelector("#refreshRate"); i.value="6"; i.dispatchEvent(new Event("change")); return "ok"; })()`);
|
||
await sleep(800);
|
||
let st = JSON.parse(await ev(`JSON.stringify({eff: motorDbg.state.refreshIntervalMs, val: document.querySelector("#refreshRate").value, valid: document.querySelector("#refreshRate").checkValidity(), map: motorDbg.state.refreshByTp})`));
|
||
check("6ms 被接受(没被钳回 50)", st.eff === 6, JSON.stringify(st));
|
||
check("输入框显示 6", st.val === "6", JSON.stringify(st));
|
||
check("通过浏览器原生校验(min 已同步)", st.valid === true, JSON.stringify(st));
|
||
check("只写进 ecat 那一份,wifi 仍 250", st.map.ecat === 6 && st.map.wifi === 250, JSON.stringify(st));
|
||
const hz1 = await measure(4);
|
||
out.push(` 实测 ${hz1.toFixed(1)} Hz(每拍一次 HTTP 往返,6ms 档约 160Hz 量级)`);
|
||
check("6ms 明显快于 50ms 档", hz1 > hz50 * 1.5, `6ms=${hz1.toFixed(1)} vs 50ms=${hz50.toFixed(1)}`);
|
||
check("但达不到理论 166Hz 以上(受每拍 HTTP 往返限制)", hz1 < 200, `实测 ${hz1.toFixed(1)}`);
|
||
|
||
out.push("\n[C2] 硬填旧的 1ms —— 应被抬到下限 6ms");
|
||
await ev(`(() => { const i=document.querySelector("#refreshRate"); i.value="1"; i.dispatchEvent(new Event("change")); return "ok"; })()`);
|
||
await sleep(500);
|
||
st = JSON.parse(await ev(`JSON.stringify({eff: motorDbg.state.refreshIntervalMs, val: document.querySelector("#refreshRate").value})`));
|
||
check("旧 1ms 被抬到 6ms", st.eff === 6, JSON.stringify(st));
|
||
check("输入框回填 6", st.val === "6", JSON.stringify(st));
|
||
|
||
out.push("\n[D] 不限速档不应亮卡顿黄标");
|
||
st = JSON.parse(await ev(`JSON.stringify({stuck: motorDbg.state.tickStuck, cls: document.querySelector("#chSelect").classList.contains("stuck"), title: document.querySelector("#chSelect").title})`));
|
||
check("tickStuck 保持 0(每拍撞上在途请求是预期行为,不算卡顿)", st.stuck === 0, JSON.stringify(st));
|
||
check("通道下拉框没有 .stuck 黄标", st.cls === false, JSON.stringify(st));
|
||
check("没有误导性的卡顿提示文案", !st.title, JSON.stringify(st));
|
||
|
||
out.push("\n[E] STATUS 面板的 Hz 应是实测值,不是理论 166Hz");
|
||
const hzText = await ev(`document.querySelector("#statusIdLabel").textContent`);
|
||
out.push(` 面板显示: ${hzText}`);
|
||
const m = String(hzText).match(/([\d.]+)\s*Hz/);
|
||
check("面板有 Hz 读数", !!m || /\u2026/.test(String(hzText)), hzText);
|
||
if (m) {
|
||
const shown = parseFloat(m[1]);
|
||
check("显示的不是谎报的理论值", shown < 200, `显示 ${shown}Hz`);
|
||
check("显示值与实测频率同量级(±60%)",
|
||
Math.abs(shown - hz1) / Math.max(hz1, 1) < 0.6, `显示 ${shown} vs 实测 ${hz1.toFixed(1)}`);
|
||
}
|
||
|
||
out.push("\n[F] 切到 WiFi:6ms 不能带过去(wifi 下限 50ms)");
|
||
await ev(`(() => { const s=document.querySelector("#transportSelect"); s.value="wifi"; s.dispatchEvent(new Event("change")); return "ok"; })()`);
|
||
await sleep(1500);
|
||
st = JSON.parse(await ev(`JSON.stringify({eff: motorDbg.state.refreshIntervalMs, val: document.querySelector("#refreshRate").value, min: document.querySelector("#refreshRate").min, step: document.querySelector("#refreshRate").step})`));
|
||
check("WiFi 生效值 250(不是 ECAT 的 6)", st.eff === 250, JSON.stringify(st));
|
||
check("WiFi 下 min 收回 50", st.min === "50", JSON.stringify(st));
|
||
check("WiFi 下 step 收回 50", st.step === "50", JSON.stringify(st));
|
||
/* 在 WiFi 下硬填 1ms,应被钳到 50 */
|
||
await ev(`(() => { const i=document.querySelector("#refreshRate"); i.value="1"; i.dispatchEvent(new Event("change")); return "ok"; })()`);
|
||
await sleep(500);
|
||
st = JSON.parse(await ev(`JSON.stringify({eff: motorDbg.state.refreshIntervalMs, val: document.querySelector("#refreshRate").value})`));
|
||
check("WiFi 下填 1ms 被钳到 50(无线往返撑不住)", st.eff === 50, JSON.stringify(st));
|
||
|
||
out.push("\n[G] 切回 ECAT:仍记着自己的 6ms");
|
||
await ev(`(() => { const s=document.querySelector("#transportSelect"); s.value="ecat"; s.dispatchEvent(new Event("change")); return "ok"; })()`);
|
||
await sleep(2500);
|
||
st = JSON.parse(await ev(`JSON.stringify({eff: motorDbg.state.refreshIntervalMs, val: document.querySelector("#refreshRate").value, min: document.querySelector("#refreshRate").min})`));
|
||
check("ECAT 生效值回到 6ms", st.eff === 6, JSON.stringify(st));
|
||
check("min 重新放开到 6", st.min === "6", JSON.stringify(st));
|
||
/* 刷新页面后 6ms 应持久化 */
|
||
await send("Page.navigate", { url: PAGE }); await sleep(3000);
|
||
st = JSON.parse(await ev(`JSON.stringify({eff: motorDbg.state.refreshIntervalMs, val: document.querySelector("#refreshRate").value, map: motorDbg.state.refreshByTp})`));
|
||
check("刷新页面后 ECAT 仍是 6ms(持久化 + 加载时不被钳高)", st.eff === 6, JSON.stringify(st));
|
||
check("刷新后 wifi 那一份仍是 50(上面手动改过)", st.map.wifi === 50, JSON.stringify(st));
|
||
|
||
console.log(out.join("\n"));
|
||
console.log("\n" + "=".repeat(56));
|
||
console.log(`通过 ${pass} 项,失败 ${fail} 项`);
|
||
sock.close();
|
||
process.exit(fail === 0 ? 0 : 1);
|