Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Conversation

@JsouLiang
Copy link
Contributor

Add a mechanism for DisplayListBuilder to optimize out unnecessary save/restore pairs.

@flar
Copy link
Contributor

flar commented Jul 28, 2022

I would write test cases along the lines of:

DisplayListBuilder builder1;
... inefficient use of save/restore ...
auto display_list1 = builder1.Build();

DisplayListBuilder builder2;
... same code with optimal save/restore usage ...
auto display_list2 = builder2.Build();

verbose compare dl1 and dl2

@flar
Copy link
Contributor

flar commented Jul 28, 2022

I deleted some earlier comments about how the nested counts would add up and result in multiple extra save records in the stream, but that was before I noticed that the deferred save count was being kept in the layer info.

Still thinking through potential issues this solution might cause, but wanted to indicate why those comments were missing now.

@flar
Copy link
Contributor

flar commented Jul 28, 2022

The failure in the smoke tests is likely due to the new deferred save counts not returning an accurate value. The framework code that calls a CustomPaint's paint method gets a save count before and after the method and expects them to match.

@flar
Copy link
Contributor

flar commented Jul 28, 2022

getSaveCount() returns the number of records in the stack, but deferred saves are not accounted in that number. This will also affect uses of restoreToCount() which will only concern itself that it restores the right number of layer infos rather than undoing the real+deferred saves since the previous state. This might have caused the errors seen in the framework smoke tests. Fixing the value of getSaveCount() to account for deferred saves should also make sure that restoreToCount() functions correctly.

@flar
Copy link
Contributor

flar commented Jul 28, 2022

I wrote the following 2 test cases on top of a local copy of the fix just to verify to myself that these cases work right (and they did). I leave them here in case you want to add them to the list of tests...

Test case 1
TEST(DisplayList, CollapseMultipleNestedSaveRestore) {
  DisplayListBuilder builder1;
  builder1.save();
  builder1.save();
  builder1.save();
  builder1.translate(10, 10);
  builder1.scale(2, 2);
  builder1.clipRect({10, 10, 20, 20}, SkClipOp::kIntersect, false);
  builder1.drawRect({0, 0, 100, 100});
  builder1.restore();
  builder1.restore();
  builder1.restore();
  auto display_list1 = builder1.Build();

  DisplayListBuilder builder2;
  builder2.save();
  builder2.translate(10, 10);
  builder2.scale(2, 2);
  builder2.clipRect({10, 10, 20, 20}, SkClipOp::kIntersect, false);
  builder2.drawRect({0, 0, 100, 100});
  builder2.restore();
  auto display_list2 = builder2.Build();

  ASSERT_TRUE(DisplayListsEQ_Verbose(display_list1, display_list2));
}
Test case 2
TEST(DisplayList, CollapseNestedSaveAndSaveLayerRestore) {
  DisplayListBuilder builder1;
  builder1.save();
  builder1.saveLayer(nullptr, false);
  builder1.translate(10, 10);
  builder1.scale(2, 2);
  builder1.clipRect({10, 10, 20, 20}, SkClipOp::kIntersect, false);
  builder1.drawRect({0, 0, 100, 100});
  builder1.restore();
  builder1.restore();
  auto display_list1 = builder1.Build();

  DisplayListBuilder builder2;
  builder2.saveLayer(nullptr, false);
  builder2.translate(10, 10);
  builder2.scale(2, 2);
  builder2.clipRect({10, 10, 20, 20}, SkClipOp::kIntersect, false);
  builder2.drawRect({0, 0, 100, 100});
  builder2.restore();
  auto display_list2 = builder2.Build();

  ASSERT_TRUE(DisplayListsEQ_Verbose(display_list1, display_list2));
}

Copy link
Contributor

@flar flar left a comment

Choose a reason for hiding this comment

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

There is a known bug discovered by the PR checks where the value we return for getSaveCount() is wrong - I've added some ideas about that in the PR comments.

I believe the changes to dl_utils.h/cc are redundant and should be removed, but let me know if I missed something.

