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

3 files changed

Lines changed: 133 additions & 84 deletions

File tree

‎lib/console.js‎

Lines changed: 122 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,15 @@ const kFormatForStdout = Symbol('kFormatForStdout');
6565
constkGetInspectOptions=Symbol('kGetInspectOptions');
6666
constkColorMode=Symbol('kColorMode');
6767
constkIsConsole=Symbol('kIsConsole');
68-
68+
constkWriteToConsole=Symbol('kWriteToConsole');
69+
constkBindProperties=Symbol('kBindProperties');
70+
constkBindStreamsEager=Symbol('kBindStreamsEager');
71+
constkBindStreamsLazy=Symbol('kBindStreamsLazy');
72+
constkUseStdout=Symbol('kUseStdout');
73+
constkUseStderr=Symbol('kUseStderr');
74+
75+
// This constructor is not used to construct the global console.
76+
// It's exported for backwards compatibility.
6977
functionConsole(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
returnnewConsole(...arguments);
7583
}
7684

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

100-
constprop={
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: newMap()});
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(typeofcolorMode!=='boolean'&&colorMode!=='auto')
122108
thrownewERR_INVALID_ARG_VALUE('colorMode',colorMode);
123109

124-
// Corresponds to https://console.spec.whatwg.org/#count-map
125-
this[kCounts]=newMap();
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
varkeys=Object.keys(Console.prototype);
133112
for(varv=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+
constconsolePropAttributes={
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+
returninstance[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+
letstdout;
149+
letstderr;
150+
Object.defineProperties(this,{
151+
'_stdout': {
152+
enumerable: false,
153+
configurable: true,
154+
get(){
155+
if(!stdout)stdout=object.stdout;
156+
returnstdout;
157+
},
158+
set(value){stdout=value;}
159+
},
160+
'_stderr': {
161+
enumerable: false,
162+
configurable: true,
163+
get(){
164+
if(!stderr){stderr=object.stderr;}
165+
returnstderr;
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: newMap()}
187+
});
188+
189+
// TODO(joyeecheung): use consolePropAttributes for these
190+
// Corresponds to https://console.spec.whatwg.org/#count-map
191+
this[kCounts]=newMap();
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-
functioncreateWriteErrorHandler(stream){
198+
functioncreateWriteErrorHandler(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+
conststream=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-
functionwrite(ignoreErrors,stream,string,errorhandler,groupIndent){
218+
Console.prototype[kWriteToConsole]=function(streamSymbol,string){
219+
constignoreErrors=this._ignoreErrors;
220+
constgroupIndent=this[kGroupIndent];
221+
222+
constuseStdout=streamSymbol===kUseStdout;
223+
conststream=useStdout ? this._stdout : this._stderr;
224+
consterrorHandler=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
constkColorInspectOptions={colors: true};
192257
constkNoColorInspectOptions={};
@@ -212,23 +277,17 @@ Console.prototype[kFormatForStderr] = function(args) {
212277
};
213278

214279
Console.prototype.log=functionlog(...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=functionwarn(...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=functiondir(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=functiontime(label='default'){
@@ -301,7 +356,7 @@ Console.prototype.trace = function trace(...args) {
301356
Console.prototype.assert=functionassert(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
constindexKey='(index)';
364419
constiterKey='(iteration index)';
365420

366-
367421
constisArray=(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
constglobalConsole=Object.create(Console.prototype);
495-
consttempConsole=newConsole({
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(constpropofObject.getOwnPropertySymbols(Console.prototype)){
504-
globalConsole[prop]=Console.prototype[prop];
505-
}
506-
507-
// Reflect.ownKeys() is used here for retrieving Symbols
508-
for(constpropofReflect.ownKeys(tempConsole)){
509-
constdesc={ ...(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(typeofConsole.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(constpropofReflect.ownKeys(Console.prototype)){
562+
if(prop==='constructor'){continue;}
563+
constdesc=Reflect.getOwnPropertyDescriptor(Console.prototype,prop);
564+
if(typeofdesc.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-
returninstance[kIsConsole];
524-
}
525-
});
570+
globalConsole[kBindStreamsLazy](process);
571+
globalConsole[kBindProperties](true,'auto');
526572

527573
module.exports=globalConsole;
574+
module.exports.Console=Console;
Lines changed: 10 additions & 8 deletions
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
constlist=process.moduleLoadList.slice();
117

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

14-
assert(list.length<=78,list);
11+
constisMainThread=common.isMainThread;
12+
constkMaxModuleCount=isMainThread ? 56 : 78;
13+
14+
assert(list.length<=kMaxModuleCount,
15+
`Total length: ${list.length}\n`+list.join('\n')
16+
);
Lines changed: 1 addition & 1 deletion
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)