Skip to content
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

Fixed a Maximum call stack exceeded err in getAlternateSourceIdForPosition #7785

Merged
merged 1 commit into from Sep 20, 2022

Conversation

Andarist
Copy link
Contributor

@Andarist Andarist commented Sep 20, 2022

I noticed this error in the console and reported it a couple of days ago on Discord:
Screenshot 2022-09-20 at 23 34 08

I couldn't repro it in Replay (it only happens for me in Chrome) - I'm not sure what exactly leads to it. However, when asked by @bvaughn for some more details about this I started to dig deeper. I was able to confirm that this happens for a minified code line that looks like this:

, i = Math.min(...o.filter((e=>e.line >= n.line)).map((e=>e.line)))

and I was able to trace it back to this in the source code:

const breakableLine = Math.min(
...lineLocations.filter(ll => ll.line >= position.line).map(ll => ll.line)
);

The problem with fn(...arr) is that it pushes all array elements onto the stack and that can blow up the stack. Usually, this is not an issue but it turns out that o is quite huge here:
Screenshot 2022-09-20 at 23 41 30

Note that on this screenshot we see both the original o and the result of the filter+map - we can notice that their lengths are the same. This might indicate a bug - but I don't know what exactly those code lines are meant to do so I'm not sure.

In case this isn't a bug on its own... I've prepared this simple fix that just doesn't rely on the spread.

You can also test the "issue" with spread by evaluating this in the Chrome's console:

var huge = Array.from({ length: 150000 }, (_, i) => i)
console.log(Math.min(...huge)) // throws Maximum call stack exceeded

Note that FF has a bigger allowed stack and it might be the reason why it doesn't happen in Replay. I've recorded the situation in Replay and added a comment to the call site receiving this huge array, you might find it helpful: https://app.replay.io/recording/max-call-stack-exceeded-in-replay-just-in-chrome--018c0ea7-1ffe-446e-8020-e7e45f14d228

@vercel
Copy link

vercel bot commented Sep 20, 2022

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Updated
devtools ✅ Ready (Inspect) Visit Preview Sep 20, 2022 at 9:48PM (UTC)

@@ -153,15 +153,28 @@ function getUniqueAlternateSourceId(
return { sourceId: alternateSourceId };
}

function min(arr: number[]) {
if (arr.length === 0) {
return Infinity;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this matches Math.min(...[])

Copy link
Collaborator

@markerikson markerikson left a comment

Choose a reason for hiding this comment

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

I like this as a temporary workaround and we can merge it now, but I think we should come up with an alternate implementation. I already made a couple tweaks to the "breakable positions" handling that use a normalized lookup table by line instead of searching a giant array in order to fix a couple perf hotspots, and this looks like another good candidate.

@markerikson markerikson merged commit 3fdcc20 into replayio:main Sep 20, 2022
@Andarist Andarist deleted the andarist/fix-max-stack-err branch September 20, 2022 22:18
@Uzlopak
Copy link

Uzlopak commented Sep 21, 2022

Why not

function minOptimized(arr) {
  const length = arr.length;
  if (length === 0) {
    return Infinity;
  } else if (length === 1) {
  	return arr[0]
  } else {
   	let lowest = arr[0];
 	for (let i = 1; i < length; ++i) {
    	(arr[i] < lowest) && (lowest = arr[i]);
	    if (lowest === 1) {
    		break
    	}
	  }
  	return lowest;
  }
}

@Andarist
Copy link
Contributor Author

Andarist commented Sep 21, 2022

Because this one is less readable, it looks like the microoptimization. It's also not correct:

minOptimized([10, 1, -1]) // actual 1, expected -1

It could be thought of as correct for this very specific use case, but I didn't bother to overoptimize this for this case here (could be done though)

@Uzlopak
Copy link

Uzlopak commented Sep 21, 2022

You can have negative lines in a file?

@Andarist
Copy link
Contributor Author

No, that’s why i’ve said that it would be OK here but i was just trying to replace the current implementation with the one that wouldnt blow up the stack. So i’ve wanted to match the previous code 1 to 1 and just went for a generic implementation for the min

@Uzlopak
Copy link

Uzlopak commented Sep 21, 2022

Yeah you are right. Probably the best optimization would be

  let breakableLine = Infinity
  const positionLine = position.line
  for (let i = 0, il = lineLocations.length; i < il; ++i) {
    const line = lineLocations[i].line
    if (
      line >= positionLine &&
      line < breakableLine
    ) {
      breakableLine = line
    }
  }

@markerikson
Copy link
Collaborator

Yeah, per comment above, I think the best improvement here is to already have a { [lineNumber] : columns[] } lookup table so we don't have to do a .filter().map(), etc.

@bvaughn
Copy link
Collaborator

bvaughn commented Sep 21, 2022

I don't have a ton of context here but I agree that we should avoid iterating over big arrays– especially in code that's hot. Looking forward to seeing what Mark has in mind 😁

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
4 participants