mirror of
https://github.com/electron/electron.git
synced 2026-10-28 11:44:46 +08:00
* perf: make fs operations on paths inside an asar archive cheaper than real files
Loading modules from app.asar was measurably slower than loading the same
files from disk because every stat/realpath/readdir on an archive path paid
for a native path split that built a V8 dictionary, a fake Stats array built
through the slow iterable path, and a real realpath() of the archive file.
- splitPath: find the archive prefix in JS and memoise the native verdict per
prefix, with a last-prefix fast path; skip path.normalize() for already
normalized absolute POSIX paths.
- internalModuleStat: consult the module stat cache before splitting the path.
- Fill a shared scratch Float64Array/BigInt64Array for Stats instead of
allocating one from a JS array per call.
- Memoise the archive file's own realpath per archive; honour the `encoding`
option for asar realpath results (previously threw for 'buffer').
- readdir(withFileTypes)/opendir: one native readdirWithTypes() call instead
of a stat per child.
- Native: allocation-free header walk over string_view, a per-archive
path->node memo, zero-copy ASCII path arguments, and DictionaryTemplate
results for stat()/getFileInfo().
- Wrap child_process eagerly instead of through a Module._load hook (init.ts
already requires it; the hook taxed every require() and missed ESM
imports of node:child_process).
- Read ELECTRON_LOG_ASAR_READS once.
* perf: locate the archive component of a path natively in one call
Replace the JS-side prefix memo and normalization regexes in the asar fs
wrapper with a lean native asar.splitPath(): it reads the path without
copying when it is ASCII, walks the components once, memoises "is this
*.asar prefix an archive file" by string, and reports when the path has
"."/".."/empty components so the caller can path.normalize() and ask again
(user-supplied paths only; the module loader's are already normalized).
The JS splitPath() is reduced to the type coercion, the /\.asar/i pre-check
that keeps unrelated paths out of native code, the call, and two slices.
Spec coverage for the binding is updated to the new integer contract and a
wrapper-level test for un-normalized archive paths is added.
* test: cover native asar path splitting and archive lookups; fix edge cases
Add an "internals" suite to asar-spec that builds its own archives (nested
directories, link chains and cycles, non-ASCII names, an upper-case .ASAR,
an archive inside a real directory named *.asar, a bogus *.asar file) and
exercises the electron_common_asar binding directly: splitPath() prefix
detection and its normalization contract, Archive.stat/getFileInfo/readdir/
readdirWithTypes/realpath, the lookup memo past its size limit, concurrent
use from worker threads, and the fs wrapper on Buffer/URL/relative/
un-normalized paths.
Fixes found while writing them and by review:
- splitPath() returned a UTF-8 byte offset; report UTF-16 code units so an
archive under a non-ASCII directory splits correctly.
- Decide "needs normalization" lexically before probing the disk, and ignore
any number of trailing separators.
- internalModuleStat goes through the normalizing splitPath() again so
un-normalized NODE_PATH / require.resolve({ paths }) entries keep working.
- Keep separate archive realpath memos for realpath() and realpath.native().
- Skip empty components in the archive header walk.
- Read ELECTRON_LOG_ASAR_READS per call again so it can be set from main.js.
* fix: keep un-openable and not-yet-existing *.asar paths behaving as before
Follow-ups from review of the asar fast path:
- Native splitPath() now rejects paths containing control characters, as the
base::FilePath converter used by Archive::New() does, so an .asar file under
such a directory stays an ordinary file instead of an "Invalid package".
- IsDirectoryCached()/IsArchivePrefix() only memoise answers for paths that
exist. A *.asar path probed before anything is created there (the win32
mkdir wrapper does this) is re-evaluated later instead of being pinned as
an archive for the life of the process. This predates the branch for
IsDirectoryCached().
- getFileInfo() fills the integrity slot with undefined when absent so the
DictionaryTemplate result keeps using its cached map.
- Spec: create the *.asar-named fixture directory through original-fs, derive
the realpath.native expectation with the native flavour (8.3 temp paths on
Windows runners), and cover the cases above.
* perf: read ELECTRON_LOG_ASAR_READS once per process
process.env goes through Node's env interceptor (getenv under a lock) and the
flag was checked on every read of a packed file. Sample it at startup instead
and document that it must be set in the launching environment.
170 lines
5.1 KiB
JavaScript
170 lines
5.1 KiB
JavaScript
const minimist = require('minimist');
|
|
|
|
const cp = require('node:child_process');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
|
|
const utils = require('./lib/utils');
|
|
const DISABLED_TESTS = require('./node-disabled-tests.json');
|
|
const FLAKY_TESTS = require('./node-flaky-tests.json');
|
|
|
|
const args = minimist(process.argv.slice(2), {
|
|
boolean: ['default', 'validateDisabled'],
|
|
string: ['jUnitDir']
|
|
});
|
|
|
|
const BASE = path.resolve(__dirname, '../..');
|
|
|
|
const ROOT_PACKAGE_JSON = path.resolve(BASE, 'package.json');
|
|
const OUT_PACKAGE_JSON = path.resolve(path.dirname(utils.getAbsoluteElectronExec()), 'package.json');
|
|
const NODE_DIR = path.resolve(BASE, 'third_party', 'electron_node');
|
|
const ROOT_STATUS = path.resolve(NODE_DIR, 'test', 'root.status');
|
|
const JUNIT_DIR = args.jUnitDir ? path.resolve(args.jUnitDir) : null;
|
|
const TAP_FILE_NAME = 'test.tap';
|
|
|
|
if (!require.main) {
|
|
throw new Error('Must call the node spec runner directly');
|
|
}
|
|
|
|
const defaultOptions = [
|
|
'tools/test.py',
|
|
'-p',
|
|
'tap',
|
|
'--logfile',
|
|
TAP_FILE_NAME,
|
|
'--mode=debug',
|
|
'default',
|
|
`--skip-tests=${DISABLED_TESTS.join(',')}`,
|
|
'--flaky-tests=dontcare',
|
|
'--measure-flakiness=9',
|
|
'--shell',
|
|
utils.getAbsoluteElectronExec(),
|
|
'-J'
|
|
];
|
|
|
|
// Files outside Electron that are rewritten for the duration of a run and put
|
|
// back afterwards. Originals are kept in sibling backup files so an interrupted
|
|
// run self-heals next time.
|
|
//
|
|
// The upstream suite assumes there is no package.json above its test files or
|
|
// rooted at process.execPath; Chromium's src/ root and the output directory
|
|
// both have a "type": "module" one, which changes module-type detection for
|
|
// test files and fixtures and emits MODULE_TYPELESS_PACKAGE_JSON warnings that
|
|
// break tests asserting clean stderr. They are removed while the suite runs.
|
|
//
|
|
// Tests that flake under Electron but should keep running are appended to the
|
|
// suite's root.status as PASS,FLAKY, which --flaky-tests=dontcare then reports
|
|
// without failing the run.
|
|
const REWRITES = [
|
|
{ file: ROOT_PACKAGE_JSON, rewrite: () => null },
|
|
{ file: OUT_PACKAGE_JSON, rewrite: () => null },
|
|
{
|
|
file: ROOT_STATUS,
|
|
rewrite: (original) => `${original}\n[true]\n${FLAKY_TESTS.map((test) => `${test}: PASS,FLAKY`).join('\n')}\n`
|
|
}
|
|
];
|
|
|
|
const backupPath = (file) => `${file}.spec-runner-backup`;
|
|
|
|
const applyRewrites = () => {
|
|
for (const { file, rewrite } of REWRITES) {
|
|
// The package.json files won't always exist in CI.
|
|
if (!fs.existsSync(file)) {
|
|
continue;
|
|
}
|
|
fs.copyFileSync(file, backupPath(file));
|
|
const rewritten = rewrite(fs.readFileSync(file, 'utf8'));
|
|
if (rewritten === null) {
|
|
fs.rmSync(file);
|
|
} else {
|
|
fs.writeFileSync(file, rewritten);
|
|
}
|
|
}
|
|
};
|
|
|
|
const restoreRewrites = () => {
|
|
for (const { file } of REWRITES) {
|
|
if (!fs.existsSync(backupPath(file))) {
|
|
continue;
|
|
}
|
|
fs.copyFileSync(backupPath(file), file);
|
|
fs.rmSync(backupPath(file));
|
|
}
|
|
};
|
|
|
|
const getCustomOptions = () => {
|
|
let customOptions = ['tools/test.py'];
|
|
|
|
// Add all custom arguments.
|
|
const extra = process.argv.slice(2);
|
|
if (extra) {
|
|
customOptions = customOptions.concat(extra);
|
|
}
|
|
|
|
// Necessary or Node.js will try to run from out/Release/node.
|
|
customOptions = customOptions.concat(['--shell', utils.getAbsoluteElectronExec()]);
|
|
|
|
return customOptions;
|
|
};
|
|
|
|
async function main() {
|
|
// Optionally validate that all disabled specs still exist.
|
|
if (args.validateDisabled) {
|
|
const missing = [];
|
|
for (const test of [...DISABLED_TESTS, ...FLAKY_TESTS]) {
|
|
const js = path.join(NODE_DIR, 'test', `${test}.js`);
|
|
const mjs = path.join(NODE_DIR, 'test', `${test}.mjs`);
|
|
if (!fs.existsSync(js) && !fs.existsSync(mjs)) {
|
|
missing.push(test);
|
|
}
|
|
}
|
|
|
|
if (missing.length > 0) {
|
|
console.error(`Found ${missing.length} missing disabled/flaky specs: \n${missing.join('\n')}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(`All ${DISABLED_TESTS.length} disabled and ${FLAKY_TESTS.length} flaky specs exist.`);
|
|
process.exit(0);
|
|
}
|
|
|
|
const options = args.default ? defaultOptions : getCustomOptions();
|
|
|
|
// Undo a previous interrupted run first, then rewrite for this one and make
|
|
// sure everything is put back even if we exit abnormally.
|
|
restoreRewrites();
|
|
applyRewrites();
|
|
process.on('exit', restoreRewrites);
|
|
process.on('SIGINT', () => process.exit(130));
|
|
process.on('SIGTERM', () => process.exit(143));
|
|
|
|
const testChild = cp.spawn('python3', options, {
|
|
env: {
|
|
...process.env,
|
|
ELECTRON_RUN_AS_NODE: 'true'
|
|
},
|
|
cwd: NODE_DIR,
|
|
stdio: 'inherit'
|
|
});
|
|
|
|
testChild.on('exit', (testCode) => {
|
|
restoreRewrites();
|
|
|
|
if (JUNIT_DIR) {
|
|
fs.mkdirSync(JUNIT_DIR);
|
|
const converterStream = require('tap-xunit')();
|
|
fs.createReadStream(path.resolve(NODE_DIR, TAP_FILE_NAME))
|
|
.pipe(converterStream)
|
|
.pipe(fs.createWriteStream(path.resolve(JUNIT_DIR, 'nodejs.xml')))
|
|
.on('close', () => {
|
|
process.exit(testCode);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error('An unhandled error occurred in the node spec runner', err);
|
|
process.exit(1);
|
|
});
|