Other than that, just a naming nit and a potential inlining suggestion.


sk_sp<DisplayList> Build();

unsigned save_count() const { return save_count_; }
Copy link
Contributor

Choose a reason for hiding this comment

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

This appears to be a new statistic for the total number of save calls added to the stream. In order to distinguish itself from getSaveCount() it should probably be called either total_save_count_ or save_records_count_?

If this is just for testing, then I think some good tests using the verbose DL comparison testing facility would be more targeted, but I do acknowledge that this adds a quick extra check to all of our existing unit tests. Were any of those additional checks of the "save_count()" informative in terms of noticing optimized save/restores?

}
}

void DisplayListBoundsCalculator::save() {
Copy link
Contributor

Choose a reason for hiding this comment

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

deferred save elimination was already done during construction. Why is it being done here as well? The stream should already be reduced so all of this code in the calculator should be completely redundant. Does it actually encounter any missed deferred save/restores?

Copy link
Contributor

Choose a reason for hiding this comment

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

This still seems unnecessary and yet it is still here. Why are you checking the save/restores in the Calculator? That operation was already done in the Builder.

@flar
Copy link
Contributor

flar commented Jul 28, 2022

I would like to get @ColdPaleLight to see how this will interact with the PR to collapse the bounds calculations into the constructor.

Also, a perhaps less "disruptive" change would be to switch to always having a layer_stack entry for every save/restore pair regardless of whether it is deferred or not and then just having a boolean in the layer info indicating whether it is deferred. That would mean that the size of the layer stack would remain a good answer for getSaveCount and it could potentially mean that it will fit in better with #34365

Pseudo-code for boolean always-layer-info solution
save() {
  // always emplace a layer_info
  // make sure it is marked as deferred
}
saveLayer() {
  // make sure its layer info is marked as not deferred
}
checkForDeferredSave() {
  if (current_layer_.is_deferred) {
    // Push Save record into stream
    current_layer_.is_deferred = false;
  }
}
restore() {
  if (!current_layer_.is_deferred) {
    // Push Restore record into stream
  }
  // always pop a layer_info
}

@ColdPaleLight
Copy link
Member

I would like to get @ColdPaleLight to see how this will interact with the PR to collapse the bounds calculations into the constructor.

Also, a perhaps less "disruptive" change would be to switch to always having a layer_stack entry for every save/restore pair regardless of whether it is deferred or not and then just having a boolean in the layer info indicating whether it is deferred. That would mean that the size of the layer stack would remain a good answer for getSaveCount and it could potentially mean that it will fit in better with #34365

Pseudo-code for boolean always-layer-info solution

save() {
  // always emplace a layer_info
  // make sure it is marked as deferred
}
saveLayer() {
  // make sure its layer info is marked as not deferred
}
checkForDeferredSave() {
  if (current_layer_.is_deferred) {
    // Push Save record into stream
    current_layer_.is_deferred = false;
  }
}
restore() {
  if (!current_layer_.is_deferred) {
    // Push Restore record into stream
  }
  // always pop a layer_info
}

I think PR #34365 can be easily adapted whether it is the current implementation by @JsouLiang or the solution mentioned by @flar .

In fact, when @JsouLiang wrote this solution, we discussed how to implement it offline. The current implementation is from SkCanvas, https://github.com/google/skia/blob/b676a02b01727ba6453ae40c7e9ac5201785aba4/src/core/SkCanvas.cpp#L611

However, it should be noted here that since the additional save/restore is not pushed to the stream, this may cause the count of ops may not be equal between Displaylist and SkPicture when draw the same content. Because Skia records all Ops when using SkPictureRecorder. Skia does not optimize unnecessary save/restore when recording Ops, but when actually drawing.

And here we have a test that checks if the op counts of Displaylist and SkPicture are the same.
c.f.

EXPECT_EQ(static_cast<int>(display_list->op_count()),

@flar
Copy link
Contributor

flar commented Jul 29, 2022

However, it should be noted here that since the additional save/restore is not pushed to the stream, this may cause the count of ops may not be equal between Displaylist and SkPicture when draw the same content. Because Skia records all Ops when using SkPictureRecorder. Skia does not optimize unnecessary save/restore when recording Ops, but when actually drawing.

And here we have a test that checks if the op counts of Displaylist and SkPicture are the same. c.f.

Where do you see Skia recording elided save records? I found the opposite which is why I had to write all of the save tests in display_list_unittests.cc to ensure that the optimization would not apply. Without that code I was getting op count mismatches. Now that DisplayList elides them as well, some of these workarounds may no longer be needed.

c.f.

// There are many reasons that save and restore can elide content, including

@flar
Copy link
Contributor

flar commented Jul 29, 2022

Note that SkRecorder records a save record on all calls to willSave(), but willSave() is not called on every call to SkCanvas::save(), it is only called when a deferred save is being realized.

c.f. https://skia.googlesource.com/skia/+/refs/heads/main/src/core/SkRecorder.cpp#326
and https://skia.googlesource.com/skia/+/refs/heads/main/src/core/SkCanvas.cpp#611

@JsouLiang
Copy link
Contributor Author

Note that SkRecorder records a save record on all calls to willSave(), but willSave() is not called on every call to SkCanvas::save(), it is only called when a deferred save is being realized.

c.f. https://skia.googlesource.com/skia/+/refs/heads/main/src/core/SkRecorder.cpp#326 and https://skia.googlesource.com/skia/+/refs/heads/main/src/core/SkCanvas.cpp#611

Yes, the save action only happened when doSave was called.

@ColdPaleLight
Copy link
Member

Note that SkRecorder records a save record on all calls to willSave(), but willSave() is not called on every call to SkCanvas::save(), it is only called when a deferred save is being realized.

c.f. https://skia.googlesource.com/skia/+/refs/heads/main/src/core/SkRecorder.cpp#326 and https://skia.googlesource.com/skia/+/refs/heads/main/src/core/SkCanvas.cpp#611

Ahh, yes, you are right. The background is that I am trying to solve the issue flutter/flutter#107580 . I've tried in DisplayListBuider to not push those draw operations that don't draw content to the stream, and I then got the ops count mismatches. So I thought save/restore would have the same problem, it doesn't seem to be, which is great.

@JsouLiang JsouLiang requested a review from flar July 29, 2022 07:47
}
}

