From c6d9a4dd44630bcfd0d89eaa507e3e6965d1f5cd Mon Sep 17 00:00:00 2001 From: Ruben Bridgewater Date: Fri, 24 Aug 2018 02:13:55 +0200 Subject: [PATCH 1/3] util: remove TODO I do not plan on doing this anymore. --- lib/util.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/util.js b/lib/util.js index f0b7285fe01035..82d924fdd8960b 100644 --- a/lib/util.js +++ b/lib/util.js @@ -430,8 +430,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 5f7d097bf6b4f9c9b2bed1f751e440202d962d85 Mon Sep 17 00:00:00 2001 From: Ruben Bridgewater Date: Mon, 13 Aug 2018 17:51:58 +0200 Subject: [PATCH 2/3] 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. --- 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/priority-queue.js | 2 +- benchmark/util/splice-one.js | 2 +- benchmark/util/type-check.js | 2 +- test/parallel/test-benchmark-util.js | 1 + 9 files changed, 19 insertions(+), 29 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/priority-queue.js b/benchmark/util/priority-queue.js index 51a696439a2864..9cff7cbbacba84 100644 --- a/benchmark/util/priority-queue.js +++ b/benchmark/util/priority-queue.js @@ -3,7 +3,7 @@ const common = require('../common'); const bench = common.createBenchmark(main, { - n: [1e6] + n: [1e5] }, { flags: ['--expose-internals'] }); function main({ n, type }) { 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 d9ba7bc209de07..d9acdaedf5a57f 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 2b1c2a46783a9496e4559e6dfe15bf7ef7cf19cd Mon Sep 17 00:00:00 2001 From: Ruben Bridgewater Date: Sun, 12 Aug 2018 21:45:36 +0200 Subject: [PATCH 3/3] 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. --- lib/util.js | 463 ++++++++++++++--------------- test/parallel/test-util-inspect.js | 2 +- 2 files changed, 223 insertions(+), 242 deletions(-) diff --git a/lib/util.js b/lib/util.js index 82d924fdd8960b..35bd9ec46f5b6f 100644 --- a/lib/util.js +++ b/lib/util.js @@ -33,11 +33,16 @@ const { isBuffer } = require('buffer').Buffer; const { internalBinding } = require('internal/bootstrap/loaders'); const { + getOwnNonIndexProperties, getPromiseDetails, getProxyDetails, kPending, kRejected, - previewEntries + previewEntries, + propertyFilter: { + ALL_PROPERTIES, + ONLY_ENUMERABLE + } } = internalBinding('util'); const types = internalBinding('types'); @@ -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; @@ -475,10 +485,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'); @@ -511,6 +526,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], @@ -531,8 +596,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 @@ -571,19 +634,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); } } @@ -614,78 +665,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 { @@ -693,34 +731,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'); @@ -739,7 +778,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) { @@ -747,14 +786,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}{`; @@ -768,50 +807,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('', '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('', '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; @@ -823,7 +854,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)}{}`; @@ -841,36 +872,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)) @@ -935,20 +964,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; @@ -957,7 +979,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. @@ -969,91 +991,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' : ''}`); - } - 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)); + output.push(formatProperty(ctx, value, recurseTimes, key, kArrayType)); + index++; + } + 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); const elementFormatter = value.length > 0 && typeof value[0] === 'number' ? formatNumber : formatBigInt; @@ -1076,52 +1074,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); @@ -1139,12 +1124,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; @@ -1175,37 +1158,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('', '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) { @@ -1222,19 +1206,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) { @@ -1255,7 +1236,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 b5ccdf019a5226..0994f262996cf5 100644 --- a/test/parallel/test-util-inspect.js +++ b/test/parallel/test-util-inspect.js @@ -872,7 +872,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 }" ); }