From 3f0e52ef3fece87a42fff3abcc6a9b31f67e21b7 Mon Sep 17 00:00:00 2001 From: Ruben Bridgewater Date: Fri, 24 Aug 2018 02:13:55 +0200 Subject: [PATCH 1/7] util: remove outdated TODO PR-URL: https://github.com/nodejs/node/pull/22503 Reviewed-By: Matteo Collina Reviewed-By: John-David Dalton Reviewed-By: Benjamin Gruenbaum Reviewed-By: Refael Ackermann --- lib/util.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/util.js b/lib/util.js index 94bde6e65ee055..09e60a76366b39 100644 --- a/lib/util.js +++ b/lib/util.js @@ -380,8 +380,6 @@ Object.defineProperty(inspect, 'defaultOptions', { if (options === null || typeof options !== 'object') { throw new ERR_INVALID_ARG_TYPE('options', 'Object', options); } - // TODO(BridgeAR): Add input validation and make sure `defaultOptions` are - // not configurable. return _extend(inspectDefaultOptions, options); } }); From 0560cd08df89da0ec9c31e8c07f25f1297eda389 Mon Sep 17 00:00:00 2001 From: Ruben Bridgewater Date: Mon, 13 Aug 2018 17:51:58 +0200 Subject: [PATCH 2/7] benchmark: refactor util benchmarks This significantly reduces the benchmark runtime. It removes to many variations that do not provide any benefit and reduces the iterations. PR-URL: https://github.com/nodejs/node/pull/22503 Reviewed-By: Matteo Collina Reviewed-By: John-David Dalton Reviewed-By: Benjamin Gruenbaum Reviewed-By: Refael Ackermann --- benchmark/util/format.js | 2 +- benchmark/util/inspect-array.js | 4 ++-- benchmark/util/inspect-proxy.js | 2 +- benchmark/util/inspect.js | 4 ++-- benchmark/util/normalize-encoding.js | 29 +++++++++------------------- benchmark/util/splice-one.js | 2 +- benchmark/util/type-check.js | 2 +- test/parallel/test-benchmark-util.js | 1 + 8 files changed, 18 insertions(+), 28 deletions(-) diff --git a/benchmark/util/format.js b/benchmark/util/format.js index 042b8a93ccfcf2..2a4a20097c72d3 100644 --- a/benchmark/util/format.js +++ b/benchmark/util/format.js @@ -16,7 +16,7 @@ const inputs = { }; const bench = common.createBenchmark(main, { - n: [4e6], + n: [1e5], type: Object.keys(inputs) }); diff --git a/benchmark/util/inspect-array.js b/benchmark/util/inspect-array.js index 8b3c54aeb942fe..4fd73785f789d1 100644 --- a/benchmark/util/inspect-array.js +++ b/benchmark/util/inspect-array.js @@ -4,8 +4,8 @@ const common = require('../common'); const util = require('util'); const bench = common.createBenchmark(main, { - n: [1e3], - len: [1e5], + n: [5e2], + len: [1e2, 1e5], type: [ 'denseArray', 'sparseArray', diff --git a/benchmark/util/inspect-proxy.js b/benchmark/util/inspect-proxy.js index 5427df9952c250..3c82d50ac3c0ce 100644 --- a/benchmark/util/inspect-proxy.js +++ b/benchmark/util/inspect-proxy.js @@ -3,7 +3,7 @@ const util = require('util'); const common = require('../common.js'); -const bench = common.createBenchmark(main, { n: [1e6] }); +const bench = common.createBenchmark(main, { n: [2e4] }); function main({ n }) { const proxyA = new Proxy({}, { get: () => {} }); diff --git a/benchmark/util/inspect.js b/benchmark/util/inspect.js index 35253ac96682eb..9ba3020fd0e55b 100644 --- a/benchmark/util/inspect.js +++ b/benchmark/util/inspect.js @@ -9,7 +9,7 @@ const opts = { none: undefined }; const bench = common.createBenchmark(main, { - n: [2e6], + n: [2e4], method: [ 'Object', 'Object_empty', @@ -81,7 +81,7 @@ function main({ method, n, option }) { benchmark(n, new Error('error'), options); break; case 'Array': - benchmark(n, Array(20).fill().map((_, i) => i), options); + benchmark(n, Array(50).fill().map((_, i) => i), options); break; case 'TypedArray': obj = new Uint8Array(Array(50).fill().map((_, i) => i)); diff --git a/benchmark/util/normalize-encoding.js b/benchmark/util/normalize-encoding.js index 73cbadff72b8ca..47c8bce277cf3d 100644 --- a/benchmark/util/normalize-encoding.js +++ b/benchmark/util/normalize-encoding.js @@ -5,26 +5,23 @@ const assert = require('assert'); const groupedInputs = { group_common: ['undefined', 'utf8', 'utf-8', 'base64', - 'binary', 'latin1', 'ucs-2'], - group_upper: ['UTF-8', 'UTF8', 'UCS2', 'UTF-16LE', - 'UTF16LE', 'BASE64', 'UCS-2'], - group_uncommon: ['foo', '1', 'false', 'undefined', '[]', '{}'], + 'binary', 'latin1', 'ucs2'], + group_upper: ['UTF-8', 'UTF8', 'UCS2', + 'UTF16LE', 'BASE64', 'UCS2'], + group_uncommon: ['foo'], group_misc: ['', 'utf16le', 'hex', 'HEX', 'BINARY'] }; const inputs = [ - '', - 'utf8', 'utf-8', 'UTF-8', - 'UTF8', 'Utf8', 'uTf-8', 'utF-8', - 'ucs2', 'UCS2', 'UcS2', - 'ucs-2', 'UCS-2', 'UcS-2', - 'utf16le', 'utf-16le', 'UTF-16LE', 'UTF16LE', + '', 'utf8', 'utf-8', 'UTF-8', 'UTF8', 'Utf8', + 'ucs2', 'UCS2', 'utf16le', 'UTF16LE', 'binary', 'BINARY', 'latin1', 'base64', 'BASE64', - 'hex', 'HEX', 'foo', '1', 'false', 'undefined', '[]', '{}']; + 'hex', 'HEX', 'foo', 'undefined' +]; const bench = common.createBenchmark(main, { input: inputs.concat(Object.keys(groupedInputs)), - n: [1e7] + n: [1e5] }, { flags: '--expose-internals' }); @@ -39,16 +36,8 @@ function getInput(input) { return groupedInputs.group_uncommon; case 'group_misc': return groupedInputs.group_misc; - case '1': - return [1]; - case 'false': - return [false]; case 'undefined': return [undefined]; - case '[]': - return [[]]; - case '{}': - return [{}]; default: return [input]; } diff --git a/benchmark/util/splice-one.js b/benchmark/util/splice-one.js index 5c2a39f6d72a11..4ca7c8564d3fc4 100644 --- a/benchmark/util/splice-one.js +++ b/benchmark/util/splice-one.js @@ -3,7 +3,7 @@ const common = require('../common'); const bench = common.createBenchmark(main, { - n: [1e7], + n: [1e5], pos: ['start', 'middle', 'end'], size: [10, 100, 500], }, { flags: ['--expose-internals'] }); diff --git a/benchmark/util/type-check.js b/benchmark/util/type-check.js index e1d1ac553fedcf..dc07f4f9d952bd 100644 --- a/benchmark/util/type-check.js +++ b/benchmark/util/type-check.js @@ -29,7 +29,7 @@ const bench = common.createBenchmark(main, { type: Object.keys(args), version: ['native', 'js'], argument: ['true', 'false-primitive', 'false-object'], - n: [5e6] + n: [1e5] }, { flags: ['--expose-internals'] }); diff --git a/test/parallel/test-benchmark-util.js b/test/parallel/test-benchmark-util.js index 838e51daac26b4..97b02bbdeed5cd 100644 --- a/test/parallel/test-benchmark-util.js +++ b/test/parallel/test-benchmark-util.js @@ -13,5 +13,6 @@ runBenchmark('util', 'pos=start', 'size=1', 'type=', + 'len=1', 'version=native'], { NODEJS_BENCHMARK_ZERO_ALLOWED: 1 }); From 15b0c80ac267df03787fb29f6f6b21ca722dbe51 Mon Sep 17 00:00:00 2001 From: Ruben Bridgewater Date: Sun, 12 Aug 2018 21:45:36 +0200 Subject: [PATCH 3/7] util: improve inspect performance This significantly improves the inspection performance for all array types. From now on only the visible elements cause work instead of having to process all array keys no matter how many entries are visible. This also moves some code out of the main function to reduce the overall function complexity. PR-URL: https://github.com/nodejs/node/pull/22503 Reviewed-By: Matteo Collina Reviewed-By: John-David Dalton Reviewed-By: Benjamin Gruenbaum Reviewed-By: Refael Ackermann --- lib/util.js | 461 ++++++++++++++--------------- test/parallel/test-util-inspect.js | 2 +- 2 files changed, 222 insertions(+), 241 deletions(-) diff --git a/lib/util.js b/lib/util.js index 09e60a76366b39..ec9651ec97c495 100644 --- a/lib/util.js +++ b/lib/util.js @@ -32,11 +32,16 @@ const { TextDecoder, TextEncoder } = require('internal/encoding'); const { isBuffer } = require('buffer').Buffer; const { + getOwnNonIndexProperties, getPromiseDetails, getProxyDetails, kPending, kRejected, - previewEntries + previewEntries, + propertyFilter: { + ALL_PROPERTIES, + ONLY_ENUMERABLE + } } = process.binding('util'); const { internalBinding } = require('internal/bootstrap/loaders'); @@ -46,6 +51,7 @@ const { isAnyArrayBuffer, isArrayBuffer, isArgumentsObject, + isBoxedPrimitive, isDataView, isExternal, isMap, @@ -61,7 +67,6 @@ const { isStringObject, isNumberObject, isBooleanObject, - isSymbolObject, isBigIntObject, isUint8Array, isUint8ClampedArray, @@ -97,6 +102,10 @@ const inspectDefaultOptions = Object.seal({ compact: true }); +const kObjectType = 0; +const kArrayType = 1; +const kArrayExtrasType = 2; + const ReflectApply = Reflect.apply; // This function is borrowed from the function with the same name on V8 Extras' @@ -122,6 +131,7 @@ const stringValueOf = uncurryThis(String.prototype.valueOf); const setValues = uncurryThis(Set.prototype.values); const mapEntries = uncurryThis(Map.prototype.entries); const dateGetTime = uncurryThis(Date.prototype.getTime); +const hasOwnProperty = uncurryThis(Object.prototype.hasOwnProperty); let CIRCULAR_ERROR_MESSAGE; let internalDeepEqual; @@ -425,10 +435,15 @@ function stylizeWithColor(str, styleType) { return str; } -function stylizeNoColor(str, styleType) { +function stylizeNoColor(str) { return str; } +// Return a new empty array to push in the results of the default formatter. +function getEmptyFormatArray() { + return []; +} + function getConstructorName(obj) { while (obj) { const descriptor = Object.getOwnPropertyDescriptor(obj, 'constructor'); @@ -461,6 +476,56 @@ function getPrefix(constructor, tag, fallback) { return ''; } +const getBoxedValue = formatPrimitive.bind(null, stylizeNoColor); + +// Look up the keys of the object. +function getKeys(value, showHidden) { + let keys; + const symbols = Object.getOwnPropertySymbols(value); + if (showHidden) { + keys = Object.getOwnPropertyNames(value); + if (symbols.length !== 0) + keys.push(...symbols); + } else { + // This might throw if `value` is a Module Namespace Object from an + // unevaluated module, but we don't want to perform the actual type + // check because it's expensive. + // TODO(devsnek): track https://github.com/tc39/ecma262/issues/1209 + // and modify this logic as needed. + try { + keys = Object.keys(value); + } catch (err) { + if (types.isNativeError(err) && + err.name === 'ReferenceError' && + types.isModuleNamespaceObject(value)) { + keys = Object.getOwnPropertyNames(value); + } else { + throw err; + } + } + if (symbols.length !== 0) { + keys.push(...symbols.filter((key) => propertyIsEnumerable(value, key))); + } + } + return keys; +} + +function formatProxy(ctx, proxy, recurseTimes) { + if (recurseTimes != null) { + if (recurseTimes < 0) + return ctx.stylize('Proxy [Array]', 'special'); + recurseTimes -= 1; + } + ctx.indentationLvl += 2; + const res = [ + formatValue(ctx, proxy[0], recurseTimes), + formatValue(ctx, proxy[1], recurseTimes) + ]; + ctx.indentationLvl -= 2; + const str = reduceToSingleString(ctx, res, '', ['[', ']']); + return `Proxy ${str}`; +} + function findTypedConstructor(value) { for (const [check, clazz] of [ [isUint8Array, Uint8Array], @@ -481,8 +546,6 @@ function findTypedConstructor(value) { } } -const getBoxedValue = formatPrimitive.bind(null, stylizeNoColor); - function noPrototypeIterator(ctx, value, recurseTimes) { let newVal; // TODO: Create a Subclass in case there's no prototype and show @@ -521,19 +584,7 @@ function formatValue(ctx, value, recurseTimes) { if (ctx.showProxy) { const proxy = getProxyDetails(value); if (proxy !== undefined) { - if (recurseTimes != null) { - if (recurseTimes < 0) - return ctx.stylize('Proxy [Array]', 'special'); - recurseTimes -= 1; - } - ctx.indentationLvl += 2; - const res = [ - formatValue(ctx, proxy[0], recurseTimes), - formatValue(ctx, proxy[1], recurseTimes) - ]; - ctx.indentationLvl -= 2; - const str = reduceToSingleString(ctx, res, '', ['[', ']']); - return `Proxy ${str}`; + return formatProxy(ctx, proxy, recurseTimes); } } @@ -574,78 +625,65 @@ function formatValue(ctx, value, recurseTimes) { if (ctx.seen.indexOf(value) !== -1) return ctx.stylize('[Circular]', 'special'); - let keys; - let symbols = Object.getOwnPropertySymbols(value); - - // Look up the keys of the object. - if (ctx.showHidden) { - keys = Object.getOwnPropertyNames(value); - } else { - // This might throw if `value` is a Module Namespace Object from an - // unevaluated module, but we don't want to perform the actual type - // check because it's expensive. - // TODO(devsnek): track https://github.com/tc39/ecma262/issues/1209 - // and modify this logic as needed. - try { - keys = Object.keys(value); - } catch (err) { - if (types.isNativeError(err) && - err.name === 'ReferenceError' && - types.isModuleNamespaceObject(value)) { - keys = Object.getOwnPropertyNames(value); - } else { - throw err; - } - } - - if (symbols.length !== 0) - symbols = symbols.filter((key) => propertyIsEnumerable(value, key)); - } + return formatRaw(ctx, value, recurseTimes); +} - const keyLength = keys.length + symbols.length; +function formatRaw(ctx, value, recurseTimes) { + let keys; const constructor = getConstructorName(value); let tag = value[Symbol.toStringTag]; if (typeof tag !== 'string') tag = ''; let base = ''; - let formatter = formatObject; + let formatter = getEmptyFormatArray; let braces; let noIterator = true; - let extra; let i = 0; + let skip = false; + const filter = ctx.showHidden ? ALL_PROPERTIES : ONLY_ENUMERABLE; + + let extrasType = kObjectType; // Iterators and the rest are split to reduce checks if (value[Symbol.iterator]) { noIterator = false; if (Array.isArray(value)) { + keys = getOwnNonIndexProperties(value, filter); // Only set the constructor for non ordinary ("Array [...]") arrays. const prefix = getPrefix(constructor, tag); braces = [`${prefix === 'Array ' ? '' : prefix}[`, ']']; - if (value.length === 0 && keyLength === 0) + if (value.length === 0 && keys.length === 0) return `${braces[0]}]`; + extrasType = kArrayExtrasType; formatter = formatArray; } else if (isSet(value)) { + keys = getKeys(value, ctx.showHidden); const prefix = getPrefix(constructor, tag); - if (value.size === 0 && keyLength === 0) + if (value.size === 0 && keys.length === 0) return `${prefix}{}`; braces = [`${prefix}{`, '}']; formatter = formatSet; } else if (isMap(value)) { + keys = getKeys(value, ctx.showHidden); const prefix = getPrefix(constructor, tag); - if (value.size === 0 && keyLength === 0) + if (value.size === 0 && keys.length === 0) return `${prefix}{}`; braces = [`${prefix}{`, '}']; formatter = formatMap; } else if (isTypedArray(value)) { + keys = getOwnNonIndexProperties(value, filter); braces = [`${getPrefix(constructor, tag)}[`, ']']; - if (value.length === 0 && keyLength === 0 && !ctx.showHidden) + if (value.length === 0 && keys.length === 0 && !ctx.showHidden) return `${braces[0]}]`; formatter = formatTypedArray; + extrasType = kArrayExtrasType; } else if (isMapIterator(value)) { + keys = getKeys(value, ctx.showHidden); braces = [`[${tag}] {`, '}']; formatter = formatMapIterator; } else if (isSetIterator(value)) { + keys = getKeys(value, ctx.showHidden); braces = [`[${tag}] {`, '}']; formatter = formatSetIterator; } else { @@ -653,34 +691,35 @@ function formatValue(ctx, value, recurseTimes) { } } if (noIterator) { + keys = getKeys(value, ctx.showHidden); braces = ['{', '}']; if (constructor === 'Object') { if (isArgumentsObject(value)) { - if (keyLength === 0) + if (keys.length === 0) return '[Arguments] {}'; braces[0] = '[Arguments] {'; } else if (tag !== '') { braces[0] = `${getPrefix(constructor, tag)}{`; - if (keyLength === 0) { + if (keys.length === 0) { return `${braces[0]}}`; } - } else if (keyLength === 0) { + } else if (keys.length === 0) { return '{}'; } } else if (typeof value === 'function') { const type = constructor || tag || 'Function'; const name = `${type}${value.name ? `: ${value.name}` : ''}`; - if (keyLength === 0) + if (keys.length === 0) return ctx.stylize(`[${name}]`, 'special'); base = `[${name}]`; } else if (isRegExp(value)) { // Make RegExps say that they are RegExps - if (keyLength === 0 || recurseTimes < 0) + if (keys.length === 0 || recurseTimes < 0) return ctx.stylize(regExpToString(value), 'regexp'); base = `${regExpToString(value)}`; } else if (isDate(value)) { // Make dates with properties first say the date - if (keyLength === 0) { + if (keys.length === 0) { if (Number.isNaN(dateGetTime(value))) return ctx.stylize(String(value), 'date'); return ctx.stylize(dateToISOString(value), 'date'); @@ -699,7 +738,7 @@ function formatValue(ctx, value, recurseTimes) { const indentation = ' '.repeat(ctx.indentationLvl); base = formatError(value).replace(/\n/g, `\n${indentation}`); } - if (keyLength === 0) + if (keys.length === 0) return base; if (ctx.compact === false && stackStart !== -1) { @@ -707,14 +746,14 @@ function formatValue(ctx, value, recurseTimes) { base = `[${base.slice(0, stackStart)}]`; } } else if (isAnyArrayBuffer(value)) { - // Fast path for ArrayBuffer and SharedArrayBuffer. - // Can't do the same for DataView because it has a non-primitive - // .buffer property that we need to recurse for. let prefix = getPrefix(constructor, tag); if (prefix === '') { prefix = isArrayBuffer(value) ? 'ArrayBuffer ' : 'SharedArrayBuffer '; } - if (keyLength === 0) + // Fast path for ArrayBuffer and SharedArrayBuffer. + // Can't do the same for DataView because it has a non-primitive + // .buffer property that we need to recurse for. + if (keys.length === 0) return prefix + `{ byteLength: ${formatNumber(ctx.stylize, value.byteLength)} }`; braces[0] = `${prefix}{`; @@ -728,50 +767,42 @@ function formatValue(ctx, value, recurseTimes) { formatter = formatPromise; } else if (isWeakSet(value)) { braces[0] = `${getPrefix(constructor, tag, 'WeakSet')}{`; - if (ctx.showHidden) { - formatter = formatWeakSet; - } else { - extra = ctx.stylize('[items unknown]', 'special'); - } + formatter = ctx.showHidden ? formatWeakSet : formatWeakCollection; } else if (isWeakMap(value)) { braces[0] = `${getPrefix(constructor, tag, 'WeakMap')}{`; - if (ctx.showHidden) { - formatter = formatWeakMap; - } else { - extra = ctx.stylize('[items unknown]', 'special'); - } + formatter = ctx.showHidden ? formatWeakMap : formatWeakCollection; } else if (types.isModuleNamespaceObject(value)) { braces[0] = `[${tag}] {`; formatter = formatNamespaceObject; - } else if (isNumberObject(value)) { - base = `[Number: ${getBoxedValue(numberValueOf(value))}]`; - if (keyLength === 0) - return ctx.stylize(base, 'number'); - } else if (isBooleanObject(value)) { - base = `[Boolean: ${getBoxedValue(booleanValueOf(value))}]`; - if (keyLength === 0) - return ctx.stylize(base, 'boolean'); - } else if (isBigIntObject(value)) { - base = `[BigInt: ${getBoxedValue(bigIntValueOf(value))}]`; - if (keyLength === 0) - return ctx.stylize(base, 'bigint'); - } else if (isSymbolObject(value)) { - base = `[Symbol: ${getBoxedValue(symbolValueOf(value))}]`; - if (keyLength === 0) - return ctx.stylize(base, 'symbol'); - } else if (isStringObject(value)) { - const raw = stringValueOf(value); - base = `[String: ${getBoxedValue(raw, ctx)}]`; - if (keyLength === raw.length) - return ctx.stylize(base, 'string'); - // For boxed Strings, we have to remove the 0-n indexed entries, - // since they just noisy up the output and are redundant - // Make boxed primitive Strings look like such - keys = keys.slice(value.length); - braces = ['{', '}']; - // The input prototype got manipulated. Special handle these. - // We have to rebuild the information so we are able to display everything. + skip = true; + } else if (isBoxedPrimitive(value)) { + let type; + if (isNumberObject(value)) { + base = `[Number: ${getBoxedValue(numberValueOf(value))}]`; + type = 'number'; + } else if (isStringObject(value)) { + base = `[String: ${getBoxedValue(stringValueOf(value), ctx)}]`; + type = 'string'; + // For boxed Strings, we have to remove the 0-n indexed entries, + // since they just noisy up the output and are redundant + // Make boxed primitive Strings look like such + keys = keys.slice(value.length); + } else if (isBooleanObject(value)) { + base = `[Boolean: ${getBoxedValue(booleanValueOf(value))}]`; + type = 'boolean'; + } else if (isBigIntObject(value)) { + base = `[BigInt: ${getBoxedValue(bigIntValueOf(value))}]`; + type = 'bigint'; + } else { + base = `[Symbol: ${getBoxedValue(symbolValueOf(value))}]`; + type = 'symbol'; + } + if (keys.length === 0) { + return ctx.stylize(base, type); + } } else { + // The input prototype got manipulated. Special handle these. We have to + // rebuild the information so we are able to display everything. const specialIterator = noPrototypeIterator(ctx, value, recurseTimes); if (specialIterator) { return specialIterator; @@ -783,7 +814,7 @@ function formatValue(ctx, value, recurseTimes) { braces = [`[${tag || 'Set Iterator'}] {`, '}']; formatter = formatSetIterator; // Handle other regular objects again. - } else if (keyLength === 0) { + } else if (keys.length === 0) { if (isExternal(value)) return ctx.stylize('[External]', 'special'); return `${getPrefix(constructor, tag)}{}`; @@ -801,36 +832,34 @@ function formatValue(ctx, value, recurseTimes) { ctx.seen.push(value); let output; - // This corresponds to a depth of at least 333 and likely 500. - if (ctx.indentationLvl < 1000) { + try { output = formatter(ctx, value, recurseTimes, keys); - } else { - try { - output = formatter(ctx, value, recurseTimes, keys); - } catch (err) { - if (errors.isStackOverflowError(err)) { - ctx.seen.pop(); - return ctx.stylize( - `[${constructor || tag || 'Object'}: Inspection interrupted ` + - 'prematurely. Maximum call stack size exceeded.]', - 'special' - ); + if (skip === false) { + for (i = 0; i < keys.length; i++) { + output.push( + formatProperty(ctx, value, recurseTimes, keys[i], extrasType)); } - throw err; } + } catch (err) { + return handleMaxCallStackSize(ctx, err, constructor, tag); } - if (extra !== undefined) - output.unshift(extra); - - for (i = 0; i < symbols.length; i++) { - output.push(formatProperty(ctx, value, recurseTimes, symbols[i], 0)); - } - ctx.seen.pop(); return reduceToSingleString(ctx, output, base, braces); } +function handleMaxCallStackSize(ctx, err, constructor, tag) { + if (errors.isStackOverflowError(err)) { + ctx.seen.pop(); + return ctx.stylize( + `[${constructor || tag || 'Object'}: Inspection interrupted ` + + 'prematurely. Maximum call stack size exceeded.]', + 'special' + ); + } + throw err; +} + function formatNumber(fn, value) { // Format -0 as '-0'. Checking `value === -0` won't distinguish 0 from -0. if (Object.is(value, -0)) @@ -891,20 +920,13 @@ function formatError(value) { return value.stack || errorToString(value); } -function formatObject(ctx, value, recurseTimes, keys) { - const len = keys.length; - const output = new Array(len); - for (var i = 0; i < len; i++) - output[i] = formatProperty(ctx, value, recurseTimes, keys[i], 0); - return output; -} - function formatNamespaceObject(ctx, value, recurseTimes, keys) { const len = keys.length; const output = new Array(len); for (var i = 0; i < len; i++) { try { - output[i] = formatProperty(ctx, value, recurseTimes, keys[i], 0); + output[i] = formatProperty(ctx, value, recurseTimes, keys[i], + kObjectType); } catch (err) { if (!(types.isNativeError(err) && err.name === 'ReferenceError')) { throw err; @@ -913,7 +935,7 @@ function formatNamespaceObject(ctx, value, recurseTimes, keys) { // line breaks are always correct. Otherwise it is very difficult to keep // this aligned, even though this is a hacky way of dealing with this. const tmp = { [keys[i]]: '' }; - output[i] = formatProperty(ctx, tmp, recurseTimes, keys[i], 0); + output[i] = formatProperty(ctx, tmp, recurseTimes, keys[i], kObjectType); const pos = output[i].lastIndexOf(' '); // We have to find the last whitespace and have to replace that value as // it will be visualized as a regular string. @@ -925,91 +947,67 @@ function formatNamespaceObject(ctx, value, recurseTimes, keys) { } // The array is sparse and/or has extra keys -function formatSpecialArray(ctx, value, recurseTimes, keys, maxLength, valLen) { - const output = []; - const keyLen = keys.length; - let i = 0; - for (const key of keys) { - if (output.length === maxLength) - break; - const index = +key; +function formatSpecialArray(ctx, value, recurseTimes, maxLength, output, i) { + const keys = Object.keys(value); + let index = i; + for (; i < keys.length && output.length < maxLength; i++) { + const key = keys[i]; + const tmp = +key; // Arrays can only have up to 2^32 - 1 entries - if (index > 2 ** 32 - 2) + if (tmp > 2 ** 32 - 2) { break; - if (`${i}` !== key) { - if (!numberRegExp.test(key)) + } + if (`${index}` !== key) { + if (!numberRegExp.test(key)) { break; - const emptyItems = index - i; + } + const emptyItems = tmp - index; const ending = emptyItems > 1 ? 's' : ''; const message = `<${emptyItems} empty item${ending}>`; output.push(ctx.stylize(message, 'undefined')); - i = index; - if (output.length === maxLength) + index = tmp; + if (output.length === maxLength) { break; + } } - output.push(formatProperty(ctx, value, recurseTimes, key, 1)); - i++; - } - if (i < valLen && output.length !== maxLength) { - const len = valLen - i; - const ending = len > 1 ? 's' : ''; - const message = `<${len} empty item${ending}>`; - output.push(ctx.stylize(message, 'undefined')); - i = valLen; - if (keyLen === 0) - return output; - } - const remaining = valLen - i; - if (remaining > 0) { - output.push(`... ${remaining} more item${remaining > 1 ? 's' : ''}`); + output.push(formatProperty(ctx, value, recurseTimes, key, kArrayType)); + index++; } - if (ctx.showHidden && keys[keyLen - 1] === 'length') { - // No extra keys - output.push(formatProperty(ctx, value, recurseTimes, 'length', 2)); - } else if (valLen === 0 || - keyLen > valLen && keys[valLen - 1] === `${valLen - 1}`) { - // The array is not sparse - for (i = valLen; i < keyLen; i++) - output.push(formatProperty(ctx, value, recurseTimes, keys[i], 2)); - } else if (keys[keyLen - 1] !== `${valLen - 1}`) { - const extra = []; - // Only handle special keys - let key; - for (i = keys.length - 1; i >= 0; i--) { - key = keys[i]; - if (numberRegExp.test(key) && +key < 2 ** 32 - 1) - break; - extra.push(formatProperty(ctx, value, recurseTimes, key, 2)); + const remaining = value.length - index; + if (output.length !== maxLength) { + if (remaining > 0) { + const ending = remaining > 1 ? 's' : ''; + const message = `<${remaining} empty item${ending}>`; + output.push(ctx.stylize(message, 'undefined')); } - for (i = extra.length - 1; i >= 0; i--) - output.push(extra[i]); + } else if (remaining > 0) { + output.push(`... ${remaining} more item${remaining > 1 ? 's' : ''}`); } return output; } -function formatArray(ctx, value, recurseTimes, keys) { - const len = Math.min(Math.max(0, ctx.maxArrayLength), value.length); - const hidden = ctx.showHidden ? 1 : 0; +function formatArray(ctx, value, recurseTimes) { const valLen = value.length; - const keyLen = keys.length - hidden; - if (keyLen !== valLen || keys[keyLen - 1] !== `${valLen - 1}`) - return formatSpecialArray(ctx, value, recurseTimes, keys, len, valLen); + const len = Math.min(Math.max(0, ctx.maxArrayLength), valLen); const remaining = valLen - len; - const output = new Array(len + (remaining > 0 ? 1 : 0) + hidden); - for (var i = 0; i < len; i++) - output[i] = formatProperty(ctx, value, recurseTimes, keys[i], 1); + const output = []; + for (var i = 0; i < len; i++) { + // Special handle sparse arrays. + if (!hasOwnProperty(value, i)) { + return formatSpecialArray(ctx, value, recurseTimes, len, output, i); + } + output.push(formatProperty(ctx, value, recurseTimes, i, kArrayType)); + } if (remaining > 0) - output[i++] = `... ${remaining} more item${remaining > 1 ? 's' : ''}`; - if (ctx.showHidden === true) - output[i] = formatProperty(ctx, value, recurseTimes, 'length', 2); + output.push(`... ${remaining} more item${remaining > 1 ? 's' : ''}`); return output; } -function formatTypedArray(ctx, value, recurseTimes, keys) { +function formatTypedArray(ctx, value, recurseTimes) { const maxLength = Math.min(Math.max(0, ctx.maxArrayLength), value.length); const remaining = value.length - maxLength; - const output = new Array(maxLength + (remaining > 0 ? 1 : 0)); + const output = new Array(maxLength); for (var i = 0; i < maxLength; ++i) output[i] = formatNumber(ctx.stylize, value[i]); if (remaining > 0) @@ -1029,52 +1027,39 @@ function formatTypedArray(ctx, value, recurseTimes, keys) { } ctx.indentationLvl -= 2; } - // TypedArrays cannot have holes. Therefore it is safe to assume that all - // extra keys are indexed after value.length. - for (i = value.length; i < keys.length; i++) { - output.push(formatProperty(ctx, value, recurseTimes, keys[i], 2)); - } return output; } -function formatSet(ctx, value, recurseTimes, keys) { - const output = new Array(value.size + keys.length + (ctx.showHidden ? 1 : 0)); - let i = 0; +function formatSet(ctx, value, recurseTimes) { + const output = []; ctx.indentationLvl += 2; for (const v of value) { - output[i++] = formatValue(ctx, v, recurseTimes); + output.push(formatValue(ctx, v, recurseTimes)); } ctx.indentationLvl -= 2; // With `showHidden`, `length` will display as a hidden property for // arrays. For consistency's sake, do the same for `size`, even though this // property isn't selected by Object.getOwnPropertyNames(). if (ctx.showHidden) - output[i++] = `[size]: ${ctx.stylize(`${value.size}`, 'number')}`; - for (var n = 0; n < keys.length; n++) { - output[i++] = formatProperty(ctx, value, recurseTimes, keys[n], 0); - } + output.push(`[size]: ${ctx.stylize(`${value.size}`, 'number')}`); return output; } -function formatMap(ctx, value, recurseTimes, keys) { - const output = new Array(value.size + keys.length + (ctx.showHidden ? 1 : 0)); - let i = 0; +function formatMap(ctx, value, recurseTimes) { + const output = []; ctx.indentationLvl += 2; for (const [k, v] of value) { - output[i++] = `${formatValue(ctx, k, recurseTimes)} => ` + - formatValue(ctx, v, recurseTimes); + output.push(`${formatValue(ctx, k, recurseTimes)} => ` + + formatValue(ctx, v, recurseTimes)); } ctx.indentationLvl -= 2; // See comment in formatSet if (ctx.showHidden) - output[i++] = `[size]: ${ctx.stylize(`${value.size}`, 'number')}`; - for (var n = 0; n < keys.length; n++) { - output[i++] = formatProperty(ctx, value, recurseTimes, keys[n], 0); - } + output.push(`[size]: ${ctx.stylize(`${value.size}`, 'number')}`); return output; } -function formatSetIterInner(ctx, value, recurseTimes, keys, entries, state) { +function formatSetIterInner(ctx, recurseTimes, entries, state) { const maxArrayLength = Math.max(ctx.maxArrayLength, 0); const maxLength = Math.min(maxArrayLength, entries.length); let output = new Array(maxLength); @@ -1092,12 +1077,10 @@ function formatSetIterInner(ctx, value, recurseTimes, keys, entries, state) { if (remaining > 0) { output.push(`... ${remaining} more item${remaining > 1 ? 's' : ''}`); } - for (i = 0; i < keys.length; i++) - output.push(formatProperty(ctx, value, recurseTimes, keys[i], 0)); return output; } -function formatMapIterInner(ctx, value, recurseTimes, keys, entries, state) { +function formatMapIterInner(ctx, recurseTimes, entries, state) { const maxArrayLength = Math.max(ctx.maxArrayLength, 0); // Entries exist as [key1, val1, key2, val2, ...] const len = entries.length / 2; @@ -1128,37 +1111,38 @@ function formatMapIterInner(ctx, value, recurseTimes, keys, entries, state) { if (remaining > 0) { output.push(`... ${remaining} more item${remaining > 1 ? 's' : ''}`); } - for (i = 0; i < keys.length; i++) - output.push(formatProperty(ctx, value, recurseTimes, keys[i], 0)); return output; } -function formatWeakSet(ctx, value, recurseTimes, keys) { +function formatWeakCollection(ctx) { + return [ctx.stylize('[items unknown]', 'special')]; +} + +function formatWeakSet(ctx, value, recurseTimes) { const entries = previewEntries(value); - return formatSetIterInner(ctx, value, recurseTimes, keys, entries, kWeak); + return formatSetIterInner(ctx, recurseTimes, entries, kWeak); } -function formatWeakMap(ctx, value, recurseTimes, keys) { +function formatWeakMap(ctx, value, recurseTimes) { const entries = previewEntries(value); - return formatMapIterInner(ctx, value, recurseTimes, keys, entries, kWeak); + return formatMapIterInner(ctx, recurseTimes, entries, kWeak); } -function formatSetIterator(ctx, value, recurseTimes, keys) { +function formatSetIterator(ctx, value, recurseTimes) { const entries = previewEntries(value); - return formatSetIterInner(ctx, value, recurseTimes, keys, entries, kIterator); + return formatSetIterInner(ctx, recurseTimes, entries, kIterator); } -function formatMapIterator(ctx, value, recurseTimes, keys) { +function formatMapIterator(ctx, value, recurseTimes) { const [entries, isKeyValue] = previewEntries(value, true); if (isKeyValue) { - return formatMapIterInner( - ctx, value, recurseTimes, keys, entries, kMapEntries); + return formatMapIterInner(ctx, recurseTimes, entries, kMapEntries); } - return formatSetIterInner(ctx, value, recurseTimes, keys, entries, kIterator); + return formatSetIterInner(ctx, recurseTimes, entries, kIterator); } -function formatPromise(ctx, value, recurseTimes, keys) { +function formatPromise(ctx, value, recurseTimes) { let output; const [state, result] = getPromiseDetails(value); if (state === kPending) { @@ -1175,19 +1159,16 @@ function formatPromise(ctx, value, recurseTimes, keys) { str ]; } - for (var n = 0; n < keys.length; n++) { - output.push(formatProperty(ctx, value, recurseTimes, keys[n], 0)); - } return output; } -function formatProperty(ctx, value, recurseTimes, key, array) { +function formatProperty(ctx, value, recurseTimes, key, type) { let name, str; let extra = ' '; const desc = Object.getOwnPropertyDescriptor(value, key) || { value: value[key], enumerable: true }; if (desc.value !== undefined) { - const diff = array !== 0 || ctx.compact === false ? 2 : 3; + const diff = (type !== kObjectType || ctx.compact === false) ? 2 : 3; ctx.indentationLvl += diff; str = formatValue(ctx, desc.value, recurseTimes); if (diff === 3) { @@ -1208,7 +1189,7 @@ function formatProperty(ctx, value, recurseTimes, key, array) { } else { str = ctx.stylize('undefined', 'undefined'); } - if (array === 1) { + if (type === kArrayType) { return str; } if (typeof key === 'symbol') { diff --git a/test/parallel/test-util-inspect.js b/test/parallel/test-util-inspect.js index df1348e95abf4d..e7585a86146b6b 100644 --- a/test/parallel/test-util-inspect.js +++ b/test/parallel/test-util-inspect.js @@ -922,7 +922,7 @@ if (typeof Symbol !== 'undefined') { const set = new Set(['foo']); set.bar = 42; assert.strictEqual( - util.inspect(set, true), + util.inspect(set, { showHidden: true }), "Set { 'foo', [size]: 1, bar: 42 }" ); } From 8513b45890591c907494265204e057767bf63b90 Mon Sep 17 00:00:00 2001 From: Ruben Bridgewater Date: Mon, 10 Sep 2018 08:16:12 +0200 Subject: [PATCH 4/7] util: fix indentationLvl when exceeding max call stack size The inspection indentation level was not always reset to it's former value in case the maximum call stack size was exceeded. PR-URL: https://github.com/nodejs/node/pull/22787 Reviewed-By: James M Snell --- lib/util.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/util.js b/lib/util.js index ec9651ec97c495..cfced8d9942d68 100644 --- a/lib/util.js +++ b/lib/util.js @@ -832,6 +832,7 @@ function formatRaw(ctx, value, recurseTimes) { ctx.seen.push(value); let output; + const indentationLvl = ctx.indentationLvl; try { output = formatter(ctx, value, recurseTimes, keys); if (skip === false) { @@ -841,16 +842,17 @@ function formatRaw(ctx, value, recurseTimes) { } } } catch (err) { - return handleMaxCallStackSize(ctx, err, constructor, tag); + return handleMaxCallStackSize(ctx, err, constructor, tag, indentationLvl); } ctx.seen.pop(); return reduceToSingleString(ctx, output, base, braces); } -function handleMaxCallStackSize(ctx, err, constructor, tag) { +function handleMaxCallStackSize(ctx, err, constructor, tag, indentationLvl) { if (errors.isStackOverflowError(err)) { ctx.seen.pop(); + ctx.indentationLvl = indentationLvl; return ctx.stylize( `[${constructor || tag || 'Object'}: Inspection interrupted ` + 'prematurely. Maximum call stack size exceeded.]', From cc9898bd7747d2884afe9da8fff7e954225ba347 Mon Sep 17 00:00:00 2001 From: chocolateboy Date: Sun, 20 May 2018 21:27:34 +0100 Subject: [PATCH 5/7] util: use a shared symbol for util.inspect.custom MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Define `util.inspect.custom` as `Symbol.for("nodejs.util.inspect.custom")` rather than `Symbol("util.inspect.custom")`. This allows `inspect` hooks to easily/safely be defined in non-Node.js environments. Fixes: https://github.com/nodejs/node/issues/20821 Refs: https://github.com/nodejs/node/pull/22684 PR-URL: https://github.com/nodejs/node/pull/20857 Reviewed-By: Ruben Bridgewater Reviewed-By: Anna Henningsen Reviewed-By: Tiancheng "Timothy" Gu Reviewed-By: Sakthipriyan Vairamani Reviewed-By: James M Snell Reviewed-By: Matteo Collina Reviewed-By: Michaƫl Zasso Reviewed-By: John-David Dalton --- doc/api/util.md | 46 ++++++++++++++++++++++++++---- lib/internal/util.js | 2 +- test/parallel/test-assert.js | 2 +- test/parallel/test-console.js | 8 +++--- test/parallel/test-util-inspect.js | 28 ++++++++++++++---- 5 files changed, 69 insertions(+), 17 deletions(-) diff --git a/doc/api/util.md b/doc/api/util.md index 2b1e71100503d5..8dfbfd874c762a 100644 --- a/doc/api/util.md +++ b/doc/api/util.md @@ -572,9 +572,10 @@ terminals. -Objects may also define their own `[util.inspect.custom](depth, opts)` -(or the equivalent but deprecated `inspect(depth, opts)`) function that -`util.inspect()` will invoke and use the result of when inspecting the object: +Objects may also define their own +[`[util.inspect.custom](depth, opts)`][util.inspect.custom] (or the equivalent +but deprecated `inspect(depth, opts)`) function, which `util.inspect()` will +invoke and use the result of when inspecting the object: ```js const util = require('util'); @@ -626,10 +627,41 @@ util.inspect(obj); ### util.inspect.custom -A {symbol} that can be used to declare custom inspect functions, see -[Custom inspection functions on Objects][]. +* {symbol} that can be used to declare custom inspect functions. + +In addition to being accessible through `util.inspect.custom`, this +symbol is [registered globally][global symbol registry] and can be +accessed in any environment as `Symbol.for('nodejs.util.inspect.custom')`. + +```js +const inspect = Symbol.for('nodejs.util.inspect.custom'); + +class Password { + constructor(value) { + this.value = value; + } + + toString() { + return 'xxxxxxxx'; + } + + [inspect]() { + return `Password <${this.toString()}>`; + } +} + +const password = new Password('r0sebud'); +console.log(password); +// Prints Password +``` + +See [Custom inspection functions on Objects][] for more details. ### util.inspect.defaultOptions