Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 128 additions & 3 deletions lib/internal/util/inspect.js
Original file line number Diff line number Diff line change
Expand Up @@ -1305,13 +1305,122 @@ function identicalSequenceRange(a, b) {
len++;
}
if (len > 3) {
return { len, offset: i };
return [len, i];
}
}
}
}

return { len: 0, offset: 0 };
return [0, 0];
}

function getDuplicateErrorFrameRanges(frames) {
// Build a map: frame line -> sorted list of indices where it occurs
const result = [];
const lineToPositions = new Map();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these are strings, is Map significantly faster than a null object or SafeMap (or even a $-key-salted normal object) such that this is the best choice?


for (let i = 0; i < frames.length; i++) {
const positions = lineToPositions.get(frames[i]);
if (positions === undefined) {
lineToPositions.set(frames[i], [i]);
} else {
positions.push(i);
}
}

const minimumDuplicateRange = 3;
// Not enough duplicate lines to consider collapsing
if (frames.length - lineToPositions.size <= minimumDuplicateRange) {
return result;
}

for (let i = 0; i < frames.length - minimumDuplicateRange; i++) {
const positions = lineToPositions.get(frames[i]);
// Find the next occurrence of the same line after i, if any
if (positions.length === 1 || positions.at(-1) === i) {
continue;
}

const current = positions.indexOf(i) + 1;
if (current === positions.length) {
continue;
}

// Theoretical maximum range, adjusted while iterating
let range = positions.at(-1) - i;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let range = positions.at(-1) - i;
let range = positions[positions.length - 1] - i;

i assume this is faster, and avoids the need for a primordial

if (range < minimumDuplicateRange) {
continue;
}
let extraSteps;
if (current + 1 < positions.length) {
// Optimize initial step size by choosing the greatest common divisor (GCD)
// of all candidate distances to the same frame line. This tends to match
// the true repeating block size and minimizes fallback iterations.
let gcdRange = 0;
for (let j = current; j < positions.length; j++) {
let distance = positions[j] - i;
while (distance !== 0) {
const remainder = gcdRange % distance;
if (gcdRange !== 0) {
// Add other possible ranges as fallback
extraSteps ??= new Set();
extraSteps.add(gcdRange);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

similarly, this seems like it could be an object, with ObjectKeys at the end, and it'd be faster than spreading a Set into an array?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would be surprised if that's faster, while it will likely not be the major issue (this path is only triggered for some cases and for those, the set / object would contain normally never more than five entries).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's not an issue, can we use SafeMap/SafeSet instead?

}
gcdRange = distance;
distance = remainder;
}
if (gcdRange === 1) break;
}
range = gcdRange;
if (extraSteps) {
extraSteps.delete(range);
extraSteps = [...extraSteps];
}
}
let maxRange = range;
let maxDuplicates = 0;

let duplicateRanges = 0;

for (let nextStart = i + range; /* ignored */ ; nextStart += range) {
let equalFrames = 0;
for (let j = 0; j < range; j++) {
if (frames[i + j] !== frames[nextStart + j]) {
break;
}
equalFrames++;
}
// Adjust the range to match different type of ranges.
if (equalFrames !== range) {
if (!extraSteps?.length) {
break;
}
// Memorize former range in case the smaller one would hide less.
if (duplicateRanges !== 0 && maxRange * maxDuplicates < range * duplicateRanges) {
maxRange = range;
maxDuplicates = duplicateRanges;
}
range = extraSteps.pop();
nextStart = i;
duplicateRanges = 0;
continue;
}
duplicateRanges++;
}

if (maxDuplicates !== 0 && maxRange * maxDuplicates >= range * duplicateRanges) {
range = maxRange;
duplicateRanges = maxDuplicates;
}

if (duplicateRanges * range >= 3) {
result.push([i + range, range, duplicateRanges]);
// Skip over the collapsed portion to avoid overlapping matches.
i += range * (duplicateRanges + 1) - 1;
}
}

return result;
}

