util: limit inspection output size to 128 MB
The maximum hard limit that `util.inspect()` could theoretically handle is the maximum string size. That is ~2 ** 28 on 32 bit systems and ~2 ** 30 on 64 bit systems. Due to the recursive algorithm a complex object could easily exceed that limit without throwing an error right away and therefore crashing the application by exceeding the heap limit. `util.inspect()` is fast enough to compute 128 MB of data below one second on an Intel(R) Core(TM) i7-5600U CPU. This hard limit allows to inspect arbitrary big objects from now on without crashing the application or blocking the event loop significantly. PR-URL: https://github.com/nodejs/node/pull/22756 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: John-David Dalton <john.david.dalton@gmail.com>
This commit is contained in:
parent
1cee085367
commit
eb61127c48
@ -360,6 +360,10 @@ stream.write('With ES6');
|
|||||||
<!-- YAML
|
<!-- YAML
|
||||||
added: v0.3.0
|
added: v0.3.0
|
||||||
changes:
|
changes:
|
||||||
|
- version: REPLACEME
|
||||||
|
pr-url: https://github.com/nodejs/node/pull/22756
|
||||||
|
description: The inspection output is now limited to about 128 MB. Data
|
||||||
|
above that size will not be fully inspected.
|
||||||
- version: v10.6.0
|
- version: v10.6.0
|
||||||
pr-url: https://github.com/nodejs/node/pull/20725
|
pr-url: https://github.com/nodejs/node/pull/20725
|
||||||
description: Inspecting linked lists and similar objects is now possible
|
description: Inspecting linked lists and similar objects is now possible
|
||||||
@ -408,11 +412,11 @@ changes:
|
|||||||
TODO(BridgeAR): Deprecate `maxArrayLength` and replace it with
|
TODO(BridgeAR): Deprecate `maxArrayLength` and replace it with
|
||||||
`maxEntries`.
|
`maxEntries`.
|
||||||
-->
|
-->
|
||||||
* `maxArrayLength` {number} Specifies the maximum number of `Array`,
|
* `maxArrayLength` {integer} Specifies the maximum number of `Array`,
|
||||||
[`TypedArray`][], [`WeakMap`][] and [`WeakSet`][] elements to include when
|
[`TypedArray`][], [`WeakMap`][] and [`WeakSet`][] elements to include when
|
||||||
formatting. Set to `null` or `Infinity` to show all elements. Set to `0` or
|
formatting. Set to `null` or `Infinity` to show all elements. Set to `0` or
|
||||||
negative to show no elements. **Default:** `100`.
|
negative to show no elements. **Default:** `100`.
|
||||||
* `breakLength` {number} The length at which an object's keys are split
|
* `breakLength` {integer} The length at which an object's keys are split
|
||||||
across multiple lines. Set to `Infinity` to format an object as a single
|
across multiple lines. Set to `Infinity` to format an object as a single
|
||||||
line. **Default:** `60` for legacy compatibility.
|
line. **Default:** `60` for legacy compatibility.
|
||||||
* `compact` {boolean} Setting this to `false` changes the default indentation
|
* `compact` {boolean} Setting this to `false` changes the default indentation
|
||||||
@ -532,9 +536,10 @@ console.log(inspect(weakSet, { showHidden: true }));
|
|||||||
```
|
```
|
||||||
|
|
||||||
Please note that `util.inspect()` is a synchronous method that is mainly
|
Please note that `util.inspect()` is a synchronous method that is mainly
|
||||||
intended as a debugging tool. Some input values can have a significant
|
intended as a debugging tool. Its maximum output length is limited to
|
||||||
performance overhead that can block the event loop. Use this function
|
approximately 128 MB and input values that result in output bigger than that
|
||||||
with care and never in a hot code path.
|
will not be inspected fully. Such values can have a significant performance
|
||||||
|
overhead that can block the event loop for a significant amount of time.
|
||||||
|
|
||||||
### Customizing `util.inspect` colors
|
### Customizing `util.inspect` colors
|
||||||
|
|
||||||
|
69
lib/util.js
69
lib/util.js
@ -406,24 +406,27 @@ function inspect(value, opts) {
|
|||||||
maxArrayLength: inspectDefaultOptions.maxArrayLength,
|
maxArrayLength: inspectDefaultOptions.maxArrayLength,
|
||||||
breakLength: inspectDefaultOptions.breakLength,
|
breakLength: inspectDefaultOptions.breakLength,
|
||||||
indentationLvl: 0,
|
indentationLvl: 0,
|
||||||
compact: inspectDefaultOptions.compact
|
compact: inspectDefaultOptions.compact,
|
||||||
|
budget: {}
|
||||||
};
|
};
|
||||||
// Legacy...
|
if (arguments.length > 1) {
|
||||||
if (arguments.length > 2) {
|
// Legacy...
|
||||||
if (arguments[2] !== undefined) {
|
if (arguments.length > 2) {
|
||||||
ctx.depth = arguments[2];
|
if (arguments[2] !== undefined) {
|
||||||
|
ctx.depth = arguments[2];
|
||||||
|
}
|
||||||
|
if (arguments.length > 3 && arguments[3] !== undefined) {
|
||||||
|
ctx.colors = arguments[3];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (arguments.length > 3 && arguments[3] !== undefined) {
|
// Set user-specified options
|
||||||
ctx.colors = arguments[3];
|
if (typeof opts === 'boolean') {
|
||||||
}
|
ctx.showHidden = opts;
|
||||||
}
|
} else if (opts) {
|
||||||
// Set user-specified options
|
const optKeys = Object.keys(opts);
|
||||||
if (typeof opts === 'boolean') {
|
for (var i = 0; i < optKeys.length; i++) {
|
||||||
ctx.showHidden = opts;
|
ctx[optKeys[i]] = opts[optKeys[i]];
|
||||||
} else if (opts) {
|
}
|
||||||
const optKeys = Object.keys(opts);
|
|
||||||
for (var i = 0; i < optKeys.length; i++) {
|
|
||||||
ctx[optKeys[i]] = opts[optKeys[i]];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (ctx.colors) ctx.stylize = stylizeWithColor;
|
if (ctx.colors) ctx.stylize = stylizeWithColor;
|
||||||
@ -623,7 +626,7 @@ function noPrototypeIterator(ctx, value, recurseTimes) {
|
|||||||
// corrected by setting `ctx.indentationLvL += diff` and then to decrease the
|
// corrected by setting `ctx.indentationLvL += diff` and then to decrease the
|
||||||
// value afterwards again.
|
// value afterwards again.
|
||||||
function formatValue(ctx, value, recurseTimes) {
|
function formatValue(ctx, value, recurseTimes) {
|
||||||
// Primitive types cannot have properties
|
// Primitive types cannot have properties.
|
||||||
if (typeof value !== 'object' && typeof value !== 'function') {
|
if (typeof value !== 'object' && typeof value !== 'function') {
|
||||||
return formatPrimitive(ctx.stylize, value, ctx);
|
return formatPrimitive(ctx.stylize, value, ctx);
|
||||||
}
|
}
|
||||||
@ -631,6 +634,11 @@ function formatValue(ctx, value, recurseTimes) {
|
|||||||
return ctx.stylize('null', 'null');
|
return ctx.stylize('null', 'null');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ctx.stop !== undefined) {
|
||||||
|
const name = getConstructorName(value) || value[Symbol.toStringTag];
|
||||||
|
return ctx.stylize(`[${name || 'Object'}]`, 'special');
|
||||||
|
}
|
||||||
|
|
||||||
if (ctx.showProxy) {
|
if (ctx.showProxy) {
|
||||||
const proxy = getProxyDetails(value);
|
const proxy = getProxyDetails(value);
|
||||||
if (proxy !== undefined) {
|
if (proxy !== undefined) {
|
||||||
@ -639,11 +647,11 @@ function formatValue(ctx, value, recurseTimes) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Provide a hook for user-specified inspect functions.
|
// Provide a hook for user-specified inspect functions.
|
||||||
// Check that value is an object with an inspect function on it
|
// Check that value is an object with an inspect function on it.
|
||||||
if (ctx.customInspect) {
|
if (ctx.customInspect) {
|
||||||
const maybeCustom = value[customInspectSymbol];
|
const maybeCustom = value[customInspectSymbol];
|
||||||
if (typeof maybeCustom === 'function' &&
|
if (typeof maybeCustom === 'function' &&
|
||||||
// Filter out the util module, its inspect function is special
|
// Filter out the util module, its inspect function is special.
|
||||||
maybeCustom !== exports.inspect &&
|
maybeCustom !== exports.inspect &&
|
||||||
// Also filter out any prototype objects using the circular check.
|
// Also filter out any prototype objects using the circular check.
|
||||||
!(value.constructor && value.constructor.prototype === value)) {
|
!(value.constructor && value.constructor.prototype === value)) {
|
||||||
@ -685,7 +693,7 @@ function formatRaw(ctx, value, recurseTimes) {
|
|||||||
|
|
||||||
let extrasType = kObjectType;
|
let extrasType = kObjectType;
|
||||||
|
|
||||||
// Iterators and the rest are split to reduce checks
|
// Iterators and the rest are split to reduce checks.
|
||||||
if (value[Symbol.iterator]) {
|
if (value[Symbol.iterator]) {
|
||||||
noIterator = false;
|
noIterator = false;
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
@ -766,7 +774,7 @@ function formatRaw(ctx, value, recurseTimes) {
|
|||||||
}
|
}
|
||||||
base = dateToISOString(value);
|
base = dateToISOString(value);
|
||||||
} else if (isError(value)) {
|
} else if (isError(value)) {
|
||||||
// Make error with message first say the error
|
// Make error with message first say the error.
|
||||||
base = formatError(value);
|
base = formatError(value);
|
||||||
// Wrap the error in brackets in case it has no stack trace.
|
// Wrap the error in brackets in case it has no stack trace.
|
||||||
const stackStart = base.indexOf('\n at');
|
const stackStart = base.indexOf('\n at');
|
||||||
@ -885,7 +893,21 @@ function formatRaw(ctx, value, recurseTimes) {
|
|||||||
}
|
}
|
||||||
ctx.seen.pop();
|
ctx.seen.pop();
|
||||||
|
|
||||||
return reduceToSingleString(ctx, output, base, braces);
|
const res = reduceToSingleString(ctx, output, base, braces);
|
||||||
|
const budget = ctx.budget[ctx.indentationLvl] || 0;
|
||||||
|
const newLength = budget + res.length;
|
||||||
|
ctx.budget[ctx.indentationLvl] = newLength;
|
||||||
|
// If any indentationLvl exceeds this limit, limit further inspecting to the
|
||||||
|
// minimum. Otherwise the recursive algorithm might continue inspecting the
|
||||||
|
// object even though the maximum string size (~2 ** 28 on 32 bit systems and
|
||||||
|
// ~2 ** 30 on 64 bit systems) exceeded. The actual output is not limited at
|
||||||
|
// exactly 2 ** 27 but a bit higher. This depends on the object shape.
|
||||||
|
// This limit also makes sure that huge objects don't block the event loop
|
||||||
|
// significantly.
|
||||||
|
if (newLength > 2 ** 27) {
|
||||||
|
ctx.stop = true;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleMaxCallStackSize(ctx, err, constructor, tag) {
|
function handleMaxCallStackSize(ctx, err, constructor, tag) {
|
||||||
@ -1057,8 +1079,9 @@ function formatTypedArray(ctx, value, recurseTimes) {
|
|||||||
formatBigInt;
|
formatBigInt;
|
||||||
for (var i = 0; i < maxLength; ++i)
|
for (var i = 0; i < maxLength; ++i)
|
||||||
output[i] = elementFormatter(ctx.stylize, value[i]);
|
output[i] = elementFormatter(ctx.stylize, value[i]);
|
||||||
if (remaining > 0)
|
if (remaining > 0) {
|
||||||
output[i] = `... ${remaining} more item${remaining > 1 ? 's' : ''}`;
|
output[i] = `... ${remaining} more item${remaining > 1 ? 's' : ''}`;
|
||||||
|
}
|
||||||
if (ctx.showHidden) {
|
if (ctx.showHidden) {
|
||||||
// .buffer goes last, it's not a primitive like the others.
|
// .buffer goes last, it's not a primitive like the others.
|
||||||
ctx.indentationLvl += 2;
|
ctx.indentationLvl += 2;
|
||||||
|
20
test/parallel/test-util-inspect-long-running.js
Normal file
20
test/parallel/test-util-inspect-long-running.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
require('../common');
|
||||||
|
|
||||||
|
// Test that huge objects don't crash due to exceeding the maximum heap size.
|
||||||
|
|
||||||
|
const util = require('util');
|
||||||
|
|
||||||
|
// Create a difficult to stringify object. Without the artificial limitation
|
||||||
|
// this would crash or throw an maximum string size error.
|
||||||
|
let last = {};
|
||||||
|
const obj = last;
|
||||||
|
|
||||||
|
for (let i = 0; i < 1000; i++) {
|
||||||
|
last.next = { circular: obj, last, obj: { a: 1, b: 2, c: true } };
|
||||||
|
last = last.next;
|
||||||
|
obj[i] = last;
|
||||||
|
}
|
||||||
|
|
||||||
|
util.inspect(obj, { depth: Infinity });
|
Loading…
x
Reference in New Issue
Block a user