// Real built Electron/preload/IPC. Measures the selected page through a painted frame, // not just the openSettings IPC return or the first loading-shell paint. const fs = require('node:fs/promises') const path = require('node:path') const assert = require('node:assert/strict') const { performance } = require('node:perf_hooks') if (!process.versions.electron) { ;(async () => { const directory = await fs.mkdtemp(path.resolve('.tmp/settings-performance-')) const data = path.join(directory, 'data'), home = path.join(directory, 'home') await fs.mkdir(data, { recursive: true }); await fs.mkdir(home, { recursive: true }) // Other tasks may build out/ concurrently. Verify one immutable build per run. const buildRoot = path.join(directory, 'build', 'out') await fs.cp(path.resolve('out'), buildRoot, { recursive: true }) await fs.cp(path.resolve('resources/model-metadata'), path.join(directory, 'build/resources/model-metadata'), { recursive: true }) const env = { ...process.env, YCODE_QA_DIRECTORY: directory, YCODE_USER_DATA_DIR: data, YCODE_OMP_HOME: home, YCODE_QA_BUILD_ROOT: buildRoot } delete env.ELECTRON_RUN_AS_NODE; delete env.ELECTRON_RENDERER_URL; delete env.YCODE_DISABLE_GPU if (process.env.YCODE_QA_RENDERER_URL) env.ELECTRON_RENDERER_URL = process.env.YCODE_QA_RENDERER_URL const child = require('node:child_process').spawn(require('electron'), ['--disable-gpu-sandbox', __filename], { env, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] }) let output = '' child.stdout.on('data', bytes => { output += bytes; if (bytes.toString().includes('SETTINGS_PERFORMANCE')) process.stdout.write(bytes) }) child.stderr.on('data', bytes => { output += bytes }) const timer = setTimeout(() => child.kill(), 150000) child.once('exit', async code => { clearTimeout(timer); await fs.writeFile(path.join(directory, 'process.log'), output) console.log(JSON.stringify({ directory, exitCode: code })); if (code) console.error(output.slice(-6000)) process.exitCode = code ? 1 : 0 }) })().catch(error => { console.error(error); process.exitCode = 1 }) } else { const { app, BrowserWindow, ipcMain } = require('electron') const { pathToFileURL } = require('node:url') const directory = process.env.YCODE_QA_DIRECTORY const buildRoot = process.env.YCODE_QA_BUILD_ROOT const sync = require('node:fs') sync.mkdirSync(process.env.YCODE_USER_DATA_DIR, { recursive: true }) app.setPath('userData', process.env.YCODE_USER_DATA_DIR); app.setPath('sessionData', process.env.YCODE_USER_DATA_DIR) app.setPath('home', path.join(directory, 'home')) const pause = ms => new Promise(resolve => setTimeout(resolve, ms)) const checks = [], samples = [], rendererErrors = [], documentLoads = [] let main, settings, phase = 'boot', sample const read = (source, win = settings) => win.webContents.executeJavaScript(source) async function wait(predicate, label) { for (let i = 0; i < 300; i++) { if (await predicate()) return; await pause(10) } throw Error('Timeout: ' + label) } function check(label, actual, expected = true) { checks.push({ label, actual, expected, pass: JSON.stringify(actual) === JSON.stringify(expected) }) } async function step(label) { phase = label; await fs.writeFile(path.join(directory, 'phase.txt'), label) } app.on('browser-window-created', (_event, win) => { if (!main) return win.webContents.on('did-start-loading', () => documentLoads.push({ windowId: win.id, at: performance.now() })) win.webContents.on('console-message', event => { if (event.level === 'error') rendererErrors.push(event.message) }) win.on('show', () => { if (sample && sample.showMs === undefined) sample.showMs = +(performance.now() - sample.started).toFixed(2) }) }) const targets = { settings: '.general-settings-panel:not(.personalization-panel):not(.extensions-settings-panel):not(.help-feedback-panel)', model: '.models-settings-panel', appearance: '.appearance-panel', keyboardShortcuts: '.keyboard-shortcuts-panel', personalization: '.personalization-panel', memory: '.memory-settings-panel', extensions: '.extensions-settings-panel', helpFeedback: '[data-about-help-panel="helpFeedback"]', getHelp: '[data-about-help-panel="getHelp"]' } async function open(section, action, label = section ?? 'default') { sample = { label, started: performance.now() } await read(`window.ycode.app.openSettings(${JSON.stringify(section)}, ${JSON.stringify(action)})`, main) await wait(() => { settings = BrowserWindow.getAllWindows().find(win => win !== main); return !!settings }, 'settings window') const expected = !section ? 'settings' : section === 'models' ? 'model' : section await wait(async () => !settings.webContents.isLoading() && settings.isVisible() && await read(`!!document.querySelector(${JSON.stringify(targets[expected])}) && !document.querySelector('.settings-panel-loading')`), 'selected panel: ' + expected) sample.contentMs = +(performance.now() - sample.started).toFixed(2) await read(`new Promise(resolve=>requestAnimationFrame(()=>requestAnimationFrame(()=>resolve(true))))`) sample.frameMs = +(performance.now() - sample.started).toFixed(2) const { started, ...result } = sample; samples.push(result); sample = undefined return result } async function close() { settings.close() await wait(() => !settings.isVisible(), 'settings hidden') await wait(async () => !await read(`document.querySelector('.settings-window-root')`), 'closed pages unmounted') } ;(async () => { await import(pathToFileURL(path.join(buildRoot, 'main/index.mjs')).href) await app.whenReady() await wait(() => { main = BrowserWindow.getAllWindows().find(win => win.getTitle() === 'YCode'); return main && !main.webContents.isLoading() }, 'main window') await wait(async () => await read(`!!document.querySelector('.user-menu-trigger--workbuddy')`, main), 'main app') await pause(300) await step('entry-budgets') const html = await fs.readFile(path.join(buildRoot, 'renderer/settings.html'), 'utf8') const assets = [...new Set([...html.matchAll(/(?:src|href)="\.\/([^"<>]+\.(?:js|css))"/g)].map(match => match[1]))] const bytes = { js: 0, css: 0 } for (const asset of assets) bytes[asset.endsWith('.js') ? 'js' : 'css'] += (await fs.stat(path.join(buildRoot, 'renderer', asset))).size check('settings static scripts stay below 1MB', bytes.js < 1000000) check('settings base and shared control styles stay below 300KB', bytes.css < 300000) check('dedicated HTML has an immediate loading shell', html.includes('aria-label="正在加载设置"')) check('main renderer cannot acknowledge settings readiness', await read(`window.ycode.app.settingsReady().then(()=>false,()=>true)`, main)) check('main renderer cannot acknowledge settings presentation', await read(`window.ycode.app.settingsRendered(1).then(()=>false,()=>true)`, main)) await step('default-general-first-open') await open(undefined, undefined, 'cold-default-general') const firstId = settings.id, firstPid = settings.webContents.getOSProcessId() check('default settings opens 通用', await read(`document.querySelector('[data-settings-tab="settings"]').getAttribute('aria-current')`), 'page') check('native settings uses its dedicated entry', new URL(settings.webContents.getURL()).pathname.endsWith('/settings.html')) check('sandbox policy still authorizes the dedicated settings page', await read(`window.ycode.app.sandboxPolicy().then(()=>true,()=>false)`)) await close() check('close hides and retains the native window', !settings.isDestroyed() && !settings.isVisible()) check('close disposes all page-local controls', await read(`document.querySelectorAll('input,textarea,[role="dialog"]').length`), 0) const loadsBefore = documentLoads.length await step('warm-reopen') for (let i = 0; i < 4; i++) { await open(undefined, undefined, 'warm-default-' + i); await close() } check('reopen keeps the same window and renderer process', [settings.id, settings.webContents.getOSProcessId()], [firstId, firstPid]) check('reopening never reloads the document', documentLoads.length, loadsBefore) await step('all-pages-and-default-entry-points') for (const section of Object.keys(targets)) await open(section, undefined, 'page-' + section) await close() await read(`document.querySelector('.user-menu-trigger--workbuddy').click()`, main) await wait(async () => await read(`!!document.querySelector('.ycode-account-menu')`, main), 'account menu') await read(`[...document.querySelectorAll('.ycode-account-menu .user-menu-item')].find(node=>node.querySelector('.user-menu-item-label')?.textContent==='设置').click()`, main) await wait(async () => settings.isVisible() && await read(`!!document.querySelector(${JSON.stringify(targets.settings)})`), 'account menu opens general') check('account menu 设置 opens 通用', await read(`document.querySelector('[data-settings-tab="settings"]').getAttribute('aria-current')`), 'page') await close() await read(`window.dispatchEvent(new KeyboardEvent('keydown',{key:',',ctrlKey:true,bubbles:true}))`, main) await wait(async () => settings.isVisible() && await read(`!!document.querySelector(${JSON.stringify(targets.settings)})`), 'Ctrl+, opens general') check('Ctrl+, opens 通用', await read(`document.querySelector('[data-settings-tab="settings"]').getAttribute('aria-current')`), 'page') await step('model-new-and-close-cleans-forms') await open('model', 'new', 'model-new') await wait(async () => await read(`!!document.querySelector('.models-settings-panel__editor-overlay')`), 'new model editor') check('explicit model/new still opens the model editor', await read(`document.querySelectorAll('.models-settings-panel__editor-overlay').length`), 1) await read(`(() => {const field=document.querySelector('.models-settings-panel__editor-overlay input[type="password"]');if(!field)return false;Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value').set.call(field,'settings-regression-unsaved-key');field.dispatchEvent(new Event('input',{bubbles:true}));return true})()`) await close() check('closing removes secret and unsaved editor controls', await read(`document.querySelectorAll('input,textarea').length`), 0) await open('model') check('reopen does not restore an unsaved model editor', await read(`!!document.querySelector('.models-settings-panel__editor-overlay')`), false) await step('refresh-after-external-save') await close() const model = { id: 'settings-perf', name: 'Settings refresh regression', providerId: 'custom', model: 'settings-perf', baseUrl: 'http://127.0.0.1:1/v1', authMode: 'none', supportsToolCall: false, supportsImages: false, supportsReasoning: false } await read(`window.ycode.models.save(${JSON.stringify(model)})`, main) await open('model') await wait(async () => await read(`document.querySelector('.models-settings-panel').textContent.includes('Settings refresh regression')`), 'fresh models on reopen') check('reopen refreshes models saved in another window', await read(`document.querySelector('.models-settings-panel').textContent.includes('Settings refresh regression')`)) await read(`window.ycode.models.save(${JSON.stringify({ ...model, name: 'Updated settings regression' })}, true)`, main) await wait(async () => await read(`document.querySelector('.models-settings-panel').textContent.includes('Updated settings regression')`), 'visible models refresh') check('visible settings follows external model changes', await read(`document.querySelector('.models-settings-panel').textContent.includes('Updated settings regression')`)) await step('fast-navigation-before-ready') settings.destroy() await read(`Promise.all([window.ycode.app.openSettings('model','new'),window.ycode.app.openSettings('appearance')])`, main) settings = BrowserWindow.getAllWindows().find(win => win !== main) await wait(async () => settings.isVisible() && !settings.webContents.isLoading() && await read(`!!document.querySelector('.appearance-panel')`), 'latest navigation after startup') check('latest startup request wins', await read(`!!document.querySelector('.appearance-panel')&&!document.querySelector('.models-settings-panel__editor-overlay')`)) check('concurrent startup creates one settings window', BrowserWindow.getAllWindows().filter(win => win !== main).length, 1) settings.minimize() await open('memory', undefined, 'restore-minimized') check('open restores a minimized settings window', !settings.isMinimized()) await step('close-during-load-does-not-reappear') settings.destroy() const opening = read(`window.ycode.app.openSettings('settings')`, main) await wait(() => { settings = BrowserWindow.getAllWindows().find(win => win !== main); return !!settings }, 'loading window') settings.close(); await opening await wait(() => !settings.webContents.isLoading(), 'closed startup load completes') await pause(150) check('late ready signals never reopen a closed window', !settings.isVisible()) await open(undefined, undefined, 'reopen-after-startup-close') check('closed-during-load window remains reusable', await read(`!!document.querySelector(${JSON.stringify(targets.settings)})`)) await close() await step('main-close-destroys-hidden-cache') app.on('before-quit', event => event.preventDefault()) main.close() await wait(() => main.isDestroyed() && settings.isDestroyed(), 'hidden settings disposed with main window') check('hidden settings does not keep app alive after main closes', settings.isDestroyed()) check('no renderer errors', rendererErrors, []) const report = { result: checks.every(item => item.pass) ? 'PASS' : 'FAIL', scope: 'Real isolated app, GPU enabled; no model/network calls. Frame timings include the selected panel and two animation frames, not just a loading shell.', rendererMode: process.env.ELECTRON_RENDERER_URL ? 'dev' : 'production', bytes, assets, samples, checks, rendererErrors } await fs.writeFile(path.join(directory, 'report.json'), JSON.stringify(report, null, 2)) console.log('SETTINGS_PERFORMANCE_' + report.result + ' ' + directory) for (const item of checks.filter(item => !item.pass)) console.log('SETTINGS_PERFORMANCE_FAILURE ' + JSON.stringify(item)) console.log('SETTINGS_PERFORMANCE_TIMINGS ' + JSON.stringify(samples)) app.exit(report.result === 'PASS' ? 0 : 1) })().catch(async error => { await fs.writeFile(path.join(directory, 'failure.json'), JSON.stringify({ phase, error: String(error), stack: error.stack, samples, checks, rendererErrors }, null, 2)) console.error(error); app.exit(1) }) }