-
-
Notifications
You must be signed in to change notification settings - Fork 115
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: improved diffing #103
Conversation
Added check of params for template's methods events and helpers
… from 1.3 onwards.
… from 1.3 onwards.
… from 1.3 onwards.
Thanks @mitar, I was only asking as it was left in limbo on the forum https://forums.meteor.com/t/community-involvement-setup/29538/15 Essentially our goal was not incremental DOM per se, it was to improve rendering and avoid wasted calls to DOM. I don't think our solution is optimal, but it is very good with the constraints around it. Look forward to you completing your review, in the mean time, I'll try to see if I can invest time in this:
It's likely a Tracker / diff issue (something MDG seems to have known about, and so have you) As far as urgency, there is a message to the community: "Blaze is getting love and it's getting better!" This could be a temporary fix. |
No, this is not what is known. It is known that if a document for a particular entry changes at all, that one will be rerun completely, because default equality for data context is conservative. But this for insertion is something new, at least to me. And is not how it was documented in the past. So if this is happening, I would see this as a bug, maybe even regression. And I would like to know where it is coming from. It should work both for arrays with What I think we have to get to is:
I think nobody in Meteor community expects "be quick and break things" approach to development. In any case, I do not see that as a good reason to not do things properly. I have also not seen 1000 upvotes or comments to quickly merge on this pull request, so I do not see any urgency from others either. I know that when you make a pull request you feel amazing and would love to get things merged. But again, when you make a pull request which was not discussed before, then you have to take responsibility to hold your own horses. This is on you. |
Yes, this is why I am proposing that we do comparison with previous value of HTMLJS for that object. So instead of converting to DOM elements, we compare HTMLJS itself. And only apply those changes which we find different between HTMLJS values. Again, this is what I am saying that could be an interesting optimization which would work similar to virtual DOM, now that you investigated incremental DOM and are saying that it is not a good match. And I agree that we should also optimize the whole pipeline for Tracker recomputations as well. But now imagine that we can make DOM rendering faster without Tracker recomputations optimization. Then when we have both, so great!
No, this is a very simple change, for this particular approach to materializing with comparing HTMLJS, because you are interested in this. Or was.
It would still be great if we can solve the issue there as well. I do not think we should worry too much about pull requests not getting in, they are pretty efficient on merging pull requests recently. And even if it goes late on, it goes. But somebody has to make, to even go in at all. |
@@ -352,13 +352,16 @@ Blaze._materializeView = function (view, parentView, _workStack, _intoArray) { | |||
view._isInRender = false; | |||
|
|||
if (! c.firstRun) { | |||
// added by ramezrafla@zegenie to skip affecting DOM for unchanged content | |||
if (Blaze._deepCompare(lastHtmljs,htmljs)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, you are comparing last and new HTMLJS already. Why you wrote (or I understood) that we are talking about data layer comparison.
So yea, this is similar to what I had in mind with virtual DOM. To compare old and new HTMLJS.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So this part of the change is something I like.
I do think we can do better though. :-)
First, why are you not simply using EJSON.equals
? If you think that one can be optimized, than I would suggest make pull requests to it. Hm, but do we want to introduce a new dependency for Blaze?
So, one thing I think we can improve is that it seems that HTMLJS can contain not-rendered templates. See this code snippet. This means that if you are comparing any such lastHtmljs
and htmljs
they will not be equal, yes? So maybe it would be better if we make one pass over the HTMLJS tree, converting it into a pure HTMLJS tree. And then compare. The issue with that is that this might confuse how rendered
callback is called. We would have to mark which segments of HTMLJS require their view 'rendered' to be called. But my worry is that now with any more complicated HTMLJS segment we are not improving much. What do you think?
Next idea: now we are all or nothing. We compare HTMLJS, if it is equal, great. But if there is one attribute difference, we still go in and then create DOM elements for whole HTMLJS tree and compare them, no, inserting/updating them? Can you point me to where DOM elements are compared?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So currently I worry that this prevents only the innermost HTMLJS trees from being rerendered.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you point me to where DOM elements are compared?
In the past, the _isContentEqual did just that. We are no longer doing that now, we are comparing data before it becomes DOM.
So currently I worry that this prevents only the innermost HTMLJS trees from being rerendered.
If the deepcompare finds the objects to be the same, it makes sense to skip re-rendering, unless I missed something.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the past, the _isContentEqual did just that. We are no longer doing that now, we are comparing data before it becomes DOM.
I mean, where it is comparing does it have to replace a DOM element with a new one. It seems to me that it does not do a new DOM element and then compares with current DOM, if necessary. But it removes a DOM element if previous view is invalided, it then creates a DOM element for new state of the view, and adds it. Even if comparing previous and new DOM element would show that it is almost the same, or even exactly the same.
If the deepcompare finds the objects to be the same, it makes sense to skip re-rendering, unless I missed something.
Yes, but I see those checks to be run only for very leaf rendering, nothing before. :-(
// Are the HTMLjs entities `a` and `b` the same? We could be | ||
// more elaborate here but the point is to catch the most basic | ||
// cases. | ||
Blaze._isContentEqual = function (a, b) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why you didn't just improve this method?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I could have simply renamed the new function as ._isContentEqual, except this function is meant for DOM comparison, and we are comparing objects / primitives, so to avoid confusion I broke with the past.
@@ -226,6 +226,11 @@ Blaze.Each = function (argFunc, contentFunc, elseFunc) { | |||
}); | |||
}, | |||
changedAt: function (id, newItem, oldItem, index) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But this is at the data level, no?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, we have two additional diff levels right (see my original PR comment)
- Data level
- Field level in htmljs
I think 1 will being gone once we improve the original diff-ing which calls the observe handlers
Can you check difference for the 01-dbmon example between current Blaze version and one with this pull request? |
@@ -352,13 +352,16 @@ Blaze._materializeView = function (view, parentView, _workStack, _intoArray) { | |||
view._isInRender = false; | |||
|
|||
if (! c.firstRun) { | |||
// added by ramezrafla@zegenie to skip affecting DOM for unchanged content | |||
if (Blaze._deepCompare(lastHtmljs,htmljs)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So I looked into this change using this example and I could not get it to in fact come to this if (! c.firstRun) {
condition with any other view than lookup:something
.
If I did:
Collection.update({}, {$set: {value: 'a'}})
Change was from aa
to a0
.
And then if I did:
Collection.update({}, {$set: {value: 'a', test: 'b'}})
Change was from aa
to aa
.
So, I think that Blaze._isContentEqual
moving up is a great idea for cases like above, where I am not minimizing my fields selector well in Minimongo. But I am not sure if there is any benefit of changing the function to deep-comparison? Can you please adapt my example to the case where you get something else than those simple values Blaze._isContentEqual
is already checking? I could imagine only very extreme cases where you would be using {{value}}
where value
is not a simple variable, but an object, and you are using its stringified value to be shown in the template. But is anyone really doing that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, we had deep variables. I think for a complex App, it is to be expected.
Here is your example adapted to our test case:
var obj = { a: { b : 1}}; Collection.update({}, {$set: obj});
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, it does not happen. So what I am saying is that you will never really do in your app:
{{value}}
Where:
value = {a: {b: 1}};
You might do:
{{value.a.b}}
But what I am saying is that in the comparison, Blaze just compares then values of b
s, so primitive values.
So can you give me a realistic example where the value of lastHtmljs
and htmljs
is something else than what it is checked by Blaze._isContentEqual
? I have updated my example with complex objects to test this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I just used deepCompare for the sake of convenience and because I haven't gone through all the options. deepCompare short circuits comparison if it's a primitive so I am reusing the same function for risk mitigation as the data level.
But I did see somewhere in the code that htmljs can be views. Also, we skipped DOM comparison altogether that was there before, which was my biggest concern. If I was too cautious, then we can simply simplify the compare function. But, why would the original developers convert to DOM if it was always primitives?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But I did see somewhere in the code that htmljs can be views. Also, we skipped DOM comparison altogether that was there before, which was my biggest concern. If I was too cautious, then we can simply simplify the compare function.
Yes, but at a higher level. These views which have !computation.firstRun
seems to be very simple ones.
Which is also a problem probably, that higher-levels are recreated.
So can you make in your codebase (on which you are doing these optimizations here) a check to print out when computation.firstRun
is false and htmljs
is something complicated.
But, why would the original developers convert to DOM if it was always primitives?
I have no idea why they convert at all before the if
. The fact that you moved if
out is great, but I have no idea why they did it like this. Does converting to DOM has some side-effects we do not know?
@@ -226,6 +226,11 @@ Blaze.Each = function (argFunc, contentFunc, elseFunc) { | |||
}); | |||
}, | |||
changedAt: function (id, newItem, oldItem, index) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In my example I could not get to this issue. If I instrument changedAt
method to print out every time is called, and then I click "add 5" button, this is not called at all, not for 5 or any other value in the Minimongo database.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried even to limit my fields selector to {value: 1, test: 1}
. Then:
Collection.update({}, {$set: {value: 'a'}})
newItem, oldItem: Object {value: "a", _id: "s84f5D5bwFgPzwvmk"} Object {value: 0, _id: "s84f5D5bwFgPzwvmk"}
Collection.update({}, {$set: {value: 'a', test: 'b'}})
newItem, oldItem: Object {value: "a", _id: "s84f5D5bwFgPzwvmk", test: "b"} Object {value: "a", _id: "s84f5D5bwFgPzwvmk"}
Collection.update({}, {$set: {value: 'a', test: 'b', test2: 'c'}})
The last change changes a field outside of the projection. And changedAt
is not called.
So, I could not reproduce that changedAt
would be called unnecessary if a value was not really changed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, our test case was as per above comment:
var obj = { a: { b : 1}}; Collection.update({}, {$set: obj});
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So I could not reproduce that changedAt
when data projected using fields
didn't really change.
For example, if I have the following cursor over which I am doing #each
:
Collection.find({}, {sort: {'value.a': 1}, fields: {'value.a': 1, test: 1}});
And values are inserted like:
Collection.insert({value: {a: 1}});
When I do:
Collection.update({}, {$set: {'value.b': 'b'}})
changedAt
is not called at all.
And if I insert one one document in the middle, only Blaze for that one document is called, and no changedAt
at all, for nothing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The only difference with what I did is that it was server side update with method. If you can replicate it, it means that the problem is in the publish / diff-ing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you please make an example showing what you observed?
We need tests for these changes anyway.
(I wanted to make this an inline reply to a inline comment, but I am realizing it is pretty valuable, so I am moving it top-level.) Yes, sorry about that. I understood something else under "data level". For me this is more data context level, which has its own issues. Sorry for misunderstanding. Maybe we should define terminology here better. To my knowledge we have issues at:
Did I miss anything? |
@mitar
We have an App that affects the DOM directly and had no issues. I think it has to do with carefully managing collisions. We do not with JS affect anything that is handled by Blaze. If that separation is done, we're good
Right! So the first one has to do with the diff-ing algorithm in the compare-sequence lib you had talked about before. This is where the real fix should be applied, unless it breaks something else (is this used by Tracker?) The second is where I believe we have a lot of savings. We don't build DOM elements to diff against DOM. This is bad design. We should always diff raw data, not display layer. This is what makes Blaze fast (like VirtualDOM) and is the reason Incremental DOM is slower than Virtual DOM. Edit: And given so many unchanged objects can be fed through, that DOM element creation for diffing is really problematic |
Oh, maybe I do know about history, but about internals we are all learning. I think you looked much deeper than I ever did in details of rendering itself. So we are all learning here.
Yes, me too. Or you leave to Blaze, or you use something else. But do not mix. :-)
But I have not been able to reproduce it so that I could look what is happening. But I was using Minimongo and not arrays directly.
Yes, but I could not really find where diffing is done. I have found diffing for attributes, but for DOM it seems like Blaze simply removes old element and adds a new one, when something changes. It does not try to diff and patch the old one? |
From view.js var rangesAndNodes = Blaze._materializeDOM(htmljs, [], view);
if (! Blaze._isContentEqual(lastHtmljs, htmljs)) {
domrange.setMembers(rangesAndNodes);
Blaze._fireCallbacks(view, 'rendered');
} So this creates the DOM elements before comparing |
Yes, but it compares HTMLJS, not DOM. For DOM it seems (inside |
BTW, this is waiting for tests/reproducible example for the The second change I like and I think we should add it to Blaze, but I will maybe add it with |
So I think this should be split into two pull requests. One for moving |
I cleaned |
Based on this pull request, I made a change in e17991a, which prevents unnecessary materialization of DOM. This is what I have able to make a reproduction myself. For the other part, checking a change in data layer I have not been able to make a reproduction. I am closing this pull request. If we find a good reproduction for that other case, we could look into optimizing that as well. For more ideas how to optimize Blaze see #111. |
@mitar Did you benchmark this change on any of your test cases? |
Sadly not, I do not have any real case where this is happening. I am relaying on the report in this pull request. Feel free to make a test case/benchmark and help here. |
I might have something useful in one of my projects for a benchmark. |
Beautiful @mitar! Thanks! @arggh, we caught this as we outputted to the console every re-render. This can improve performance dramatically by skipping DOM manipulation if we have a very dynamic page. I am guessing this change is going to be very noticeable for a lot of people. Look forward to your benchmark. There is also room for improvement in the actual diffing but that's a bigger job. |
One more thing. I was unable to get to |
See Issue #45
Summary: the diff-ing engine used by Meteor lets through content that has not changed, especially relevant when we are display arrays. We added a fast comparison function and used it in two key locations to prevent needless changes to the DOM.