fix: reduce userscript page blocking and stale option clicks
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
'use strict';
|
||||
|
||||
// Local-only performance regression probe. It compares the committed 1.2.4
|
||||
// sources with the working tree without rewriting dist or visiting an account.
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
const http = require('node:http');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
const DATASET = path.join(ROOT, '题库/华谊集团化工总控工(高级)-合并题库.json');
|
||||
const OUTPUT = path.join(os.tmpdir(), 'huayi-qb-performance');
|
||||
const RECORDS_KEY = 'huayi-qb.records.v1';
|
||||
const SOURCES = ['src/core.js', 'src/adapter.js', 'src/autofill.js', 'src/app.js'];
|
||||
// This was HEAD/package version 1.2.4 when the reported regression was captured.
|
||||
// Pin it so committing the fix does not silently turn the baseline into itself.
|
||||
const BASELINE_REF = process.env.QB_PERF_BASE_REF || '26e0a920b6d56603bfee1061014c5052a7366ec2';
|
||||
|
||||
function loadPlaywright() {
|
||||
if (process.env.QB_PLAYWRIGHT_MODULE) return require(process.env.QB_PLAYWRIGHT_MODULE);
|
||||
try { return require('playwright'); }
|
||||
catch (_) { return require(path.join(os.homedir(), '.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright')); }
|
||||
}
|
||||
function browserOptions() {
|
||||
const requested = process.env.QB_CHROMIUM_EXECUTABLE || process.env.QB_CHROMIUM_PATH;
|
||||
const cached = path.join(os.homedir(), 'AppData/Local/ms-playwright/chromium_headless_shell-1228/chrome-headless-shell-win64/chrome-headless-shell.exe');
|
||||
return { headless: true, ...(requested || fs.existsSync(cached) ? { executablePath: requested || cached } : {}) };
|
||||
}
|
||||
function html(value) { return String(value).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); }
|
||||
function fixture(record) {
|
||||
const title = html(record.bankName);
|
||||
const inputs = record.options.map(option => `<label class="yxtulcdsdk-ques-radio"><span class="yxtf-radio__input"><input type="radio" name="probe-answer" value="${html(option.id)}"></span><span class="yxtf-radio__label"><span data-rich-text="1">${html(option.text)}</span></span></label>`).join('');
|
||||
return `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><title>${title}</title><style>
|
||||
body{font:16px/1.7 system-ui;margin:28px;background:#f2f5f8;color:#304455}.task-content-active{padding:10px;border:1px solid #cbd9e2;background:white}.yxtulcdsdk-exam-container{padding:24px;background:white;margin-top:20px;max-width:800px}.yxtulcdsdk-ques-radio{display:block;padding:10px;border:1px solid #dce4ec;margin:12px 0}#probe-clock{font-variant-numeric:tabular-nums}#probe-cards{display:flex;gap:5px}#probe-cards i{font-style:normal;padding:3px 8px;background:#edf1f5}.current{color:#008d78}
|
||||
</style></head><body><div class="task-content-active" data-task-id="perf-task"><span class="group-title">${title}</span></div><header class="o2oPlayFrame-right"><div class="title"><span class="yxtf-tooltip">${title}</span></div></header>
|
||||
<p>本地性能回归:1884 道题库记录,面板保持隐藏。</p><p>页面计时器:<span id="probe-clock">0</span></p><div id="probe-cards">${Array.from({ length: 20 }, (_, i) => `<i>${i + 1}</i>`).join('')}</div>
|
||||
<div class="yxtulcdsdk-exam-container" task-id="perf-task"><div class="yxtulcdsdk-exam-header"><span class="yxtf-tooltip">${title}</span></div><div class="yxtulcdsdk-pc-marking-main"><div class="standard-size-14"><span>判断题</span></div><div class="yxtulcdsdk-form"><div class="yxtulcdsdk-form__sub">${(record.questionIndex || 0) + 1}.</div><div class="yxtulcdsdk-form__content"><div data-rich-text="1">${html(record.stem)}</div><div class="yxtulcdsdk-ques-radio-group">${inputs}</div></div></div></div></div></body></html>`;
|
||||
}
|
||||
|
||||
function installProbe(seed) {
|
||||
const metrics = { calls: {}, times: {}, recordsWrites: 0, storageWrites: 0, storageReads: 0, questionScans: 0, hiddenResultRebuilds: 0, hiddenBankRebuilds: 0, hiddenTextWrites: 0 };
|
||||
const values = new Map([['huayi-qb.records.v1', seed], ['huayi-qb.config.v1', { mode: 'hidden', keepLauncher: false, collecting: true, copyEnabled: true, tab: 'search' }]]);
|
||||
window.__perf = { metrics, started: performance.now(), rootReadyAt: null, errors: [] };
|
||||
window.unsafeWindow = window;
|
||||
window.GM_getValue = (key, fallback) => { metrics.storageReads++; return structuredClone(values.has(key) ? values.get(key) : fallback); };
|
||||
window.GM_setValue = (key, value) => { metrics.storageWrites++; if (key === 'huayi-qb.records.v1') metrics.recordsWrites++; values.set(key, structuredClone(value)); };
|
||||
window.GM_registerMenuCommand = () => 1;
|
||||
window.GM_addValueChangeListener = () => 1;
|
||||
window.GM_removeValueChangeListener = () => {};
|
||||
const questionQuery = Document.prototype.querySelectorAll;
|
||||
Document.prototype.querySelectorAll = function (selector) {
|
||||
if (String(selector).includes('.yxtulcdsdk-pc-marking-main') && String(selector).includes('.yxtulcdsdk-form')) metrics.questionScans++;
|
||||
return questionQuery.call(this, selector);
|
||||
};
|
||||
const replace = Element.prototype.replaceChildren;
|
||||
Element.prototype.replaceChildren = function (...items) {
|
||||
const root = this.getRootNode();
|
||||
if (root.host?.id === 'huayi-qb-root' && root.querySelector('#qb-panel')?.hidden) {
|
||||
if (this.id === 'qb-results') metrics.hiddenResultRebuilds++;
|
||||
if (this.id === 'qb-bank-list') metrics.hiddenBankRebuilds++;
|
||||
}
|
||||
return replace.apply(this, items);
|
||||
};
|
||||
const content = Object.getOwnPropertyDescriptor(Node.prototype, 'textContent');
|
||||
Object.defineProperty(Node.prototype, 'textContent', { ...content, set(value) {
|
||||
const root = this.getRootNode();
|
||||
if (root.host?.id === 'huayi-qb-root' && root.querySelector('#qb-panel')?.hidden) metrics.hiddenTextWrites++;
|
||||
content.set.call(this, value);
|
||||
} });
|
||||
const appearance = new MutationObserver(() => {
|
||||
if (window.__perf.rootReadyAt == null && document.getElementById('huayi-qb-root')) window.__perf.rootReadyAt = performance.now();
|
||||
});
|
||||
appearance.observe(document, { childList: true, subtree: true });
|
||||
window.__perf.cloneMetrics = () => JSON.parse(JSON.stringify(metrics));
|
||||
window.__perf.recordCount = () => values.get('huayi-qb.records.v1').length;
|
||||
}
|
||||
|
||||
const CORE_PROBE = `
|
||||
for (const name of Object.keys(QBCore)) {
|
||||
if (typeof QBCore[name] !== 'function') continue;
|
||||
const original = QBCore[name];
|
||||
QBCore[name] = function (...args) {
|
||||
const start = performance.now(), m = window.__perf.metrics;
|
||||
m.calls[name] = (m.calls[name] || 0) + 1;
|
||||
try { return original.apply(this, args); }
|
||||
finally { m.times[name] = (m.times[name] || 0) + performance.now() - start; }
|
||||
};
|
||||
}
|
||||
`;
|
||||
function sourceFor(committed) {
|
||||
return `(function(){'use strict';\n${SOURCES.map((file, index) => {
|
||||
const code = committed ? execFileSync('git', ['show', `${BASELINE_REF}:${file}`], { cwd: ROOT, encoding: 'utf8', maxBuffer: 8 * 1024 * 1024 }) : fs.readFileSync(path.join(ROOT, file), 'utf8');
|
||||
return code + (index === 0 ? '\n' + CORE_PROBE : '');
|
||||
}).join('\n')}\n})();`;
|
||||
}
|
||||
function delta(end, start) {
|
||||
const result = {};
|
||||
for (const key of Object.keys(end)) {
|
||||
if (end[key] && typeof end[key] === 'object') {
|
||||
result[key] = {};
|
||||
for (const name of new Set([...Object.keys(end[key]), ...Object.keys(start[key] || {})])) {
|
||||
const value = (end[key][name] || 0) - (start[key]?.[name] || 0);
|
||||
if (value) result[key][name] = key === 'times' ? +value.toFixed(2) : value;
|
||||
}
|
||||
} else result[key] = end[key] - (start[key] || 0);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
test('1884题真实浏览器性能:启动、无关DOM变化和一次语义换题', { timeout: 180000, skip: !fs.existsSync(DATASET) }, async t => {
|
||||
const payload = JSON.parse(fs.readFileSync(DATASET, 'utf8').replace(/^\uFEFF/, ''));
|
||||
const records = Array.isArray(payload) ? payload : payload.records;
|
||||
assert.equal(records.length, 1884, '性能样本应为用户当前合并的1884条题库');
|
||||
const initial = records.find(record => record.type === 'judge' && !record.stem.includes('[图片:'));
|
||||
assert.ok(initial);
|
||||
fs.mkdirSync(OUTPUT, { recursive: true });
|
||||
const server = http.createServer((request, response) => {
|
||||
if (request.url === '/favicon.ico') { response.writeHead(204); response.end(); return; }
|
||||
response.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); response.end(fixture(initial));
|
||||
});
|
||||
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
|
||||
const origin = `http://127.0.0.1:${server.address().port}`;
|
||||
const { chromium } = loadPlaywright();
|
||||
const browser = await chromium.launch(browserOptions());
|
||||
const mode = process.env.QB_PERF_MODE || 'compare';
|
||||
const results = {};
|
||||
try {
|
||||
for (const [name, committed] of [['before', true], ['after', false]]) {
|
||||
if ((mode === 'baseline' && !committed) || (mode === 'after' && committed)) continue;
|
||||
const context = await browser.newContext({ viewport: { width: 1280, height: 900 } });
|
||||
await context.route('**/*', route => route.request().url().startsWith(origin) ? route.continue() : route.fulfill({ status: 204, body: '' }));
|
||||
await context.addInitScript({ content: `(${installProbe.toString()})(${JSON.stringify(records)});\n${sourceFor(committed)}` });
|
||||
const page = await context.newPage();
|
||||
const errors = [];
|
||||
page.on('pageerror', error => errors.push(error.message));
|
||||
try {
|
||||
await page.goto(`${origin}/o2o/#/playinfo?taskId=perf-task`, { waitUntil: 'domcontentloaded', timeout: 60000 });
|
||||
await page.locator('#huayi-qb-root').waitFor({ state: 'attached' });
|
||||
await page.waitForTimeout(1000);
|
||||
const startup = await page.evaluate(() => ({ ...window.__perf.cloneMetrics(), readyMs: +(window.__perf.rootReadyAt - window.__perf.started).toFixed(2), storedCount: window.__perf.recordCount() }));
|
||||
assert.equal(await page.locator('#qb-panel').isVisible(), false);
|
||||
const beforeNoise = await page.evaluate(() => window.__perf.cloneMetrics());
|
||||
await page.evaluate(() => {
|
||||
let tick = 0;
|
||||
window.__noiseTimer = setInterval(() => {
|
||||
document.getElementById('probe-clock').textContent = String(++tick);
|
||||
document.querySelectorAll('#probe-cards i').forEach((card, index) => card.classList.toggle('current', index === tick % 20));
|
||||
}, 50);
|
||||
});
|
||||
await page.waitForTimeout(1800);
|
||||
await page.evaluate(() => clearInterval(window.__noiseTimer));
|
||||
await page.waitForTimeout(500);
|
||||
const afterNoise = await page.evaluate(() => window.__perf.cloneMetrics());
|
||||
const noise = delta(afterNoise, beforeNoise);
|
||||
const beforeChange = await page.evaluate(() => window.__perf.cloneMetrics());
|
||||
await page.evaluate(() => {
|
||||
document.querySelector('.yxtulcdsdk-form__sub').textContent = '2.';
|
||||
document.querySelector('.yxtulcdsdk-form__content > [data-rich-text]').textContent = '本地性能探针:新增一题用于检测合批持久化。';
|
||||
});
|
||||
await page.waitForTimeout(1700);
|
||||
const afterChange = await page.evaluate(() => window.__perf.cloneMetrics());
|
||||
const change = delta(afterChange, beforeChange);
|
||||
results[name] = { startup, unrelatedChanges: noise, semanticChange: change, finalStoredCount: await page.evaluate(() => window.__perf.recordCount()), errors };
|
||||
await page.screenshot({ path: path.join(OUTPUT, `${name}-hidden.png`) });
|
||||
assert.deepEqual(errors, [], '本地性能夹具不能出现脚本异常');
|
||||
if (!committed) {
|
||||
assert.equal(noise.calls.upsert || 0, 0, '无关计时器/答题卡class变化不能合并全题库');
|
||||
assert.equal(noise.calls.compactRecords || 0, 0, '无关变化不能重新整理全题库');
|
||||
assert.equal(noise.recordsWrites, 0, '无关变化不能写回题库');
|
||||
assert.equal(noise.hiddenResultRebuilds, 0, '隐藏时不能重建检索结果');
|
||||
assert.equal(change.hiddenResultRebuilds, 0, '语义换题也不应重建隐藏检索结果');
|
||||
assert.equal(change.recordsWrites, 1, '一次新题语义变化应合批为一次题库写入');
|
||||
assert.ok((change.calls.upsert || 0) <= 1, '一次变化不得在保存时再次全量upsert');
|
||||
}
|
||||
} finally { await context.close(); }
|
||||
}
|
||||
} finally {
|
||||
await browser.close();
|
||||
await new Promise(resolve => server.close(resolve));
|
||||
fs.writeFileSync(path.join(OUTPUT, `performance-${mode}.json`), JSON.stringify(results, null, 2));
|
||||
}
|
||||
t.diagnostic(`本地真实1884题;Browser plugin not available,使用已有Playwright/Chromium;结果 ${path.join(OUTPUT, `performance-${mode}.json`)}`);
|
||||
t.diagnostic(JSON.stringify(results));
|
||||
});
|
||||
Reference in New Issue
Block a user