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

track assignment locations for infinite loop reporting #13344

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

Rich-Harris
Copy link
Member

An idea to close #13256. I haven't completely worked out if I like it or not.

Basically, it's tricky to add location information to effects, because sometimes template effects are grouped together (and the assignment that causes the infinite loop could be in a <span>{n++}</span> or whatever). But we can add location information for the assignments that cause the infinite loops, which gives us more granular information:

image

Caveats:

  • it can't track assignments that happen outside .svelte/.svelte.js files (though it fails gracefully in these cases)
  • it doesn't (yet?) track array mutations or e.g. Object.assign(...) or set.add(blah)
  • right now it just reports the most recent assignment, maybe that's insufficient

Notwithstanding those caveats, I kinda prefer this to what we have currently.

Before submitting the PR, please make sure you do the following

  • It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs
  • Prefix your PR title with feat:, fix:, chore:, or docs:.
  • This message body should clearly illustrate what problems it solves.
  • Ideally, include a test that fails without this PR but passes with it.

Tests and linting

  • Run the tests with pnpm test and lint the project with pnpm lint

Copy link

changeset-bot bot commented Sep 20, 2024

⚠️ No Changeset found

Latest commit: f5b525e

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@trueadm
Copy link
Contributor

trueadm commented Sep 20, 2024

I'm also not really sure if I like it too. It seems useful, but if it is fragile then will it just cause more frustrations in the long term?

@dummdidumm
Copy link
Member

Knowing which assignment it was exactly last is useful, but for more involved loops I also want to know the stack. What about doing this as an additional thing rather than replacing what was there before? i.e. you get the "last ten effects were ..." (maybe with a note that you'll see the transformed contents and can look up the source by exanding it) + what we have here.

@trueadm
Copy link
Contributor

trueadm commented Sep 20, 2024

@dummdidumm Having used the last 10 effects logic since it landed, I've actually not found it terribly useful at all. Firstly, 10 is far too many and many of them are internal effects which are just confusing to present to a user – then then effects I look at don't really tell me anything. Like, you really need to know the effect and the dependency, without the dependency it's lost.

@Rich-Harris
Copy link
Member Author

if it is fragile

All that happens is you don't get the location — for example if you do array.push(...) inside an effect, because we don't currently mark those call expressions (unclear whether we should)

@dummdidumm
Copy link
Member

"all that happens" sounds like it's no big deal, but it's basically impossible to know what caused this then just by looking at the error message.

Like, you really need to know the effect and the dependency, without the dependency it's lost

what do you mean by "you need to know the dependency"? How could we present that?

many of them are internal effects which are just confusing to present to a user

we can filter them out. I didn't do that originally because I feared that it could make loops harder to debug where a render effect plays a role (AFAIK it could happen if you have a binding that is repeatedly set from above and the below again, but maybe that's not true / not worth for the common case where it's noisy)

@Rich-Harris
Copy link
Member Author

One thing we could do is annotate user effects, so that if an update happens that isn't tracked (because it's an array.push or a map.set or whatever), we still track the effect in which it happened. So if the offending effect does x++ then you'll get

effect_update_depth_exceeded Maximum update depth exceeded after assignment at App.svelte:9:2

but if it does array.push(blah) then you'll get

effect_update_depth_exceeded Maximum update depth exceeded during effect at App.svelte:9:2

We'd miss cases like <span>{array.push(blah)}</span> but those are imaginary. If we needed to add additional logic for bindings then we could do that too

@trueadm
Copy link
Contributor

trueadm commented Sep 20, 2024

One thing we could do is annotate user effects, so that if an update happens that isn't tracked (because it's an array.push or a map.set or whatever), we still track the effect in which it happened. So if the offending effect does x++ then you'll get

effect_update_depth_exceeded Maximum update depth exceeded after assignment at App.svelte:9:2

but if it does array.push(blah) then you'll get

effect_update_depth_exceeded Maximum update depth exceeded during effect at App.svelte:9:2

We'd miss cases like <span>{array.push(blah)}</span> but those are imaginary. If we needed to add additional logic for bindings then we could do that too

That sounds like a good idea.

@dummdidumm
Copy link
Member

I'd still like to see a list of effects leading up to this. I'm confident we can get that on the effect object to then print it out.

@adminy
Copy link

adminy commented Sep 25, 2024

I also had an infinite loop scenario and I feel like this is chicken / egg problem to solve.

let page = $state({
  props: { a: 1, b: 2, c: 3, d: 4, e: 5, f: 6 }
})

$effect(async () => {
   const props = { method: 'post', body: JSON.stringify(page)}
   const res = await (await fetch(url, props)).json()
   Object.assign(page.props, res)
})

And so this causes an infinite loop. I kind of have to hash the result and the page to see if neither has changed for me not to do an object assign. This feels wrong, maybe there is another solution for this in svelte 5.

anyways for the time being I switched to onMount, but of course now page changes won't get fresh information from the server. still, what I did not expect is there to be 6 page state changes on top of the initial event when I traced this with $inspect. why isn't object.assign one reactive operation?
why does it create an event for every property of the object? Sounds kind of expensive no?

@paoloricciuti
Copy link
Member

And so this causes an infinite loop. I kind of have to hash the result and the page to see if neither has changed for me not to do an object assign. This feels wrong, maybe there is another solution for this in svelte 5.

What's the thing that should cause a re-fetch? Url changing? In that case you can use untrack when JSON.stringify to solve the infinite loop problem.

expect is there to be 6 page state changes on top of the initial event when I traced this with $inspect.

$inpect it's a bit special in the sense that tracks every change made to the object. If you try to console.log in an $effect you should see only one log.

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

Successfully merging this pull request may close these issues.

Better effect stack logging
5 participants