Skip to content

Commit 4052aec

Browse files
joyeecheungBridgeAR
authored andcommitted
console: lazy load process.stderr and process.stdout
This patch: - Refactors the Console constructor: moves the property binding code into and the writable streams binding code into two methods defined on the Console.prototype with symbols. - Refactors the global console creation: we only need to share the property binding code from the Console constructor. To bind the streams we can lazy load `process.stdio` and `process.stderr` so that we don't create these streams when they are not used. This significantly reduces the number of modules loaded during bootstrap. Also, by calling the refactored-out method directly we can skip the unnecessary typechecks when creating the global console and there is no need to create a temporary Console anymore. - Refactors the error handler creation and the `write` method: use a `kUseStdout` symbol to tell the internals which stream should be loaded from the console instance. Also put the `write` method on the Console prototype so it just loads other properties directly off the console instance which simplifies the call sites. Also leaves a few TODOs for further refactoring of the console bootstrap. PR-URL: #24534 Reviewed-By: Gus Caplan <me@gus.host>
1 parent 7f5bb9d commit 4052aec

File tree

3 files changed

+133
-84
lines changed

3 files changed

+133
-84
lines changed

lib/console.js

+122-75
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,15 @@ const kFormatForStdout = Symbol('kFormatForStdout');
6565
const kGetInspectOptions = Symbol('kGetInspectOptions');
6666
const kColorMode = Symbol('kColorMode');
6767
const kIsConsole = Symbol('kIsConsole');
68-
68+
const kWriteToConsole = Symbol('kWriteToConsole');
69+
const kBindProperties = Symbol('kBindProperties');
70+
const kBindStreamsEager = Symbol('kBindStreamsEager');
71+
const kBindStreamsLazy = Symbol('kBindStreamsLazy');
72+
const kUseStdout = Symbol('kUseStdout');
73+
const kUseStderr = Symbol('kUseStderr');
74+
75+
// This constructor is not used to construct the global console.
76+
// It's exported for backwards compatibility.
6977
function Console(options /* or: stdout, stderr, ignoreErrors = true */) {
7078
// We have to test new.target here to see if this function is called
7179
// with new, because we need to define a custom instanceof to accommodate
@@ -74,7 +82,6 @@ function Console(options /* or: stdout, stderr, ignoreErrors = true */) {
7482
return new Console(...arguments);
7583
}
7684

77-
this[kIsConsole] = true;
7885
if (!options || typeof options.write === 'function') {
7986
options = {
8087
stdout: options,
@@ -97,37 +104,9 @@ function Console(options /* or: stdout, stderr, ignoreErrors = true */) {
97104
throw new ERR_CONSOLE_WRITABLE_STREAM('stderr');
98105
}
99106

100-
const prop = {
101-
writable: true,
102-
enumerable: false,
103-
configurable: true
104-
};
105-
Object.defineProperty(this, '_stdout', { ...prop, value: stdout });
106-
Object.defineProperty(this, '_stderr', { ...prop, value: stderr });
107-
Object.defineProperty(this, '_ignoreErrors', {
108-
...prop,
109-
value: Boolean(ignoreErrors),
110-
});
111-
Object.defineProperty(this, '_times', { ...prop, value: new Map() });
112-
Object.defineProperty(this, '_stdoutErrorHandler', {
113-
...prop,
114-
value: createWriteErrorHandler(stdout),
115-
});
116-
Object.defineProperty(this, '_stderrErrorHandler', {
117-
...prop,
118-
value: createWriteErrorHandler(stderr),
119-
});
120-
121107
if (typeof colorMode !== 'boolean' && colorMode !== 'auto')
122108
throw new ERR_INVALID_ARG_VALUE('colorMode', colorMode);
123109

124-
// Corresponds to https://console.spec.whatwg.org/#count-map
125-
this[kCounts] = new Map();
126-
this[kColorMode] = colorMode;
127-
128-
Object.defineProperty(this, kGroupIndent, { writable: true });
129-
this[kGroupIndent] = '';
130-
131110
// Bind the prototype functions to this Console instance
132111
var keys = Object.keys(Console.prototype);
133112
for (var v = 0; v < keys.length; v++) {
@@ -137,14 +116,92 @@ function Console(options /* or: stdout, stderr, ignoreErrors = true */) {
137116
// from the prototype chain of the subclass.
138117
this[k] = this[k].bind(this);
139118
}
119+
120+
this[kBindStreamsEager](stdout, stderr);
121+
this[kBindProperties](ignoreErrors, colorMode);
140122
}
141123

124+
const consolePropAttributes = {
125+
writable: true,
126+
enumerable: false,
127+
configurable: true
128+
};
129+
130+
// Fixup global.console instanceof global.console.Console
131+
Object.defineProperty(Console, Symbol.hasInstance, {
132+
value(instance) {
133+
return instance[kIsConsole];
134+
}
135+
});
136+
137+
// Eager version for the Console constructor
138+
Console.prototype[kBindStreamsEager] = function(stdout, stderr) {
139+
Object.defineProperties(this, {
140+
'_stdout': { ...consolePropAttributes, value: stdout },
141+
'_stderr': { ...consolePropAttributes, value: stderr }
142+
});
143+
};
144+
145+
// Lazily load the stdout and stderr from an object so we don't
146+
// create the stdio streams when they are not even accessed
147+
Console.prototype[kBindStreamsLazy] = function(object) {
148+
let stdout;
149+
let stderr;
150+
Object.defineProperties(this, {
151+
'_stdout': {
152+
enumerable: false,
153+
configurable: true,
154+
get() {
155+
if (!stdout) stdout = object.stdout;
156+
return stdout;
157+
},
158+
set(value) { stdout = value; }
159+
},
160+
'_stderr': {
161+
enumerable: false,
162+
configurable: true,
163+
get() {
164+
if (!stderr) { stderr = object.stderr; }
165+
return stderr;
166+
},
167+
set(value) { stderr = value; }
168+
}
169+
});
170+
};
171+
172+
Console.prototype[kBindProperties] = function(ignoreErrors, colorMode) {
173+
Object.defineProperties(this, {
174+
'_stdoutErrorHandler': {
175+
...consolePropAttributes,
176+
value: createWriteErrorHandler(this, kUseStdout)
177+
},
178+
'_stderrErrorHandler': {
179+
...consolePropAttributes,
180+
value: createWriteErrorHandler(this, kUseStderr)
181+
},
182+
'_ignoreErrors': {
183+
...consolePropAttributes,
184+
value: Boolean(ignoreErrors)
185+
},
186+
'_times': { ...consolePropAttributes, value: new Map() }
187+
});
188+
189+
// TODO(joyeecheung): use consolePropAttributes for these
190+
// Corresponds to https://console.spec.whatwg.org/#count-map
191+
this[kCounts] = new Map();
192+
this[kColorMode] = colorMode;
193+
this[kIsConsole] = true;
194+
this[kGroupIndent] = '';
195+
};
196+
142197
// Make a function that can serve as the callback passed to `stream.write()`.
143-
function createWriteErrorHandler(stream) {
198+
function createWriteErrorHandler(instance, streamSymbol) {
144199
return (err) => {
145200
// This conditional evaluates to true if and only if there was an error
146201
// that was not already emitted (which happens when the _write callback
147202
// is invoked asynchronously).
203+
const stream = streamSymbol === kUseStdout ?
204+
instance._stdout : instance._stderr;
148205
if (err !== null && !stream._writableState.errorEmitted) {
149206
// If there was an error, it will be emitted on `stream` as
150207
// an `error` event. Adding a `once` listener will keep that error
@@ -158,7 +215,15 @@ function createWriteErrorHandler(stream) {
158215
};
159216
}
160217

161-
function write(ignoreErrors, stream, string, errorhandler, groupIndent) {
218+
Console.prototype[kWriteToConsole] = function(streamSymbol, string) {
219+
const ignoreErrors = this._ignoreErrors;
220+
const groupIndent = this[kGroupIndent];
221+
222+
const useStdout = streamSymbol === kUseStdout;
223+
const stream = useStdout ? this._stdout : this._stderr;
224+
const errorHandler = useStdout ?
225+
this._stdoutErrorHandler : this._stderrErrorHandler;
226+
162227
if (groupIndent.length !== 0) {
163228
if (string.indexOf('\n') !== -1) {
164229
string = string.replace(/\n/g, `\n${groupIndent}`);
@@ -176,7 +241,7 @@ function write(ignoreErrors, stream, string, errorhandler, groupIndent) {
176241
// Add and later remove a noop error handler to catch synchronous errors.
177242
stream.once('error', noop);
178243

179-
stream.write(string, errorhandler);
244+
stream.write(string, errorHandler);
180245
} catch (e) {
181246
// Console is a debugging utility, so it swallowing errors is not desirable
182247
// even in edge cases such as low stack space.
@@ -186,7 +251,7 @@ function write(ignoreErrors, stream, string, errorhandler, groupIndent) {
186251
} finally {
187252
stream.removeListener('error', noop);
188253
}
189-
}
254+
};
190255

191256
const kColorInspectOptions = { colors: true };
192257
const kNoColorInspectOptions = {};
@@ -212,23 +277,17 @@ Console.prototype[kFormatForStderr] = function(args) {
212277
};
213278

214279
Console.prototype.log = function log(...args) {
215-
write(this._ignoreErrors,
216-
this._stdout,
217-
this[kFormatForStdout](args),
218-
this._stdoutErrorHandler,
219-
this[kGroupIndent]);
280+
this[kWriteToConsole](kUseStdout, this[kFormatForStdout](args));
220281
};
282+
221283
Console.prototype.debug = Console.prototype.log;
222284
Console.prototype.info = Console.prototype.log;
223285
Console.prototype.dirxml = Console.prototype.log;
224286

225287
Console.prototype.warn = function warn(...args) {
226-
write(this._ignoreErrors,
227-
this._stderr,
228-
this[kFormatForStderr](args),
229-
this._stderrErrorHandler,
230-
this[kGroupIndent]);
288+
this[kWriteToConsole](kUseStderr, this[kFormatForStderr](args));
231289
};
290+
232291
Console.prototype.error = Console.prototype.warn;
233292

234293
Console.prototype.dir = function dir(object, options) {
@@ -237,11 +296,7 @@ Console.prototype.dir = function dir(object, options) {
237296
...this[kGetInspectOptions](this._stdout),
238297
...options
239298
};
240-
write(this._ignoreErrors,
241-
this._stdout,
242-
util.inspect(object, options),
243-
this._stdoutErrorHandler,
244-
this[kGroupIndent]);
299+
this[kWriteToConsole](kUseStdout, util.inspect(object, options));
245300
};
246301

247302
Console.prototype.time = function time(label = 'default') {
@@ -301,7 +356,7 @@ Console.prototype.trace = function trace(...args) {
301356
Console.prototype.assert = function assert(expression, ...args) {
302357
if (!expression) {
303358
args[0] = `Assertion failed${args.length === 0 ? '' : `: ${args[0]}`}`;
304-
this.warn(this[kFormatForStderr](args));
359+
this.warn(...args); // the arguments will be formatted in warn() again
305360
}
306361
};
307362

@@ -363,7 +418,6 @@ const valuesKey = 'Values';
363418
const indexKey = '(index)';
364419
const iterKey = '(iteration index)';
365420

366-
367421
const isArray = (v) => ArrayIsArray(v) || isTypedArray(v) || isBuffer(v);
368422

369423
// https://console.spec.whatwg.org/#table
@@ -490,38 +544,31 @@ function noop() {}
490544
// we cannot actually use `new Console` to construct the global console.
491545
// Therefore, the console.Console.prototype is not
492546
// in the global console prototype chain anymore.
547+
548+
// TODO(joyeecheung):
549+
// - Move the Console constructor into internal/console.js
550+
// - Move the global console creation code along with the inspector console
551+
// wrapping code in internal/bootstrap/node.js into a separate file.
552+
// - Make this file a simple re-export of those two files.
493553
// This is only here for v11.x conflict resolution.
494554
const globalConsole = Object.create(Console.prototype);
495-
const tempConsole = new Console({
496-
stdout: process.stdout,
497-
stderr: process.stderr
498-
});
499555

500556
// Since Console is not on the prototype chain of the global console,
501557
// the symbol properties on Console.prototype have to be looked up from
502-
// the global console itself.
503-
for (const prop of Object.getOwnPropertySymbols(Console.prototype)) {
504-
globalConsole[prop] = Console.prototype[prop];
505-
}
506-
507-
// Reflect.ownKeys() is used here for retrieving Symbols
508-
for (const prop of Reflect.ownKeys(tempConsole)) {
509-
const desc = { ...(Reflect.getOwnPropertyDescriptor(tempConsole, prop)) };
510-
// Since Console would bind method calls onto the instance,
511-
// make sure the methods are called on globalConsole instead of
512-
// tempConsole.
513-
if (typeof Console.prototype[prop] === 'function') {
514-
desc.value = Console.prototype[prop].bind(globalConsole);
558+
// the global console itself. In addition, we need to make the global
559+
// console a namespace by binding the console methods directly onto
560+
// the global console with the receiver fixed.
561+
for (const prop of Reflect.ownKeys(Console.prototype)) {
562+
if (prop === 'constructor') { continue; }
563+
const desc = Reflect.getOwnPropertyDescriptor(Console.prototype, prop);
564+
if (typeof desc.value === 'function') { // fix the receiver
565+
desc.value = desc.value.bind(globalConsole);
515566
}
516567
Reflect.defineProperty(globalConsole, prop, desc);
517568
}
518569

519-
globalConsole.Console = Console;
520-
521-
Object.defineProperty(Console, Symbol.hasInstance, {
522-
value(instance) {
523-
return instance[kIsConsole];
524-
}
525-
});
570+
globalConsole[kBindStreamsLazy](process);
571+
globalConsole[kBindProperties](true, 'auto');
526572

527573
module.exports = globalConsole;
574+
module.exports.Console = Console;
+10-8
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
1-
/* eslint-disable node-core/required-modules */
2-
1+
// Flags: --expose-internals
32
'use strict';
43

5-
// Ordinarily test files must require('common') but that action causes
6-
// the global console to be compiled, defeating the purpose of this test.
7-
// This makes sure no additional files are added without carefully considering
8-
// lazy loading. Please adjust the value if necessary.
9-
4+
// This list must be computed before we require any modules to
5+
// to eliminate the noise.
106
const list = process.moduleLoadList.slice();
117

8+
const common = require('../common');
129
const assert = require('assert');
1310

14-
assert(list.length <= 78, list);
11+
const isMainThread = common.isMainThread;
12+
const kMaxModuleCount = isMainThread ? 56 : 78;
13+
14+
assert(list.length <= kMaxModuleCount,
15+
`Total length: ${list.length}\n` + list.join('\n')
16+
);
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
calling stdout._refreshSize
21
calling stderr._refreshSize
2+
calling stdout._refreshSize

0 commit comments

Comments
 (0)