Skip to content

Commit 745a43e

Browse files
committed
repl: lazy-load acorn and defer vm context creation
Merely loading the repl builtin used to eagerly require acorn and acorn-walk (~250KB of JS) and create an entire V8 context via vm.runInNewContext() just to enumerate global property names, even though both are only needed once REPL input is actually parsed or tab-completion is used. Require acorn and acorn-walk at their function-level use sites instead, and wrap the global builtins set in getLazy(). The cost moves to the first preview/completion/recoverable-error check, where the one-time ~1ms is imperceptible. Benchmark results (Linux x64, misc/startup-core.js, 20 runs): require-builtins.js: +3.12% ops/s (t=3.57, p<0.01) import-builtins.mjs: +2.20% ops/s (t=3.00, p<0.01) In isolation, require('repl') drops from 4.64ms to 2.84ms (-39%) and interactive `node -i` startup improves by ~13%. Signed-off-by: Daijiro Wachi <daijiro.wachi@gmail.com> PR-URL: #63879 Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
1 parent 1434a07 commit 745a43e

3 files changed

Lines changed: 22 additions & 22 deletions

File tree

lib/internal/repl/completion.js

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ const {
3333
const {
3434
kContextId,
3535
getREPLResourceName,
36-
globalBuiltins,
36+
getGlobalBuiltins,
3737
getReplBuiltinLibs,
3838
fixReplRequire,
3939
} = require('internal/repl/utils');
@@ -61,13 +61,6 @@ const {
6161
getOwnNonIndexProperties,
6262
} = internalBinding('util');
6363

64-
const {
65-
isIdentifierStart,
66-
isIdentifierChar,
67-
parse: acornParse,
68-
} = require('internal/deps/acorn/acorn/dist/acorn');
69-
const acornWalk = require('internal/deps/acorn/acorn-walk/dist/walk');
70-
7164
const importRE = /\bimport\s*\(\s*['"`](([\w@./:-]+\/)?(?:[\w@./:-]*))(?![^'"`])$/;
7265
const requireRE = /\brequire\s*\(\s*['"`](([\w@./:-]+\/)?(?:[\w@./:-]*))(?![^'"`])$/;
7366
const fsAutoCompleteRE = /fs(?:\.promises)?\.\s*[a-z][a-zA-Z]+\(\s*["'](.*)/;
@@ -87,6 +80,8 @@ function isIdentifier(str) {
8780
if (str === '') {
8881
return false;
8982
}
83+
const { isIdentifierStart, isIdentifierChar } =
84+
require('internal/deps/acorn/acorn/dist/acorn');
9085
const first = StringPrototypeCodePointAt(str, 0);
9186
if (!isIdentifierStart(first)) {
9287
return false;
@@ -374,8 +369,8 @@ function complete(line, callback) {
374369
if (!this.useGlobal) {
375370
// When the context is not `global`, builtins are not own
376371
// properties of it.
377-
// `globalBuiltins` is a `SafeSet`, not an Array-like.
378-
ArrayPrototypePush(contextOwnNames, ...globalBuiltins);
372+
// `getGlobalBuiltins()` is a `SafeSet`, not an Array-like.
373+
ArrayPrototypePush(contextOwnNames, ...getGlobalBuiltins());
379374
}
380375
ArrayPrototypePush(completionGroups, contextOwnNames);
381376
if (filter !== '') addCommonWords(completionGroups);
@@ -387,6 +382,7 @@ function complete(line, callback) {
387382
// so in order to make it correct we add an identifier to its end (e.g. `obj.foo.x`)
388383
const parsableCompleteTarget = completeTarget.endsWith('.') ? `${completeTarget}x` : completeTarget;
389384

385+
const { parse: acornParse } = require('internal/deps/acorn/acorn/dist/acorn');
390386
let completeTargetAst;
391387
try {
392388
completeTargetAst = acornParse(
@@ -551,6 +547,7 @@ function findExpressionCompleteTarget(code) {
551547
return !result ? result : `${result}.`;
552548
}
553549

550+
const { parse: acornParse } = require('internal/deps/acorn/acorn/dist/acorn');
554551
let ast;
555552
try {
556553
ast = acornParse(code, { __proto__: null, sourceType: 'module', ecmaVersion: 'latest' });
@@ -625,6 +622,7 @@ function findExpressionCompleteTarget(code) {
625622

626623
// Walk the AST for the current block of code, and check whether it contains any
627624
// statement or expression type that would potentially have side effects if evaluated.
625+
const acornWalk = require('internal/deps/acorn/acorn-walk/dist/walk');
628626
let isAllowed = true;
629627
const disallow = () => isAllowed = false;
630628
acornWalk.simple(lastBodyStatement, {

lib/internal/repl/utils.js

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,8 @@ const {
2020
Symbol,
2121
} = primordials;
2222

23-
const { tokTypes: tt, Parser: AcornParser } =
24-
require('internal/deps/acorn/acorn/dist/acorn');
25-
2623
const { sendInspectorCommand } = require('internal/util/inspector');
24+
const { getLazy } = require('internal/util');
2725

2826
const {
2927
ERR_INSPECTOR_NOT_AVAILABLE,
@@ -80,6 +78,9 @@ function isRecoverableError(e, code) {
8078
isRecoverableError(e, `(${code}`))
8179
return true;
8280

81+
const { tokTypes: tt, Parser: AcornParser } =
82+
require('internal/deps/acorn/acorn/dist/acorn');
83+
8384
let recoverable = false;
8485

8586
// Determine if the point of any error raised is at the end of the input.
@@ -756,6 +757,8 @@ function setupReverseSearch(repl) {
756757
const startsWithBraceRegExp = /^\s*{/;
757758
const endsWithSemicolonRegExp = /;\s*$/;
758759
function isValidSyntax(input) {
760+
const { Parser: AcornParser } =
761+
require('internal/deps/acorn/acorn/dist/acorn');
759762
try {
760763
AcornParser.parse(input, {
761764
ecmaVersion: 'latest',
@@ -815,8 +818,9 @@ function getREPLResourceName() {
815818
return `REPL${nextREPLResourceNumber++}`;
816819
}
817820

818-
const globalBuiltins =
819-
new SafeSet(vm.runInNewContext('Object.getOwnPropertyNames(globalThis)'));
821+
// Creating a new context is expensive, so only do it on first use.
822+
const getGlobalBuiltins = getLazy(() =>
823+
new SafeSet(vm.runInNewContext('Object.getOwnPropertyNames(globalThis)')));
820824

821825
let _builtinLibs = ArrayPrototypeFilter(
822826
CJSModule.builtinModules,
@@ -848,7 +852,7 @@ module.exports = {
848852
isValidSyntax,
849853
kContextId,
850854
getREPLResourceName,
851-
globalBuiltins,
855+
getGlobalBuiltins,
852856
getReplBuiltinLibs,
853857
setReplBuiltinLibs,
854858
fixReplRequire,

lib/repl.js

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,6 @@ const {
8989
makeRequireFunction,
9090
addBuiltinLibsToObject,
9191
} = require('internal/modules/helpers');
92-
const {
93-
parse: acornParse,
94-
} = require('internal/deps/acorn/acorn/dist/acorn');
95-
const acornWalk = require('internal/deps/acorn/acorn-walk/dist/walk');
9692
const {
9793
decorateErrorStack,
9894
isError,
@@ -153,7 +149,7 @@ const {
153149
isValidSyntax,
154150
kContextId,
155151
getREPLResourceName,
156-
globalBuiltins,
152+
getGlobalBuiltins,
157153
getReplBuiltinLibs,
158154
setReplBuiltinLibs,
159155
fixReplRequire,
@@ -249,6 +245,8 @@ writer.options = { ...inspect.defaultOptions, showProxy: true };
249245

250246
// Converts static import statement to dynamic import statement
251247
const toDynamicImport = (codeLine) => {
248+
const { parse: acornParse } = require('internal/deps/acorn/acorn/dist/acorn');
249+
const acornWalk = require('internal/deps/acorn/acorn-walk/dist/walk');
252250
let dynamicImportStatement = '';
253251
const ast = acornParse(codeLine, { __proto__: null, sourceType: 'module', ecmaVersion: 'latest' });
254252
acornWalk.ancestor(ast, {
@@ -1139,7 +1137,7 @@ class REPLServer extends Interface {
11391137
});
11401138
ArrayPrototypeForEach(ObjectGetOwnPropertyNames(globalThis), (name) => {
11411139
// Only set properties that do not already exist as a global builtin.
1142-
if (!globalBuiltins.has(name)) {
1140+
if (!getGlobalBuiltins().has(name)) {
11431141
ObjectDefineProperty(context, name,
11441142
{
11451143
__proto__: null,

0 commit comments

Comments
 (0)