Skip to content

Commit 65e4e68

Browse files
BridgeARtargos
authored andcommitted
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. PR-URL: #59447 Reviewed-By: James M Snell <[email protected]> Reviewed-By: Michaël Zasso <[email protected]> Reviewed-By: Chengzhong Wu <[email protected]> Reviewed-By: Moshe Atlow <[email protected]> Reviewed-By: Jordan Harband <[email protected]>
1 parent 70d2d6d commit 65e4e68

File tree

2 files changed

+300
-3
lines changed

2 files changed

+300
-3
lines changed

‎lib/internal/util/inspect.js‎

Lines changed: 131 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1315,13 +1315,122 @@ function identicalSequenceRange(a, b){
13151315
len++;
13161316
}
13171317
if(len>3){
1318-
return{len,offset: i};
1318+
return[len,i];
13191319
}
13201320
}
13211321
}
13221322
}
13231323

1324-
return{len: 0,offset: 0};
1324+
return[0,0];
1325+
}
1326+
1327+
functiongetDuplicateErrorFrameRanges(frames){
1328+
// Build a map: frame line -> sorted list of indices where it occurs
1329+
constresult=[];
1330+
constlineToPositions=newSafeMap();
1331+
1332+
for(leti=0;i<frames.length;i++){
1333+
constpositions=lineToPositions.get(frames[i]);
1334+
if(positions===undefined){
1335+
lineToPositions.set(frames[i],[i]);
1336+
}else{
1337+
positions[positions.length]=i;
1338+
}
1339+
}
1340+
1341+
constminimumDuplicateRange=3;
1342+
// Not enough duplicate lines to consider collapsing
1343+
if(frames.length-lineToPositions.size<=minimumDuplicateRange){
1344+
returnresult;
1345+
}
1346+
1347+
for(leti=0;i<frames.length-minimumDuplicateRange;i++){
1348+
constpositions=lineToPositions.get(frames[i]);
1349+
// Find the next occurrence of the same line after i, if any
1350+
if(positions.length===1||positions[positions.length-1]===i){
1351+
continue;
1352+
}
1353+
1354+
constcurrent=positions.indexOf(i)+1;
1355+
if(current===positions.length){
1356+
continue;
1357+
}
1358+
1359+
// Theoretical maximum range, adjusted while iterating
1360+
letrange=positions[positions.length-1]-i;
1361+
if(range<minimumDuplicateRange){
1362+
continue;
1363+
}
1364+
letextraSteps;
1365+
if(current+1<positions.length){
1366+
// Optimize initial step size by choosing the greatest common divisor (GCD)
1367+
// of all candidate distances to the same frame line. This tends to match
1368+
// the true repeating block size and minimizes fallback iterations.
1369+
letgcdRange=0;
1370+
for(letj=current;j<positions.length;j++){
1371+
letdistance=positions[j]-i;
1372+
while(distance!==0){
1373+
constremainder=gcdRange%distance;
1374+
if(gcdRange!==0){
1375+
// Add other possible ranges as fallback
1376+
extraSteps??=newSafeSet();
1377+
extraSteps.add(gcdRange);
1378+
}
1379+
gcdRange=distance;
1380+
distance=remainder;
1381+
}
1382+
if(gcdRange===1)break;
1383+
}
1384+
range=gcdRange;
1385+
if(extraSteps){
1386+
extraSteps.delete(range);
1387+
extraSteps=[...extraSteps];
1388+
}
1389+
}
1390+
letmaxRange=range;
1391+
letmaxDuplicates=0;
1392+
1393+
letduplicateRanges=0;
1394+
1395+
for(letnextStart=i+range;/* ignored */;nextStart+=range){
1396+
letequalFrames=0;
1397+
for(letj=0;j<range;j++){
1398+
if(frames[i+j]!==frames[nextStart+j]){
1399+
break;
1400+
}
1401+
equalFrames++;
1402+
}
1403+
// Adjust the range to match different type of ranges.
1404+
if(equalFrames!==range){
1405+
if(!extraSteps?.length){
1406+
break;
1407+
}
1408+
// Memorize former range in case the smaller one would hide less.
1409+
if(duplicateRanges!==0&&maxRange*maxDuplicates<range*duplicateRanges){
1410+
maxRange=range;
1411+
maxDuplicates=duplicateRanges;
1412+
}
1413+
range=extraSteps.pop();
1414+
nextStart=i;
1415+
duplicateRanges=0;
1416+
continue;
1417+
}
1418+
duplicateRanges++;
1419+
}
1420+
1421+
if(maxDuplicates!==0&&maxRange*maxDuplicates>=range*duplicateRanges){
1422+
range=maxRange;
1423+
duplicateRanges=maxDuplicates;
1424+
}
1425+
1426+
if(duplicateRanges*range>=3){
1427+
result.push(i+range,range,duplicateRanges);
1428+
// Skip over the collapsed portion to avoid overlapping matches.
1429+
i+=range*(duplicateRanges+1)-1;
1430+
}
1431+
}
1432+
1433+
returnresult;
13251434
}
13261435

13271436
functiongetStackString(ctx,error){
@@ -1355,14 +1464,33 @@ function getStackFrames(ctx, err, stack){
13551464
constcauseStackStart=StringPrototypeIndexOf(causeStack,'\n at');
13561465
if(causeStackStart!==-1){
13571466
constcauseFrames=StringPrototypeSplit(StringPrototypeSlice(causeStack,causeStackStart+1),'\n');
1358-
const{ len, offset }=identicalSequenceRange(frames,causeFrames);
1467+
const{0: len,1:offset}=identicalSequenceRange(frames,causeFrames);
13591468
if(len>0){
13601469
constskipped=len-2;
13611470
constmsg=` ... ${skipped} lines matching cause stack trace ...`;
13621471
frames.splice(offset+1,skipped,ctx.stylize(msg,'undefined'));
13631472
}
13641473
}
13651474
}
1475+
1476+
// Remove recursive repetitive stack frames in long stacks
1477+
if(frames.length>10){
1478+
constranges=getDuplicateErrorFrameRanges(frames);
1479+
1480+
for(leti=ranges.length-3;i>=0;i-=3){
1481+
constoffset=ranges[i];
1482+
constlength=ranges[i+1];
1483+
constduplicateRanges=ranges[i+2];
1484+
1485+
constmsg=` ... collapsed ${length*duplicateRanges} duplicate lines `+
1486+
'matching above '+
1487+
(duplicateRanges>1 ?
1488+
`${length} lines ${duplicateRanges} times...` :
1489+
'lines ...');
1490+
frames.splice(offset,length*duplicateRanges,ctx.stylize(msg,'undefined'));
1491+
}
1492+
}
1493+
13661494
returnframes;
13671495
}
13681496

‎test/parallel/test-util-inspect.js‎

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2920,6 +2920,175 @@ assert.strictEqual(
29202920
process.cwd=originalCWD;
29212921
}
29222922

2923+
{
2924+
// Use a fake stack to verify the expected colored outcome.
2925+
consterr=newError('Hide duplicate frames in long stack');
2926+
err.stack=[
2927+
'Error: Hide duplicate frames in long stack',
2928+
' at A.<anonymous> (/foo/node_modules/bar/baz.js:2:7)',
2929+
' at A.<anonymous> (/foo/node_modules/bar/baz.js:2:7)',
2930+
' at Module._compile (node:internal/modules/cjs/loader:827:30)',
2931+
' at Fancy (node:vm:697:32)',
2932+
' at tryModuleLoad (node:internal/modules/cjs/foo:629:12)',
2933+
' at Function.Module._load (node:internal/modules/cjs/loader:621:3)',
2934+
' at Fancy (node:vm:697:32)',
2935+
' at tryModuleLoad (node:internal/modules/cjs/foo:629:12)',
2936+
' at Function.Module._load (node:internal/modules/cjs/loader:621:3)',
2937+
' at Function.Module._load (node:internal/modules/cjs/loader:621:3)',
2938+
' at Function.Module._load (node:internal/modules/cjs/loader:621:3)',
2939+
' at Function.Module._load (node:internal/modules/cjs/loader:621:3)',
2940+
' at Function.Module._load (node:internal/modules/cjs/loader:621:3)',
2941+
' at Function.Module._load (node:internal/modules/cjs/loader:621:3)',
2942+
' at Function.Module._load (node:internal/modules/cjs/loader:621:3)',
2943+
' at Module.require [as weird/name] (node:internal/aaaaa/loader:735:19)',
2944+
' at require (node:internal/modules/helpers:14:16)',
2945+
' at Array.forEach (<anonymous>)',
2946+
' at require (node:internal/modules/helpers:14:16)',
2947+
' at Array.forEach (<anonymous>)',
2948+
` at foobar/test/parallel/test-util-inspect.js:2760:12`,
2949+
` at Object.<anonymous> (foobar/node_modules/m/folder/file.js:2753:10)`,
2950+
' at Module.require [as weird/name] (node:internal/aaaaa/loader:735:19)',
2951+
' at require (node:internal/modules/helpers:14:16)',
2952+
' at Array.forEach (<anonymous>)',
2953+
` at foobar/test/parallel/test-util-inspect.js:2760:12`,
2954+
` at Object.<anonymous> (foobar/node_modules/m/folder/file.js:2753:10)`,
2955+
' at Module.require [as weird/name] (node:internal/aaaaa/loader:735:19)',
2956+
' at require (node:internal/modules/helpers:14:16)',
2957+
' at Array.forEach (<anonymous>)',
2958+
` at foobar/test/parallel/test-util-inspect.js:2760:12`,
2959+
` at Object.<anonymous> (foobar/node_modules/m/folder/file.js:2753:10)`,
2960+
' at Module.require [as weird/name] (node:internal/aaaaa/loader:735:19)',
2961+
' at require (node:internal/modules/helpers:14:16)',
2962+
' at Array.forEach (<anonymous>)',
2963+
` at foobar/test/parallel/test-util-inspect.js:2760:12`,
2964+
` at Object.<anonymous> (foobar/node_modules/m/folder/file.js:2753:10)`,
2965+
' at /test/test-util-inspect.js:2239:9',
2966+
' at getActual (node:assert:592:5)',
2967+
' at /test/test-util-inspect.js:2239:9',
2968+
' at getActual (node:assert:592:5)',
2969+
' at /test/test-util-inspect.js:2239:9',
2970+
' at getActual (node:assert:592:5)',
2971+
].join('\n');
2972+
2973+
assert.strictEqual(
2974+
util.inspect(err,{colors: true}),
2975+
'Error: Hide duplicate frames in long stack\n'+
2976+
' at A.<anonymous> (/foo/node_modules/\x1B[4mbar\x1B[24m/baz.js:2:7)\n'+
2977+
' at A.<anonymous> (/foo/node_modules/\x1B[4mbar\x1B[24m/baz.js:2:7)\n'+
2978+
'\x1B[90m at Module._compile (node:internal/modules/cjs/loader:827:30)\x1B[39m\n'+
2979+
'\x1B[90m at Fancy (node:vm:697:32)\x1B[39m\n'+
2980+
' at tryModuleLoad (node:internal/modules/cjs/foo:629:12)\n'+
2981+
'\x1B[90m at Function.Module._load (node:internal/modules/cjs/loader:621:3)\x1B[39m\n'+
2982+
'\x1B[90m ... collapsed 3 duplicate lines matching above lines ...\x1B[39m\n'+
2983+
2984+
'\x1B[90m at Function.Module._load (node:internal/modules/cjs/loader:621:3)\x1B[39m\n'+
2985+
'\x1B[90m ... collapsed 5 duplicate lines matching above 1 lines 5 times...\x1B[39m\n'+
2986+
2987+
' at Module.require [as weird/name] (node:internal/aaaaa/loader:735:19)\n'+
2988+
'\x1B[90m at require (node:internal/modules/helpers:14:16)\x1B[39m\n'+
2989+
' at Array.forEach (<anonymous>)\n'+
2990+
'\x1B[90m at require (node:internal/modules/helpers:14:16)\x1B[39m\n'+
2991+
' at Array.forEach (<anonymous>)\n'+
2992+
' at foobar/test/parallel/test-util-inspect.js:2760:12\n'+
2993+
' at Object.<anonymous> (foobar/node_modules/\x1B[4mm\x1B[24m/folder/file.js:2753:10)\n'+
2994+
' at Module.require [as weird/name] (node:internal/aaaaa/loader:735:19)\n'+
2995+
'\x1B[90m ... collapsed 10 duplicate lines matching above 5 lines 2 times...\x1B[39m\n'+
2996+
2997+
'\x1B[90m at require (node:internal/modules/helpers:14:16)\x1B[39m\n'+
2998+
' at Array.forEach (<anonymous>)\n'+
2999+
' at foobar/test/parallel/test-util-inspect.js:2760:12\n'+
3000+
' at Object.<anonymous> (foobar/node_modules/\x1B[4mm\x1B[24m/folder/file.js:2753:10)\n'+
3001+
' at /test/test-util-inspect.js:2239:9\n'+
3002+
'\x1B[90m at getActual (node:assert:592:5)\x1B[39m\n'+
3003+
'\x1B[90m ... collapsed 4 duplicate lines matching above 2 lines 2 times...\x1B[39m',
3004+
);
3005+
3006+
// Use a fake stack to verify the expected colored outcome.
3007+
consterr2=newError('Hide duplicate frames in long stack');
3008+
err2.stack=[
3009+
'Error: Hide duplicate frames in long stack',
3010+
' at A.<anonymous> (/foo/node_modules/bar/baz.js:2:7)',
3011+
' at A.<anonymous> (/foo/node_modules/bar/baz.js:2:7)',
3012+
' at Module._compile (node:internal/modules/cjs/loader:827:30)',
3013+
3014+
// 3
3015+
' at Fancy (node:vm:697:32)',
3016+
' at tryModuleLoad (node:internal/modules/cjs/foo:629:12)',
3017+
' at Function.Module._load (node:internal/modules/cjs/loader:621:3)',
3018+
' at Fancy (node:vm:697:32)',
3019+
' at tryModuleLoad (node:internal/modules/cjs/foo:629:12)',
3020+
' at Function.Module._load (node:internal/modules/cjs/loader:621:3)',
3021+
3022+
// 6 * 1
3023+
' at Function.Module._load (node:internal/modules/cjs/loader:621:3)',
3024+
' at Function.Module._load (node:internal/modules/cjs/loader:621:3)',
3025+
' at Function.Module._load (node:internal/modules/cjs/loader:621:3)',
3026+
' at Function.Module._load (node:internal/modules/cjs/loader:621:3)',
3027+
' at Function.Module._load (node:internal/modules/cjs/loader:621:3)',
3028+
' at Function.Module._load (node:internal/modules/cjs/loader:621:3)',
3029+
' at Function.Module._load (node:internal/modules/cjs/loader:621:3)',
3030+
3031+
// 10
3032+
' at require (node:internal/modules/helpers:14:16)',
3033+
' at Array.forEach (<anonymous>)',
3034+
` at foobar/test/parallel/test-util-inspect.js:2760:12`,
3035+
` at Object.<anonymous> (foobar/node_modules/m/folder/file.js:2753:10)`,
3036+
' at Module.require [as weird/name] (node:internal/aaaaa/loader:735:19)',
3037+
' at Module.require [as weird/name] (node:internal/aaaaa/loader:735:19)',
3038+
' at require (node:internal/modules/helpers:14:16)',
3039+
' at Array.forEach (<anonymous>)',
3040+
` at foobar/test/parallel/test-util-inspect.js:2760:12`,
3041+
` at Object.<anonymous> (foobar/node_modules/m/folder/file.js:2753:10)`,
3042+
3043+
' at require (node:internal/modules/helpers:14:16)',
3044+
' at Array.forEach (<anonymous>)',
3045+
` at foobar/test/parallel/test-util-inspect.js:2760:12`,
3046+
` at Object.<anonymous> (foobar/node_modules/m/folder/file.js:2753:10)`,
3047+
' at Module.require [as weird/name] (node:internal/aaaaa/loader:735:19)',
3048+
' at Module.require [as weird/name] (node:internal/aaaaa/loader:735:19)',
3049+
' at require (node:internal/modules/helpers:14:16)',
3050+
' at Array.forEach (<anonymous>)',
3051+
` at foobar/test/parallel/test-util-inspect.js:2760:12`,
3052+
` at Object.<anonymous> (foobar/node_modules/m/folder/file.js:2753:10)`,
3053+
3054+
// 2 * 2
3055+
' at /test/test-util-inspect.js:2239:9',
3056+
' at getActual (node:assert:592:5)',
3057+
' at /test/test-util-inspect.js:2239:9',
3058+
' at getActual (node:assert:592:5)',
3059+
' at /test/test-util-inspect.js:2239:9',
3060+
' at getActual (node:assert:592:5)',
3061+
].join('\n');
3062+
3063+
assert.strictEqual(
3064+
util.inspect(err2,{colors: true}),
3065+
'Error: Hide duplicate frames in long stack\n'+
3066+
' at A.<anonymous> (/foo/node_modules/\x1B[4mbar\x1B[24m/baz.js:2:7)\n'+
3067+
' at A.<anonymous> (/foo/node_modules/\x1B[4mbar\x1B[24m/baz.js:2:7)\n'+
3068+
'\x1B[90m at Module._compile (node:internal/modules/cjs/loader:827:30)\x1B[39m\n'+
3069+
'\x1B[90m at Fancy (node:vm:697:32)\x1B[39m\n'+
3070+
' at tryModuleLoad (node:internal/modules/cjs/foo:629:12)\n'+
3071+
'\x1B[90m at Function.Module._load (node:internal/modules/cjs/loader:621:3)\x1B[39m\n'+
3072+
'\x1B[90m ... collapsed 3 duplicate lines matching above lines ...\x1B[39m\n'+
3073+
'\x1B[90m at Function.Module._load (node:internal/modules/cjs/loader:621:3)\x1B[39m\n'+
3074+
'\x1B[90m ... collapsed 6 duplicate lines matching above 1 lines 6 times...\x1B[39m\n'+
3075+
'\x1B[90m at require (node:internal/modules/helpers:14:16)\x1B[39m\n'+
3076+
' at Array.forEach (<anonymous>)\n'+
3077+
' at foobar/test/parallel/test-util-inspect.js:2760:12\n'+
3078+
' at Object.<anonymous> (foobar/node_modules/\x1B[4mm\x1B[24m/folder/file.js:2753:10)\n'+
3079+
' at Module.require [as weird/name] (node:internal/aaaaa/loader:735:19)\n'+
3080+
' at Module.require [as weird/name] (node:internal/aaaaa/loader:735:19)\n'+
3081+
'\x1B[90m at require (node:internal/modules/helpers:14:16)\x1B[39m\n'+
3082+
' at Array.forEach (<anonymous>)\n'+
3083+
' at foobar/test/parallel/test-util-inspect.js:2760:12\n'+
3084+
' at Object.<anonymous> (foobar/node_modules/\x1B[4mm\x1B[24m/folder/file.js:2753:10)\n'+
3085+
'\x1B[90m ... collapsed 10 duplicate lines matching above lines ...\x1B[39m\n'+
3086+
' at /test/test-util-inspect.js:2239:9\n'+
3087+
'\x1B[90m at getActual (node:assert:592:5)\x1B[39m\n'+
3088+
'\x1B[90m ... collapsed 4 duplicate lines matching above 2 lines 2 times...\x1B[39m',
3089+
);
3090+
}
3091+
29233092
{
29243093
// Cross platform checks.
29253094
consterr=newError('foo');

0 commit comments

Comments
(0)