Skip to content

Commit

Permalink
util: add minComplexity option to inspect
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
BridgeAR committed Sep 7, 2018
1 parent 12ed7c9 commit 2de779a
Show file tree
Hide file tree
Showing 3 changed files with 98 additions and 25 deletions.
11 changes: 9 additions & 2 deletions doc/api/util.md
Original file line number Diff line number Diff line change
Expand Up @@ -408,11 +408,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
Expand All @@ -422,6 +422,13 @@ changes:
objects the same as arrays. Note that no text will be reduced below 16
characters, no matter the `breakLength` size. For more information, see the
example below. **Default:** `true`.
* `budget` {integer} This limits the maximum time spend inspecting an object.
The budget corresponds roughly to the number of properties that are
inspected. If the object exceeds that complexity while inspecting, the
inspection is continued up to one more second. If the inspection does not
complete in that second, it will limit all further inspection to the
absolute minimum and an `INSPECTION_ABORTED` warning is emitted.
**Default:** `Infinity`.
* Returns: {string} The representation of passed object

The `util.inspect()` method returns a string representation of `object` that is
Expand Down
83 changes: 60 additions & 23 deletions lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,8 @@ const inspectDefaultOptions = Object.seal({
showProxy: false,
maxArrayLength: 100,
breakLength: 60,
compact: true
compact: true,
budget: Infinity
});

const kObjectType = 0;
Expand Down Expand Up @@ -406,24 +407,27 @@ function inspect(value, opts) {
maxArrayLength: inspectDefaultOptions.maxArrayLength,
breakLength: inspectDefaultOptions.breakLength,
indentationLvl: 0,
compact: inspectDefaultOptions.compact
compact: inspectDefaultOptions.compact,
budget: inspectDefaultOptions.budget
};
// 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 > 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];
}
}
}
// 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;
Expand Down Expand Up @@ -619,18 +623,45 @@ function noPrototypeIterator(ctx, value, recurseTimes) {
}
}

function getClockTime(start) {
const ts = process.hrtime(start);
return ts[0] * 1e3 + ts[1] / 1e6;
}

// Note: using `formatValue` directly requires the indentation level to be
// 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);
}
if (value === null) {
return ctx.stylize('null', 'null');
}

if (ctx.budget < 0) {
if (ctx.stop === true) {
const name = getConstructorName(value) || value[Symbol.toStringTag];
return ctx.stylize(`[${name || 'Object'}]`, 'special');
}
if (ctx.time === undefined) {
ctx.time = process.hrtime();
} else if (getClockTime(ctx.time) > 1e3) {
process.emitWarning('util.inspect took to long.', {
code: 'INSPECTION_ABORTED',
detail: 'util.inspect() received an object that was very big and ' +
'complex to inspect. Further inspection was limited to a ' +
'minimum to stop blocking the event loop.'
});
// Since we only measure the time each 1e5 the output should be almost
// deterministic.
ctx.stop = true;
}
// Subtract 1e5 to know when to check again.
ctx.budget += 1e5;
}

if (ctx.showProxy) {
const proxy = getProxyDetails(value);
if (proxy !== undefined) {
Expand All @@ -639,11 +670,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)) {
Expand Down Expand Up @@ -685,7 +716,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)) {
Expand Down Expand Up @@ -766,7 +797,9 @@ function formatRaw(ctx, value, recurseTimes) {
}
base = dateToISOString(value);
} else if (isError(value)) {
// Make error with message first say the error
// Normalize budget because error inspection is very slow.
ctx.budget -= 5;
// 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');
Expand Down Expand Up @@ -885,6 +918,7 @@ function formatRaw(ctx, value, recurseTimes) {
}
ctx.seen.pop();

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

Expand Down Expand Up @@ -1057,8 +1091,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;
Expand Down Expand Up @@ -1247,6 +1282,8 @@ function formatProperty(ctx, value, recurseTimes, key, type) {
} else if (keyStrRegExp.test(key)) {
name = ctx.stylize(key, 'name');
} else {
// Normalize budget because replacing keys is slow.
ctx.budget -= 3;
name = ctx.stylize(strEscape(key), 'string');
}
return `${name}:${extra}${str}`;
Expand Down
29 changes: 29 additions & 0 deletions test/parallel/test-util-inspect-long-running.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
'use strict';
const common = require('../common');

// `util.inspect` is capable of computing a string that is bigger than 2 ** 28
// in one second, so let's just skip this test on 32bit systems.
common.skipIf32Bits();

// Test that big objects are running only up to the maximum complexity plus ~1
// second.

const util = require('util');

// Create a difficult to stringify object. Without the limit this would crash.
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;
}

common.expectWarning(
'Warning',
'util.inspect took to long.',
'INSPECTION_ABORTED'
);

util.inspect(obj, { depth: Infinity, budget: 1e6 });

0 comments on commit 2de779a

Please sign in to comment.