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

Conversation

@farchond
Copy link
Contributor

@farchond farchond commented Apr 12, 2021

This PR rewrites the VsyncWaiter for Fuchsia class from scratch. This change is ready for review, modulo some additional unittests I'm working on involving creating a FakeSessionConnection.

Implementation Details

A VsyncWaiter's only purposes are to wait for vsyncs, and call FireCallback() when appropriate to let the Animator know when they can produce a new frame, or do some trace bookkeeping. This PR changes how this is done on Fuchsia in some key ways, listed below.

SessionConnection integration

VsyncWaiter now has a reference to SessionConnection. SessionConnection is responsible for talking to Scenic, and SessionConnection therefore knows when vsyncs are reported back. This means that VsyncWaiter no longer needs to post tasks and guess when vsyncs happen, but rather hear back from SessionConnection directly. There are some caveats with this approach however, as there can be occasional delays in getting this signal due to the amount of thread hops that need to happen. For this reason, VsyncWaiter still schedules based on SnapToNextPhase, but receiving the vsync tick means it doesn't post tasks for future work, which significantly reduces code complexity.

The integration of SessionConnection in the VsyncWaiter also allows for backpressure from Scenic to translate up into Flutter framework.

Use of AwaitVSyncForSecondaryCallback() (#25787)

There are two flavors of waiting for vsync - one is intended for frame scheduling, and the other for miscellaneous tasks. The problem I noticed while testing is that every input event would call AwaitVsync() for trace related purposes, but VsyncWaiter() would interpret this as a frame request, which wasn't often the case. This would lead to an implementation that either lacked robustness by not implementing backpressure, or one that was too conservative in its scheduling.

By separating the two intentions, it is possible to easily perform better for both cases. AwaitVSyncForSecondaryCallback() requests do not update the last_targetted_time_, meaning that when real frame requests come in they are not delayed by the occurrence of prior input events.

Use of GetTargetTimes()

Similar to SnapToNextPhase() in VsyncWaiter, and CalculateNextLatchPoint() in SessionConnection, it is useful to extract all scheduling logic into an easy-to-test static function. Whereas the previous implementation had its scheduling logic scattered around the class, and via PostTask timing, now the decision of what arguments to pass into FireCallback() is easier to read and maintain. This makes it easy to solve and unittest for the performance problems I noticed, more on that below.

Motivations

Performance

This implementation solves two main problems, as well as enabling input resampling on Fuchsia.

The first problem I noticed was that we would previously often schedule N+1 frames in an N vsync interval window. This guarantees wasted effort, as one of those frames will be produced only to never be shown. This also meant that the frames that would be displayed would be produced later than necessary, due to the wasted effort.

The second issue is that even when the frames would be scheduled in a reasonable time frame, sometimes we'd have a "galloping" behavior where 2 frames would be scheduled in one vsync interval, then 0 in the following interval, then 2 in the one after, etc. This did mean N frames produced in N vsync intervals given optimal scheduling, yes, but the cadence of the frames was uneven. This hampered our ability to resample input, which led to it being disabled on Fuchsia.

I ran perfcompare tests on common workflows on Fuchsia, and saw a statistically significant increase in FPS, GPU utilization, and other key benchmarks.

Smooth input

This change will enable input resampling on Fuchsia again, as it had to be disabled to do inconsistencies in scheduling.

Testability

This implementation is much easier to test, and it has performance unittests as a result. This PR extracts all the logic of when to schedule next (aka call FireCallback()) into a single static function, GetTargetTimes(), which takes in as input all the relevant variables for when to schedule the next available frame.

This change also adds a FakeSessionConnection, which allows us to better test the VsyncWaiter without needing a real Scenic or display. Specifically, we can test backpressure limits as well as high CPU load by delaying OnVsync()s.

Robustness

This implementation takes into account Scenic backpressure, which means that it is no longer possible for Flutter to produce more GPU work than Scenic and the system can handle. This is accomplished by having the VsyncWaiter take a pointer to the SessionConnection, which knows about how many frames in flight Flutter is allowed to have.

@chinmaygarde
Copy link
Member

Can you describe the patch and what its trying to do?

@chinmaygarde
Copy link
Member

Ping. Is this WIP or a draft? If progress on this is not likely, lets close it for now.

@farchond
Copy link
Contributor Author

Ping. Is this WIP or a draft? If progress on this is not likely, lets close it for now.

Hi Chinmay. I've been OOO recovering from a medical operation, but this PR and the associated documentation is something I'm actively working on now.

@chinmaygarde
Copy link
Member

I've been OOO recovering.

Oh, take you time. No rush. Hope you feel better soon.

std::shared_ptr<flutter::ExternalViewEmbedder> external_view_embedder,
fml::TimeDelta vsync_offset,
zx_handle_t vsync_event_handle)
SessionConnection* session_connection)
Copy link
Contributor

Choose a reason for hiding this comment

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

Lets be good about tracking lifetimes and make this a shared_ptr

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, I should've marked this as TODO.

I left it as a raw pointer until I have a FakeSessionConnection class available to use for platform_view_unittests.

std::set<std::string /* channel */> unregistered_channels_;

fml::TimeDelta vsync_offset_;
zx_handle_t vsync_event_handle_ = 0;
Copy link
Contributor

Choose a reason for hiding this comment

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

Hallelujah!


