-
-
Notifications
You must be signed in to change notification settings - Fork 4.2k
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
[FEATURE] Tracked Props Performance Tuning #18223
Conversation
@pzuraq - Looks like there are some conflicts here, mind rebasing? |
Yeah, I was aware of those, I didn't want to rebase until people have had a chance to review, since each build is based on a commit currently, and the commit list is up top. Given we'd have to rebase and rerun the tests (which took a while), and the Glimmer changes have to be merged and released first anyways, I figured it would be alright to review in isolation. |
Yeah, definitely aware of this! I always make sure to do explicit checks in hot paths |
@pzuraq - How goes the work on this? |
glimmerjs/glimmer-vm#963 is currently ready for review, pending on @wycats and @krisselden currently. Once they review and approve, we can merge that, cut a release, and rebase/merge this PR. |
Awesome, thank you. Just a reminder for when this is rebased to prep for landing: we'll need to prefix the commits with |
- No longer fetching leaf nodes when traversing chains - No longer creating arrays when traversing chains - Correctly memoizing alias tag chains - Adding `untrack` to fully ignore trackable contexts - Using a map instead of an array for lazy tag chains - Flattening tag chains
Updates to the latest version of Glimmer with the functional tag API and perf improvements/tuning.
1f0be85
to
bd7f492
Compare
Looks like some TS failures from upgrading:
|
28155cd
to
fca3e98
Compare
fca3e98
to
5bbd43e
Compare
The |
This PR represents a journey I've taken over the past 2 weeks, tuning and tweaking Ember and Glimmer to try to make up the regression caused by enabling tracked properties. The TL;DR: With all of the work done in this PR and its companion over in Glimmer, the regression has been reduced by ~90% in emberobserver.com, from ~413ms to ~48ms (with 8x CPU slowdown).
Here are the results, and the data that backs them:
results.pdf
results.json.txt
There are a number of optimizations that were made to make this happen. Each individual test case represents one step in those, and I'll cover each in detail. The commits for these steps are as follows:
master
49ea6fe9e
5de7da10
ember-optimizations
905ee3cf4
5de7da10
glimmer-bugfixes
905ee3cf4
6f3680a9
make-updatable-tags-dirtyable
8f9991a2a
6f3680a9
better-caching
8f9991a2a
84afef73
monomorphic-validation
8f9991a2a
1c0a90ce
monomorphic-tag
d0d0339ba
599467ee
tracked-disabled
b6a3ecdb6
5de7da10
The Glimmer portion of the work is in this PR: glimmerjs/glimmer-vm#963
Big thanks to @krisselden for helping out with this work!
ember-optimizations
The first step in optimizing was to try to optimize the new code in Ember itself. The feature work had been a bit hasty, so there were definitely a few opportunities in there. The optimizations include:
foo.bar.baz
for a CP dependency, we need the tags forfoo
,bar
, andbaz
. We need to actually accessfoo
to getbar
's tag, and then we need to accessbar
to getbaz
's tag, but we don't need to accessbaz
, since its the end of the chain.untrack
feature that allows us to explicitly disable autotracking and avoid creating/tracking tagsEach of these micro-optimizations ultimately led to a ~125ms improvement in performance. So far, things were looking good, and we seemed to be on track!
glimmer-bugfixes
Unfortunately, we also uncovered a few bugs in Glimmer that needed to be fixed. Specifically, Glimmer's tags couldn't handle cycles, and their caching was too aggressive.
The cycles issue was not overall too much of a hit to fix. The bigger regression was in fixing caching. They would cache immediately on being calculated, and later on if they were updated to point to a new tag, this update would not bust their cache. We do these updates during the computation of a reference's value, so by the time the reference was done calculating, the tag's cache (set before the reference started calculating) was already stale.
The fix was to bump the global revision counter for every update. This meant that the act of updating state based on "real" dirtiness would cause us to recheck many, many tags (counts jumped from lows 1000s of checks to hundreds of thousands). This set us back quite a bit, leading to a ~177ms regression, setting us back to a worse place than we started.
make-updatable-tag-dirtyable
Up until this point, there was an
UpdatableTag
class and aDirtyableTag
class, but not one that could do both, which is what Ember needed. We were using a combinator tag to combine the two for every property tag in Ember, which was a fair amount of work, and meant we had that many more layers of tags to check and recalculate frequently due to the caching fixes.I originally began by backporting the
UpdatableDirtyableTag
that is in Glimmer's master branch, but didn't see as much on an improvement as I thought I would. This pointed to some kind of polymorphism being introduced, where a code path that was used to running for only one type of object regressed because it now had to deal with more types. Sure enough, combining theUpdatableDirtyableTag
APIs intoUpdatableTag
proved to be a much better improvement.With this change, we saw a ~161ms improvement, which almost fixed the regression caused by our bugfixes.
better-caching
The caching was clearly a huge bottleneck in tag calculation, especially the re-dirtying that could be caused by updates. The thing was, updates also didn't really need to bump the global revision count. They didn't signal a change in state, as much as a change in organization of tags.
We tried a few different strategies here, including separating out the validation logic so it wouldn't eagerly calculate and cache the value, and that helped a bit, but not as much as we would have liked. After thinking about it some more, I realized a few things were true here:
Using these principles, we can make a large change here, and have all tags always return the current revision count when we ask for their value. We can then compare the tag's own "inner" value against the last revision count to know if anything has changed in the tag since the last revision bump.
This change brought the counts for cache busts/checks back down into the 1000s, and netted us a ~121ms improvement.
monomorphic-validation
The fact that the
UpdatableDirtyableTag
fix showed more polymorphism was bugging me still, so I returned to that next. In looking at the code for validators, I realized that many tags shared methods via subclassing. These methods would interact with different shaped objects all the time, each of the subclassed tags.Duplicating the logic they shared via subclassing directly into each subclass resulted in ~36ms improvement, not the best change but still a decent chunk, pointing to polymorphism.
monomorphic-tag
The final optimization was to replace all of the tag classes, except
CURRENT_TAG
which had to have its own class, with a single class. This also allowed us to get rid of theTagWrapper
class (which existed to prevent polymorphic access) and simplify tag logic quite a bit.This was a relatively large bump, showing a ~93ms improvement over the last change.
This is also definitely the largest change in this series. It would simplify tags, but also make them less flexible, and potentially easier to abuse since any tag can do any operation (combinations, updates, dirties). This may be less of an issue if we switch to a more functional API for tags, since it would allow us to control their usage more.