From 03fec6a754e4a04a06d90b931d4eb9456727c2a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCrg=C3=BCn=20Day=C4=B1o=C4=9Flu?= Date: Sun, 26 Oct 2025 20:42:07 +0100 Subject: [PATCH] console: optimize single-string logging --- benchmark/console/log.js | 78 +++++++++++++++++++++++++++++ lib/internal/console/constructor.js | 34 ++++++++++--- 2 files changed, 105 insertions(+), 7 deletions(-) create mode 100644 benchmark/console/log.js diff --git a/benchmark/console/log.js b/benchmark/console/log.js new file mode 100644 index 00000000000000..37fb2e466bc576 --- /dev/null +++ b/benchmark/console/log.js @@ -0,0 +1,78 @@ +'use strict'; + +// A console throughput benchmark. +// Uses a custom Console with null Writable streams to avoid I/O latency. + +const common = require('../common.js'); +const { Writable } = require('stream'); +const { Console } = require('console'); + +const bench = common.createBenchmark(main, { + n: [2e6], + variant: ['plain', 'format', 'object', 'group', 'info', 'warn', 'error'], +}); + +class Null extends Writable { + _write(chunk, enc, cb) { cb(); } +} + +function makeConsole() { + const dn = new Null(); + return new Console({ stdout: dn, stderr: dn, ignoreErrors: true, colorMode: false }); +} + +function main({ n, variant }) { + const c = makeConsole(); + + switch (variant) { + case 'plain': { + bench.start(); + for (let i = 0; i < n; i++) c.log('hello world'); + bench.end(n); + break; + } + case 'format': { + bench.start(); + for (let i = 0; i < n; i++) c.log('%s %d %j', 'a', 42, { x: 1 }); + bench.end(n); + break; + } + case 'object': { + const obj = { a: 1, b: 2, c: 3 }; + bench.start(); + for (let i = 0; i < n; i++) c.log(obj); + bench.end(n); + break; + } + case 'group': { + bench.start(); + for (let i = 0; i < n; i++) { + c.group('g'); + c.log('x'); + c.groupEnd(); + } + bench.end(n); + break; + } + case 'info': { + bench.start(); + for (let i = 0; i < n; i++) c.info('hello world'); + bench.end(n); + break; + } + case 'warn': { + bench.start(); + for (let i = 0; i < n; i++) c.warn('hello world'); + bench.end(n); + break; + } + case 'error': { + bench.start(); + for (let i = 0; i < n; i++) c.error('hello world'); + bench.end(n); + break; + } + default: + throw new Error('unknown variant'); + } +} diff --git a/lib/internal/console/constructor.js b/lib/internal/console/constructor.js index f3ed1bb7e76282..bb79678001ec66 100644 --- a/lib/internal/console/constructor.js +++ b/lib/internal/console/constructor.js @@ -190,7 +190,7 @@ ObjectDefineProperty(Console, SymbolHasInstance, { const kColorInspectOptions = { colors: true }; const kNoColorInspectOptions = {}; -const internalIndentationMap = new SafeWeakMap(); +const kGroupIndentationString = Symbol('kGroupIndentationString'); ObjectDefineProperties(Console.prototype, { [kBindStreamsEager]: { @@ -264,6 +264,11 @@ ObjectDefineProperties(Console.prototype, { ...consolePropAttributes, value: groupIndentation, }, + [kGroupIndentationString]: { + __proto__: null, + ...consolePropAttributes, + value: '', + }, [SymbolToStringTag]: { __proto__: null, writable: false, @@ -279,7 +284,7 @@ ObjectDefineProperties(Console.prototype, { ...consolePropAttributes, value: function(streamSymbol, string) { const ignoreErrors = this._ignoreErrors; - const groupIndent = internalIndentationMap.get(this) || ''; + const groupIndent = this[kGroupIndentationString]; const useStdout = streamSymbol === kUseStdout; const stream = useStdout ? this._stdout : this._stderr; @@ -342,6 +347,14 @@ ObjectDefineProperties(Console.prototype, { __proto__: null, ...consolePropAttributes, value: function(args) { + if (args.length === 1) { + // Fast path: single string, don't call format. + // Avoids ReflectApply and validation overhead. + const a0 = args[0]; + if (typeof a0 === 'string') { + return a0; + } + } const opts = this[kGetInspectOptions](this._stdout); ArrayPrototypeUnshift(args, opts); return ReflectApply(formatWithOptions, null, args); @@ -351,6 +364,14 @@ ObjectDefineProperties(Console.prototype, { __proto__: null, ...consolePropAttributes, value: function(args) { + if (args.length === 1) { + // Fast path: single string, don't call format. + // Avoids ReflectApply and validation overhead. + const a0 = args[0]; + if (typeof a0 === 'string') { + return a0; + } + } const opts = this[kGetInspectOptions](this._stderr); ArrayPrototypeUnshift(args, opts); return ReflectApply(formatWithOptions, null, args); @@ -513,21 +534,20 @@ const consoleMethods = { ReflectApply(this.log, this, data); } - let currentIndentation = internalIndentationMap.get(this) || ''; + let currentIndentation = this[kGroupIndentationString]; currentIndentation += StringPrototypeRepeat(' ', this[kGroupIndentationWidth]); - - internalIndentationMap.set(this, currentIndentation); + this[kGroupIndentationString] = currentIndentation; }, groupEnd() { - const currentIndentation = internalIndentationMap.get(this) || ''; + const currentIndentation = this[kGroupIndentationString]; const newIndentation = StringPrototypeSlice( currentIndentation, 0, currentIndentation.length - this[kGroupIndentationWidth], ); - internalIndentationMap.set(this, newIndentation); + this[kGroupIndentationString] = newIndentation; }, // https://console.spec.whatwg.org/#table