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
|
||||
added: v0.3.0
|
||||
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
|
||||
pr-url: https://github.com/nodejs/node/pull/20725
|
||||
description: Inspecting linked lists and similar objects is now possible
|
||||
@ -408,11 +412,11 @@ changes:
|
||||
TODO(BridgeAR): Deprecate `maxArrayLength` and replace it with
|
||||
`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
|
||||
formatting. Set to `null` or `Infinity` to show all elements. Set to `0` or
|
||||
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
|
||||
line. **Default:** `60` for legacy compatibility.
|
||||
* `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
|
||||
intended as a debugging tool. Some input values can have a significant
|
||||
performance overhead that can block the event loop. Use this function
|
||||
with care and never in a hot code path.
|
||||
intended as a debugging tool. Its maximum output length is limited to
|
||||
approximately 128 MB and input values that result in output bigger than that
|
||||
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
|
||||
|
||||
|
69
lib/util.js
69
lib/util.js
@ -406,24 +406,27 @@ function inspect(value, opts) {
|
||||
maxArrayLength: inspectDefaultOptions.maxArrayLength,
|
||||
breakLength: inspectDefaultOptions.breakLength,
|
||||
indentationLvl: 0,
|
||||
compact: inspectDefaultOptions.compact
|
||||
compact: inspectDefaultOptions.compact,
|
||||
budget: {}
|
||||
};
|
||||
// Legacy...
|
||||
if (arguments.length > 2) {
|
||||
if (arguments[2] !== undefined) {
|
||||
ctx.depth = arguments[2];
|
||||
if (arguments.length > 1) {
|
||||
// Legacy...
|
||||
if (arguments.length > 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) {
|
||||
ctx.colors = arguments[3];
|
||||
}
|
||||
}
|
||||
// Set user-specified options
|
||||
if (typeof opts === 'boolean') {
|
||||
ctx.showHidden = opts;
|
||||
} else if (opts) {
|
||||
const optKeys = Object.keys(opts);
|
||||
for (var i = 0; i < optKeys.length; i++) {
|
||||
ctx[optKeys[i]] = opts[optKeys[i]];
|
||||
// Set user-specified options
|
||||
if (typeof opts === 'boolean') {
|
||||
ctx.showHidden = opts;
|
||||
} 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;
|
||||
@ -623,7 +626,7 @@ function noPrototypeIterator(ctx, value, recurseTimes) {
|
||||
// corrected by setting `ctx.indentationLvL += diff` and then to decrease the
|
||||
// value afterwards again.
|
||||
function formatValue(ctx, value, recurseTimes) {
|
||||
// Primitive types cannot have properties
|
||||
// Primitive types cannot have properties.
|
||||
if (typeof value !== 'object' && typeof value !== 'function') {
|
||||
return formatPrimitive(ctx.stylize, value, ctx);
|
||||
}
|
||||
@ -631,6 +634,11 @@ function formatValue(ctx, value, recurseTimes) {
|
||||
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) {
|
||||
const proxy = getProxyDetails(value);
|
||||
if (proxy !== undefined) {
|
||||
@ -639,11 +647,11 @@ function formatValue(ctx, value, recurseTimes) {
|
||||
}
|
||||
|
||||
// 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) {
|
||||
const maybeCustom = value[customInspectSymbol];
|
||||
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 &&
|
||||
// Also filter out any prototype objects using the circular check.
|
||||
!(value.constructor && value.constructor.prototype === value)) {
|
||||
@ -685,7 +693,7 @@ function formatRaw(ctx, value, recurseTimes) {
|
||||
|
||||
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]) {
|
||||
noIterator = false;
|
||||
if (Array.isArray(value)) {
|
||||
@ -766,7 +774,7 @@ function formatRaw(ctx, value, recurseTimes) {
|
||||
}
|
||||
base = dateToISOString(value);
|
||||
} else if (isError(value)) {
|
||||
// Make error with message first say the error
|
||||
// Make error with message first say the error.
|
||||
base = formatError(value);
|
||||
// Wrap the error in brackets in case it has no stack trace.
|
||||
const stackStart = base.indexOf('\n at');
|
||||
@ -885,7 +893,21 @@ function formatRaw(ctx, value, recurseTimes) {
|
||||
}
|
||||
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) {
|
||||
@ -1057,8 +1079,9 @@ function formatTypedArray(ctx, value, recurseTimes) {
|
||||
formatBigInt;
|
||||
for (var i = 0; i < maxLength; ++i)
|
||||
output[i] = elementFormatter(ctx.stylize, value[i]);
|
||||
if (remaining > 0)
|
||||
if (remaining > 0) {
|
||||
output[i] = `... ${remaining} more item${remaining > 1 ? 's' : ''}`;
|
||||
}
|
||||
if (ctx.showHidden) {
|
||||
// .buffer goes last, it's not a primitive like the others.
|
||||
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