function getStackString(ctx, error) {
Expand Down Expand Up @@ -1345,14 +1454,30 @@ function getStackFrames(ctx, err, stack) {
const causeStackStart = StringPrototypeIndexOf(causeStack, '\n at');
if (causeStackStart !== -1) {
const causeFrames = StringPrototypeSplit(StringPrototypeSlice(causeStack, causeStackStart + 1), '\n');
const { len, offset } = identicalSequenceRange(frames, causeFrames);
const { 0: len, 1: offset } = identicalSequenceRange(frames, causeFrames);
if (len > 0) {
const skipped = len - 2;
const msg = ` ... ${skipped} lines matching cause stack trace ...`;
frames.splice(offset + 1, skipped, ctx.stylize(msg, 'undefined'));
}
}
}

// Remove recursive repetitive stack frames in long stacks
if (frames.length > 10) {
const ranges = getDuplicateErrorFrameRanges(frames);

while (ranges.length) {
const { 0: offset, 1: length, 2: duplicateRanges } = ranges.pop();
const msg = ` ... collapsed ${length * duplicateRanges} duplicate lines ` +
'matching above ' +
(duplicateRanges > 1 ?
`${length} lines ${duplicateRanges} times...` :
'lines ...');
frames.splice(offset, length * duplicateRanges, ctx.stylize(msg, 'undefined'));
}
}

return frames;
}

Expand Down
169 changes: 169 additions & 0 deletions test/parallel/test-util-inspect.js
Original file line number Diff line number Diff line change
Expand Up @@ -2920,6 +2920,175 @@ assert.strictEqual(
process.cwd = originalCWD;
}