void DisplayListBoundsCalculator::save() {
Copy link
Contributor

Choose a reason for hiding this comment

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

This still seems unnecessary and yet it is still here. Why are you checking the save/restores in the Calculator? That operation was already done in the Builder.

@flar
Copy link
Contributor

flar commented Jul 29, 2022

Note that SkRecorder records a save record on all calls to willSave(), but willSave() is not called on every call to SkCanvas::save(), it is only called when a deferred save is being realized.
c.f. https://skia.googlesource.com/skia/+/refs/heads/main/src/core/SkRecorder.cpp#326 and https://skia.googlesource.com/skia/+/refs/heads/main/src/core/SkCanvas.cpp#611

Ahh, yes, you are right. The background is that I am trying to solve the issue flutter/flutter#107580 . I've tried in DisplayListBuider to not push those draw operations that don't draw content to the stream, and I then got the ops count mismatches. So I thought save/restore would have the same problem, it doesn't seem to be, which is great.

Where do you get the op count mismatches? In the unit tests? If the unit tests are pushing NOP operations then the unit tests should be fixed. If they are not pushing NOP operations then there was something wrong with your logic that avoided pushing them.

@ColdPaleLight
Copy link
Member

Where do you get the op count mismatches? In the unit tests? If the unit tests are pushing NOP operations then the unit tests should be fixed. If they are not pushing NOP operations then there was something wrong with your logic that avoided pushing them.

@flar Yes,in the unit tests. I didn't push NOP at first, so I encountered a count mismatches error. pushing NOP can fix them.

@flar
Copy link
Contributor

flar commented Jul 29, 2022

Where do you get the op count mismatches? In the unit tests? If the unit tests are pushing NOP operations then the unit tests should be fixed. If they are not pushing NOP operations then there was something wrong with your logic that avoided pushing them.

@flar Yes,in the unit tests. I didn't push NOP at first, so I encountered a count mismatches error. pushing NOP can fix them.

Which tests? As far as I know all of our tests are testing non-NOP rendering. If the tests encountered cases where you failed to push an op then either the test is wrong or your code was wrong.

@ColdPaleLight
Copy link
Member

Which tests? As far as I know all of our tests are testing non-NOP rendering. If the tests encountered cases where you failed to push an op then either the test is wrong or your code was wrong.

@flar please see the draft PR #35024 and the commit f8ad372 (based on PR #34365) for the detail.

@flar
Copy link
Contributor

flar commented Jul 31, 2022

Which tests? As far as I know all of our tests are testing non-NOP rendering. If the tests encountered cases where you failed to push an op then either the test is wrong or your code was wrong.

@flar please see the draft PR #35024 and the commit f8ad372 (based on PR #34365) for the detail.

Ignore previous comments, I discovered a reason why those unit tests had an omitted op. We need to find a better solution that doesn't trigger that condition. I'll follow up with a comment in #35024.

@JsouLiang JsouLiang requested a review from flar July 31, 2022 05:01
Copy link
Contributor

@flar flar left a comment

Choose a reason for hiding this comment

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

Almost there. Apologies for not double-checking the coverage for deferred save calls.

I suggested a new test for an odd "late save" case that we should check. Also, should we have a quick test for each of the methods that does a deferred check to verify that it does its check? Something small like:

{
  save
  <deferred-trigger-call>
  drawRect
  restore
  ...
  EQ_Verbose(...)
}
// Repeat that for each of:
// - translate
// - scale
// - skew
// - transform2D
// - transformPersp
// - resetTransform
// - clipRect
// - RRect
// - Path

To be even more complete in testing we might want to check things like NOP-transforms don't realize the deferred save, and that methods that forward their operation to a simpler method such as transformPerspective(2D affine matrix) and clipPath(rectangle/oval/rrect path) also trigger a deferred save.

@JsouLiang JsouLiang requested a review from flar August 3, 2022 08:35
@JsouLiang JsouLiang force-pushed the optimize-out-unnecessary-save-restore-pairs branch from a8f004a to 6fa6ce0 Compare August 9, 2022 03:16
@JsouLiang JsouLiang force-pushed the optimize-out-unnecessary-save-restore-pairs branch from 6fa6ce0 to d280061 Compare August 9, 2022 03:20
@JsouLiang JsouLiang requested a review from flar August 9, 2022 03:20
Copy link
Contributor

@flar flar left a comment

Choose a reason for hiding this comment

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

This is looking ready to go. Please fix the TranslateTriggersDeferredSave test that seems to have strayed from the pattern of the other Triggers tests.

@JsouLiang JsouLiang added the autosubmit Merge PR when tree becomes green via auto submit App label Aug 11, 2022
@auto-submit
Copy link
Contributor

auto-submit bot commented Aug 11, 2022

  • The status or check suite Mac Host Engine has failed. Please fix the issues identified (or deflake) before re-applying this label.

@auto-submit auto-submit bot removed the autosubmit Merge PR when tree becomes green via auto submit App label Aug 11, 2022
@JsouLiang JsouLiang force-pushed the optimize-out-unnecessary-save-restore-pairs branch from 3531af1 to 085100e Compare August 11, 2022 06:33
@JsouLiang JsouLiang merged commit ac81750 into flutter:main Aug 11, 2022
engine-flutter-autoroll added a commit to engine-flutter-autoroll/flutter that referenced this pull request Aug 11, 2022
emilyabest pushed a commit to emilyabest/engine that referenced this pull request Aug 12, 2022
* drafting the solution to optimize out unnecessary save restore pairs

* remove unnecessary save/restore pairs

* delete the calculator change;

* fix some logic; Add some testcases

* Add test for set DlPaint

* update test cases

* Prune TranslateTriggersDeferredSave unittest
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants