-
-
Notifications
You must be signed in to change notification settings - Fork 32.8k
util: hide duplicated stack frames when using util.inspect #59447
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -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(); | ||||||
|
||||||
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; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
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); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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). There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) { | ||||||
|
@@ -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; | ||||||
} | ||||||
|
||||||
|
There was a problem hiding this comment.
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?