{
// Use a fake stack to verify the expected colored outcome.
const err = new Error('Hide duplicate frames in long stack');
err.stack = [
'Error: Hide duplicate frames in long stack',
' at A.<anonymous> (/foo/node_modules/bar/baz.js:2:7)',
' at A.<anonymous> (/foo/node_modules/bar/baz.js:2:7)',
' at Module._compile (node:internal/modules/cjs/loader:827:30)',
' at Fancy (node:vm:697:32)',
' at tryModuleLoad (node:internal/modules/cjs/foo:629:12)',
' at Function.Module._load (node:internal/modules/cjs/loader:621:3)',
' at Fancy (node:vm:697:32)',
' at tryModuleLoad (node:internal/modules/cjs/foo:629:12)',
' at Function.Module._load (node:internal/modules/cjs/loader:621:3)',
' at Function.Module._load (node:internal/modules/cjs/loader:621:3)',
' at Function.Module._load (node:internal/modules/cjs/loader:621:3)',
' at Function.Module._load (node:internal/modules/cjs/loader:621:3)',
' at Function.Module._load (node:internal/modules/cjs/loader:621:3)',
' at Function.Module._load (node:internal/modules/cjs/loader:621:3)',
' at Function.Module._load (node:internal/modules/cjs/loader:621:3)',
' at Module.require [as weird/name] (node:internal/aaaaa/loader:735:19)',
' at require (node:internal/modules/helpers:14:16)',
' at Array.forEach (<anonymous>)',
' at require (node:internal/modules/helpers:14:16)',
' at Array.forEach (<anonymous>)',
` at foobar/test/parallel/test-util-inspect.js:2760:12`,
` at Object.<anonymous> (foobar/node_modules/m/folder/file.js:2753:10)`,
' at Module.require [as weird/name] (node:internal/aaaaa/loader:735:19)',
' at require (node:internal/modules/helpers:14:16)',
' at Array.forEach (<anonymous>)',
` at foobar/test/parallel/test-util-inspect.js:2760:12`,
` at Object.<anonymous> (foobar/node_modules/m/folder/file.js:2753:10)`,
' at Module.require [as weird/name] (node:internal/aaaaa/loader:735:19)',
' at require (node:internal/modules/helpers:14:16)',
' at Array.forEach (<anonymous>)',
` at foobar/test/parallel/test-util-inspect.js:2760:12`,
` at Object.<anonymous> (foobar/node_modules/m/folder/file.js:2753:10)`,
' at Module.require [as weird/name] (node:internal/aaaaa/loader:735:19)',
' at require (node:internal/modules/helpers:14:16)',
' at Array.forEach (<anonymous>)',
` at foobar/test/parallel/test-util-inspect.js:2760:12`,
` at Object.<anonymous> (foobar/node_modules/m/folder/file.js:2753:10)`,
' at /test/test-util-inspect.js:2239:9',
' at getActual (node:assert:592:5)',
' at /test/test-util-inspect.js:2239:9',
' at getActual (node:assert:592:5)',
' at /test/test-util-inspect.js:2239:9',
' at getActual (node:assert:592:5)',
].join('\n');

assert.strictEqual(
util.inspect(err, { colors: true }),
'Error: Hide duplicate frames in long stack\n' +
' at A.<anonymous> (/foo/node_modules/\x1B[4mbar\x1B[24m/baz.js:2:7)\n' +
' at A.<anonymous> (/foo/node_modules/\x1B[4mbar\x1B[24m/baz.js:2:7)\n' +
'\x1B[90m at Module._compile (node:internal/modules/cjs/loader:827:30)\x1B[39m\n' +
'\x1B[90m at Fancy (node:vm:697:32)\x1B[39m\n' +
' at tryModuleLoad (node:internal/modules/cjs/foo:629:12)\n' +
'\x1B[90m at Function.Module._load (node:internal/modules/cjs/loader:621:3)\x1B[39m\n' +
'\x1B[90m ... collapsed 3 duplicate lines matching above lines ...\x1B[39m\n' +

'\x1B[90m at Function.Module._load (node:internal/modules/cjs/loader:621:3)\x1B[39m\n' +
'\x1B[90m ... collapsed 5 duplicate lines matching above 1 lines 5 times...\x1B[39m\n' +

' at Module.require [as weird/name] (node:internal/aaaaa/loader:735:19)\n' +
'\x1B[90m at require (node:internal/modules/helpers:14:16)\x1B[39m\n' +
' at Array.forEach (<anonymous>)\n' +
'\x1B[90m at require (node:internal/modules/helpers:14:16)\x1B[39m\n' +
' at Array.forEach (<anonymous>)\n' +
' at foobar/test/parallel/test-util-inspect.js:2760:12\n' +
' at Object.<anonymous> (foobar/node_modules/\x1B[4mm\x1B[24m/folder/file.js:2753:10)\n' +
' at Module.require [as weird/name] (node:internal/aaaaa/loader:735:19)\n' +
'\x1B[90m ... collapsed 10 duplicate lines matching above 5 lines 2 times...\x1B[39m\n' +

'\x1B[90m at require (node:internal/modules/helpers:14:16)\x1B[39m\n' +
' at Array.forEach (<anonymous>)\n' +
' at foobar/test/parallel/test-util-inspect.js:2760:12\n' +
' at Object.<anonymous> (foobar/node_modules/\x1B[4mm\x1B[24m/folder/file.js:2753:10)\n' +
' at /test/test-util-inspect.js:2239:9\n' +
'\x1B[90m at getActual (node:assert:592:5)\x1B[39m\n' +
'\x1B[90m ... collapsed 4 duplicate lines matching above 2 lines 2 times...\x1B[39m',
);

// Use a fake stack to verify the expected colored outcome.
const err2 = new Error('Hide duplicate frames in long stack');
err2.stack = [
'Error: Hide duplicate frames in long stack',
' at A.<anonymous> (/foo/node_modules/bar/baz.js:2:7)',
' at A.<anonymous> (/foo/node_modules/bar/baz.js:2:7)',
' at Module._compile (node:internal/modules/cjs/loader:827:30)',

// 3
' at Fancy (node:vm:697:32)',
' at tryModuleLoad (node:internal/modules/cjs/foo:629:12)',
' at Function.Module._load (node:internal/modules/cjs/loader:621:3)',
' at Fancy (node:vm:697:32)',
' at tryModuleLoad (node:internal/modules/cjs/foo:629:12)',
' at Function.Module._load (node:internal/modules/cjs/loader:621:3)',

// 6 * 1
' at Function.Module._load (node:internal/modules/cjs/loader:621:3)',
' at Function.Module._load (node:internal/modules/cjs/loader:621:3)',
' at Function.Module._load (node:internal/modules/cjs/loader:621:3)',
' at Function.Module._load (node:internal/modules/cjs/loader:621:3)',
' at Function.Module._load (node:internal/modules/cjs/loader:621:3)',
' at Function.Module._load (node:internal/modules/cjs/loader:621:3)',
' at Function.Module._load (node:internal/modules/cjs/loader:621:3)',

// 10
' at require (node:internal/modules/helpers:14:16)',
' at Array.forEach (<anonymous>)',
` at foobar/test/parallel/test-util-inspect.js:2760:12`,
` at Object.<anonymous> (foobar/node_modules/m/folder/file.js:2753:10)`,
' at Module.require [as weird/name] (node:internal/aaaaa/loader:735:19)',
' at Module.require [as weird/name] (node:internal/aaaaa/loader:735:19)',
' at require (node:internal/modules/helpers:14:16)',
' at Array.forEach (<anonymous>)',
` at foobar/test/parallel/test-util-inspect.js:2760:12`,
` at Object.<anonymous> (foobar/node_modules/m/folder/file.js:2753:10)`,

' at require (node:internal/modules/helpers:14:16)',
' at Array.forEach (<anonymous>)',
` at foobar/test/parallel/test-util-inspect.js:2760:12`,
` at Object.<anonymous> (foobar/node_modules/m/folder/file.js:2753:10)`,
' at Module.require [as weird/name] (node:internal/aaaaa/loader:735:19)',
' at Module.require [as weird/name] (node:internal/aaaaa/loader:735:19)',
' at require (node:internal/modules/helpers:14:16)',
' at Array.forEach (<anonymous>)',
` at foobar/test/parallel/test-util-inspect.js:2760:12`,
` at Object.<anonymous> (foobar/node_modules/m/folder/file.js:2753:10)`,

// 2 * 2
' at /test/test-util-inspect.js:2239:9',
' at getActual (node:assert:592:5)',
' at /test/test-util-inspect.js:2239:9',
' at getActual (node:assert:592:5)',
' at /test/test-util-inspect.js:2239:9',
' at getActual (node:assert:592:5)',
].join('\n');

assert.strictEqual(
util.inspect(err2, { colors: true }),
'Error: Hide duplicate frames in long stack\n' +
' at A.<anonymous> (/foo/node_modules/\x1B[4mbar\x1B[24m/baz.js:2:7)\n' +
' at A.<anonymous> (/foo/node_modules/\x1B[4mbar\x1B[24m/baz.js:2:7)\n' +
'\x1B[90m at Module._compile (node:internal/modules/cjs/loader:827:30)\x1B[39m\n' +
'\x1B[90m at Fancy (node:vm:697:32)\x1B[39m\n' +
' at tryModuleLoad (node:internal/modules/cjs/foo:629:12)\n' +
'\x1B[90m at Function.Module._load (node:internal/modules/cjs/loader:621:3)\x1B[39m\n' +
'\x1B[90m ... collapsed 3 duplicate lines matching above lines ...\x1B[39m\n' +
'\x1B[90m at Function.Module._load (node:internal/modules/cjs/loader:621:3)\x1B[39m\n' +
'\x1B[90m ... collapsed 6 duplicate lines matching above 1 lines 6 times...\x1B[39m\n' +
'\x1B[90m at require (node:internal/modules/helpers:14:16)\x1B[39m\n' +
' at Array.forEach (<anonymous>)\n' +
' at foobar/test/parallel/test-util-inspect.js:2760:12\n' +
' at Object.<anonymous> (foobar/node_modules/\x1B[4mm\x1B[24m/folder/file.js:2753:10)\n' +
' at Module.require [as weird/name] (node:internal/aaaaa/loader:735:19)\n' +
' at Module.require [as weird/name] (node:internal/aaaaa/loader:735:19)\n' +
'\x1B[90m at require (node:internal/modules/helpers:14:16)\x1B[39m\n' +
' at Array.forEach (<anonymous>)\n' +
' at foobar/test/parallel/test-util-inspect.js:2760:12\n' +
' at Object.<anonymous> (foobar/node_modules/\x1B[4mm\x1B[24m/folder/file.js:2753:10)\n' +
'\x1B[90m ... collapsed 10 duplicate lines matching above lines ...\x1B[39m\n' +
' at /test/test-util-inspect.js:2239:9\n' +
'\x1B[90m at getActual (node:assert:592:5)\x1B[39m\n' +
'\x1B[90m ... collapsed 4 duplicate lines matching above 2 lines 2 times...\x1B[39m',
);
}

{
// Cross platform checks.
const err = new Error('foo');
Expand Down
Loading