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

fix: improve performance of scheduling effects #13282

Closed
wants to merge 13 commits into from
Closed

Conversation

trueadm
Copy link
Contributor

@trueadm trueadm commented Sep 17, 2024

This very simple change has quite a significant impact on our local benchmark. In the case of some of the benchmarks, they're not wrapped in a branch effect, only a render effect. We now wrap them with a branch effect!

Copy link

changeset-bot bot commented Sep 17, 2024

🦋 Changeset detected

Latest commit: 9423767

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
svelte Patch

Not sure what this means? Click here to learn what changesets are.

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

@dummdidumm
Copy link
Member

Does it makes sense to investigate avoiding the includes check and possibly even the walking up to the parents logic? I see that two usage sites first set the effect to dirty, then schedule it. Can we use a method in these places that does both, and bails early if we detect that the effect is already dirty? The mark_reactions usage site seems more involved, not sure how to improve it there.

@trueadm
Copy link
Contributor Author

trueadm commented Sep 17, 2024

@dummdidumm I looked into that, but we can't. schedule_effect doesn't visit the same things that mark_reactions visits, one is the reactive graph the other is the effect tree. We propagate up to the effect tree to the root and mark all the branches as maybe dirty to avoid doing too much work later.

I also tried doing a simple queued_root_effects[0] !== effect and because queued_root_effects is generally always empty or has one thing in it (except this benchmark) it didn't make any perf difference. I also tried marking the root as maybe dirty too, but that literally breaks every test as roots are never marked as dirty – and knowing when to mark them as clean isn't straightforward either.

@Rich-Harris
Copy link
Member

In the case of some of the benchmarks, they're not wrapped in a branch effect, only a render effect

Are we saying that this only occurs inside benchmarks, and not inside real apps? If so, won't this make real apps slower for the sake of those benchmarks?

@trueadm
Copy link
Contributor Author

trueadm commented Sep 17, 2024

@Rich-Harris It's definitely possible in real world applications that make use of $effect.root too, as there's no branch effect anywhere.

@trueadm
Copy link
Contributor Author

trueadm commented Sep 17, 2024

Okay, I've removed the includes logic and instead opted to wrap the $effect.root with a branch now which also solves the problem nicely for everyone!

@Rich-Harris
Copy link
Member

We call branch(() => ...) directly inside the effect root created in _mount — should effect_root just always create a branch? The only other place it's used is inside toStore, where a branch seems harmless.

Though, going deeper: I don't really follow what the if ((flags & BRANCH_EFFECT) !== 0) { inside schedule_effect is for — why do we only bail out early if it's a branch effect? I'm sure there's a good reason but it's unclear from the code — at the very least it could use an explanatory comment. If we weren't doing that then the other changes would presumably be unnecessary

@trueadm
Copy link
Contributor Author

trueadm commented Sep 17, 2024

Yes we only bail out on branches. That's because they're not part of the reactive graph and thus cannot be made dirty from the reactive graph, so we use the MAYBE_DIRTY to flag that we need to visit them otherwise we skip them entirely in both schedule_effect and also process_effects. I'll add a comment.

We call branch(() => ...) directly inside the effect root created in _mount — should effect_root just always create a branch? The only other place it's used is inside toStore, where a branch seems harmless.

I can do that too.

@trueadm
Copy link
Contributor Author

trueadm commented Sep 17, 2024

Right ready for review, I've made all the changes based off feedback.

@trueadm
Copy link
Contributor Author

trueadm commented Sep 17, 2024

I actually feel pretty confident that this PR is a better direction than #13297. Writes are always more expensive than reads, and the less of them we have to do, the better. Ironically branch effects just happen to be a good place for that logic to exist IMO.

Comment on lines +197 to +199
flushSync(() => {
map.delete(1);
});
Copy link
Member

Choose a reason for hiding this comment

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

what's the purpose of these changes? they pass on main already, so what are they testing exactly?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's because effect_root didn't create a branch before. Because child effects get attached to their parent after their contents run, the flushSync doesn't do anything as the root has no child effects.

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.

3 participants