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

Expression caching #9082

Closed
wants to merge 4 commits into from
Closed

Conversation

IgorMinar
Copy link
Contributor

improved version of #9006 (which was an improved version of #8942)

@IgorMinar IgorMinar mentioned this pull request Sep 14, 2014
@IgorMinar
Copy link
Contributor Author

@jbedard I rebased your PR #9006 and made several improvements and fixed some small bugs.

I think this is pretty close to being mergable. The missing pieces are:

  • rename externalInput to something more intuitive?? and prefix it with $
  • better docs and commit messages

Please take a look at my changes and let me know if you see anything odd

@jbedard
Copy link
Contributor

jbedard commented Sep 14, 2014

Looks like you didn't rebase the latest commit? I had a couple additional
changes to support short-circuiting operators and interceptors better, also
added tests and fixed a watchCollection issue...

@IgorMinar
Copy link
Contributor Author

Yup. You are right. I'll update my PR later. I'm on cell so it's hard to read your diff but from what I've seen, I'm not quite sure how does the isBranching check make things any better. Can you shed some light on this?

expect(watcherCalls).toBe(1);
scope.$digest();
expect(filterCalls).toBe(1);
expect(watcherCalls).toBe(1);
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't really understand this test --- "should calculate the literal every single time", but asserting that the filter and watcher was only called once in two digests.

What are we actually testing here? That, because the inputs didn't change, we don't call the filter / consider the watch to have changed?

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 think I pointed out the same. I'm going to take a closer look at the
tests before merging.
On Sep 15, 2014 4:18 PM, "Caitlin Potter" notifications@github.com wrote:

In test/ng/parseSpec.js:

  •        $filterProvider.register('foo', valueFn(function(input) {
    
  •          filterCalls++;
    
  •          return input;
    
  •        }));
    
  •        var watcherCalls = 0;
    
  •        scope.$watch($parse('{x: 1} | foo'), function(input) {
    
  •          expect(input).toEqual({x:1});
    
  •          watcherCalls++;
    
  •        });
    
  •        scope.$digest();
    
  •        expect(filterCalls).toBe(1);
    
  •        expect(watcherCalls).toBe(1);
    
  •        scope.$digest();
    
  •        expect(filterCalls).toBe(1);
    
  •        expect(watcherCalls).toBe(1);
    

I don't really understand this test --- "should calculate the literal
every single time", but asserting that the filter and watcher was only
called once in two digests.

What are we actually testing here? That, because the inputs didn't change,
we don't call the filter / consider the watch to have changed?


Reply to this email directly or view it on GitHub
https://github.com/angular/angular.js/pull/9082/files#r17543950.

Copy link
Contributor

Choose a reason for hiding this comment

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

The tests were also updated in the latest commit. Really I think all this test should do is...

expect($parse('{x: 1} | foo').constant).toBe(true);

Copy link
Contributor

Choose a reason for hiding this comment

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

This test got moved down after the merge/cherry-pick, although I think it should be simplified like I mentioned above.

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 removed this dupe.

the other copy does contain the "constant" assertion. I kept the other assertions as they were. I think that they are useful.

@jbedard
Copy link
Contributor

jbedard commented Sep 15, 2014

The isBranching is set for AND/OR operators and is mainly to fix short-circuiting such as foo || bar(). AND/OR will no longer be broken down to left/right inputs but instead the full expression is watched so the short-circuiting runs as normal. I also added a AND/OR operator benchpress case where this actually seemed faster then watching both foo and bar(), but the main reason for the isBranching flag was to fix short-circuiting so I didn't look into the performance too much. Expressions such as a == 1 || a == 2 | a == 3 | ... would probably improve by only watch the input a so this could be improved in the future (by detecting that the expression has no possible side-effects and then only watching the inputs?).

@tbosch tbosch modified the milestones: 1.3.0-rc.2, 1.3.0-rc.3 Sep 15, 2014
@IgorMinar
Copy link
Contributor Author

@jbedard I pulled in the changes from your branch and cherry-picked them on top of this branch. The use of isBranching makes sense. I didn't make the connection when reading the code on tiny screen.

As far as valueOf goes, we do need a different strategy because both of our solutions are incorrect. I was also thinking of keeping the primitive value around and comparing that. I'll give it a shot.

