|
| 1 | +/******************************************************************************** |
| 2 | + * Copyright (C) 2021 STMicroelectronics and others. |
| 3 | + * |
| 4 | + * This program and the accompanying materials are made available under the |
| 5 | + * terms of the Eclipse Public License v. 2.0 which is available at |
| 6 | + * http://www.eclipse.org/legal/epl-2.0. |
| 7 | + * |
| 8 | + * This Source Code may also be made available under the following Secondary |
| 9 | + * Licenses when the conditions for such availability set forth in the Eclipse |
| 10 | + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 |
| 11 | + * with the GNU Classpath Exception which is available at |
| 12 | + * https://www.gnu.org/software/classpath/license.html. |
| 13 | + * |
| 14 | + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 |
| 15 | + ********************************************************************************/ |
| 16 | +// @ts-check |
| 17 | +const fsx = require('fs-extra'); |
| 18 | +const { resolve } = require('path'); |
| 19 | +const { spawn, ChildProcess } = require('child_process'); |
| 20 | +const { delay, lcp, isLCP, measure } = require('./common-performance'); |
| 21 | +const traceConfigTemplate = require('./electron-trace-config.json'); |
| 22 | +const { exit } = require('process'); |
| 23 | + |
| 24 | +const basePath = resolve(__dirname, '../..'); |
| 25 | +const profilesPath = resolve(__dirname, './profiles/'); |
| 26 | +const electronExample = resolve(basePath, 'examples/electron'); |
| 27 | +const theia = resolve(electronExample, 'node_modules/.bin/theia'); |
| 28 | + |
| 29 | +let name = 'Electron Frontend Startup'; |
| 30 | +let folder = 'electron'; |
| 31 | +let runs = 10; |
| 32 | +let workspace = resolve('./workspace'); |
| 33 | +let debugging = false; |
| 34 | + |
| 35 | +(async () => { |
| 36 | + let defaultWorkspace = true; |
| 37 | + |
| 38 | + const yargs = require('yargs'); |
| 39 | + const args = yargs(process.argv.slice(2)).option('name', { |
| 40 | + alias: 'n', |
| 41 | + desc: 'A name for the test suite', |
| 42 | + type: 'string', |
| 43 | + default: name |
| 44 | + }).option('folder', { |
| 45 | + alias: 'f', |
| 46 | + desc: 'Name of a folder within the "profiles" folder in which to collect trace logs', |
| 47 | + type: 'string', |
| 48 | + default: folder |
| 49 | + }).option('runs', { |
| 50 | + alias: 'r', |
| 51 | + desc: 'The number of times to run the test', |
| 52 | + type: 'number', |
| 53 | + default: runs |
| 54 | + }).option('workspace', { |
| 55 | + alias: 'w', |
| 56 | + desc: 'Path to a Theia workspace to open', |
| 57 | + type: 'string', |
| 58 | + default: workspace |
| 59 | + }).option('debug', { |
| 60 | + alias: 'X', |
| 61 | + desc: 'Whether to log debug information', |
| 62 | + boolean: true |
| 63 | + }).wrap(Math.min(120, yargs.terminalWidth())).argv; |
| 64 | + |
| 65 | + if (args.name) { |
| 66 | + name = args.name.toString(); |
| 67 | + } |
| 68 | + if (args.folder) { |
| 69 | + folder = args.folder.toString(); |
| 70 | + } |
| 71 | + if (args.workspace) { |
| 72 | + workspace = args.workspace.toString(); |
| 73 | + if (resolve(workspace) !== workspace) { |
| 74 | + console.log('Workspace path must be an absolute path:', workspace); |
| 75 | + exit(1); |
| 76 | + } |
| 77 | + defaultWorkspace = false; |
| 78 | + } |
| 79 | + if (args.runs) { |
| 80 | + runs = parseInt(args.runs.toString()); |
| 81 | + } |
| 82 | + debugging = args.debug; |
| 83 | + |
| 84 | + // Verify that the application exists |
| 85 | + const indexHTML = resolve(electronExample, 'src-gen/frontend/index.html'); |
| 86 | + if (!fsx.existsSync(indexHTML)) { |
| 87 | + console.error('Electron example app does not exist. Please build it before running this script.'); |
| 88 | + process.exit(1); |
| 89 | + } |
| 90 | + |
| 91 | + if (defaultWorkspace) { |
| 92 | + // Ensure that it exists |
| 93 | + fsx.ensureDirSync(workspace); |
| 94 | + } |
| 95 | + |
| 96 | + await measurePerformance(); |
| 97 | +})(); |
| 98 | + |
| 99 | +async function measurePerformance() { |
| 100 | + fsx.emptyDirSync(resolve(profilesPath, folder)); |
| 101 | + const traceConfigPath = resolve(profilesPath, folder, 'trace-config.json'); |
| 102 | + |
| 103 | + /** |
| 104 | + * Generate trace config from the template. |
| 105 | + * @param {number} runNr |
| 106 | + * @returns {string} the output trace file path |
| 107 | + */ |
| 108 | + const traceConfigGenerator = (runNr) => { |
| 109 | + const traceConfig = { ...traceConfigTemplate }; |
| 110 | + const traceFilePath = resolve(profilesPath, folder, `${runNr}.json`); |
| 111 | + traceConfig.result_file = traceFilePath |
| 112 | + fsx.writeFileSync(traceConfigPath, JSON.stringify(traceConfig, undefined, 2), 'utf-8'); |
| 113 | + return traceFilePath; |
| 114 | + }; |
| 115 | + |
| 116 | + const exitHandler = (andExit = false) => { |
| 117 | + return () => { |
| 118 | + if (electron && !electron.killed) { |
| 119 | + process.kill(-electron.pid, 'SIGINT'); |
| 120 | + } |
| 121 | + if (andExit) { |
| 122 | + process.exit(); |
| 123 | + } |
| 124 | + } |
| 125 | + }; |
| 126 | + |
| 127 | + // Be sure not to leave a detached Electron child process |
| 128 | + process.on('exit', exitHandler()); |
| 129 | + process.on('SIGINT', exitHandler(true)); |
| 130 | + process.on('SIGTERM', exitHandler(true)); |
| 131 | + |
| 132 | + let electron; |
| 133 | + |
| 134 | + /** @type import('./common-performance').TestFunction */ |
| 135 | + const testScenario = async (runNr) => { |
| 136 | + const traceFile = traceConfigGenerator(runNr); |
| 137 | + electron = await launchElectron(traceConfigPath); |
| 138 | + |
| 139 | + electron.stderr.on('data', data => analyzeStderr(data.toString())); |
| 140 | + |
| 141 | + // Wait long enough to be sure that tracing has finished. Kill the process group |
| 142 | + // because the 'theia' child process was detached |
| 143 | + await delay(traceConfigTemplate.startup_duration * 1_000 * 3 / 2) |
| 144 | + .then(() => electron.exitCode !== null || process.kill(-electron.pid, 'SIGINT')); |
| 145 | + electron = undefined; |
| 146 | + return traceFile; |
| 147 | + }; |
| 148 | + |
| 149 | + measure(name, lcp, runs, testScenario, hasNonzeroTimestamp, isLCP); |
| 150 | +} |
| 151 | + |
| 152 | +/** |
| 153 | + * Launch the Electron app as a detached child process with tracing configured to start |
| 154 | + * immediately upon launch. The child process is detached because otherwise the attempt |
| 155 | + * to signal it to terminate when the test run is complete will not terminate the entire |
| 156 | + * process tree but only the root `theia` process, leaving the electron app instance |
| 157 | + * running until eventually this script itself exits. |
| 158 | + * |
| 159 | + * @param {string} traceConfigPath the path to the tracing configuration file with which to initiate tracing |
| 160 | + * @returns {Promise<ChildProcess>} the Electron child process, if successfully launched |
| 161 | + */ |
| 162 | +async function launchElectron(traceConfigPath) { |
| 163 | + const args = ['start', workspace, '--plugins=local-dir:../../plugins', `--trace-config-file=${traceConfigPath}`, `--fastStartup`]; |
| 164 | + if (process.platform === 'linux') { |
| 165 | + args.push('--headless'); |
| 166 | + } |
| 167 | + return spawn(theia, args, { cwd: electronExample, detached: true }); |
| 168 | +} |
| 169 | + |
| 170 | +function hasNonzeroTimestamp(traceEvent) { |
| 171 | + return traceEvent.hasOwnProperty('ts') // The traces don't have explicit nulls or undefineds |
| 172 | + && traceEvent.ts > 0; |
| 173 | +} |
| 174 | + |
| 175 | +/** |
| 176 | + * Analyze a `chunk` of text on the standard error stream of the child process. |
| 177 | + * If running in debug mode, this will always at least print out the `chunk` to the console. |
| 178 | + * In addition, the text is analyzed to look for known conditions that will invalidate the |
| 179 | + * test procedure and cause the script to bail. These include: |
| 180 | + * |
| 181 | + * - the native browser modules not being built correctly for Electron |
| 182 | + * |
| 183 | + * @param {string} chunk a chunk of standard error text from the child process |
| 184 | + */ |
| 185 | +function analyzeStderr(chunk) { |
| 186 | + if (debugging) { |
| 187 | + console.error('>', chunk.trimEnd()); |
| 188 | + } |
| 189 | + |
| 190 | + if (chunk.includes('Error: Module did not self-register')) { |
| 191 | + console.error('Native browser modules are not built properly. Please rebuild the workspace and try again.'); |
| 192 | + exit(1); |
| 193 | + } |
| 194 | +} |
0 commit comments