'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, '"'); } function fixture(record) { const title = html(record.bankName); const inputs = record.options.map(option => ``).join(''); return `${title}
${title}
${title}

本地性能回归:1884 道题库记录,面板保持隐藏。

页面计时器:0

${Array.from({ length: 20 }, (_, i) => `${i + 1}`).join('')}
${title}
判断题
${(record.questionIndex || 0) + 1}.
${html(record.stem)}
${inputs}
`; } 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)); });