expect(watcherCalls).toBe(1);
}));
it('should not treat constants passed to filters with externalInput as constants', inject(function($parse) {

Copy link
Contributor

Choose a reason for hiding this comment

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

And this one should just set the externalInput flag on the foo filter, and expect(parsed.constant).toBe(false).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

such expression would result in infinite digest though because we'd recreate the constant object during each reevaluation.

this could be fixed in the future, but I'm not going to deal with it now. instead I'll just remove this single spec.

Copy link
Contributor

Choose a reason for hiding this comment

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

Right, didn't think of that. And it won't have the normal literal handling
because the literal is not the root...

In test/ng/parseSpec.js:

  •      var watcherCalls = 0;
    
  •      scope.$watch(parsed, function(input) {
    
  •        expect(input).toEqual({x:1});
    
  •        watcherCalls++;
    
  •      });
    
  •      scope.$digest();
    
  •      expect(filterCalls).toBe(1);
    
  •      expect(watcherCalls).toBe(1);
    
  •      scope.$digest();
    
  •      expect(filterCalls).toBe(1);
    
  •      expect(watcherCalls).toBe(1);
    
  •    }));
    
  •    it('should not treat constants passed to filters with externalInput as constants', inject(function($parse) {
    

such expression would result in infinite digest though because we'd
recreate the constant object during each reevaluation.

this could be fixed in the future, but I'm not going to deal with it now.
instead I'll just remove this single spec.


Reply to this email directly or view it on GitHub
https://github.com/angular/angular.js/pull/9082/files#r17571465.

Copy link
Contributor

Choose a reason for hiding this comment

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

such expression would result in infinite digest though because we'd
recreate the constant object during each reevaluation.

Actually, that sounds like a problem --- because it sounds like it could break peoples code. I might be missing something, but I don't think we want to not fix that

Copy link
Contributor

Choose a reason for hiding this comment

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

I think that is nothing new, but now it will only happen when the filter
also has the "externalInput" flag. I think the test was just bad...

For example $scope.$watch("[0] | filter") would always cause this because
the output is a literal (recreated each digest) but is not constant because
of the filter.

On 15 September 2014 15:21, Caitlin Potter notifications@github.com wrote:

In test/ng/parseSpec.js:

  •      var watcherCalls = 0;
    
  •      scope.$watch(parsed, function(input) {
    
  •        expect(input).toEqual({x:1});
    
  •        watcherCalls++;
    
  •      });
    
  •      scope.$digest();
    
  •      expect(filterCalls).toBe(1);
    
  •      expect(watcherCalls).toBe(1);
    
  •      scope.$digest();
    
  •      expect(filterCalls).toBe(1);
    
  •      expect(watcherCalls).toBe(1);
    
  •    }));
    
  •    it('should not treat constants passed to filters with externalInput as constants', inject(function($parse) {
    

such expression would result in infinite digest though because we'd
recreate the constant object during each reevaluation.

Actually, that sounds like a problem --- because it sounds like it could
break peoples code. I might be missing something, but I don't think we want
to not fix that


Reply to this email directly or view it on GitHub
https://github.com/angular/angular.js/pull/9082/files#r17572834.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yeah. this has always been this way. so i don't think that it's worth fixing right now.

@IgorMinar
Copy link
Contributor Author

@jbedard I fixed the valueOf issue and added some more tests. PTAL

var newInputValue = inputExpressions(scope);
if (!expressionInputDirtyCheck(newInputValue, oldInputValue)) {
lastResult = parsedExpression(scope);
oldInputValue = newInputValue.valueOf();
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this need newInputValue && to handle null/undefined like the one below?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yup. fixed

@jbedard
Copy link
Contributor

jbedard commented Sep 15, 2014

Other then the null/undefined comment everything LGTM. Except externalInput still needs a better public name (that's the hardest part :)

I had a few other ideas/questions but they could all wait:

  • Should the filter filter set externalInput since it almost always takes non-primitive input?
  • Should the loop in expressionInputsWatch break if it hits a non-primitive? That might reduce the overhead for expressions that always have non-primitives.
  • Should externalInput not be a flag but be a watched input/getter just like filter arguments? Then for things like translate filters it could be the language, so when the language changes the filter will be re-evaluated. But this would be a more complicated API then just a flag.
  • Currently expression inputs are recursed, combined, constants removed etc at $watch time and cached as $$inputs for any future watchers. But after $$inputs is calculated and cached all the inputs are left behind and never used again. Should those be cleaned up as we recurse (if !sharedGetter)? Or move the recursing/combining/removing-constants to parse time instead of watch time? Or is a small array of expressions not worth cleaning up (the expressions are all referenced elsewhere so only the array itself is can be GCed if we remove the reference)?

IgorMinar pushed a commit to IgorMinar/angular.js that referenced this pull request Sep 16, 2014
With this change, expressions like "firstName + ' ' + lastName | uppercase"
will be analyzed and only the inputs for the expression will be watched
(in this case "firstName" and "lastName"). Only when at least one of the inputs
change, the expression will be evaluated.

This change speeds up simple expressions like `firstName | noop` by 20%
and more complex expressions like `startDate | date` by 2500%.

BREAKING CHANGE: all filters are assumed to be stateless functions

Previously it was a good practice to make all filters stateless, but now
it's a requirement in order for the model change-observation to pick up
all changes.

If an existing filter is statefull, it can be flagged as such but keep in
mind that this will result in a significant performance-penalty that will
affect the $digest duration.

To flag a filter as stateful do the following:

myApp.filter('myFilter', function() {
  function myFilter(input) { ... };
  myFilter.$stateful = true;
  return myFilter;
});

Closes angular#9006
Closes angular#9082
@IgorMinar
Copy link
Contributor Author

I cleaned up the commits, squashed most of them and wrote a better commit message.

To answer your questions

  • I'm hoping that filter filter could take advantage of custom valueOf() for objects and arrays which would turn it into a really fast filter
  • I thought of breaking out of the expressionInputsWatch faster but it looked like it would make things worse. We should profile some common scenarios and then decide.
  • externalInput is now $stateful flag. we can consider providing a way to watch external input later
  • I don't think that freeing up the inputs array would result in noticeable mem improvements for the reasons you mentioned.

IgorMinar pushed a commit to IgorMinar/angular.js that referenced this pull request Sep 16, 2014
With this change, expressions like "firstName + ' ' + lastName | uppercase"
will be analyzed and only the inputs for the expression will be watched
(in this case "firstName" and "lastName"). Only when at least one of the inputs
change, the expression will be evaluated.

This change speeds up simple expressions like `firstName | noop` by ~15%
and more complex expressions like `startDate | date` by ~2500%.

BREAKING CHANGE: all filters are assumed to be stateless functions

Previously it was a good practice to make all filters stateless, but now
it's a requirement in order for the model change-observation to pick up
all changes.

If an existing filter is statefull, it can be flagged as such but keep in
mind that this will result in a significant performance-penalty that will
affect the $digest duration.

To flag a filter as stateful do the following:

myApp.filter('myFilter', function() {
  function myFilter(input) { ... };
  myFilter.$stateful = true;
  return myFilter;
});

Closes angular#9006
Closes angular#9082
jbedard and others added 3 commits September 16, 2014 12:17
With this change, expressions like "firstName + ' ' + lastName | uppercase"
will be analyzed and only the inputs for the expression will be watched
(in this case "firstName" and "lastName"). Only when at least one of the inputs
change, the expression will be evaluated.

This change speeds up simple expressions like `firstName | noop` by ~15%
and more complex expressions like `startDate | date` by ~2500%.

BREAKING CHANGE: all filters are assumed to be stateless functions

Previously it was a good practice to make all filters stateless, but now
it's a requirement in order for the model change-observation to pick up
all changes.

If an existing filter is statefull, it can be flagged as such but keep in
mind that this will result in a significant performance-penalty (or rather
lost opportunity to benefit from a major perf improvement) that will
affect the $digest duration.

To flag a filter as stateful do the following:

myApp.filter('myFilter', function() {
  function myFilter(input) { ... };
  myFilter.$stateful = true;
  return myFilter;
});

Closes angular#9006
Closes angular#9082
splitting the code into two chunks doesn't buy us anything and only makes the code harder to follow
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants