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(ivy): fix views manipulation logic #22656
fix(ivy): fix views manipulation logic #22656
Conversation
1af57fb
to
239ea2d
Compare
packages/core/src/render3/di.ts
Outdated
@@ -576,6 +576,10 @@ class ViewContainerRef implements viewEngine_ViewContainerRef { | |||
const lView = (viewRef as EmbeddedViewRef<any>)._lViewNode; | |||
insertView(this._node, lView, index); | |||
|
|||
// TODO(pk): this is super-hackish, but we need to adjust nextIndex so the containerRefreshEnd |
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.
Not clear what should be done here ?
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.
It would be nice to have an explanation of under what conditions this arrises and why we are fixing it this way.
Always answer: what, when, why questions in your comments.
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.
Ha! Me neither at this point :-) To be discussed!
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.
Updated this comment
|
||
if (viewUpdateMode) { | ||
previousOrParentNode = views[lContainer.nextIndex++]; | ||
const existingViewNode = previousOrParentNode = views[existingViewIdx]; |
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 introducing a new const here ?
If this is to improve readability:
- add a type
const existingViewNode: LViewNode = ...
? remove type cast on l 1199 - use the new const l 1197
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.
Yep, readability
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.
Refactored this part in the fixup commit
// no views to delete between expected and actual position of a view being processed | ||
if (existingViewIdx > containerState.nextIndex) { | ||
for (let i = containerState.nextIndex; i < existingViewIdx; i++) { | ||
removeView(container, i); |
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 it happen that the views are moved ? Would remove + insert be efficient in this case ?
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, views can't move based on instructions in a template. They can be created / removed but always stay in the same order.
For the VCRef support we will need proper move, but at this point I'm not sure if we are going to use exact same code for VCRef support.
I don't think that insert / remove is a good substitute for move - too many data structures to re-create 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.
No, views can't move based on instructions in a template. They can be created / removed but always stay in the same order.
How does this play with NgFor then ?
*/ | ||
function Template(ctx: any, cm: boolean) { | ||
if (cm) { | ||
container(0); |
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.
Thinking about it:
- We should have 3 containers not 1.
- BlockID was a mistake and should be removed.
Case for 3 containers
I don't believe this is a valid test. Each %
block should have its own container
so I think we should have
container(0);
container(1);
container(2);
each if
should be surrounded with a separate containerRefreshStart
and containerRefreshEnd
.
Think of it this way.
% if (exp) {
text1
% }
text2
% if (exp) {
text3
% }
The above clearly has two containers:
container(1);
text('text2');
container(2);
Now remove the text2
and you should have:
container(1);
container(2);
But our test shows that we have:
container(1);
and that is wrong since inserting extra text nodes should not fundamentally change the behavior of the code.
I believe that this issue would not show up with multiple containers.
Case for removing BlockID.
Each block has a blockId
as seen in embeddedViewStart(blockId)
. This is needed because in the case of if
the then
and else
block are fundamentally different shape (ie they can have different bindings). To be able to differentiate between the two cases we came up with the blockId
solution. This test demonstrates to me that that solution was wrong because it is a solution for the else
block. No other situation has the else
block problem.
Assume if (exp) { } else {}
is same as if (exp) {} if (!exp) {}
and I have already argued that the second case should have two containers. So we should drop blockId
and instead generate more containers. The issue is that } else {
requires us to insert code into the else
part which we can't do in else
(and in the case of this test would require us to insert code into } /*HERE*/ if (exp) {
which we don't want to do.
% if (exp) {
textTrue
% } else {
textFalse
% }
should compile to:
function (ctx, cm) {
if (cm) {
container(0);
container(1);
}
containerRefreshStartRange(/*startContainer*/0, /*endContainer*/1);
if (ctx.exp) {
embeddedViewStart(/*containerId*/ 0);
embeddedViewEnd();
} else {
embeddedViewStart(/*containerId*/ 1);
embeddedViewEnd();
}
containerRefreshEndRange(/*startContainer*/0, /*endContainer*/1);
}
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.
@mhevery I've discussed at length with @marclaval and @b-laporte pros and cons of one vs. multiple containers and the conclusion was that we don't see one approach being superior to another.
We see those things more like a tradeoff, each solution having pros and cons. If we go with multiple containers we will create more data structures so slow down creation mode. Also, I don't think that we are introducing any perf penalty with one container, see: #22656 (comment)
In any case we are talking about a corner cases really (if / else
and blocks being opened / closed on the same line).
Given all the above could we re-evaluate the decision to make the refactoring at this point of time? An alternative curse of action would be to:
- merge this fix (we've got a bug that we should fix);
- use the time to work on
ViewContainerRef
/ViewRef
to get better understanding of all problems / use-cases around views and containers; - review the one vs. multiple containers decision after
ViewContainerRef
/ViewRef
is done.
WDYT?
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.
Per our discussion in the meeting, I agree with your assessment.
* 1 | ||
* % }; if(ctx.condition2) { | ||
* 2 | ||
* % } ; if(ctx.condition3) { |
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.
nit: extra space } ;
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.
fixed
packages/core/src/render3/di.ts
Outdated
@@ -576,6 +576,10 @@ class ViewContainerRef implements viewEngine_ViewContainerRef { | |||
const lView = (viewRef as EmbeddedViewRef<any>)._lViewNode; | |||
insertView(this._node, lView, index); | |||
|
|||
// TODO(pk): this is super-hackish, but we need to adjust nextIndex so the containerRefreshEnd | |||
// instruction is not removing dynamically inserted views |
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.
what do you mean by dynamically inserted
What other inserts are there?
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.
clarified
packages/core/src/render3/di.ts
Outdated
@@ -576,6 +576,10 @@ class ViewContainerRef implements viewEngine_ViewContainerRef { | |||
const lView = (viewRef as EmbeddedViewRef<any>)._lViewNode; | |||
insertView(this._node, lView, index); | |||
|
|||
// TODO(pk): this is super-hackish, but we need to adjust nextIndex so the containerRefreshEnd |
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.
It would be nice to have an explanation of under what conditions this arrises and why we are fixing it this way.
Always answer: what, when, why questions in your comments.
containerState.views[containerState.nextIndex - 1] as LViewNode : | ||
null; | ||
const viewIdChanged = previousView == null ? true : previousView.data.id !== viewNode.data.id; | ||
const existingViewIdx = viewIndex(containerState, containerState.nextIndex, viewNode.data.id); |
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 am not sure I like the fact that we have to do linear scan here. This will be slow with large container sets.
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.
@mhevery my current understanding is that we need a loop in certain, rare conditions. I've refactored the code to make it obvious. Let me explain in more details.
Case where views need to be refreshed
In the update case we will find a view at the next possible position so there is no iteration.
Case where a view need to be inserted
In this case we will find a view with a higher block id or reach the end of the views collection. No loop in this case.
Case where views need to be removed
This is the only case where we can have a real scan. Let's consider the "worst-case" scenario. Given the following template:
% if (true) {
start
% }; for (let i = 0; i < collection.length; i++) {
item {{i}}
% }; if (true) {
end
% }
and a collection shrinking from, say, 10k items to 0 items, we would have to scan 10k items.
But we must do this loop anyway to remove obsolete views!
We need to have this loop no matter what and this PR doesn't add any additional processing in the current form (I've refactored the code to make it more clear).
239ea2d
to
e852902
Compare
Can you get |
This commit fixes a bug that would result in views insert / remove even if a view needed only refresh operation. The crux of the bug was that we were looking for a view to update only in the LContainer.nextIndex position. This is incorrect as a view with a given block id could be present later in the views array (this happens if we about to remove a view in the middle of the views array). The code in this fix searches for a view to update in the views array and can remove views in the middle of the views collection. Previously we would remove views at the end of the collection only.
f10162b
to
f01d4c1
Compare
f01d4c1
to
6d5eede
Compare
This commit fixes a bug that would result in views insert / remove even if a view needed only refresh operation. The crux of the bug was that we were looking for a view to update only in the LContainer.nextIndex position. This is incorrect as a view with a given block id could be present later in the views array (this happens if we about to remove a view in the middle of the views array). The code in this fix searches for a view to update in the views array and can remove views in the middle of the views collection. Previously we would remove views at the end of the collection only. PR Close angular#22656
This issue has been automatically locked due to inactivity. Read more about our automatic conversation locking policy. This action has been performed automatically by a bot. |
This commit fixes a bug that would result in views insert / remove
even if a view needed only refresh operation.
The crux of the bug was that we were looking for a view to update
only in the LContainer.nextIndex position. This is incorrect as a
view with a given block id could be present later in the views
array (this happens if we about to remove a view in the middle of
the views array).
The code in this fix searches for a view to update in the views array and
can remove views in the middle of the views collection. Previously we
would remove views at the end of the collection only.