Skip to content

Commit 2de779a

Browse files
committed
util: add minComplexity option to inspect
To limit the maximum computation time this adds a `minComplexity` option. Up to that complexity any object will be fully inspected. As soon as that limit is reached the time is going to be measured for the further inspection and the inspection is limited to the absolute minimum after one second has passed. That makes sure the event loop is not blocked for to long while still producing a good output in almost all cases. To make sure the output is almost deterministic even though a timeout is used, it will only measure the time each 1e5 complexity units. This also limits the performance overhead to the minimum.
1 parent 12ed7c9 commit 2de779a

File tree

3 files changed

+98
-25
lines changed

3 files changed

+98
-25
lines changed

doc/api/util.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -408,11 +408,11 @@ changes:
408408
TODO(BridgeAR): Deprecate `maxArrayLength` and replace it with
409409
`maxEntries`.
410410
-->
411-
* `maxArrayLength` {number} Specifies the maximum number of `Array`,
411+
* `maxArrayLength` {integer} Specifies the maximum number of `Array`,
412412
[`TypedArray`][], [`WeakMap`][] and [`WeakSet`][] elements to include when
413413
formatting. Set to `null` or `Infinity` to show all elements. Set to `0` or
414414
negative to show no elements. **Default:** `100`.
415-
* `breakLength` {number} The length at which an object's keys are split
415+
* `breakLength` {integer} The length at which an object's keys are split
416416
across multiple lines. Set to `Infinity` to format an object as a single
417417
line. **Default:** `60` for legacy compatibility.
418418
* `compact` {boolean} Setting this to `false` changes the default indentation
@@ -422,6 +422,13 @@ changes:
422422
objects the same as arrays. Note that no text will be reduced below 16
423423
characters, no matter the `breakLength` size. For more information, see the
424424
example below. **Default:** `true`.
425+
* `budget` {integer} This limits the maximum time spend inspecting an object.
426+
The budget corresponds roughly to the number of properties that are
427+
inspected. If the object exceeds that complexity while inspecting, the
428+
inspection is continued up to one more second. If the inspection does not
429+
complete in that second, it will limit all further inspection to the
430+
absolute minimum and an `INSPECTION_ABORTED` warning is emitted.
431+
**Default:** `Infinity`.
425432
* Returns: {string} The representation of passed object
426433

427434
The `util.inspect()` method returns a string representation of `object` that is

lib/util.js

Lines changed: 60 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,8 @@ const inspectDefaultOptions = Object.seal({
9999
showProxy: false,
100100
maxArrayLength: 100,
101101
breakLength: 60,
102-
compact: true
102+
compact: true,
103+
budget: Infinity
103104
});
104105