VsyncWaiter(std::string debug_label,
zx_handle_t session_present_handle,
VsyncWaiter(SessionConnection* session_connection,
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: please move constructor below static methods (google style guide dictates this)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

}

VsyncWaiter::~VsyncWaiter() {
FML_LOG(WARNING) << "~VsyncWaiter()";
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: remove

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done


FrameTargetTimes GetTargetTimesHelper(bool secondary_callback);

SessionConnection* session_connection_;
Copy link
Contributor

Choose a reason for hiding this comment

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

Use shared_ptr please (not a nit)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

max_frames_in_flight = product_config.get_max_frames_in_flight(),
vsync_handle = vsync_event_.get(), &view_embedder_latch]() mutable {
&view_embedder_latch]() mutable {
session_connection_.emplace(
Copy link
Contributor

Choose a reason for hiding this comment

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

At this point, this should be a shared_ptr too

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

// Initialize the callback that will run every time the SessionConnection
// receives one or more frame presented events on Vsync.
session_connection_->InitializeVsyncWaiterCallback([this]() {
task_runners_.GetUITaskRunner()->PostTask([&weak_factory_ui =
Copy link
Contributor

Choose a reason for hiding this comment

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

It is not neccesary to rethread this callback to the UI thread.

This callback bottoms out in VsyncWaiter::FireCallback. If you read the implementation of it, it protects variables w/ a mutex, and also performs the necesary re-threading internally.

So, 3 things:

  1. Can you update the common VsyncWaiter documentation to indicate that FireCallback may be called from any thread safely?
  2. Session callbacks happen on the raster thread. So change "weak_factory_ui" to "weak_factory_raster" + initialize and destroy it on the raster thread instead of UI. I plan to move the scenic::Session to the platform thread soon (the thread that weak_factory_ is on), and then the 2nd weak factory will vanish entirely.
  3. Since all of this code now runs on the raster thread, and the presentation callbacks + Present also run on the raster thread, VsyncRecorder has no more reason to exist. Everything happens on one thread. See my other comment on vsync_recorder.h

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As we discussed, this is not currently possible due to decreased performance. I filed flutter/flutter#81984 to track this issue.

// information will be saved (in order to handle edge cases involving
// multiple Scenic sessions in the same process). This function is safe to
// multiple Scenic sessions in the same process). This function is safe to
// call from any thread.
Copy link
Contributor

Choose a reason for hiding this comment

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

If you follow my other suggestion below, then this class no longer has a reason to exist. Everything happens on one thread now, and mostly inside of the SessionConnection.

At that point, you can just collapse this class into SessionConnection without violating any sort of encapsulation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This also is not possible now unfortunately, so I filed flutter/flutter#81984

Copy link
Contributor

@mikaelpessa mikaelpessa left a comment

Choose a reason for hiding this comment

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

LGTM around the scheduling logic.

Comment on lines 21 to 24
// (frame_start_time, frame_target_time)
using FrameTargetTimes = std::pair<fml::TimePoint, fml::TimePoint>;
// (latch point time, vsync time)
using LatchPointVsyncPair = std::pair<fml::TimePoint, fml::TimePoint>;
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: making these structs would be more readable and make the comments unnecessary.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

farchond added a commit to farchond/engine that referenced this pull request May 12, 2021
In preparation for flutter#25551, rename SessionConnection to
DefaultSessionConnection. The plan is to have SessionConnection be an
interface we can create a fake of for testing.
farchond added a commit to farchond/engine that referenced this pull request May 12, 2021
In preparation for flutter#25551, rename SessionConnection to
DefaultSessionConnection. The plan is to have SessionConnection be an
interface we can create a fake of for testing.
farchond added a commit to farchond/engine that referenced this pull request May 12, 2021
In preparation for flutter#25551, rename SessionConnection to
DefaultSessionConnection. The plan is to have SessionConnection be an
interface we can create a fake of for testing.
farchond added a commit to farchond/engine that referenced this pull request May 13, 2021
In preparation for flutter#25551, rename SessionConnection to
DefaultSessionConnection. The plan is to have SessionConnection be an
interface we can create a fake of for testing.
farchond added a commit to farchond/engine that referenced this pull request May 13, 2021
In preparation for flutter#25551, rename SessionConnection to
DefaultSessionConnection. The plan is to have SessionConnection be an
interface we can create a fake of for testing.
farchond added a commit to farchond/engine that referenced this pull request May 13, 2021
In preparation for flutter#25551, rename SessionConnection to
DefaultSessionConnection. The plan is to have SessionConnection be an
interface we can create a fake of for testing.
farchond added a commit that referenced this pull request May 13, 2021
In preparation for #25551, rename SessionConnection to
DefaultSessionConnection. The plan is to have SessionConnection be an
interface we can create a fake of for testing purposes.
Copy link
Contributor

@nathanrogersgoogle nathanrogersgoogle left a comment

Choose a reason for hiding this comment

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

+1

// FireCallback(). This value should be strictly increasing in order to
// guarantee that animation code that relies on target vsyncs works correctly,
// and that Flutter is not producing multiple frames in a small interval.
fml::TimePoint last_targetted_vsync_;
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit, but FWIW I think "targeted" is a more common spelling vs "targetted".

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done


// VsyncRecorder logic.

VsyncInfo GetCurrentVsyncInfo() const;
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Move this above all of the fields, with PresentSession and friends

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

VsyncInfo GetCurrentVsyncInfo() const;

static void ToggleSignal(zx_handle_t handle, bool raise);
fuchsia::scenic::scheduling::PresentationInfo next_presentation_info_;
Copy link
Contributor

Choose a reason for hiding this comment

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

Are these also guarded by the mutex? Its not clear because the GetCurrentVsyncInfo breaks things up

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants