diff --git a/ci/licenses_golden/excluded_files b/ci/licenses_golden/excluded_files index b0ac79b70716a..11b0bcfd970fc 100644 --- a/ci/licenses_golden/excluded_files +++ b/ci/licenses_golden/excluded_files @@ -251,6 +251,7 @@ ../../../flutter/shell/common/base64_unittests.cc ../../../flutter/shell/common/context_options_unittests.cc ../../../flutter/shell/common/dl_op_spy_unittests.cc +../../../flutter/shell/common/engine_animator_unittests.cc ../../../flutter/shell/common/engine_unittests.cc ../../../flutter/shell/common/fixtures ../../../flutter/shell/common/input_events_unittests.cc diff --git a/flow/frame_timings.cc b/flow/frame_timings.cc index 339374d77b837..07a398a81c42e 100644 --- a/flow/frame_timings.cc +++ b/flow/frame_timings.cc @@ -13,6 +13,31 @@ namespace flutter { +namespace { + +const char* StateToString(FrameTimingsRecorder::State state) { +#ifndef NDEBUG + switch (state) { + case FrameTimingsRecorder::State::kUninitialized: + return "kUninitialized"; + case FrameTimingsRecorder::State::kVsync: + return "kVsync"; + case FrameTimingsRecorder::State::kBuildStart: + return "kBuildStart"; + case FrameTimingsRecorder::State::kBuildEnd: + return "kBuildEnd"; + case FrameTimingsRecorder::State::kRasterStart: + return "kRasterStart"; + case FrameTimingsRecorder::State::kRasterEnd: + return "kRasterEnd"; + }; + FML_UNREACHABLE(); +#endif + return ""; +} + +} // namespace + std::atomic FrameTimingsRecorder::frame_number_gen_ = {1}; FrameTimingsRecorder::FrameTimingsRecorder() @@ -255,7 +280,8 @@ const char* FrameTimingsRecorder::GetFrameNumberTraceArg() const { } void FrameTimingsRecorder::AssertInState(State state) const { - FML_DCHECK(state_ == state); + FML_DCHECK(state_ == state) << "Expected state " << StateToString(state) + << ", actual state " << StateToString(state_); } } // namespace flutter diff --git a/flow/frame_timings.h b/flow/frame_timings.h index c81bcc3362298..ccccd89dddb88 100644 --- a/flow/frame_timings.h +++ b/flow/frame_timings.h @@ -31,6 +31,7 @@ class FrameTimingsRecorder { public: /// Various states that the recorder can be in. When created the recorder is /// in an unitialized state and transtions in sequential order of the states. + // After adding an item to this enum, modify StateToString accordingly. enum class State : uint32_t { kUninitialized, kVsync, @@ -121,6 +122,8 @@ class FrameTimingsRecorder { /// /// Instead of adding a `GetState` method and asserting on the result, this /// method prevents other logic from relying on the state. + /// + /// In release builds, this call is a no-op. void AssertInState(State state) const; private: diff --git a/lib/ui/painting/image_dispose_unittests.cc b/lib/ui/painting/image_dispose_unittests.cc index 0f8bb6d027062..93600ca83c93d 100644 --- a/lib/ui/painting/image_dispose_unittests.cc +++ b/lib/ui/painting/image_dispose_unittests.cc @@ -5,6 +5,7 @@ #define FML_USED_ON_EMBEDDER #include "flutter/common/task_runners.h" +#include "flutter/fml/synchronization/count_down_latch.h" #include "flutter/fml/synchronization/waitable_event.h" #include "flutter/lib/ui/painting/canvas.h" #include "flutter/lib/ui/painting/image.h" @@ -57,6 +58,10 @@ TEST_F(ImageDisposeTest, ImageReleasedAfterFrameAndDisposePictureAndLayer) { }; Settings settings = CreateSettingsForFixture(); + fml::CountDownLatch frame_latch{2}; + settings.frame_rasterized_callback = [&frame_latch](const FrameTiming& t) { + frame_latch.CountDown(); + }; auto task_runner = CreateNewThread(); TaskRunners task_runners("test", // label GetCurrentTaskRunner(), // platform @@ -83,12 +88,15 @@ TEST_F(ImageDisposeTest, ImageReleasedAfterFrameAndDisposePictureAndLayer) { shell->RunEngine(std::move(configuration), [&](auto result) { ASSERT_EQ(result, Engine::RunStatus::Success); }); - message_latch_.Wait(); ASSERT_TRUE(current_display_list_); ASSERT_TRUE(current_image_); + // Wait for 2 frames to be rasterized. The 2nd frame releases resources of the + // 1st frame. + frame_latch.Wait(); + // Force a drain the SkiaUnrefQueue. The engine does this normally as frames // pump, but we force it here to make the test more deterministic. message_latch_.Reset(); diff --git a/lib/ui/window/platform_configuration.cc b/lib/ui/window/platform_configuration.cc index 5e325fbdf4b10..45290d3a28cc7 100644 --- a/lib/ui/window/platform_configuration.cc +++ b/lib/ui/window/platform_configuration.cc @@ -453,12 +453,9 @@ void PlatformConfigurationNativeApi::Render(int64_t view_id, Scene* scene, double width, double height) { - // TODO(dkwingsmt): Currently only supports a single window. - // See https://github.com/flutter/flutter/issues/135530, item 2. - FML_DCHECK(view_id == kFlutterImplicitViewId); UIDartState::ThrowIfUIOperationsProhibited(); UIDartState::Current()->platform_configuration()->client()->Render( - scene, width, height); + view_id, scene, width, height); } void PlatformConfigurationNativeApi::SetNeedsReportTimings(bool value) { diff --git a/lib/ui/window/platform_configuration.h b/lib/ui/window/platform_configuration.h index 5f0cad26f4a20..c9162a26873b8 100644 --- a/lib/ui/window/platform_configuration.h +++ b/lib/ui/window/platform_configuration.h @@ -77,7 +77,10 @@ class PlatformConfigurationClient { /// @brief Updates the client's rendering on the GPU with the newly /// provided Scene. /// - virtual void Render(Scene* scene, double width, double height) = 0; + virtual void Render(int64_t view_id, + Scene* scene, + double width, + double height) = 0; //-------------------------------------------------------------------------- /// @brief Receives an updated semantics tree from the Framework. diff --git a/runtime/dart_isolate_unittests.cc b/runtime/dart_isolate_unittests.cc index aade0cedff3c7..845d7237d431b 100644 --- a/runtime/dart_isolate_unittests.cc +++ b/runtime/dart_isolate_unittests.cc @@ -707,7 +707,10 @@ class FakePlatformConfigurationClient : public PlatformConfigurationClient { std::string DefaultRouteName() override { return ""; } void ScheduleFrame() override {} void EndWarmUpFrame() override {} - void Render(Scene* scene, double width, double height) override {} + void Render(int64_t view_id, + Scene* scene, + double width, + double height) override {} void UpdateSemantics(SemanticsUpdate* update) override {} void HandlePlatformMessage( std::unique_ptr message) override {} diff --git a/runtime/runtime_controller.cc b/runtime/runtime_controller.cc index 0454837a4d050..f6b9b5422ce0d 100644 --- a/runtime/runtime_controller.cc +++ b/runtime/runtime_controller.cc @@ -47,8 +47,8 @@ RuntimeController::RuntimeController( std::unique_ptr RuntimeController::Spawn( RuntimeDelegate& p_client, - std::string advisory_script_uri, - std::string advisory_script_entrypoint, + const std::string& advisory_script_uri, + const std::string& advisory_script_entrypoint, const std::function& p_idle_notification_callback, const fml::closure& p_isolate_create_callback, const fml::closure& p_isolate_shutdown_callback, @@ -57,13 +57,18 @@ std::unique_ptr RuntimeController::Spawn( fml::WeakPtr image_decoder, fml::WeakPtr image_generator_registry, fml::TaskRunnerAffineWeakPtr snapshot_delegate) const { - UIDartState::Context spawned_context{ - context_.task_runners, std::move(snapshot_delegate), - std::move(io_manager), context_.unref_queue, - std::move(image_decoder), std::move(image_generator_registry), - std::move(advisory_script_uri), std::move(advisory_script_entrypoint), - context_.volatile_path_tracker, context_.concurrent_task_runner, - context_.enable_impeller, context_.runtime_stage_backend}; + UIDartState::Context spawned_context{context_.task_runners, + std::move(snapshot_delegate), + std::move(io_manager), + context_.unref_queue, + std::move(image_decoder), + std::move(image_generator_registry), + advisory_script_uri, + advisory_script_entrypoint, + context_.volatile_path_tracker, + context_.concurrent_task_runner, + context_.enable_impeller, + context_.runtime_stage_backend}; auto result = std::make_unique(p_client, // vm_, // @@ -226,6 +231,7 @@ bool RuntimeController::SetAccessibilityFeatures(int32_t flags) { bool RuntimeController::BeginFrame(fml::TimePoint frame_time, uint64_t frame_number) { + MarkAsFrameBorder(); if (auto* platform_configuration = GetPlatformConfigurationIfAvailable()) { platform_configuration->BeginFrame(frame_time, frame_number); return true; @@ -340,22 +346,37 @@ void RuntimeController::ScheduleFrame() { client_.ScheduleFrame(); } -// |PlatformConfigurationClient| void RuntimeController::EndWarmUpFrame() { - client_.EndWarmUpFrame(); + client_.OnAllViewsRendered(); } // |PlatformConfigurationClient| -void RuntimeController::Render(Scene* scene, double width, double height) { - // TODO(dkwingsmt): Currently only supports a single window. - int64_t view_id = kFlutterImplicitViewId; +void RuntimeController::Render(int64_t view_id, + Scene* scene, + double width, + double height) { const ViewportMetrics* view_metrics = UIDartState::Current()->platform_configuration()->GetMetrics(view_id); if (view_metrics == nullptr) { return; } - client_.Render(scene->takeLayerTree(width, height), + client_.Render(view_id, scene->takeLayerTree(width, height), view_metrics->device_pixel_ratio); + rendered_views_during_frame_.insert(view_id); + CheckIfAllViewsRendered(); +} + +void RuntimeController::MarkAsFrameBorder() { + rendered_views_during_frame_.clear(); +} + +void RuntimeController::CheckIfAllViewsRendered() { + if (rendered_views_during_frame_.size() != 0 && + rendered_views_during_frame_.size() == + platform_data_.viewport_metrics_for_views.size()) { + client_.OnAllViewsRendered(); + MarkAsFrameBorder(); + } } // |PlatformConfigurationClient| diff --git a/runtime/runtime_controller.h b/runtime/runtime_controller.h index e7cba3f91b813..cb1618f9d3326 100644 --- a/runtime/runtime_controller.h +++ b/runtime/runtime_controller.h @@ -104,8 +104,8 @@ class RuntimeController : public PlatformConfigurationClient { /// std::unique_ptr Spawn( RuntimeDelegate& p_client, - std::string advisory_script_uri, - std::string advisory_script_entrypoint, + const std::string& advisory_script_uri, + const std::string& advisory_script_entrypoint, const std::function& idle_notification_callback, const fml::closure& isolate_create_callback, const fml::closure& isolate_shutdown_callback, @@ -660,6 +660,26 @@ class RuntimeController : public PlatformConfigurationClient { std::shared_ptr(new PlatformIsolateManager()); bool has_flushed_runtime_state_ = false; + // Tracks the views that have been called `Render` during a frame. + // + // If all views that have been registered by `AddView` have been called + // `Render`, then the runtime controller notifies the client of the end of + // frame immediately, allowing the client to submit the views to the pipeline + // a bit earlier than having to wait for the end of `BeginFrame`. See also + // `Animator::OnAllViewsRendered`. + // + // This mechanism fixes https://github.com/flutter/flutter/issues/144584 with + // option 2 and + // https://github.com/flutter/engine/pull/51186#issuecomment-1977820525 with + // option a in most cases, except if there are multiple views and only part of + // them are rendered. + // TODO(dkwingsmt): Fix these problems for all cases. + std::unordered_set rendered_views_during_frame_; + + void MarkAsFrameBorder(); + + void CheckIfAllViewsRendered(); + PlatformConfiguration* GetPlatformConfigurationIfAvailable(); bool FlushRuntimeStateToIsolate(); @@ -674,7 +694,10 @@ class RuntimeController : public PlatformConfigurationClient { void EndWarmUpFrame() override; // |PlatformConfigurationClient| - void Render(Scene* scene, double width, double height) override; + void Render(int64_t view_id, + Scene* scene, + double width, + double height) override; // |PlatformConfigurationClient| void UpdateSemantics(SemanticsUpdate* update) override; diff --git a/runtime/runtime_delegate.h b/runtime/runtime_delegate.h index 6d3707d27737a..7c031f1a53068 100644 --- a/runtime/runtime_delegate.h +++ b/runtime/runtime_delegate.h @@ -25,9 +25,10 @@ class RuntimeDelegate { virtual void ScheduleFrame(bool regenerate_layer_trees = true) = 0; - virtual void EndWarmUpFrame() = 0; + virtual void OnAllViewsRendered() = 0; - virtual void Render(std::unique_ptr layer_tree, + virtual void Render(int64_t view_id, + std::unique_ptr layer_tree, float device_pixel_ratio) = 0; virtual void UpdateSemantics(SemanticsNodeUpdates update, diff --git a/shell/common/BUILD.gn b/shell/common/BUILD.gn index 90449786b8187..c570e660f367b 100644 --- a/shell/common/BUILD.gn +++ b/shell/common/BUILD.gn @@ -310,6 +310,7 @@ if (enable_unittests) { "base64_unittests.cc", "context_options_unittests.cc", "dl_op_spy_unittests.cc", + "engine_animator_unittests.cc", "engine_unittests.cc", "input_events_unittests.cc", "persistent_cache_unittests.cc", diff --git a/shell/common/animator.cc b/shell/common/animator.cc index ad3cea4d1adf2..94a037c71f370 100644 --- a/shell/common/animator.cc +++ b/shell/common/animator.cc @@ -62,6 +62,10 @@ void Animator::BeginFrame( std::unique_ptr frame_timings_recorder) { TRACE_EVENT_ASYNC_END0("flutter", "Frame Request Pending", frame_request_number_); + // Clear layer trees rendered out of a frame. Only Animator::Render called + // within a frame is used. + layer_trees_tasks_.clear(); + frame_request_number_++; frame_timings_recorder_ = std::move(frame_timings_recorder); @@ -112,6 +116,44 @@ void Animator::BeginFrame( dart_frame_deadline_ = frame_target_time.ToEpochDelta(); uint64_t frame_number = frame_timings_recorder_->GetFrameNumber(); delegate_.OnAnimatorBeginFrame(frame_target_time, frame_number); +} + +void Animator::EndFrame() { + if (frame_timings_recorder_ == nullptr) { + // `EndFrame` has been called in this frame. This happens if the engine has + // called `OnAllViewsRendered` and then the end of the vsync task calls + // `EndFrame` again. + return; + } + if (!layer_trees_tasks_.empty()) { + // The build is completed in OnAnimatorBeginFrame. + frame_timings_recorder_->RecordBuildEnd(fml::TimePoint::Now()); + + delegate_.OnAnimatorUpdateLatestFrameTargetTime( + frame_timings_recorder_->GetVsyncTargetTime()); + + // Commit the pending continuation. + std::vector> layer_tree_task_list; + layer_tree_task_list.reserve(layer_trees_tasks_.size()); + for (auto& [view_id, layer_tree_task] : layer_trees_tasks_) { + layer_tree_task_list.push_back(std::move(layer_tree_task)); + } + layer_trees_tasks_.clear(); + PipelineProduceResult result = producer_continuation_.Complete( + std::make_unique(std::move(layer_tree_task_list), + std::move(frame_timings_recorder_))); + + if (!result.success) { + FML_DLOG(INFO) << "Failed to commit to the pipeline"; + } else if (!result.is_first_item) { + // Do nothing. It has been successfully pushed to the pipeline but not as + // the first item. Eventually the 'Rasterizer' will consume it, so we + // don't need to notify the delegate. + } else { + delegate_.OnAnimatorDraw(layer_tree_pipeline_); + } + } + frame_timings_recorder_ = nullptr; if (!frame_scheduled_ && has_rendered_) { // Wait a tad more than 3 60hz frames before reporting a big idle period. @@ -139,14 +181,18 @@ void Animator::BeginFrame( }, kNotifyIdleTaskWaitTime); } + FML_DCHECK(layer_trees_tasks_.empty()); + FML_DCHECK(frame_timings_recorder_ == nullptr); } -void Animator::Render(std::unique_ptr layer_tree, +void Animator::Render(int64_t view_id, + std::unique_ptr layer_tree, float device_pixel_ratio) { has_rendered_ = true; if (!frame_timings_recorder_) { - // Framework can directly call render with a built scene. + // Framework can directly call render with a built scene. A major reason is + // to render warm up frames. frame_timings_recorder_ = std::make_unique(); const fml::TimePoint placeholder_time = fml::TimePoint::Now(); frame_timings_recorder_->RecordVsync(placeholder_time, placeholder_time); @@ -156,35 +202,12 @@ void Animator::Render(std::unique_ptr layer_tree, TRACE_EVENT_WITH_FRAME_NUMBER(frame_timings_recorder_, "flutter", "Animator::Render", /*flow_id_count=*/0, /*flow_ids=*/nullptr); - frame_timings_recorder_->RecordBuildEnd(fml::TimePoint::Now()); - - delegate_.OnAnimatorUpdateLatestFrameTargetTime( - frame_timings_recorder_->GetVsyncTargetTime()); - - // TODO(dkwingsmt): Currently only supports a single window. - // See https://github.com/flutter/flutter/issues/135530, item 2. - int64_t view_id = kFlutterImplicitViewId; - std::vector> layer_trees_tasks; - layer_trees_tasks.push_back(std::make_unique( - view_id, std::move(layer_tree), device_pixel_ratio)); - // Commit the pending continuation. - PipelineProduceResult result = - producer_continuation_.Complete(std::make_unique( - std::move(layer_trees_tasks), std::move(frame_timings_recorder_))); - - if (!result.success) { - FML_DLOG(INFO) << "No pending continuation to commit"; - return; - } - if (!result.is_first_item) { - // It has been successfully pushed to the pipeline but not as the first - // item. Eventually the 'Rasterizer' will consume it, so we don't need to - // notify the delegate. - return; - } - - delegate_.OnAnimatorDraw(layer_tree_pipeline_); + // Only inserts if the view ID has not been rendered before, ignoring + // duplicate Render calls. + layer_trees_tasks_.try_emplace( + view_id, std::make_unique(view_id, std::move(layer_tree), + device_pixel_ratio)); } const std::weak_ptr Animator::GetVsyncWaiter() const { @@ -256,6 +279,7 @@ void Animator::AwaitVSync() { self->DrawLastLayerTrees(std::move(frame_timings_recorder)); } else { self->BeginFrame(std::move(frame_timings_recorder)); + self->EndFrame(); } } }); @@ -264,10 +288,10 @@ void Animator::AwaitVSync() { } } -void Animator::EndWarmUpFrame() { - // Do nothing. The warm up frame does not need any additional work to end the - // frame for now. This will change once the pipeline supports multi-view. - // https://github.com/flutter/flutter/issues/142851 +void Animator::OnAllViewsRendered() { + if (!layer_trees_tasks_.empty()) { + EndFrame(); + } } void Animator::ScheduleSecondaryVsyncCallback(uintptr_t id, diff --git a/shell/common/animator.h b/shell/common/animator.h index 51bea1d273871..d559945358b99 100644 --- a/shell/common/animator.h +++ b/shell/common/animator.h @@ -54,20 +54,27 @@ class Animator final { void RequestFrame(bool regenerate_layer_trees = true); //-------------------------------------------------------------------------- - /// @brief Tells the Animator that a warm up frame has ended. + /// @brief Tells the Animator that all views that should render for this + /// frame have been rendered. /// - /// In a warm up frame, `Animator::Render` is called out of vsync - /// tasks, and Animator requires an explicit end-of-frame call to - /// know when to send the layer trees to the pipeline. + /// In regular frames, since all `Render` calls must take place + /// during a vsync task, the Animator knows that all views have + /// been rendered at the end of the vsync task, therefore calling + /// this method is not needed. /// - /// This is different from regular frames, where Animator::Render is - /// always called within a vsync task, and Animator can send - /// the views at the end of the vsync task. + /// However, the engine might decide to start it a bit earlier, for + /// example, if the engine decides that no more views can be + /// rendered, so that the rasterization can start a bit earlier. + /// + /// This method is also useful in warm-up frames. In a warm up + /// frame, `Animator::Render` is called out of vsync tasks, and + /// Animator requires an explicit end-of-frame call to know when to + /// send the layer trees to the pipeline. /// /// For more about warm up frames, see /// `PlatformDispatcher.scheduleWarmUpFrame`. /// - void EndWarmUpFrame(); + void OnAllViewsRendered(); //-------------------------------------------------------------------------- /// @brief Tells the Animator that this frame needs to render another view. @@ -76,7 +83,8 @@ class Animator final { /// technically, between Animator::BeginFrame and Animator::EndFrame /// (both private methods). Otherwise, this call will be ignored. /// - void Render(std::unique_ptr layer_tree, + void Render(int64_t view_id, + std::unique_ptr layer_tree, float device_pixel_ratio); const std::weak_ptr GetVsyncWaiter() const; @@ -105,7 +113,13 @@ class Animator final { void EnqueueTraceFlowId(uint64_t trace_flow_id); private: + // Animator's work during a vsync is split into two methods, BeginFrame and + // EndFrame. The two methods should be called synchronously back-to-back to + // avoid being interrupted by a regular vsync. The reason to split them is to + // allow ShellTest::PumpOneFrame to insert a Render in between. + void BeginFrame(std::unique_ptr frame_timings_recorder); + void EndFrame(); bool CanReuseLastLayerTrees(); @@ -122,6 +136,8 @@ class Animator final { std::shared_ptr waiter_; std::unique_ptr frame_timings_recorder_; + std::unordered_map> + layer_trees_tasks_; uint64_t frame_request_number_ = 1; fml::TimeDelta dart_frame_deadline_; std::shared_ptr layer_tree_pipeline_; diff --git a/shell/common/animator_unittests.cc b/shell/common/animator_unittests.cc index 19a851a864c74..a1f96e7992376 100644 --- a/shell/common/animator_unittests.cc +++ b/shell/common/animator_unittests.cc @@ -23,6 +23,8 @@ namespace flutter { namespace testing { +constexpr int64_t kImplicitViewId = 0; + class FakeAnimatorDelegate : public Animator::Delegate { public: MOCK_METHOD(void, @@ -158,20 +160,30 @@ TEST_F(ShellTest, AnimatorDoesNotNotifyIdleBeforeRender) { latch.Wait(); ASSERT_FALSE(delegate.notify_idle_called_); + fml::AutoResetWaitableEvent render_latch; // Validate it has not notified idle and try to render. task_runners.GetUITaskRunner()->PostDelayedTask( [&] { ASSERT_FALSE(delegate.notify_idle_called_); - auto layer_tree = std::make_unique(LayerTree::Config(), - SkISize::Make(600, 800)); - animator->Render(std::move(layer_tree), 1.0); + EXPECT_CALL(delegate, OnAnimatorBeginFrame).WillOnce([&] { + auto layer_tree = std::make_unique( + LayerTree::Config(), SkISize::Make(600, 800)); + animator->Render(kImplicitViewId, std::move(layer_tree), 1.0); + render_latch.Signal(); + }); + // Request a frame that builds a layer tree and renders a frame. + // When the frame is rendered, render_latch will be signaled. + animator->RequestFrame(true); task_runners.GetPlatformTaskRunner()->PostTask(flush_vsync_task); }, // See kNotifyIdleTaskWaitTime in animator.cc. fml::TimeDelta::FromMilliseconds(60)); latch.Wait(); + render_latch.Wait(); - // Still hasn't notified idle because there has been no frame request. + // A frame has been rendered, and the next frame request will notify idle. + // But at the moment there isn't another frame request, therefore it still + // hasn't notified idle. task_runners.GetUITaskRunner()->PostTask([&] { ASSERT_FALSE(delegate.notify_idle_called_); // False to avoid getting cals to BeginFrame that will request more frames @@ -224,11 +236,6 @@ TEST_F(ShellTest, AnimatorDoesNotNotifyDelegateIfPipelineIsNotEmpty) { }); fml::AutoResetWaitableEvent begin_frame_latch; - EXPECT_CALL(delegate, OnAnimatorBeginFrame) - .WillRepeatedly( - [&](fml::TimePoint frame_target_time, uint64_t frame_number) { - begin_frame_latch.Signal(); - }); // It must always be called when the method 'Animator::Render' is called, // regardless of whether the pipeline is empty or not. EXPECT_CALL(delegate, OnAnimatorUpdateLatestFrameTargetTime).Times(2); @@ -239,16 +246,16 @@ TEST_F(ShellTest, AnimatorDoesNotNotifyDelegateIfPipelineIsNotEmpty) { for (int i = 0; i < 2; i++) { task_runners.GetUITaskRunner()->PostTask([&] { + EXPECT_CALL(delegate, OnAnimatorBeginFrame).WillOnce([&] { + auto layer_tree = std::make_unique(LayerTree::Config(), + SkISize::Make(600, 800)); + animator->Render(kImplicitViewId, std::move(layer_tree), 1.0); + begin_frame_latch.Signal(); + }); animator->RequestFrame(); task_runners.GetPlatformTaskRunner()->PostTask(flush_vsync_task); }); begin_frame_latch.Wait(); - - PostTaskSync(task_runners.GetUITaskRunner(), [&] { - auto layer_tree = std::make_unique(LayerTree::Config(), - SkISize::Make(600, 800)); - animator->Render(std::move(layer_tree), 1.0); - }); } PostTaskSync(task_runners.GetUITaskRunner(), [&] { animator.reset(); }); diff --git a/shell/common/engine.cc b/shell/common/engine.cc index 3f8e01769b90e..062f9e6b9be9c 100644 --- a/shell/common/engine.cc +++ b/shell/common/engine.cc @@ -40,11 +40,11 @@ fml::MallocMapping MakeMapping(const std::string& str) { Engine::Engine( Delegate& delegate, const PointerDataDispatcherMaker& dispatcher_maker, - std::shared_ptr image_decoder_task_runner, + const std::shared_ptr& image_decoder_task_runner, const TaskRunners& task_runners, const Settings& settings, std::unique_ptr animator, - fml::WeakPtr io_manager, + const fml::WeakPtr& io_manager, const std::shared_ptr& font_collection, std::unique_ptr runtime_controller, const std::shared_ptr& gpu_disabled_switch) @@ -55,8 +55,8 @@ Engine::Engine( font_collection_(font_collection), image_decoder_(ImageDecoder::Make(settings_, task_runners, - std::move(image_decoder_task_runner), - std::move(io_manager), + image_decoder_task_runner, + io_manager, gpu_disabled_switch)), task_runners_(task_runners), weak_factory_(this) { @@ -72,7 +72,7 @@ Engine::Engine(Delegate& delegate, const Settings& settings, std::unique_ptr animator, fml::WeakPtr io_manager, - fml::RefPtr unref_queue, + const fml::RefPtr& unref_queue, fml::TaskRunnerAffineWeakPtr snapshot_delegate, std::shared_ptr volatile_path_tracker, const std::shared_ptr& gpu_disabled_switch, @@ -100,7 +100,7 @@ Engine::Engine(Delegate& delegate, task_runners_, // task runners std::move(snapshot_delegate), // snapshot delegate std::move(io_manager), // io manager - std::move(unref_queue), // Skia unref queue + unref_queue, // Skia unref queue image_decoder_->GetWeakPtr(), // image decoder image_generator_registry_.GetWeakPtr(), // image generator registry settings_.advisory_script_uri, // advisory script uri @@ -464,11 +464,12 @@ void Engine::ScheduleFrame(bool regenerate_layer_trees) { animator_->RequestFrame(regenerate_layer_trees); } -void Engine::EndWarmUpFrame() { - animator_->EndWarmUpFrame(); +void Engine::OnAllViewsRendered() { + animator_->OnAllViewsRendered(); } -void Engine::Render(std::unique_ptr layer_tree, +void Engine::Render(int64_t view_id, + std::unique_ptr layer_tree, float device_pixel_ratio) { if (!layer_tree) { return; @@ -479,7 +480,7 @@ void Engine::Render(std::unique_ptr layer_tree, return; } - animator_->Render(std::move(layer_tree), device_pixel_ratio); + animator_->Render(view_id, std::move(layer_tree), device_pixel_ratio); } void Engine::UpdateSemantics(SemanticsNodeUpdates update, diff --git a/shell/common/engine.h b/shell/common/engine.h index bbc6be9c8ab9a..501e47178eafe 100644 --- a/shell/common/engine.h +++ b/shell/common/engine.h @@ -336,11 +336,12 @@ class Engine final : public RuntimeDelegate, PointerDataDispatcher::Delegate { /// Engine(Delegate& delegate, const PointerDataDispatcherMaker& dispatcher_maker, - std::shared_ptr image_decoder_task_runner, + const std::shared_ptr& + image_decoder_task_runner, const TaskRunners& task_runners, const Settings& settings, std::unique_ptr animator, - fml::WeakPtr io_manager, + const fml::WeakPtr& io_manager, const std::shared_ptr& font_collection, std::unique_ptr runtime_controller, const std::shared_ptr& gpu_disabled_switch); @@ -396,7 +397,7 @@ class Engine final : public RuntimeDelegate, PointerDataDispatcher::Delegate { const Settings& settings, std::unique_ptr animator, fml::WeakPtr io_manager, - fml::RefPtr unref_queue, + const fml::RefPtr& unref_queue, fml::TaskRunnerAffineWeakPtr snapshot_delegate, std::shared_ptr volatile_path_tracker, const std::shared_ptr& gpu_disabled_switch, @@ -838,7 +839,7 @@ class Engine final : public RuntimeDelegate, PointerDataDispatcher::Delegate { void ScheduleFrame() { ScheduleFrame(true); } // |RuntimeDelegate| - void EndWarmUpFrame() override; + void OnAllViewsRendered() override; // |RuntimeDelegate| FontCollection& GetFontCollection() override; @@ -972,7 +973,8 @@ class Engine final : public RuntimeDelegate, PointerDataDispatcher::Delegate { std::string DefaultRouteName() override; // |RuntimeDelegate| - void Render(std::unique_ptr layer_tree, + void Render(int64_t view_id, + std::unique_ptr layer_tree, float device_pixel_ratio) override; // |RuntimeDelegate| diff --git a/shell/common/engine_animator_unittests.cc b/shell/common/engine_animator_unittests.cc new file mode 100644 index 0000000000000..331bab77e9a16 --- /dev/null +++ b/shell/common/engine_animator_unittests.cc @@ -0,0 +1,653 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "flutter/shell/common/engine.h" + +// #include + +#include "flutter/common/constants.h" +#include "flutter/lib/ui/compositing/scene_builder.h" +#include "flutter/shell/common/shell_test.h" +#include "flutter/testing/fixture_test.h" +#include "gmock/gmock.h" + +// CREATE_NATIVE_ENTRY is leaky by design +// NOLINTBEGIN(clang-analyzer-core.StackAddressEscape) + +namespace flutter { + +namespace { + +using ::testing::Invoke; +using ::testing::ReturnRef; + +fml::AutoResetWaitableEvent native_latch; + +void PostSync(const fml::RefPtr& task_runner, + const fml::closure& task) { + fml::AutoResetWaitableEvent latch; + fml::TaskRunner::RunNowOrPostTask(task_runner, [&latch, &task] { + task(); + latch.Signal(); + }); + latch.Wait(); +} + +// Sort the argument list of `LayerTreeTask` into a new list that is sorted by +// their view IDs. `FrameItem::layer_tree_tasks` might not come sorted. +std::vector Sorted( + const std::vector>& layer_tree_tasks) { + std::vector result; + result.reserve(layer_tree_tasks.size()); + for (auto& task_ptr : layer_tree_tasks) { + result.push_back(task_ptr.get()); + } + std::sort(result.begin(), result.end(), + [](const LayerTreeTask* a, const LayerTreeTask* b) { + return a->view_id < b->view_id; + }); + return result; +} + +class MockDelegate : public Engine::Delegate { + public: + MOCK_METHOD(void, + OnEngineUpdateSemantics, + (SemanticsNodeUpdates, CustomAccessibilityActionUpdates), + (override)); + MOCK_METHOD(void, + OnEngineHandlePlatformMessage, + (std::unique_ptr), + (override)); + MOCK_METHOD(void, OnPreEngineRestart, (), (override)); + MOCK_METHOD(void, OnRootIsolateCreated, (), (override)); + MOCK_METHOD(void, + UpdateIsolateDescription, + (const std::string, int64_t), + (override)); + MOCK_METHOD(void, SetNeedsReportTimings, (bool), (override)); + MOCK_METHOD(std::unique_ptr>, + ComputePlatformResolvedLocale, + (const std::vector&), + (override)); + MOCK_METHOD(void, RequestDartDeferredLibrary, (intptr_t), (override)); + MOCK_METHOD(fml::TimePoint, GetCurrentTimePoint, (), (override)); + MOCK_METHOD(const std::shared_ptr&, + GetPlatformMessageHandler, + (), + (const, override)); + MOCK_METHOD(void, OnEngineChannelUpdate, (std::string, bool), (override)); + MOCK_METHOD(double, + GetScaledFontSize, + (double font_size, int configuration_id), + (const, override)); +}; + +class MockAnimatorDelegate : public Animator::Delegate { + public: + /* Animator::Delegate */ + MOCK_METHOD(void, + OnAnimatorBeginFrame, + (fml::TimePoint frame_target_time, uint64_t frame_number), + (override)); + MOCK_METHOD(void, + OnAnimatorNotifyIdle, + (fml::TimeDelta deadline), + (override)); + MOCK_METHOD(void, + OnAnimatorUpdateLatestFrameTargetTime, + (fml::TimePoint frame_target_time), + (override)); + MOCK_METHOD(void, + OnAnimatorDraw, + (std::shared_ptr pipeline), + (override)); + MOCK_METHOD(void, + OnAnimatorDrawLastLayerTrees, + (std::unique_ptr frame_timings_recorder), + (override)); +}; + +class MockPlatformMessageHandler : public PlatformMessageHandler { + public: + MOCK_METHOD(void, + HandlePlatformMessage, + (std::unique_ptr message), + (override)); + MOCK_METHOD(bool, + DoesHandlePlatformMessageOnPlatformThread, + (), + (const, override)); + MOCK_METHOD(void, + InvokePlatformMessageResponseCallback, + (int response_id, std::unique_ptr mapping), + (override)); + MOCK_METHOD(void, + InvokePlatformMessageEmptyResponseCallback, + (int response_id), + (override)); +}; + +class EngineAnimatorTest : public testing::FixtureTest { + public: + EngineAnimatorTest() + : thread_host_("EngineAnimatorTest", + ThreadHost::Type::kPlatform | ThreadHost::Type::kIo | + ThreadHost::Type::kUi | ThreadHost::Type::kRaster), + task_runners_({ + "EngineAnimatorTest", + thread_host_.platform_thread->GetTaskRunner(), // platform + thread_host_.raster_thread->GetTaskRunner(), // raster + thread_host_.ui_thread->GetTaskRunner(), // ui + thread_host_.io_thread->GetTaskRunner() // io + }) {} + + void PostUITaskSync(const std::function& function) { + fml::AutoResetWaitableEvent latch; + task_runners_.GetUITaskRunner()->PostTask([&] { + function(); + latch.Signal(); + }); + latch.Wait(); + } + + protected: + void SetUp() override { + settings_ = CreateSettingsForFixture(); + dispatcher_maker_ = [](PointerDataDispatcher::Delegate&) { + return nullptr; + }; + } + + MockDelegate delegate_; + PointerDataDispatcherMaker dispatcher_maker_; + ThreadHost thread_host_; + TaskRunners task_runners_; + Settings settings_; + std::unique_ptr animator_; + fml::WeakPtr io_manager_; + std::unique_ptr runtime_controller_; + std::shared_ptr image_decoder_task_runner_; + fml::TaskRunnerAffineWeakPtr snapshot_delegate_; +}; + +// A class that can launch an Engine with the specified Engine::Delegate. +// +// To use this class, contruct this class with Create, call Run, and use the +// engine with EngineTaskSync(). +class EngineContext { + public: + using EngineCallback = std::function; + + [[nodiscard]] static std::unique_ptr Create( + Engine::Delegate& delegate, // + Settings settings, // + const TaskRunners& task_runners, // + std::unique_ptr animator) { + auto [vm, isolate_snapshot] = Shell::InferVmInitDataFromSettings(settings); + FML_CHECK(vm) << "Must be able to initialize the VM."; + // Construct the class with `new` because `make_unique` has no access to the + // private constructor. + EngineContext* raw_pointer = + new EngineContext(delegate, settings, task_runners, std::move(animator), + vm, isolate_snapshot); + return std::unique_ptr(raw_pointer); + } + + void Run(RunConfiguration configuration) { + PostSync(task_runners_.GetUITaskRunner(), [this, &configuration] { + Engine::RunStatus run_status = engine_->Run(std::move(configuration)); + FML_CHECK(run_status == Engine::RunStatus::Success) + << "Engine failed to run."; + (void)run_status; // Suppress unused-variable warning + }); + } + + // Run a task that operates the Engine on the UI thread, and wait for the + // task to end. + // + // If called on the UI thread, the task is executed synchronously. + void EngineTaskSync(EngineCallback task) { + ASSERT_TRUE(engine_); + ASSERT_TRUE(task); + auto runner = task_runners_.GetUITaskRunner(); + if (runner->RunsTasksOnCurrentThread()) { + task(*engine_); + } else { + PostSync(task_runners_.GetUITaskRunner(), [&]() { task(*engine_); }); + } + } + + ~EngineContext() { + PostSync(task_runners_.GetUITaskRunner(), [this] { engine_.reset(); }); + } + + private: + EngineContext(Engine::Delegate& delegate, // + Settings settings, // + const TaskRunners& task_runners, // + std::unique_ptr animator, // + const DartVMRef& vm, // + fml::RefPtr isolate_snapshot) + : task_runners_(task_runners), vm_(vm) { + PostSync(task_runners.GetUITaskRunner(), [this, &settings, &animator, + &delegate, &isolate_snapshot] { + auto dispatcher_maker = + [](DefaultPointerDataDispatcher::Delegate& delegate) { + return std::make_unique(delegate); + }; + engine_ = std::make_unique( + /*delegate=*/delegate, + /*dispatcher_maker=*/dispatcher_maker, + /*vm=*/*&vm_, + /*isolate_snapshot=*/std::move(isolate_snapshot), + /*task_runners=*/task_runners_, + /*platform_data=*/PlatformData(), + /*settings=*/settings, + /*animator=*/std::move(animator), + /*io_manager=*/io_manager_, + /*unref_queue=*/nullptr, + /*snapshot_delegate=*/snapshot_delegate_, + /*volatile_path_tracker=*/nullptr, + /*gpu_disabled_switch=*/std::make_shared()); + }); + } + + TaskRunners task_runners_; + DartVMRef vm_; + std::unique_ptr engine_; + + fml::WeakPtr io_manager_; + fml::TaskRunnerAffineWeakPtr snapshot_delegate_; +}; +} // namespace + +TEST_F(EngineAnimatorTest, AnimatorAcceptsMultipleRenders) { + MockAnimatorDelegate animator_delegate; + std::unique_ptr engine_context; + + std::shared_ptr platform_message_handler = + std::make_shared(); + EXPECT_CALL(delegate_, GetPlatformMessageHandler) + .WillOnce(ReturnRef(platform_message_handler)); + fml::AutoResetWaitableEvent draw_latch; + EXPECT_CALL(animator_delegate, OnAnimatorDraw) + .WillOnce( + Invoke([&draw_latch](const std::shared_ptr& pipeline) { + auto status = + pipeline->Consume([&](std::unique_ptr item) { + auto tasks = Sorted(item->layer_tree_tasks); + EXPECT_EQ(tasks.size(), 2u); + EXPECT_EQ(tasks[0]->view_id, 1); + EXPECT_EQ(tasks[1]->view_id, 2); + }); + EXPECT_EQ(status, PipelineConsumeResult::Done); + draw_latch.Signal(); + })); + EXPECT_CALL(animator_delegate, OnAnimatorBeginFrame) + .WillOnce(Invoke([&engine_context](fml::TimePoint frame_target_time, + uint64_t frame_number) { + engine_context->EngineTaskSync([&](Engine& engine) { + engine.BeginFrame(frame_target_time, frame_number); + }); + })); + + native_latch.Reset(); + AddNativeCallback("NotifyNative", [](auto args) { native_latch.Signal(); }); + + std::unique_ptr animator; + PostSync(task_runners_.GetUITaskRunner(), + [&animator, &animator_delegate, &task_runners = task_runners_] { + animator = std::make_unique( + animator_delegate, task_runners, + static_cast>( + std::make_unique( + task_runners))); + }); + + engine_context = EngineContext::Create(delegate_, settings_, task_runners_, + std::move(animator)); + auto configuration = RunConfiguration::InferFromSettings(settings_); + configuration.SetEntrypoint("onDrawFrameRenderAllViews"); + engine_context->Run(std::move(configuration)); + + engine_context->EngineTaskSync([](Engine& engine) { + engine.AddView(1, ViewportMetrics{1, 10, 10, 22, 0}); + engine.AddView(2, ViewportMetrics{1, 10, 10, 22, 0}); + }); + + native_latch.Wait(); + + engine_context->EngineTaskSync( + [](Engine& engine) { engine.ScheduleFrame(); }); + draw_latch.Wait(); +} + +TEST_F(EngineAnimatorTest, IgnoresOutOfFrameRenders) { + MockAnimatorDelegate animator_delegate; + std::unique_ptr engine_context; + + std::shared_ptr platform_message_handler = + std::make_shared(); + EXPECT_CALL(delegate_, GetPlatformMessageHandler) + .WillOnce(ReturnRef(platform_message_handler)); + fml::AutoResetWaitableEvent draw_latch; + EXPECT_CALL(animator_delegate, OnAnimatorDraw) + .WillOnce( + Invoke([&draw_latch](const std::shared_ptr& pipeline) { + auto status = + pipeline->Consume([&](std::unique_ptr item) { + // View 1 is rendered before the frame, and is ignored. + // View 2 is rendered within the frame, and is accepted. + EXPECT_EQ(item->layer_tree_tasks.size(), 1u); + EXPECT_EQ(item->layer_tree_tasks[0]->view_id, 2); + }); + EXPECT_EQ(status, PipelineConsumeResult::Done); + draw_latch.Signal(); + })); + EXPECT_CALL(animator_delegate, OnAnimatorBeginFrame) + .WillOnce(Invoke([&engine_context](fml::TimePoint frame_target_time, + uint64_t frame_number) { + engine_context->EngineTaskSync([&](Engine& engine) { + engine.BeginFrame(frame_target_time, frame_number); + }); + })); + + std::unique_ptr animator; + PostSync(task_runners_.GetUITaskRunner(), + [&animator, &animator_delegate, &task_runners = task_runners_] { + animator = std::make_unique( + animator_delegate, task_runners, + static_cast>( + std::make_unique( + task_runners))); + }); + + engine_context = EngineContext::Create(delegate_, settings_, task_runners_, + std::move(animator)); + + engine_context->EngineTaskSync([](Engine& engine) { + engine.AddView(1, ViewportMetrics{1, 10, 10, 22, 0}); + engine.AddView(2, ViewportMetrics{1, 10, 10, 22, 0}); + }); + + auto configuration = RunConfiguration::InferFromSettings(settings_); + configuration.SetEntrypoint("renderViewsInFrameAndOutOfFrame"); + engine_context->Run(std::move(configuration)); + + draw_latch.Wait(); +} + +TEST_F(EngineAnimatorTest, IgnoresDuplicateRenders) { + MockAnimatorDelegate animator_delegate; + std::unique_ptr engine_context; + + std::vector> benchmark_layers; + auto capture_root_layer = [&benchmark_layers](Dart_NativeArguments args) { + auto handle = Dart_GetNativeArgument(args, 0); + intptr_t peer = 0; + Dart_Handle result = Dart_GetNativeInstanceField( + handle, tonic::DartWrappable::kPeerIndex, &peer); + ASSERT_FALSE(Dart_IsError(result)); + SceneBuilder* scene_builder = reinterpret_cast(peer); + ASSERT_TRUE(scene_builder); + std::shared_ptr root_layer = + scene_builder->layer_stack()[0]; + ASSERT_TRUE(root_layer); + benchmark_layers = root_layer->layers(); + }; + + std::shared_ptr platform_message_handler = + std::make_shared(); + EXPECT_CALL(delegate_, GetPlatformMessageHandler) + .WillOnce(ReturnRef(platform_message_handler)); + fml::AutoResetWaitableEvent draw_latch; + EXPECT_CALL(animator_delegate, OnAnimatorDraw) + .WillOnce(Invoke([&draw_latch, &benchmark_layers]( + const std::shared_ptr& pipeline) { + auto status = pipeline->Consume([&](std::unique_ptr item) { + EXPECT_EQ(item->layer_tree_tasks.size(), 1u); + EXPECT_EQ(item->layer_tree_tasks[0]->view_id, kFlutterImplicitViewId); + ContainerLayer* root_layer = reinterpret_cast( + item->layer_tree_tasks[0]->layer_tree->root_layer()); + std::vector> result_layers = + root_layer->layers(); + EXPECT_EQ(result_layers.size(), benchmark_layers.size()); + EXPECT_EQ(result_layers[0], benchmark_layers[0]); + }); + EXPECT_EQ(status, PipelineConsumeResult::Done); + draw_latch.Signal(); + })); + EXPECT_CALL(animator_delegate, OnAnimatorBeginFrame) + .WillOnce(Invoke([&engine_context](fml::TimePoint frame_target_time, + uint64_t frame_number) { + engine_context->EngineTaskSync([&](Engine& engine) { + engine.BeginFrame(frame_target_time, frame_number); + }); + })); + + AddNativeCallback("CaptureRootLayer", + CREATE_NATIVE_ENTRY(capture_root_layer)); + + std::unique_ptr animator; + PostSync(task_runners_.GetUITaskRunner(), + [&animator, &animator_delegate, &task_runners = task_runners_] { + animator = std::make_unique( + animator_delegate, task_runners, + static_cast>( + std::make_unique( + task_runners))); + }); + + engine_context = EngineContext::Create(delegate_, settings_, task_runners_, + std::move(animator)); + + engine_context->EngineTaskSync([](Engine& engine) { + engine.AddView(kFlutterImplicitViewId, ViewportMetrics{1, 10, 10, 22, 0}); + }); + + auto configuration = RunConfiguration::InferFromSettings(settings_); + configuration.SetEntrypoint("renderTwiceForOneView"); + engine_context->Run(std::move(configuration)); + + draw_latch.Wait(); +} + +TEST_F(EngineAnimatorTest, AnimatorSubmitsImplicitViewBeforeDrawFrameEnds) { + MockAnimatorDelegate animator_delegate; + std::unique_ptr engine_context; + + std::shared_ptr platform_message_handler = + std::make_shared(); + EXPECT_CALL(delegate_, GetPlatformMessageHandler) + .WillOnce(ReturnRef(platform_message_handler)); + + bool rasterization_started = false; + EXPECT_CALL(animator_delegate, OnAnimatorDraw) + .WillOnce(Invoke([&rasterization_started]( + const std::shared_ptr& pipeline) { + rasterization_started = true; + auto status = pipeline->Consume([&](std::unique_ptr item) { + EXPECT_EQ(item->layer_tree_tasks.size(), 1u); + EXPECT_EQ(item->layer_tree_tasks[0]->view_id, kFlutterImplicitViewId); + }); + EXPECT_EQ(status, PipelineConsumeResult::Done); + })); + EXPECT_CALL(animator_delegate, OnAnimatorBeginFrame) + .WillRepeatedly(Invoke([&engine_context](fml::TimePoint frame_target_time, + uint64_t frame_number) { + engine_context->EngineTaskSync([&](Engine& engine) { + engine.BeginFrame(frame_target_time, frame_number); + }); + })); + + std::unique_ptr animator; + PostSync(task_runners_.GetUITaskRunner(), + [&animator, &animator_delegate, &task_runners = task_runners_] { + animator = std::make_unique( + animator_delegate, task_runners, + static_cast>( + std::make_unique( + task_runners))); + }); + + native_latch.Reset(); + // The native_latch is signaled at the end of handleDrawFrame. + AddNativeCallback("NotifyNative", + CREATE_NATIVE_ENTRY([&rasterization_started](auto args) { + EXPECT_EQ(rasterization_started, true); + native_latch.Signal(); + })); + + engine_context = EngineContext::Create(delegate_, settings_, task_runners_, + std::move(animator)); + + engine_context->EngineTaskSync([](Engine& engine) { + engine.AddView(kFlutterImplicitViewId, ViewportMetrics{1.0, 10, 10, 1, 0}); + }); + + auto configuration = RunConfiguration::InferFromSettings(settings_); + configuration.SetEntrypoint("renderSingleViewAndCallAfterOnDrawFrame"); + engine_context->Run(std::move(configuration)); + + native_latch.Wait(); +} + +// The animator should submit to the pipeline the implicit view rendered in a +// warm up frame if there's already a continuation (i.e. Animator::BeginFrame +// has been called) +TEST_F(EngineAnimatorTest, AnimatorSubmitWarmUpImplicitView) { + MockAnimatorDelegate animator_delegate; + std::unique_ptr engine_context; + + std::shared_ptr platform_message_handler = + std::make_shared(); + EXPECT_CALL(delegate_, GetPlatformMessageHandler) + .WillOnce(ReturnRef(platform_message_handler)); + + fml::AutoResetWaitableEvent continuation_ready_latch; + fml::AutoResetWaitableEvent draw_latch; + EXPECT_CALL(animator_delegate, OnAnimatorDraw) + .WillOnce(Invoke([&draw_latch]( + const std::shared_ptr& pipeline) { + auto status = pipeline->Consume([&](std::unique_ptr item) { + EXPECT_EQ(item->layer_tree_tasks.size(), 1u); + EXPECT_EQ(item->layer_tree_tasks[0]->view_id, kFlutterImplicitViewId); + }); + EXPECT_EQ(status, PipelineConsumeResult::Done); + draw_latch.Signal(); + })); + EXPECT_CALL(animator_delegate, OnAnimatorBeginFrame) + .WillRepeatedly( + Invoke([&engine_context, &continuation_ready_latch]( + fml::TimePoint frame_target_time, uint64_t frame_number) { + continuation_ready_latch.Signal(); + engine_context->EngineTaskSync([&](Engine& engine) { + engine.BeginFrame(frame_target_time, frame_number); + }); + })); + + std::unique_ptr animator; + PostSync(task_runners_.GetUITaskRunner(), + [&animator, &animator_delegate, &task_runners = task_runners_] { + animator = std::make_unique( + animator_delegate, task_runners, + static_cast>( + std::make_unique( + task_runners))); + }); + + engine_context = EngineContext::Create(delegate_, settings_, task_runners_, + std::move(animator)); + + engine_context->EngineTaskSync([](Engine& engine) { + // Schedule a frame to trigger Animator::BeginFrame to create a + // continuation. The continuation needs to be available before `Engine::Run` + // since the Dart program immediately schedules a warm up frame. + engine.ScheduleFrame(true); + // Add the implicit view so that the engine recognizes it and that its + // metrics is not empty. + engine.AddView(kFlutterImplicitViewId, ViewportMetrics{1.0, 10, 10, 1, 0}); + }); + continuation_ready_latch.Wait(); + + auto configuration = RunConfiguration::InferFromSettings(settings_); + configuration.SetEntrypoint("renderWarmUpImplicitView"); + engine_context->Run(std::move(configuration)); + + draw_latch.Wait(); +} + +// The warm up frame should work if only some of the registered views are +// included. +// +// This test also verifies that the warm up frame can render multiple views. +TEST_F(EngineAnimatorTest, AnimatorSubmitPartialViewsForWarmUp) { + MockAnimatorDelegate animator_delegate; + std::unique_ptr engine_context; + + std::shared_ptr platform_message_handler = + std::make_shared(); + EXPECT_CALL(delegate_, GetPlatformMessageHandler) + .WillOnce(ReturnRef(platform_message_handler)); + + fml::AutoResetWaitableEvent continuation_ready_latch; + fml::AutoResetWaitableEvent draw_latch; + EXPECT_CALL(animator_delegate, OnAnimatorDraw) + .WillOnce( + Invoke([&draw_latch](const std::shared_ptr& pipeline) { + auto status = + pipeline->Consume([&](std::unique_ptr item) { + auto tasks = Sorted(item->layer_tree_tasks); + EXPECT_EQ(tasks.size(), 2u); + EXPECT_EQ(tasks[0]->view_id, 1); + EXPECT_EQ(tasks[1]->view_id, 2); + }); + EXPECT_EQ(status, PipelineConsumeResult::Done); + draw_latch.Signal(); + })); + EXPECT_CALL(animator_delegate, OnAnimatorBeginFrame) + .WillRepeatedly( + Invoke([&engine_context, &continuation_ready_latch]( + fml::TimePoint frame_target_time, uint64_t frame_number) { + continuation_ready_latch.Signal(); + engine_context->EngineTaskSync([&](Engine& engine) { + engine.BeginFrame(frame_target_time, frame_number); + }); + })); + + std::unique_ptr animator; + PostSync(task_runners_.GetUITaskRunner(), + [&animator, &animator_delegate, &task_runners = task_runners_] { + animator = std::make_unique( + animator_delegate, task_runners, + static_cast>( + std::make_unique( + task_runners))); + }); + + engine_context = EngineContext::Create(delegate_, settings_, task_runners_, + std::move(animator)); + + engine_context->EngineTaskSync([](Engine& engine) { + // Schedule a frame to make the animator create a continuation. + engine.ScheduleFrame(true); + // Add multiple views. + engine.AddView(0, ViewportMetrics{1, 10, 10, 22, 0}); + engine.AddView(1, ViewportMetrics{1, 10, 10, 22, 0}); + engine.AddView(2, ViewportMetrics{1, 10, 10, 22, 0}); + }); + + continuation_ready_latch.Wait(); + + auto configuration = RunConfiguration::InferFromSettings(settings_); + configuration.SetEntrypoint("renderWarmUpView1and2"); + engine_context->Run(std::move(configuration)); + + draw_latch.Wait(); +} + +} // namespace flutter + +// NOLINTEND(clang-analyzer-core.StackAddressEscape) diff --git a/shell/common/engine_unittests.cc b/shell/common/engine_unittests.cc index e5c7e99fb6d0d..7a822c5dca1b7 100644 --- a/shell/common/engine_unittests.cc +++ b/shell/common/engine_unittests.cc @@ -6,10 +6,7 @@ #include -#include "flutter/common/constants.h" #include "flutter/runtime/dart_vm_lifecycle.h" -#include "flutter/shell/common/shell.h" -#include "flutter/shell/common/shell_test.h" #include "flutter/shell/common/thread_host.h" #include "flutter/testing/fixture_test.h" #include "flutter/testing/testing.h" @@ -26,19 +23,6 @@ namespace flutter { namespace { -using ::testing::Invoke; -using ::testing::ReturnRef; - -static void PostSync(const fml::RefPtr& task_runner, - const fml::closure& task) { - fml::AutoResetWaitableEvent latch; - fml::TaskRunner::RunNowOrPostTask(task_runner, [&latch, &task] { - task(); - latch.Signal(); - }); - latch.Wait(); -} - class FontManifestAssetResolver : public AssetResolver { public: FontManifestAssetResolver() {} @@ -118,10 +102,10 @@ class MockRuntimeDelegate : public RuntimeDelegate { public: MOCK_METHOD(std::string, DefaultRouteName, (), (override)); MOCK_METHOD(void, ScheduleFrame, (bool), (override)); - MOCK_METHOD(void, EndWarmUpFrame, (), (override)); + MOCK_METHOD(void, OnAllViewsRendered, (), (override)); MOCK_METHOD(void, Render, - (std::unique_ptr, float), + (int64_t, std::unique_ptr, float), (override)); MOCK_METHOD(void, UpdateSemantics, @@ -173,51 +157,6 @@ class MockRuntimeController : public RuntimeController { MOCK_METHOD(bool, NotifyIdle, (fml::TimeDelta), (override)); }; -class MockAnimatorDelegate : public Animator::Delegate { - public: - /* Animator::Delegate */ - MOCK_METHOD(void, - OnAnimatorBeginFrame, - (fml::TimePoint frame_target_time, uint64_t frame_number), - (override)); - MOCK_METHOD(void, - OnAnimatorNotifyIdle, - (fml::TimeDelta deadline), - (override)); - MOCK_METHOD(void, - OnAnimatorUpdateLatestFrameTargetTime, - (fml::TimePoint frame_target_time), - (override)); - MOCK_METHOD(void, - OnAnimatorDraw, - (std::shared_ptr pipeline), - (override)); - MOCK_METHOD(void, - OnAnimatorDrawLastLayerTrees, - (std::unique_ptr frame_timings_recorder), - (override)); -}; - -class MockPlatformMessageHandler : public PlatformMessageHandler { - public: - MOCK_METHOD(void, - HandlePlatformMessage, - (std::unique_ptr message), - (override)); - MOCK_METHOD(bool, - DoesHandlePlatformMessageOnPlatformThread, - (), - (const, override)); - MOCK_METHOD(void, - InvokePlatformMessageResponseCallback, - (int response_id, std::unique_ptr mapping), - (override)); - MOCK_METHOD(void, - InvokePlatformMessageEmptyResponseCallback, - (int response_id), - (override)); -}; - class MockFontCollection : public FontCollection { public: MOCK_METHOD(void, @@ -294,96 +233,6 @@ class EngineTest : public testing::FixtureTest { std::shared_ptr image_decoder_task_runner_; fml::TaskRunnerAffineWeakPtr snapshot_delegate_; }; - -// A class that can launch an Engine with the specified Engine::Delegate. -// -// To use this class, contruct this class with Create, call Run, and use the -// engine with EngineTaskSync(). -class EngineContext { - public: - using EngineCallback = std::function; - - [[nodiscard]] static std::unique_ptr Create( - Engine::Delegate& delegate, // - Settings settings, // - const TaskRunners& task_runners, // - std::unique_ptr animator) { - auto [vm, isolate_snapshot] = Shell::InferVmInitDataFromSettings(settings); - FML_CHECK(vm) << "Must be able to initialize the VM."; - // Construct the class with `new` because `make_unique` has no access to the - // private constructor. - EngineContext* raw_pointer = - new EngineContext(delegate, settings, task_runners, std::move(animator), - std::move(vm), isolate_snapshot); - return std::unique_ptr(raw_pointer); - } - - void Run(RunConfiguration configuration) { - PostSync(task_runners_.GetUITaskRunner(), [this, &configuration] { - Engine::RunStatus run_status = engine_->Run(std::move(configuration)); - FML_CHECK(run_status == Engine::RunStatus::Success) - << "Engine failed to run."; - (void)run_status; // Suppress unused-variable warning - }); - } - - // Run a task that operates the Engine on the UI thread, and wait for the - // task to end. - // - // If called on the UI thread, the task is executed synchronously. - void EngineTaskSync(EngineCallback task) { - ASSERT_TRUE(engine_); - ASSERT_TRUE(task); - auto runner = task_runners_.GetUITaskRunner(); - if (runner->RunsTasksOnCurrentThread()) { - task(*engine_); - } else { - PostSync(task_runners_.GetUITaskRunner(), [&]() { task(*engine_); }); - } - } - - ~EngineContext() { - PostSync(task_runners_.GetUITaskRunner(), [this] { engine_.reset(); }); - } - - private: - EngineContext(Engine::Delegate& delegate, // - Settings settings, // - const TaskRunners& task_runners, // - std::unique_ptr animator, // - DartVMRef vm, // - fml::RefPtr isolate_snapshot) - : task_runners_(task_runners), vm_(std::move(vm)) { - PostSync(task_runners.GetUITaskRunner(), [this, &settings, &animator, - &delegate, &isolate_snapshot] { - auto dispatcher_maker = - [](DefaultPointerDataDispatcher::Delegate& delegate) { - return std::make_unique(delegate); - }; - engine_ = std::make_unique( - /*delegate=*/delegate, - /*dispatcher_maker=*/dispatcher_maker, - /*vm=*/*&vm_, - /*isolate_snapshot=*/std::move(isolate_snapshot), - /*task_runners=*/task_runners_, - /*platform_data=*/PlatformData(), - /*settings=*/settings, - /*animator=*/std::move(animator), - /*io_manager=*/io_manager_, - /*unref_queue=*/nullptr, - /*snapshot_delegate=*/snapshot_delegate_, - /*volatile_path_tracker=*/nullptr, - /*gpu_disabled_switch=*/std::make_shared()); - }); - } - - TaskRunners task_runners_; - DartVMRef vm_; - std::unique_ptr engine_; - - fml::WeakPtr io_manager_; - fml::TaskRunnerAffineWeakPtr snapshot_delegate_; -}; } // namespace TEST_F(EngineTest, Create) { @@ -617,71 +466,6 @@ TEST_F(EngineTest, PassesLoadDartDeferredLibraryErrorToRuntime) { }); } -// The animator should submit to the pipeline the implicit view rendered in a -// warm up frame if there's already a continuation (i.e. Animator::BeginFrame -// has been called) -TEST_F(EngineTest, AnimatorSubmitWarmUpImplicitView) { - MockAnimatorDelegate animator_delegate; - std::unique_ptr engine_context; - - std::shared_ptr platform_message_handler = - std::make_shared(); - EXPECT_CALL(delegate_, GetPlatformMessageHandler) - .WillOnce(ReturnRef(platform_message_handler)); - - fml::AutoResetWaitableEvent continuation_ready_latch; - fml::AutoResetWaitableEvent draw_latch; - EXPECT_CALL(animator_delegate, OnAnimatorDraw) - .WillOnce(Invoke([&draw_latch]( - const std::shared_ptr& pipeline) { - auto status = pipeline->Consume([&](std::unique_ptr item) { - EXPECT_EQ(item->layer_tree_tasks.size(), 1u); - EXPECT_EQ(item->layer_tree_tasks[0]->view_id, kFlutterImplicitViewId); - }); - EXPECT_EQ(status, PipelineConsumeResult::Done); - draw_latch.Signal(); - })); - EXPECT_CALL(animator_delegate, OnAnimatorBeginFrame) - .WillRepeatedly( - Invoke([&engine_context, &continuation_ready_latch]( - fml::TimePoint frame_target_time, uint64_t frame_number) { - continuation_ready_latch.Signal(); - engine_context->EngineTaskSync([&](Engine& engine) { - engine.BeginFrame(frame_target_time, frame_number); - }); - })); - - std::unique_ptr animator; - PostSync(task_runners_.GetUITaskRunner(), - [&animator, &animator_delegate, &task_runners = task_runners_] { - animator = std::make_unique( - animator_delegate, task_runners, - static_cast>( - std::make_unique( - task_runners))); - }); - - engine_context = EngineContext::Create(delegate_, settings_, task_runners_, - std::move(animator)); - - engine_context->EngineTaskSync([](Engine& engine) { - // Schedule a frame to trigger Animator::BeginFrame to create a - // continuation. The continuation needs to be available before `Engine::Run` - // since the Dart program immediately schedules a warm up frame. - engine.ScheduleFrame(true); - // Add the implicit view so that the engine recognizes it and that its - // metrics is not empty. - engine.AddView(kFlutterImplicitViewId, ViewportMetrics{1.0, 10, 10, 1, 0}); - }); - continuation_ready_latch.Wait(); - - auto configuration = RunConfiguration::InferFromSettings(settings_); - configuration.SetEntrypoint("renderWarmUpImplicitView"); - engine_context->Run(std::move(configuration)); - - draw_latch.Wait(); -} - TEST_F(EngineTest, SpawnedEngineInheritsAssetManager) { PostUITaskSync([this] { MockRuntimeDelegate client; diff --git a/shell/common/fixtures/shell_test.dart b/shell/common/fixtures/shell_test.dart index 1b52fabe1efe7..a7727903af352 100644 --- a/shell/common/fixtures/shell_test.dart +++ b/shell/common/fixtures/shell_test.dart @@ -532,6 +532,91 @@ void testReportViewWidths() { }; } +void renderDummyToView(FlutterView view) { + final SceneBuilder builder = SceneBuilder(); + final PictureRecorder recorder = PictureRecorder(); + final Canvas canvas = Canvas(recorder); + canvas.drawPaint(Paint()..color = const Color(0xFFABCDEF)); + final Picture picture = recorder.endRecording(); + builder.addPicture(Offset.zero, picture); + + final Scene scene = builder.build(); + view.render(scene); + + scene.dispose(); + picture.dispose(); +} + +@pragma('vm:entry-point') +void onDrawFrameRenderAllViews() { + PlatformDispatcher.instance.onDrawFrame = () { + for (final FlutterView view in PlatformDispatcher.instance.views) { + renderDummyToView(view); + } + }; + notifyNative(); +} + +@pragma('vm:entry-point') +void renderViewsInFrameAndOutOfFrame() { + renderDummyToView(PlatformDispatcher.instance.view(id: 1)!); + PlatformDispatcher.instance.onDrawFrame = () { + renderDummyToView(PlatformDispatcher.instance.view(id: 2)!); + }; + PlatformDispatcher.instance.scheduleFrame(); +} + +@pragma('vm:external-name', 'CaptureRootLayer') +external _captureRootLayer(SceneBuilder sceneBuilder); + +@pragma('vm:entry-point') +void renderTwiceForOneView() { + final SceneBuilder builder = SceneBuilder(); + final PictureRecorder recorder = PictureRecorder(); + final Canvas canvas = Canvas(recorder); + canvas.drawPaint(Paint()..color = const Color(0xFFABCDEF)); + final Picture picture = recorder.endRecording(); + builder.addPicture(Offset.zero, picture); + + PlatformDispatcher.instance.onBeginFrame = (_) { + // Tell engine the correct layer tree. + _captureRootLayer(builder); + }; + + PlatformDispatcher.instance.onDrawFrame = () { + final Scene scene = builder.build(); + PlatformDispatcher.instance.implicitView!.render(scene); + scene.dispose(); + picture.dispose(); + + // Render a second time. This duplicate render should be ignored. + renderDummyToView(PlatformDispatcher.instance.implicitView!); + }; + PlatformDispatcher.instance.scheduleFrame(); +} + +@pragma('vm:entry-point') +void renderSingleViewAndCallAfterOnDrawFrame() { + PlatformDispatcher.instance.onDrawFrame = () { + final SceneBuilder builder = SceneBuilder(); + final PictureRecorder recorder = PictureRecorder(); + final Canvas canvas = Canvas(recorder); + canvas.drawPaint(Paint()..color = const Color(0xFFABCDEF)); + final Picture picture = recorder.endRecording(); + builder.addPicture(Offset.zero, picture); + + final Scene scene = builder.build(); + PlatformDispatcher.instance.implicitView!.render(scene); + // Notify the engine after the render before the disposal. + // The view should have been submitted for rasterization at this moment. + notifyNative(); + + scene.dispose(); + picture.dispose(); + }; + PlatformDispatcher.instance.scheduleFrame(); +} + @pragma('vm:entry-point') void renderWarmUpImplicitView() { bool beginFrameCalled = false; @@ -543,19 +628,26 @@ void renderWarmUpImplicitView() { }, drawFrame: () { expect(beginFrameCalled, true); + renderDummyToView(PlatformDispatcher.instance.implicitView!); + }, + ); +} - final SceneBuilder builder = SceneBuilder(); - final PictureRecorder recorder = PictureRecorder(); - final Canvas canvas = Canvas(recorder); - canvas.drawPaint(Paint()..color = const Color(0xFFABCDEF)); - final Picture picture = recorder.endRecording(); - builder.addPicture(Offset.zero, picture); - - final Scene scene = builder.build(); - PlatformDispatcher.instance.implicitView!.render(scene); +@pragma('vm:entry-point') +void renderWarmUpView1and2() { + bool beginFrameCalled = false; - scene.dispose(); - picture.dispose(); + PlatformDispatcher.instance.scheduleWarmUpFrame( + beginFrame: () { + expect(beginFrameCalled, false); + beginFrameCalled = true; }, + drawFrame: () { + expect(beginFrameCalled, true); + + for (final int viewId in [1, 2]) { + renderDummyToView(PlatformDispatcher.instance.view(id: viewId)!); + } + } ); } diff --git a/shell/common/input_events_unittests.cc b/shell/common/input_events_unittests.cc index 3824dc4d92bae..66a2b64a39e02 100644 --- a/shell/common/input_events_unittests.cc +++ b/shell/common/input_events_unittests.cc @@ -127,11 +127,11 @@ static void TestSimulatedInputEvents( ShellTest::DispatchFakePointerData(shell.get()); i += 1; } - ShellTest::VSyncFlush(shell.get(), will_draw_new_frame); + ShellTest::VSyncFlush(shell.get(), &will_draw_new_frame); } // Finally, issue a vsync for the pending event that may be generated duing // the last vsync. - ShellTest::VSyncFlush(shell.get(), will_draw_new_frame); + ShellTest::VSyncFlush(shell.get(), &will_draw_new_frame); }); simulation.wait(); @@ -345,8 +345,7 @@ TEST_F(ShellTest, CanCorrectlyPipePointerPacket) { CreateSimulatedPointerData(data, PointerData::Change::kRemove, 3.0, 4.0); packet->SetPointerData(5, data); ShellTest::DispatchPointerData(shell.get(), std::move(packet)); - bool will_draw_new_frame; - ShellTest::VSyncFlush(shell.get(), will_draw_new_frame); + ShellTest::VSyncFlush(shell.get()); reportLatch.Wait(); size_t expect_length = 6; @@ -407,8 +406,7 @@ TEST_F(ShellTest, CanCorrectlySynthesizePointerPacket) { CreateSimulatedPointerData(data, PointerData::Change::kRemove, 3.0, 4.0); packet->SetPointerData(3, data); ShellTest::DispatchPointerData(shell.get(), std::move(packet)); - bool will_draw_new_frame; - ShellTest::VSyncFlush(shell.get(), will_draw_new_frame); + ShellTest::VSyncFlush(shell.get()); reportLatch.Wait(); size_t expect_length = 6; diff --git a/shell/common/rasterizer.cc b/shell/common/rasterizer.cc index 8d6edce8d958a..ed7bee5a9376c 100644 --- a/shell/common/rasterizer.cc +++ b/shell/common/rasterizer.cc @@ -261,6 +261,7 @@ DrawStatus Rasterizer::Draw(const std::shared_ptr& pipeline) { bool should_resubmit_frame = ShouldResubmitFrame(draw_result); if (should_resubmit_frame) { + FML_CHECK(draw_result.resubmitted_item); auto front_continuation = pipeline->ProduceIfEmpty(); PipelineProduceResult pipeline_result = front_continuation.Complete(std::move(draw_result.resubmitted_item)); diff --git a/shell/common/shell_test.cc b/shell/common/shell_test.cc index e321826ca1615..efcf43ce82668 100644 --- a/shell/common/shell_test.cc +++ b/shell/common/shell_test.cc @@ -22,6 +22,39 @@ namespace testing { constexpr int64_t kImplicitViewId = 0; +FrameContent ViewContent::NoViews() { + return std::map(); +} + +FrameContent ViewContent::DummyView(double width, double height) { + FrameContent result; + result[kImplicitViewId] = ViewContent{ + .viewport_metrics = {1.0, width, height, 22, 0}, + .builder = {}, + }; + return result; +} + +FrameContent ViewContent::DummyView(flutter::ViewportMetrics viewport_metrics) { + FrameContent result; + result[kImplicitViewId] = ViewContent{ + .viewport_metrics = std::move(viewport_metrics), + .builder = {}, + }; + return result; +} + +FrameContent ViewContent::ImplicitView(double width, + double height, + LayerTreeBuilder builder) { + FrameContent result; + result[kImplicitViewId] = ViewContent{ + .viewport_metrics = {1.0, width, height, 22, 0}, + .builder = std::move(builder), + }; + return result; +} + ShellTest::ShellTest() : thread_host_("io.flutter.test." + GetCurrentTestName() + ".", ThreadHost::Type::kPlatform | ThreadHost::Type::kIo | @@ -92,16 +125,18 @@ void ShellTest::RestartEngine(Shell* shell, RunConfiguration configuration) { ASSERT_TRUE(restarted.get_future().get()); } -void ShellTest::VSyncFlush(Shell* shell, bool& will_draw_new_frame) { +void ShellTest::VSyncFlush(Shell* shell, bool* will_draw_new_frame) { fml::AutoResetWaitableEvent latch; fml::TaskRunner::RunNowOrPostTask( shell->GetTaskRunners().GetPlatformTaskRunner(), - [shell, &will_draw_new_frame, &latch] { + [shell, will_draw_new_frame, &latch] { // The following UI task ensures that all previous UI tasks are flushed. fml::AutoResetWaitableEvent ui_latch; shell->GetTaskRunners().GetUITaskRunner()->PostTask( - [&ui_latch, &will_draw_new_frame]() { - will_draw_new_frame = true; + [&ui_latch, will_draw_new_frame]() { + if (will_draw_new_frame != nullptr) { + *will_draw_new_frame = true; + } ui_latch.Signal(); }); @@ -154,6 +189,7 @@ void ShellTest::SetViewportMetrics(Shell* shell, double width, double height) { std::make_unique(); recorder->RecordVsync(frame_begin_time, frame_end_time); engine->animator_->BeginFrame(std::move(recorder)); + engine->animator_->EndFrame(); } latch.Signal(); }); @@ -172,23 +208,22 @@ void ShellTest::NotifyIdle(Shell* shell, fml::TimeDelta deadline) { latch.Wait(); } -void ShellTest::PumpOneFrame(Shell* shell, - double width, - double height, - LayerTreeBuilder builder) { - PumpOneFrame(shell, {1.0, width, height, 22, 0}, std::move(builder)); +void ShellTest::PumpOneFrame(Shell* shell) { + PumpOneFrame(shell, ViewContent::DummyView()); } -void ShellTest::PumpOneFrame(Shell* shell, - const flutter::ViewportMetrics& viewport_metrics, - LayerTreeBuilder builder) { +void ShellTest::PumpOneFrame(Shell* shell, FrameContent frame_content) { // Set viewport to nonempty, and call Animator::BeginFrame to make the layer // tree pipeline nonempty. Without either of this, the layer tree below // won't be rasterized. fml::AutoResetWaitableEvent latch; + fml::WeakPtr runtime_delegate = shell->weak_engine_; shell->GetTaskRunners().GetUITaskRunner()->PostTask( - [&latch, engine = shell->weak_engine_, viewport_metrics]() { - engine->SetViewportMetrics(kImplicitViewId, viewport_metrics); + [&latch, engine = shell->weak_engine_, &frame_content, + runtime_delegate]() { + for (auto& [view_id, view_content] : frame_content) { + engine->SetViewportMetrics(view_id, view_content.viewport_metrics); + } const auto frame_begin_time = fml::TimePoint::Now(); const auto frame_end_time = frame_begin_time + fml::TimeDelta::FromSecondsF(1.0 / 60.0); @@ -196,28 +231,28 @@ void ShellTest::PumpOneFrame(Shell* shell, std::make_unique(); recorder->RecordVsync(frame_begin_time, frame_end_time); engine->animator_->BeginFrame(std::move(recorder)); - latch.Signal(); - }); - latch.Wait(); - latch.Reset(); - // Call |Render| to rasterize a layer tree and trigger |OnFrameRasterized| - fml::WeakPtr runtime_delegate = shell->weak_engine_; - shell->GetTaskRunners().GetUITaskRunner()->PostTask( - [&latch, runtime_delegate, &builder, viewport_metrics]() { - SkMatrix identity; - identity.setIdentity(); - auto root_layer = std::make_shared(identity); - auto layer_tree = std::make_unique( - LayerTree::Config{.root_layer = root_layer}, - SkISize::Make(viewport_metrics.physical_width, - viewport_metrics.physical_height)); - float device_pixel_ratio = - static_cast(viewport_metrics.device_pixel_ratio); - if (builder) { - builder(root_layer); + // The BeginFrame phase and the EndFrame phase must be performed in a + // single task, otherwise a normal vsync might be inserted in between, + // causing flaky assertion errors. + + for (auto& [view_id, view_content] : frame_content) { + SkMatrix identity; + identity.setIdentity(); + auto root_layer = std::make_shared(identity); + auto layer_tree = std::make_unique( + LayerTree::Config{.root_layer = root_layer}, + SkISize::Make(view_content.viewport_metrics.physical_width, + view_content.viewport_metrics.physical_height)); + float device_pixel_ratio = static_cast( + view_content.viewport_metrics.device_pixel_ratio); + if (view_content.builder) { + view_content.builder(root_layer); + } + runtime_delegate->Render(view_id, std::move(layer_tree), + device_pixel_ratio); } - runtime_delegate->Render(std::move(layer_tree), device_pixel_ratio); + engine->animator_->EndFrame(); latch.Signal(); }); latch.Wait(); diff --git a/shell/common/shell_test.h b/shell/common/shell_test.h index 7ded997fbcdc5..c11ad1174dc88 100644 --- a/shell/common/shell_test.h +++ b/shell/common/shell_test.h @@ -29,6 +29,38 @@ namespace flutter { namespace testing { +// The signature of ViewContent::builder. +using LayerTreeBuilder = + std::function root)>; +struct ViewContent; +// Defines the content to be rendered to all views of a frame in PumpOneFrame. +using FrameContent = std::map; +// Defines the content to be rendered to a view in PumpOneFrame. +struct ViewContent { + flutter::ViewportMetrics viewport_metrics; + // Given the root layer, this callback builds the layer tree to be rasterized + // in PumpOneFrame. + LayerTreeBuilder builder; + + // Build a frame with no views. This is useful when PumpOneFrame is used just + // to schedule the frame while the frame content is defined by other means. + static FrameContent NoViews(); + + // Build a frame with a single implicit view with the specific size and no + // content. + static FrameContent DummyView(double width = 1, double height = 1); + + // Build a frame with a single implicit view with the specific viewport + // metrics and no content. + static FrameContent DummyView(flutter::ViewportMetrics viewport_metrics); + + // Build a frame with a single implicit view with the specific size and + // content. + static FrameContent ImplicitView(double width, + double height, + LayerTreeBuilder builder); +}; + class ShellTest : public FixtureTest { public: struct Config { @@ -70,24 +102,14 @@ class ShellTest : public FixtureTest { static void RestartEngine(Shell* shell, RunConfiguration configuration); /// Issue as many VSYNC as needed to flush the UI tasks so far, and reset - /// the `will_draw_new_frame` to true. - static void VSyncFlush(Shell* shell, bool& will_draw_new_frame); - - /// Given the root layer, this callback builds the layer tree to be rasterized - /// in PumpOneFrame. - using LayerTreeBuilder = - std::function root)>; + /// the content of `will_draw_new_frame` to true if it's not nullptr. + static void VSyncFlush(Shell* shell, bool* will_draw_new_frame = nullptr); static void SetViewportMetrics(Shell* shell, double width, double height); static void NotifyIdle(Shell* shell, fml::TimeDelta deadline); - static void PumpOneFrame(Shell* shell, - double width = 1, - double height = 1, - LayerTreeBuilder = {}); - static void PumpOneFrame(Shell* shell, - const flutter::ViewportMetrics& viewport_metrics, - LayerTreeBuilder = {}); + static void PumpOneFrame(Shell* shell); + static void PumpOneFrame(Shell* shell, FrameContent frame_content); static void DispatchFakePointerData(Shell* shell); static void DispatchPointerData(Shell* shell, std::unique_ptr packet); diff --git a/shell/common/shell_unittests.cc b/shell/common/shell_unittests.cc index e123769660655..162f13d626016 100644 --- a/shell/common/shell_unittests.cc +++ b/shell/common/shell_unittests.cc @@ -42,6 +42,7 @@ #include "flutter/shell/common/switches.h" #include "flutter/shell/common/thread_host.h" #include "flutter/shell/common/vsync_waiter_fallback.h" +#include "flutter/shell/common/vsync_waiters_test.h" #include "flutter/shell/version/version.h" #include "flutter/testing/mock_canvas.h" #include "flutter/testing/testing.h" @@ -884,7 +885,7 @@ TEST_F(ShellTest, ExternalEmbedderNoThreadMerger) { root->Add(display_list_layer); }; - PumpOneFrame(shell.get(), 100, 100, builder); + PumpOneFrame(shell.get(), ViewContent::ImplicitView(100, 100, builder)); end_frame_latch.Wait(); ASSERT_TRUE(end_frame_called); @@ -958,7 +959,7 @@ TEST_F(ShellTest, PushBackdropFilterToVisitedPlatformViews) { backdrop_filter_layer->Add(platform_view_layer2); }; - PumpOneFrame(shell.get(), 100, 100, builder); + PumpOneFrame(shell.get(), ViewContent::ImplicitView(100, 100, builder)); end_frame_latch.Wait(); ASSERT_EQ(visited_platform_views, (std::vector{50, 75})); ASSERT_TRUE(stack_75.is_empty()); @@ -1019,7 +1020,7 @@ TEST_F(ShellTest, root->Add(display_list_layer); }; - PumpOneFrame(shell.get(), 100, 100, builder); + PumpOneFrame(shell.get(), ViewContent::ImplicitView(100, 100, builder)); end_frame_latch.Wait(); ASSERT_TRUE(end_frame_called); @@ -1065,9 +1066,12 @@ TEST_F(ShellTest, OnPlatformViewDestroyDisablesThreadMerger) { root->Add(display_list_layer); }; - PumpOneFrame(shell.get(), 100, 100, builder); + PumpOneFrame(shell.get(), ViewContent::ImplicitView(100, 100, builder)); auto result = shell->WaitForFirstFrame(fml::TimeDelta::Max()); + // Wait for the rasterizer to process the frame. WaitForFirstFrame only waits + // for the Animator, but end_frame_callback is called by the Rasterizer. + PostSync(shell->GetTaskRunners().GetRasterTaskRunner(), [] {}); ASSERT_TRUE(result.ok()) << "Result: " << static_cast(result.code()) << ": " << result.message(); @@ -1132,12 +1136,12 @@ TEST_F(ShellTest, OnPlatformViewDestroyAfterMergingThreads) { root->Add(display_list_layer); }; - PumpOneFrame(shell.get(), 100, 100, builder); + PumpOneFrame(shell.get(), ViewContent::ImplicitView(100, 100, builder)); // Pump one frame to trigger thread merging. end_frame_latch.Wait(); // Pump another frame to ensure threads are merged and a regular layer tree is // submitted. - PumpOneFrame(shell.get(), 100, 100, builder); + PumpOneFrame(shell.get(), ViewContent::ImplicitView(100, 100, builder)); // Threads are merged here. PlatformViewNotifyDestroy should be executed // successfully. ASSERT_TRUE(fml::TaskRunnerChecker::RunsOnTheSameThread( @@ -1201,7 +1205,7 @@ TEST_F(ShellTest, OnPlatformViewDestroyWhenThreadsAreMerging) { root->Add(display_list_layer); }; - PumpOneFrame(shell.get(), 100, 100, builder); + PumpOneFrame(shell.get(), ViewContent::ImplicitView(100, 100, builder)); // Pump one frame and threads aren't merged end_frame_latch.Wait(); ASSERT_FALSE(fml::TaskRunnerChecker::RunsOnTheSameThread( @@ -1212,7 +1216,7 @@ TEST_F(ShellTest, OnPlatformViewDestroyWhenThreadsAreMerging) { // threads external_view_embedder->UpdatePostPrerollResult( PostPrerollResult::kResubmitFrame); - PumpOneFrame(shell.get(), 100, 100, builder); + PumpOneFrame(shell.get(), ViewContent::ImplicitView(100, 100, builder)); // Now destroy the platform view immediately. // Two things can happen here: @@ -1268,7 +1272,7 @@ TEST_F(ShellTest, SkPoint::Make(10, 10), MakeSizedDisplayList(80, 80), false, false); root->Add(display_list_layer); }; - PumpOneFrame(shell.get(), 100, 100, builder); + PumpOneFrame(shell.get(), ViewContent::ImplicitView(100, 100, builder)); end_frame_latch.Wait(); // Threads should not be merged. @@ -1307,7 +1311,7 @@ TEST_F(ShellTest, OnPlatformViewDestroyWithoutRasterThreadMerger) { SkPoint::Make(10, 10), MakeSizedDisplayList(80, 80), false, false); root->Add(display_list_layer); }; - PumpOneFrame(shell.get(), 100, 100, builder); + PumpOneFrame(shell.get(), ViewContent::ImplicitView(100, 100, builder)); // Threads should not be merged. ASSERT_FALSE(fml::TaskRunnerChecker::RunsOnTheSameThread( @@ -1373,7 +1377,7 @@ TEST_F(ShellTest, OnPlatformViewDestroyWithStaticThreadMerging) { SkPoint::Make(10, 10), MakeSizedDisplayList(80, 80), false, false); root->Add(display_list_layer); }; - PumpOneFrame(shell.get(), 100, 100, builder); + PumpOneFrame(shell.get(), ViewContent::ImplicitView(100, 100, builder)); end_frame_latch.Wait(); ValidateDestroyPlatformView(shell.get()); @@ -1419,7 +1423,7 @@ TEST_F(ShellTest, GetUsedThisFrameShouldBeSetBeforeEndFrame) { SkPoint::Make(10, 10), MakeSizedDisplayList(80, 80), false, false); root->Add(display_list_layer); }; - PumpOneFrame(shell.get(), 100, 100, builder); + PumpOneFrame(shell.get(), ViewContent::ImplicitView(100, 100, builder)); end_frame_latch.Wait(); ASSERT_FALSE(used_this_frame); @@ -1569,10 +1573,11 @@ TEST_F(ShellTest, WaitForFirstFrameZeroSizeFrame) { configuration.SetEntrypoint("emptyMain"); RunEngine(shell.get(), std::move(configuration)); - PumpOneFrame(shell.get(), {1.0, 0.0, 0.0, 22, 0}); + PumpOneFrame(shell.get(), ViewContent::DummyView({1.0, 0.0, 0.0, 22, 0})); fml::Status result = shell->WaitForFirstFrame(fml::TimeDelta::Zero()); - ASSERT_FALSE(result.ok()); - ASSERT_EQ(result.code(), fml::StatusCode::kDeadlineExceeded); + EXPECT_FALSE(result.ok()); + EXPECT_EQ(result.message(), "timeout"); + EXPECT_EQ(result.code(), fml::StatusCode::kDeadlineExceeded); DestroyShell(std::move(shell)); } @@ -2088,6 +2093,7 @@ TEST_F(ShellTest, CanScheduleFrameFromPlatform) { TEST_F(ShellTest, SecondaryVsyncCallbackShouldBeCalledAfterVsyncCallback) { bool is_on_begin_frame_called = false; bool is_secondary_callback_called = false; + bool test_started = false; Settings settings = CreateSettingsForFixture(); TaskRunners task_runners = GetTaskRunnersForFixture(); fml::AutoResetWaitableEvent latch; @@ -2097,12 +2103,18 @@ TEST_F(ShellTest, SecondaryVsyncCallbackShouldBeCalledAfterVsyncCallback) { fml::CountDownLatch count_down_latch(2); AddNativeCallback("NativeOnBeginFrame", CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { + if (!test_started) { + return; + } EXPECT_FALSE(is_on_begin_frame_called); EXPECT_FALSE(is_secondary_callback_called); is_on_begin_frame_called = true; count_down_latch.CountDown(); })); - std::unique_ptr shell = CreateShell(settings, task_runners); + std::unique_ptr shell = CreateShell({ + .settings = settings, + .task_runners = task_runners, + }); ASSERT_TRUE(shell->IsSetup()); auto configuration = RunConfiguration::InferFromSettings(settings); @@ -2115,12 +2127,16 @@ TEST_F(ShellTest, SecondaryVsyncCallbackShouldBeCalledAfterVsyncCallback) { fml::TaskRunner::RunNowOrPostTask( shell->GetTaskRunners().GetUITaskRunner(), [&]() { shell->GetEngine()->ScheduleSecondaryVsyncCallback(0, [&]() { + if (!test_started) { + return; + } EXPECT_TRUE(is_on_begin_frame_called); EXPECT_FALSE(is_secondary_callback_called); is_secondary_callback_called = true; count_down_latch.CountDown(); }); shell->GetEngine()->ScheduleFrame(); + test_started = true; }); count_down_latch.Wait(); EXPECT_TRUE(is_on_begin_frame_called); @@ -2165,7 +2181,7 @@ TEST_F(ShellTest, Screenshot) { root->Add(display_list_layer); }; - PumpOneFrame(shell.get(), 100, 100, builder); + PumpOneFrame(shell.get(), ViewContent::ImplicitView(100, 100, builder)); firstFrameLatch.Wait(); std::promise screenshot_promise; @@ -2550,7 +2566,13 @@ TEST_F(ShellTest, OnServiceProtocolRenderFrameWithRasterStatsWorks) { configuration.SetEntrypoint("scene_with_red_box"); RunEngine(shell.get(), std::move(configuration)); - PumpOneFrame(shell.get()); + // Set a non-zero viewport metrics, otherwise the scene would be discarded. + PostSync(shell->GetTaskRunners().GetUITaskRunner(), + [engine = shell->GetEngine()]() { + engine->SetViewportMetrics(kImplicitViewId, + ViewportMetrics{1, 1, 1, 22, 0}); + }); + PumpOneFrame(shell.get(), ViewContent::NoViews()); ServiceProtocol::Handler::ServiceProtocolMap empty_params; rapidjson::Document document; @@ -2662,7 +2684,7 @@ TEST_F(ShellTest, OnServiceProtocolRenderFrameWithRasterStatsDisableImpeller) { configuration.SetEntrypoint("scene_with_red_box"); RunEngine(shell.get(), std::move(configuration)); - PumpOneFrame(shell.get()); + PumpOneFrame(shell.get(), ViewContent::NoViews()); ServiceProtocol::Handler::ServiceProtocolMap empty_params; rapidjson::Document document; @@ -2726,14 +2748,16 @@ TEST_F(ShellTest, DISABLED_DiscardLayerTreeOnResize) { RunEngine(shell.get(), std::move(configuration)); - PumpOneFrame(shell.get(), static_cast(wrong_size.width()), - static_cast(wrong_size.height())); + PumpOneFrame(shell.get(), ViewContent::DummyView( + static_cast(wrong_size.width()), + static_cast(wrong_size.height()))); end_frame_latch.Wait(); // Wrong size, no frames are submitted. ASSERT_EQ(0, external_view_embedder->GetSubmittedFrameCount()); - PumpOneFrame(shell.get(), static_cast(expected_size.width()), - static_cast(expected_size.height())); + PumpOneFrame(shell.get(), ViewContent::DummyView( + static_cast(expected_size.width()), + static_cast(expected_size.height()))); end_frame_latch.Wait(); // Expected size, 1 frame submitted. ASSERT_EQ(1, external_view_embedder->GetSubmittedFrameCount()); @@ -2804,8 +2828,9 @@ TEST_F(ShellTest, DISABLED_DiscardResubmittedLayerTreeOnResize) { RunEngine(shell.get(), std::move(configuration)); - PumpOneFrame(shell.get(), static_cast(origin_size.width()), - static_cast(origin_size.height())); + PumpOneFrame(shell.get(), ViewContent::DummyView( + static_cast(origin_size.width()), + static_cast(origin_size.height()))); end_frame_latch.Wait(); ASSERT_EQ(0, external_view_embedder->GetSubmittedFrameCount()); @@ -2826,8 +2851,9 @@ TEST_F(ShellTest, DISABLED_DiscardResubmittedLayerTreeOnResize) { ASSERT_EQ(0, external_view_embedder->GetSubmittedFrameCount()); // Threads will be merged at the end of this frame. - PumpOneFrame(shell.get(), static_cast(new_size.width()), - static_cast(new_size.height())); + PumpOneFrame(shell.get(), + ViewContent::DummyView(static_cast(new_size.width()), + static_cast(new_size.height()))); end_frame_latch.Wait(); ASSERT_TRUE(raster_thread_merger_ref->IsMerged()); diff --git a/testing/dart/platform_view_test.dart b/testing/dart/platform_view_test.dart index 146c865899952..cd581a22d581d 100644 --- a/testing/dart/platform_view_test.dart +++ b/testing/dart/platform_view_test.dart @@ -10,10 +10,12 @@ void main() { test('PlatformView layers do not emit errors from tester', () async { final SceneBuilder builder = SceneBuilder(); builder.addPlatformView(1); - final Scene scene = builder.build(); - - PlatformDispatcher.instance.implicitView!.render(scene); - scene.dispose(); + PlatformDispatcher.instance.onBeginFrame = (Duration duration) { + final Scene scene = builder.build(); + PlatformDispatcher.instance.implicitView!.render(scene); + scene.dispose(); + }; + PlatformDispatcher.instance.scheduleFrame(); // Test harness asserts that this does not emit an error from the shell logs. }); }