Skip to content
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

Conversation

pkozlowski-opensource
Copy link
Member

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.

@pkozlowski-opensource pkozlowski-opensource added target: major This PR is targeted for the next major release comp: ivy action: review The PR is still awaiting reviews from at least one requested reviewer labels Mar 8, 2018
@pkozlowski-opensource pkozlowski-opensource force-pushed the views_with_different_id_in_a_container branch from 1af57fb to 239ea2d Compare March 8, 2018 16:40
@@ -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
Copy link
Contributor

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 ?

Copy link
Contributor

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.

Copy link
Member Author

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!

Copy link
Member Author

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];
Copy link
Contributor

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

Copy link
Member Author

Choose a reason for hiding this comment

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

Yep, readability

Copy link
Member Author

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);
Copy link
Contributor

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 ?

Copy link
Member Author

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....

Copy link
Contributor

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);
Copy link
Contributor

@mhevery mhevery Mar 8, 2018

Choose a reason for hiding this comment

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

Thinking about it:

  1. We should have 3 containers not 1.
  2. 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);
}

Copy link
Member Author

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?

Copy link
Contributor

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) {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: extra space } ;

Copy link
Member Author

Choose a reason for hiding this comment

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

fixed

@@ -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
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 mean by dynamically inserted What other inserts are there?

Copy link
Member Author

Choose a reason for hiding this comment

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

clarified

@@ -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
Copy link
Contributor

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);
Copy link
Contributor

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.

Copy link
Member Author

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).

@pkozlowski-opensource pkozlowski-opensource force-pushed the views_with_different_id_in_a_container branch from 239ea2d to e852902 Compare March 13, 2018 11:04
@mhevery
Copy link
Contributor

mhevery commented Mar 13, 2018

Can you get lint to pass?

@mhevery mhevery added action: merge The PR is ready for merge by the caretaker and removed action: review The PR is still awaiting reviews from at least one requested reviewer labels Mar 13, 2018
@ngbot
Copy link

ngbot bot commented Mar 13, 2018

I see that you just added the PR action: merge label, but the following checks are still failing:
    failure status "ci/circleci: lint" is failing
    pending status "google3" is pending

If you want your PR to be merged, it has to pass all the CI checks.

If you can't get the PR to a green state due to flakes or broken master, please try rebasing to master and/or restarting the CI job. If that fails and you believe that the issue is not due to your change, please contact the caretaker and ask for help.

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.
@pkozlowski-opensource pkozlowski-opensource force-pushed the views_with_different_id_in_a_container branch 2 times, most recently from f10162b to f01d4c1 Compare March 14, 2018 13:58
@pkozlowski-opensource pkozlowski-opensource force-pushed the views_with_different_id_in_a_container branch from f01d4c1 to 6d5eede Compare March 14, 2018 14:10
@kara kara closed this in c09bd67 Mar 14, 2018
leo6104 pushed a commit to leo6104/angular that referenced this pull request Mar 25, 2018
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
@angular-automatic-lock-bot
Copy link

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators Sep 13, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
action: merge The PR is ready for merge by the caretaker cla: yes target: major This PR is targeted for the next major release
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants