Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

scope.$watchGroup + $interpolation speedup #7158

Closed
wants to merge 11 commits into from

Conversation

IgorMinar
Copy link
Contributor

This is my refactoring of @rodyhaddad's PR #6598

I tried several approaches to clean it up and ended up doing a full circle by keeping the $interpolate optimization that Rody did for parsing but then reimplementing the $watchGroup logic inside of the function returned by $interpolate. This resulted in almost no breaking change (the only difference is that fn returned by $interpolate now expects a proper Scope instance rather than an object), removal of all hacks and still preserving the performance boost.

There are three minor todos left before this can be merged, but I'm throwing it out here so that others can start reviewing.

Todos left:

  • remove the single expression optimization from watchGroup, we don't need it any more
  • hide the compute fn currently exposed as a property of the fn returned by $interpolate. this requires changes to 17 tests.
  • squash

@IgorMinar
Copy link
Contributor Author

@rodyhaddad what do you think about these changes?

rodyhaddad and others added 7 commits April 18, 2014 16:56
Given an array of expressions, if any one expression changes then the listener function fires
with an arrays of old and new values.

$scope.watchGroup([expression1, expression2, expression3], function(newVals, oldVals) {
 // newVals and oldVals are arrays of values corresponding to expression1..3
 ...
});

Port of dart-archive/angular.dart@a3c31ce
BREAKING CHANGE: the function returned by $interpolate
no longer has a `.parts` array set on it.
It has been replaced by two arrays:
* `.expressions`, an array of the expressions in the
  interpolated text. The expressions are parsed with
  $parse, with an extra layer converting them to strings
  when computed
* `.separators`, an array of strings representing the
  separations between interpolations in the text.
  This array is **always** 1 item longer than the
  `.expressions` array for easy merging with it
concat.length = separators.length + expressions.length;

var lastValues = [];
var lastResult;
Copy link
Contributor

Choose a reason for hiding this comment

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

Having these variables be "per interpolationFn" has the following effect:
If an interpolationFn gets watched on multiple scopes, the optimization disappears. The inputsChanged below will always be true, making it re-compute things like before.

The frequency of this scenario is relatively high. The textInterpolateDirective calls $interpolate once and watches it every time it's linked to a scope.
This implies that the optimization we're after no longer has an effect for {{ }} in text nodes. when they're in a DOM that gets linked to more than 1 scope at the same time. I believe ng-repeat is the only directive that does that.

Example:

<div ng-repeat="val in [0, 1]">
  {{ $index }}
</div>

The lastValues will keep switching from [0] to [1] and vice-versa on every digest. This won't cause a 10 $digest error, because interpolationFn will still return the correct string for each scope. But it would be re-computing it (like before this PR), maybe making it a bit worse because there's 2 for-loops in play now (speculation, they're smaller now)


A quick way to solve this is by calling $interpolate again every time the textInterpolateDirective gets linked. I'm not sure how you feel about that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

interesting observation! using watchGroup keeps the state associated with the scope so then we don't have this problem. But watchGroup also means that you moving the responsibility of watching for changes onto the client of $interpolate which means that it's an awkward opt-in optimization.

I wonder if we could keep a local catch of values in $interpolate that would be associate the values with scopes identified via scope ids.

I'll try to write this up now.

@rodyhaddad
Copy link
Contributor

Is the breaking change regarding a proper Scope instance necessary?
We're only using it for its $eval.

Why not do val = $parse(expressions[i])(context), which is 100% what $eval does anyway.

This also means we can hide the compute fn and remove its use in our tests.

@@ -777,6 +777,89 @@ describe('Scope', function() {
});
});

describe('$watchGroup', function() {
Copy link
Contributor

Choose a reason for hiding this comment

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

What do you think of these tests instead.

Although they don't test that the api works with 1 expression in the array, they use the log service, and spies for the deregistration test, instead of a log string variable.

You can take this commit and squash it to my first one. It also removes the single expression optimization, and makes use of self = this for nicer code. (I had added the commit to my last PR. I'm guessing you pulled it before I pushed)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I already made other changes to those tests, so I just rewrote them to use log service.

@IgorMinar
Copy link
Contributor Author

I change the branch to use $parse directly instead of relying on scope. This has additional tiny benefit of being able to cache parsed getter functions closer to $interpolate, but I don't think that make a big difference.

@IgorMinar
Copy link
Contributor Author

@rodyhaddad alright, so I made all the changes + rebased on top of master and push to my branch

the only thing that remains is the issue with ngRepeat that you mentioned earlier. I'm looking into that now

@IgorMinar
Copy link
Contributor Author

in 0b6cbf7 I also added per-scope cache, so I think we should be good now. the implementation is not ideal, but at this point I don't think that an ideal solution exists that is not a huge breaking change.

@IgorMinar
Copy link
Contributor Author

the cache is per interpolation + per scope, so I don't think that the problem you describe is really a problem. but I'll take another look.

@rodyhaddad
Copy link
Contributor

Oh! my bad
Idk why I thought lastValuesCache was global to all interpolations :o

Actually this is great!

@IgorMinar
Copy link
Contributor Author

yeah. I think it's fine. I'm landing it now

@IgorMinar
Copy link
Contributor Author

landed as 21f9316...546cb42

@IgorMinar IgorMinar closed this Apr 21, 2014
@IgorMinar
Copy link
Contributor Author

Thanks rody!

@rodyhaddad
Copy link
Contributor

Thank you Igor!
It was definitely fun to work on this :-)

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

Successfully merging this pull request may close these issues.

2 participants