From ea10a7baa1e780c4160c125c7da5e0315dc56f0c Mon Sep 17 00:00:00 2001 From: Ruben Bridgewater Date: Mon, 11 Aug 2025 18:22:18 -0400 Subject: [PATCH 1/8] util: hide duplicated stack frames when using util.inspect Long stack traces often have duplicated stack frames from recursive calls. These make it difficult to identify important parts of the stack. This hides the duplicated ones and notifies the user which lines were hidden. --- lib/internal/util/inspect.js | 56 +++++++++++++++++++++++-- test/parallel/test-util-inspect.js | 66 ++++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+), 3 deletions(-) diff --git a/lib/internal/util/inspect.js b/lib/internal/util/inspect.js index 78318243e65eca..51f4de972e8490 100644 --- a/lib/internal/util/inspect.js +++ b/lib/internal/util/inspect.js @@ -1305,13 +1305,47 @@ 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) { + const result = []; + for (let i = 0; i < frames.length - 3; i++) { + // Find the first duplicate frame. + const pos = frames.indexOf(frames[i], i + 1); + const range = pos - i; + if (pos !== -1 && range >= 3) { + let duplicateRanges = 0; + // Compare ranges line-by-line + for (let nextStart = i + range; nextStart < frames.length - range; nextStart += range) { + let allEqual = true; + for (let j = 0; j < range; j++) { + if (frames[i + j] !== frames[nextStart + j]) { + allEqual = false; + break; + } + } + if (allEqual) { + // Matched. We have a duplicate range + duplicateRanges++; + } else { + break; + } + } + if (duplicateRanges !== 0) { + result.push([i + range, range, duplicateRanges]); + } + i += range * duplicateRanges; + } + } + + return result; } function getStackString(ctx, error) { @@ -1345,7 +1379,7 @@ 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 ...`; @@ -1353,6 +1387,22 @@ function getStackFrames(ctx, err, stack) { } } } + + // Remove recursive repetitive stack frames in long stacks + if (frames.length > 10) { + const ranges = getDuplicateErrorFrameRanges(frames); + + while (ranges.length) { + const { 0: offset, 1: len, 2: duplicateRanges } = ranges.pop(); + const msg = ` ... removed ${len * duplicateRanges} duplicate lines ` + + 'matching former ' + + (duplicateRanges > 1 ? + `${len} lines ${duplicateRanges} times...` : + 'lines ...'); + frames.splice(offset, len * duplicateRanges, ctx.stylize(msg, 'undefined')); + } + } + return frames; } diff --git a/test/parallel/test-util-inspect.js b/test/parallel/test-util-inspect.js index b06f6814e4985a..5fb3357a1c6f56 100644 --- a/test/parallel/test-util-inspect.js +++ b/test/parallel/test-util-inspect.js @@ -2920,6 +2920,72 @@ 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: CWD is grayed out, even cwd that are percent encoded!', + ' at A. (/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 Module.require [as weird/name] (node:internal/aaaaa/loader:735:19)', + ' at require (node:internal/modules/helpers:14:16)', + ' at Array.forEach ()', + ' at require (node:internal/modules/helpers:14:16)', + ' at Array.forEach ()', + ` at foobar/test/parallel/test-util-inspect.js:2760:12`, + ` at Object. (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 ()', + ` at foobar/test/parallel/test-util-inspect.js:2760:12`, + ` at Object. (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 ()', + ` at foobar/test/parallel/test-util-inspect.js:2760:12`, + ` at Object. (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 ()', + ` at foobar/test/parallel/test-util-inspect.js:2760:12`, + ` at Object. (foobar/node_modules/m/folder/file.js:2753:10)`, + ' at /test/test-util-inspect.js:2239:9', + ' at getActual (node:assert:592:5)', + ].join('\n'); + + assert.strictEqual( + util.inspect(err, { colors: true }), + 'Error: CWD is grayed out, even cwd that are percent encoded!\n' + + ' at A. (/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 ... removed 3 duplicate lines matching former lines ...\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 ()\n' + + '\x1B[90m at require (node:internal/modules/helpers:14:16)\x1B[39m\n' + + ' at Array.forEach ()\n' + + ' at foobar/test/parallel/test-util-inspect.js:2760:12\n' + + ' at Object. (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 ... removed 10 duplicate lines matching former 5 lines 2 times...\x1B[39m\n' + + '\x1B[90m at require (node:internal/modules/helpers:14:16)\x1B[39m\n' + + ' at Array.forEach ()\n' + + ' at foobar/test/parallel/test-util-inspect.js:2760:12\n' + + ' at Object. (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' + ); +} + { // Cross platform checks. const err = new Error('foo'); From 5b60626fa01fafebcfac8bbc7ef949a4615ffac9 Mon Sep 17 00:00:00 2001 From: Ruben Bridgewater Date: Tue, 12 Aug 2025 15:01:07 -0400 Subject: [PATCH 2/8] fixup! improve error message and collapse more lines properly --- lib/internal/util/inspect.js | 12 ++++++------ test/parallel/test-util-inspect.js | 25 ++++++++++++++++++++----- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/lib/internal/util/inspect.js b/lib/internal/util/inspect.js index 51f4de972e8490..d30ffb103faa3a 100644 --- a/lib/internal/util/inspect.js +++ b/lib/internal/util/inspect.js @@ -1320,10 +1320,10 @@ function getDuplicateErrorFrameRanges(frames) { // Find the first duplicate frame. const pos = frames.indexOf(frames[i], i + 1); const range = pos - i; - if (pos !== -1 && range >= 3) { + if (pos !== -1) { let duplicateRanges = 0; // Compare ranges line-by-line - for (let nextStart = i + range; nextStart < frames.length - range; nextStart += range) { + for (let nextStart = i + range; nextStart <= frames.length - range; nextStart += range) { let allEqual = true; for (let j = 0; j < range; j++) { if (frames[i + j] !== frames[nextStart + j]) { @@ -1338,10 +1338,10 @@ function getDuplicateErrorFrameRanges(frames) { break; } } - if (duplicateRanges !== 0) { + if (duplicateRanges * range >= 3) { result.push([i + range, range, duplicateRanges]); + i += range * duplicateRanges + 2; } - i += range * duplicateRanges; } } @@ -1394,8 +1394,8 @@ function getStackFrames(ctx, err, stack) { while (ranges.length) { const { 0: offset, 1: len, 2: duplicateRanges } = ranges.pop(); - const msg = ` ... removed ${len * duplicateRanges} duplicate lines ` + - 'matching former ' + + const msg = ` ... collapsed ${len * duplicateRanges} duplicate lines ` + + 'matching above ' + (duplicateRanges > 1 ? `${len} lines ${duplicateRanges} times...` : 'lines ...'); diff --git a/test/parallel/test-util-inspect.js b/test/parallel/test-util-inspect.js index 5fb3357a1c6f56..3a3311249159a7 100644 --- a/test/parallel/test-util-inspect.js +++ b/test/parallel/test-util-inspect.js @@ -2924,7 +2924,8 @@ assert.strictEqual( // Use a fake stack to verify the expected colored outcome. const err = new Error('Hide duplicate frames in long stack'); err.stack = [ - 'Error: CWD is grayed out, even cwd that are percent encoded!', + 'Error: Hide duplicate frames in long stack', + ' at A. (/foo/node_modules/bar/baz.js:2:7)', ' at A. (/foo/node_modules/bar/baz.js:2:7)', ' at Module._compile (node:internal/modules/cjs/loader:827:30)', ' at Fancy (node:vm:697:32)', @@ -2933,6 +2934,12 @@ assert.strictEqual( ' 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 ()', @@ -2957,17 +2964,24 @@ assert.strictEqual( ` at Object. (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: CWD is grayed out, even cwd that are percent encoded!\n' + + 'Error: Hide duplicate frames in long stack\n' + + ' at A. (/foo/node_modules/\x1B[4mbar\x1B[24m/baz.js:2:7)\n' + ' at A. (/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 ... removed 3 duplicate lines matching former lines ...\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 ()\n' + @@ -2976,13 +2990,14 @@ assert.strictEqual( ' at foobar/test/parallel/test-util-inspect.js:2760:12\n' + ' at Object. (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 ... removed 10 duplicate lines matching former 5 lines 2 times...\x1B[39m\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 ()\n' + ' at foobar/test/parallel/test-util-inspect.js:2760:12\n' + ' at Object. (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' + '\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', ); } From 17a66730ef70dd902f57d0c416e3916835745659 Mon Sep 17 00:00:00 2001 From: Ruben Bridgewater Date: Wed, 13 Aug 2025 11:15:09 -0400 Subject: [PATCH 3/8] fixup! improve worst case algorithm perf --- lib/internal/util/inspect.js | 123 +++++++++++++++++++++++------ test/parallel/test-util-inspect.js | 88 +++++++++++++++++++++ 2 files changed, 187 insertions(+), 24 deletions(-) diff --git a/lib/internal/util/inspect.js b/lib/internal/util/inspect.js index d30ffb103faa3a..af67052d4915c5 100644 --- a/lib/internal/util/inspect.js +++ b/lib/internal/util/inspect.js @@ -1315,33 +1315,108 @@ function identicalSequenceRange(a, b) { } function getDuplicateErrorFrameRanges(frames) { + // Build a map: frame line -> sorted list of indices where it occurs const result = []; - for (let i = 0; i < frames.length - 3; i++) { - // Find the first duplicate frame. - const pos = frames.indexOf(frames[i], i + 1); - const range = pos - i; - if (pos !== -1) { - let duplicateRanges = 0; - // Compare ranges line-by-line - for (let nextStart = i + range; nextStart <= frames.length - range; nextStart += range) { - let allEqual = true; - for (let j = 0; j < range; j++) { - if (frames[i + j] !== frames[nextStart + j]) { - allEqual = false; - break; + const lineToPositions = new Map(); + + 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; + 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.push(gcdRange); } + gcdRange = distance; + distance = remainder; } - if (allEqual) { - // Matched. We have a duplicate range - duplicateRanges++; - } else { + 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++; } - if (duplicateRanges * range >= 3) { - result.push([i + range, range, duplicateRanges]); - i += range * duplicateRanges + 2; + // 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; } } @@ -1393,13 +1468,13 @@ function getStackFrames(ctx, err, stack) { const ranges = getDuplicateErrorFrameRanges(frames); while (ranges.length) { - const { 0: offset, 1: len, 2: duplicateRanges } = ranges.pop(); - const msg = ` ... collapsed ${len * duplicateRanges} duplicate lines ` + + const { 0: offset, 1: length, 2: duplicateRanges } = ranges.pop(); + const msg = ` ... collapsed ${length * duplicateRanges} duplicate lines ` + 'matching above ' + (duplicateRanges > 1 ? - `${len} lines ${duplicateRanges} times...` : + `${length} lines ${duplicateRanges} times...` : 'lines ...'); - frames.splice(offset, len * duplicateRanges, ctx.stylize(msg, 'undefined')); + frames.splice(offset, length * duplicateRanges, ctx.stylize(msg, 'undefined')); } } diff --git a/test/parallel/test-util-inspect.js b/test/parallel/test-util-inspect.js index 3a3311249159a7..cc38199127dfd5 100644 --- a/test/parallel/test-util-inspect.js +++ b/test/parallel/test-util-inspect.js @@ -2980,8 +2980,10 @@ assert.strictEqual( ' 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 ()\n' + @@ -2991,10 +2993,96 @@ assert.strictEqual( ' at Object. (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 ()\n' + + ' at foobar/test/parallel/test-util-inspect.js:2760:12\n' + + ' at Object. (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. (/foo/node_modules/bar/baz.js:2:7)', + ' at A. (/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 ()', + ` at foobar/test/parallel/test-util-inspect.js:2760:12`, + ` at Object. (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 ()', + ` at foobar/test/parallel/test-util-inspect.js:2760:12`, + ` at Object. (foobar/node_modules/m/folder/file.js:2753:10)`, + + ' at require (node:internal/modules/helpers:14:16)', + ' at Array.forEach ()', + ` at foobar/test/parallel/test-util-inspect.js:2760:12`, + ` at Object. (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 ()', + ` at foobar/test/parallel/test-util-inspect.js:2760:12`, + ` at Object. (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. (/foo/node_modules/\x1B[4mbar\x1B[24m/baz.js:2:7)\n' + + ' at A. (/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 ()\n' + + ' at foobar/test/parallel/test-util-inspect.js:2760:12\n' + + ' at Object. (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 ()\n' + ' at foobar/test/parallel/test-util-inspect.js:2760:12\n' + ' at Object. (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', From d8a96864bbf83f3b3e417a3ce17b558959e56874 Mon Sep 17 00:00:00 2001 From: Ruben Bridgewater Date: Mon, 18 Aug 2025 20:16:42 +0200 Subject: [PATCH 4/8] fixup --- lib/internal/util/inspect.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/internal/util/inspect.js b/lib/internal/util/inspect.js index af67052d4915c5..5f6b12964a5df6 100644 --- a/lib/internal/util/inspect.js +++ b/lib/internal/util/inspect.js @@ -1364,7 +1364,7 @@ function getDuplicateErrorFrameRanges(frames) { if (gcdRange !== 0) { // Add other possible ranges as fallback extraSteps ??= new Set(); - extraSteps.push(gcdRange); + extraSteps.add(gcdRange); } gcdRange = distance; distance = remainder; From 72ebdcd7d07dadb033d889f5413886a967e45cb7 Mon Sep 17 00:00:00 2001 From: Ruben Bridgewater Date: Sat, 30 Aug 2025 17:46:55 +0200 Subject: [PATCH 5/8] fixup! address comments --- lib/internal/util/inspect.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/internal/util/inspect.js b/lib/internal/util/inspect.js index 5f6b12964a5df6..a419a8ca7f1fc6 100644 --- a/lib/internal/util/inspect.js +++ b/lib/internal/util/inspect.js @@ -1317,7 +1317,7 @@ function identicalSequenceRange(a, b) { function getDuplicateErrorFrameRanges(frames) { // Build a map: frame line -> sorted list of indices where it occurs const result = []; - const lineToPositions = new Map(); + const lineToPositions = new SafeMap(); for (let i = 0; i < frames.length; i++) { const positions = lineToPositions.get(frames[i]); @@ -1337,7 +1337,7 @@ function getDuplicateErrorFrameRanges(frames) { 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) { + if (positions.length === 1 || positions[positions.length - 1] === i) { continue; } @@ -1363,7 +1363,7 @@ function getDuplicateErrorFrameRanges(frames) { const remainder = gcdRange % distance; if (gcdRange !== 0) { // Add other possible ranges as fallback - extraSteps ??= new Set(); + extraSteps ??= new SafeSet(); extraSteps.add(gcdRange); } gcdRange = distance; From 44490c58a9931c43213f6013913d783b4631ab5e Mon Sep 17 00:00:00 2001 From: Ruben Bridgewater Date: Tue, 2 Sep 2025 02:02:24 +0200 Subject: [PATCH 6/8] fixup! address comments --- lib/internal/util/inspect.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/internal/util/inspect.js b/lib/internal/util/inspect.js index a419a8ca7f1fc6..6574e149e11521 100644 --- a/lib/internal/util/inspect.js +++ b/lib/internal/util/inspect.js @@ -1347,7 +1347,7 @@ function getDuplicateErrorFrameRanges(frames) { } // Theoretical maximum range, adjusted while iterating - let range = positions.at(-1) - i; + let range = positions[positions.length - 1] - i; if (range < minimumDuplicateRange) { continue; } @@ -1414,7 +1414,7 @@ function getDuplicateErrorFrameRanges(frames) { } if (duplicateRanges * range >= 3) { - result.push([i + range, range, duplicateRanges]); + result.push(i + range, range, duplicateRanges); // Skip over the collapsed portion to avoid overlapping matches. i += range * (duplicateRanges + 1) - 1; } @@ -1467,8 +1467,11 @@ function getStackFrames(ctx, err, stack) { if (frames.length > 10) { const ranges = getDuplicateErrorFrameRanges(frames); - while (ranges.length) { - const { 0: offset, 1: length, 2: duplicateRanges } = ranges.pop(); + for (let i = 0; i < ranges.length; i += 3) { + const offset = ranges[i]; + const length = ranges[i + 1]; + const duplicateRanges = ranges[i + 2]; + const msg = ` ... collapsed ${length * duplicateRanges} duplicate lines ` + 'matching above ' + (duplicateRanges > 1 ? From 83648434af337f0b2dc86f7cc7b053be1c0e84f0 Mon Sep 17 00:00:00 2001 From: Ruben Bridgewater Date: Tue, 2 Sep 2025 02:41:37 +0200 Subject: [PATCH 7/8] fixup! --- lib/internal/util/inspect.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/internal/util/inspect.js b/lib/internal/util/inspect.js index 6574e149e11521..9d719b43107bea 100644 --- a/lib/internal/util/inspect.js +++ b/lib/internal/util/inspect.js @@ -1467,7 +1467,7 @@ function getStackFrames(ctx, err, stack) { if (frames.length > 10) { const ranges = getDuplicateErrorFrameRanges(frames); - for (let i = 0; i < ranges.length; i += 3) { + for (let i = ranges.length - 3; i >= 0; i -= 3) { const offset = ranges[i]; const length = ranges[i + 1]; const duplicateRanges = ranges[i + 2]; From 290935db834d7e57382b3b2ef9e61e1f29ec4141 Mon Sep 17 00:00:00 2001 From: Ruben Bridgewater Date: Thu, 4 Sep 2025 01:22:49 +0200 Subject: [PATCH 8/8] fixup! address comment --- lib/internal/util/inspect.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/internal/util/inspect.js b/lib/internal/util/inspect.js index 9d719b43107bea..354832379c9823 100644 --- a/lib/internal/util/inspect.js +++ b/lib/internal/util/inspect.js @@ -1324,7 +1324,7 @@ function getDuplicateErrorFrameRanges(frames) { if (positions === undefined) { lineToPositions.set(frames[i], [i]); } else { - positions.push(i); + positions[positions.length] = i; } }