105106
const kObjectType = 0;
@@ -406,24 +407,27 @@ function inspect(value, opts) {
406407
maxArrayLength: inspectDefaultOptions.maxArrayLength,
407408
breakLength: inspectDefaultOptions.breakLength,
408409
indentationLvl: 0,
409-
compact: inspectDefaultOptions.compact
410+
compact: inspectDefaultOptions.compact,
411+
budget: inspectDefaultOptions.budget
410412
};
411-
// Legacy...
412-
if (arguments.length > 2) {
413-
if (arguments[2] !== undefined) {
414-
ctx.depth = arguments[2];
415-
}
416-
if (arguments.length > 3 && arguments[3] !== undefined) {
417-
ctx.colors = arguments[3];
413+
if (arguments.length > 1) {
414+
// Legacy...
415+
if (arguments.length > 2) {
416+
if (arguments[2] !== undefined) {
417+
ctx.depth = arguments[2];
418+
}
419+
if (arguments.length > 3 && arguments[3] !== undefined) {
420+
ctx.colors = arguments[3];
421+
}
418422
}
419-
}
420-
// Set user-specified options
421-
if (typeof opts === 'boolean') {
422-
ctx.showHidden = opts;
423-
} else if (opts) {
424-
const optKeys = Object.keys(opts);
425-
for (var i = 0; i < optKeys.length; i++) {
426-
ctx[optKeys[i]] = opts[optKeys[i]];
423+
// Set user-specified options
424+
if (typeof opts === 'boolean') {
425+
ctx.showHidden = opts;
426+
} else if (opts) {
427+
const optKeys = Object.keys(opts);
428+
for (var i = 0; i < optKeys.length; i++) {
429+
ctx[optKeys[i]] = opts[optKeys[i]];
430+
}
427431
}
428432
}
429433
if (ctx.colors) ctx.stylize = stylizeWithColor;
@@ -619,18 +623,45 @@ function noPrototypeIterator(ctx, value, recurseTimes) {
619623
}
620624
}
621625

626+
function getClockTime(start) {
627+
const ts = process.hrtime(start);
628+
return ts[0] * 1e3 + ts[1] / 1e6;
629+
}
630+
622631
// Note: using `formatValue` directly requires the indentation level to be
623632
// corrected by setting `ctx.indentationLvL += diff` and then to decrease the
624633
// value afterwards again.
625634
function formatValue(ctx, value, recurseTimes) {
626-
// Primitive types cannot have properties
635+
// Primitive types cannot have properties.
627636
if (typeof value !== 'object' && typeof value !== 'function') {
628637
return formatPrimitive(ctx.stylize, value, ctx);
629638
}
630639
if (value === null) {
631640
return ctx.stylize('null', 'null');
632641
}
633642

643+
if (ctx.budget < 0) {
644+
if (ctx.stop === true) {
645+
const name = getConstructorName(value) || value[Symbol.toStringTag];
646+
return ctx.stylize(`[${name || 'Object'}]`, 'special');
647+
}
648+
if (ctx.time === undefined) {
649+
ctx.time = process.hrtime();
650+
} else if (getClockTime(ctx.time) > 1e3) {
651+
process.emitWarning('util.inspect took to long.', {
652+
code: 'INSPECTION_ABORTED',
653+
detail: 'util.inspect() received an object that was very big and ' +
654+
'complex to inspect. Further inspection was limited to a ' +
655+
'minimum to stop blocking the event loop.'
656+
});
657+
// Since we only measure the time each 1e5 the output should be almost
658+
// deterministic.
659+
ctx.stop = true;
660+
}
661+
// Subtract 1e5 to know when to check again.
662+
ctx.budget += 1e5;
663+
}
664+
634665
if (ctx.showProxy) {
635666
const proxy = getProxyDetails(value);
636667
if (proxy !== undefined) {
@@ -639,11 +670,11 @@ function formatValue(ctx, value, recurseTimes) {
639670
}
640671

641672
// Provide a hook for user-specified inspect functions.
642-
// Check that value is an object with an inspect function on it
673+
// Check that value is an object with an inspect function on it.
643674
if (ctx.customInspect) {
644675
const maybeCustom = value[customInspectSymbol];
645676
if (typeof maybeCustom === 'function' &&
646-
// Filter out the util module, its inspect function is special
677+
// Filter out the util module, its inspect function is special.
647678
maybeCustom !== exports.inspect &&
648679
// Also filter out any prototype objects using the circular check.
649680
!(value.constructor && value.constructor.prototype === value)) {
@@ -685,7 +716,7 @@ function formatRaw(ctx, value, recurseTimes) {
685716

686717
let extrasType = kObjectType;
687718

688-
// Iterators and the rest are split to reduce checks
719+
// Iterators and the rest are split to reduce checks.
689720
if (value[Symbol.iterator]) {
690721
noIterator = false;
691722
if (Array.isArray(value)) {
@@ -766,7 +797,9 @@ function formatRaw(ctx, value, recurseTimes) {
766797
}
767798
base = dateToISOString(value);
768799
} else if (isError(value)) {
769-
// Make error with message first say the error
800+
// Normalize budget because error inspection is very slow.
801+
ctx.budget -= 5;
802+
// Make error with message first say the error.
770803
base = formatError(value);
771804
// Wrap the error in brackets in case it has no stack trace.
772805
const stackStart = base.indexOf('\n at');
@@ -885,6 +918,7 @@ function formatRaw(ctx, value, recurseTimes) {
885918
}
886919
ctx.seen.pop();
887920

921+
ctx.budget += output.length;
888922
return reduceToSingleString(ctx, output, base, braces);
889923
}
890924

@@ -1057,8 +1091,9 @@ function formatTypedArray(ctx, value, recurseTimes) {
10571091
formatBigInt;
10581092
for (var i = 0; i < maxLength; ++i)
10591093
output[i] = elementFormatter(ctx.stylize, value[i]);
1060-
if (remaining > 0)
1094+
if (remaining > 0) {
10611095
output[i] = `... ${remaining} more item${remaining > 1 ? 's' : ''}`;
1096+
}
10621097
if (ctx.showHidden) {
10631098
// .buffer goes last, it's not a primitive like the others.
10641099
ctx.indentationLvl += 2;
@@ -1247,6 +1282,8 @@ function formatProperty(ctx, value, recurseTimes, key, type) {
12471282
} else if (keyStrRegExp.test(key)) {
12481283
name = ctx.stylize(key, 'name');
12491284
} else {
1285+
// Normalize budget because replacing keys is slow.
1286+
ctx.budget -= 3;
12501287
name = ctx.stylize(strEscape(key), 'string');
12511288
}
12521289
return `${name}:${extra}${str}`;
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
'use strict';
2+
const common = require('../common');
3+
4+
// `util.inspect` is capable of computing a string that is bigger than 2 ** 28
5+
// in one second, so let's just skip this test on 32bit systems.
6+
common.skipIf32Bits();
7+
8+
// Test that big objects are running only up to the maximum complexity plus ~1
9+
// second.
10+
11+
const util = require('util');
12+
13+
// Create a difficult to stringify object. Without the limit this would crash.
14+
let last = {};
15+
const obj = last;
16+
17+
for (let i = 0; i < 1000; i++) {
18+
last.next = { circular: obj, last, obj: { a: 1, b: 2, c: true } };
19+
last = last.next;
20+
obj[i] = last;
21+
}
22+
23+
common.expectWarning(
24+
'Warning',
25+
'util.inspect took to long.',
26+
'INSPECTION_ABORTED'
27+
);
28+
29+
util.inspect(obj, { depth: Infinity, budget: 1e6 });

0 commit comments

Comments
 (0)