From c5e0858a017a7fdd45df53e77aaa09d7ea9ca52f Mon Sep 17 00:00:00 2001 From: Tong Mu Date: Wed, 21 Feb 2024 16:46:11 -0800 Subject: [PATCH] Add scheduleWarmUpFrame (flutter/engine#50570) This PR adds `PlatformDispatcher.scheduleWarmUpFrame`. This PR is needed for the follow up changes: * The framework will switch to using this function to render warmup frames in https://github.com/flutter/flutter/pull/143290. * Then the engine will finally be able to switch to multiview pipeline with no regression on startup timing in https://github.com/flutter/engine/pull/49950. For why the warm up frame must involve the engine to render, see https://github.com/flutter/flutter/issues/142851. ## Pre-launch Checklist - [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [ ] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [ ] I read and followed the [Flutter Style Guide] and the [C++, Objective-C, Java style guides]. - [ ] I listed at least one issue that this PR fixes in the description above. - [ ] I added new tests to check the change I am making or feature I am adding, or the PR is [test-exempt]. See [testing the engine] for instructions on writing and running engine tests. - [ ] I updated/added relevant documentation (doc comments with `///`). - [ ] I signed the [CLA]. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. [Contributor Guide]: https://github.com/flutter/flutter/wiki/Tree-hygiene#overview [Tree Hygiene]: https://github.com/flutter/flutter/wiki/Tree-hygiene [test-exempt]: https://github.com/flutter/flutter/wiki/Tree-hygiene#tests [Flutter Style Guide]: https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo [C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style [testing the engine]: https://github.com/flutter/flutter/wiki/Testing-the-engine [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/wiki/Tree-hygiene#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/wiki/Chat --- engine/src/flutter/lib/ui/dart_ui.cc | 1 + .../flutter/lib/ui/platform_dispatcher.dart | 36 +++ .../lib/ui/window/platform_configuration.cc | 5 + .../lib/ui/window/platform_configuration.h | 9 + .../lib/web_ui/lib/platform_dispatcher.dart | 2 + .../web_ui/lib/src/engine/initialization.dart | 3 +- .../lib/src/engine/platform_dispatcher.dart | 13 ++ .../platform_dispatcher_test.dart | 17 ++ .../src/flutter/runtime/runtime_controller.cc | 5 + .../src/flutter/runtime/runtime_controller.h | 3 + engine/src/flutter/runtime/runtime_delegate.h | 2 + engine/src/flutter/shell/common/animator.cc | 6 + engine/src/flutter/shell/common/animator.h | 16 ++ engine/src/flutter/shell/common/engine.cc | 4 + engine/src/flutter/shell/common/engine.h | 3 + .../flutter/shell/common/engine_unittests.cc | 217 ++++++++++++++++++ .../shell/common/fixtures/shell_test.dart | 42 +++- .../dart/platform_dispatcher_test.dart | 28 +++ 18 files changed, 405 insertions(+), 7 deletions(-) diff --git a/engine/src/flutter/lib/ui/dart_ui.cc b/engine/src/flutter/lib/ui/dart_ui.cc index a1ba414316..59d141852f 100644 --- a/engine/src/flutter/lib/ui/dart_ui.cc +++ b/engine/src/flutter/lib/ui/dart_ui.cc @@ -98,6 +98,7 @@ typedef CanvasPath Path; V(NativeStringAttribute::initSpellOutStringAttribute) \ V(PlatformConfigurationNativeApi::DefaultRouteName) \ V(PlatformConfigurationNativeApi::ScheduleFrame) \ + V(PlatformConfigurationNativeApi::EndWarmUpFrame) \ V(PlatformConfigurationNativeApi::Render) \ V(PlatformConfigurationNativeApi::UpdateSemantics) \ V(PlatformConfigurationNativeApi::SetNeedsReportTimings) \ diff --git a/engine/src/flutter/lib/ui/platform_dispatcher.dart b/engine/src/flutter/lib/ui/platform_dispatcher.dart index 48e63e1a86..63d7211dcc 100644 --- a/engine/src/flutter/lib/ui/platform_dispatcher.dart +++ b/engine/src/flutter/lib/ui/platform_dispatcher.dart @@ -801,11 +801,47 @@ class PlatformDispatcher { /// /// * [SchedulerBinding], the Flutter framework class which manages the /// scheduling of frames. + /// * [scheduleWarmUpFrame], which should only be used to schedule warm up + /// frames. void scheduleFrame() => _scheduleFrame(); @Native(symbol: 'PlatformConfigurationNativeApi::ScheduleFrame') external static void _scheduleFrame(); + /// Schedule a frame to run as soon as possible, rather than waiting for the + /// engine to request a frame in response to a system "Vsync" signal. + /// + /// The application can call this method as soon as it starts up so that the + /// first frame (which is likely to be quite expensive) can start a few extra + /// milliseconds earlier. Using it in other situations might lead to + /// unintended results, such as screen tearing. Depending on platforms and + /// situations, the warm up frame might or might not be actually rendered onto + /// the screen. + /// + /// For more introduction to the warm up frame, see + /// [SchedulerBinding.scheduleWarmUpFrame]. + /// + /// This method uses the provided callbacks as the begin frame callback and + /// the draw frame callback instead of [onBeginFrame] and [onDrawFrame]. + /// + /// See also: + /// + /// * [SchedulerBinding.scheduleWarmUpFrame], which uses this method, and + /// introduces the warm up frame in more details. + /// * [scheduleFrame], which schedules the frame at the next appropriate + /// opportunity and should be used to render regular frames. + void scheduleWarmUpFrame({required VoidCallback beginFrame, required VoidCallback drawFrame}) { + // We use timers here to ensure that microtasks flush in between. + Timer.run(beginFrame); + Timer.run(() { + drawFrame(); + _endWarmUpFrame(); + }); + } + + @Native(symbol: 'PlatformConfigurationNativeApi::EndWarmUpFrame') + external static void _endWarmUpFrame(); + /// Additional accessibility features that may be enabled by the platform. AccessibilityFeatures get accessibilityFeatures => _configuration.accessibilityFeatures; diff --git a/engine/src/flutter/lib/ui/window/platform_configuration.cc b/engine/src/flutter/lib/ui/window/platform_configuration.cc index 3ecadac12f..5e325fbdf4 100644 --- a/engine/src/flutter/lib/ui/window/platform_configuration.cc +++ b/engine/src/flutter/lib/ui/window/platform_configuration.cc @@ -589,6 +589,11 @@ void PlatformConfigurationNativeApi::ScheduleFrame() { UIDartState::Current()->platform_configuration()->client()->ScheduleFrame(); } +void PlatformConfigurationNativeApi::EndWarmUpFrame() { + UIDartState::ThrowIfUIOperationsProhibited(); + UIDartState::Current()->platform_configuration()->client()->EndWarmUpFrame(); +} + void PlatformConfigurationNativeApi::UpdateSemantics(SemanticsUpdate* update) { UIDartState::ThrowIfUIOperationsProhibited(); UIDartState::Current()->platform_configuration()->client()->UpdateSemantics( diff --git a/engine/src/flutter/lib/ui/window/platform_configuration.h b/engine/src/flutter/lib/ui/window/platform_configuration.h index 5f21051110..8914bbb756 100644 --- a/engine/src/flutter/lib/ui/window/platform_configuration.h +++ b/engine/src/flutter/lib/ui/window/platform_configuration.h @@ -65,6 +65,13 @@ class PlatformConfigurationClient { /// virtual void ScheduleFrame() = 0; + //-------------------------------------------------------------------------- + /// @brief Called when a warm up frame has ended. + /// + /// For more introduction, see `Animator::EndWarmUpFrame`. + /// + virtual void EndWarmUpFrame() = 0; + //-------------------------------------------------------------------------- /// @brief Updates the client's rendering on the GPU with the newly /// provided Scene. @@ -557,6 +564,8 @@ class PlatformConfigurationNativeApi { static void ScheduleFrame(); + static void EndWarmUpFrame(); + static void Render(int64_t view_id, Scene* scene, double width, diff --git a/engine/src/flutter/lib/web_ui/lib/platform_dispatcher.dart b/engine/src/flutter/lib/web_ui/lib/platform_dispatcher.dart index fb351b27c2..9b91935beb 100644 --- a/engine/src/flutter/lib/web_ui/lib/platform_dispatcher.dart +++ b/engine/src/flutter/lib/web_ui/lib/platform_dispatcher.dart @@ -90,6 +90,8 @@ abstract class PlatformDispatcher { void scheduleFrame(); + void scheduleWarmUpFrame({required VoidCallback beginFrame, required VoidCallback drawFrame}); + AccessibilityFeatures get accessibilityFeatures; VoidCallback? get onAccessibilityFeaturesChanged; diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/initialization.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/initialization.dart index 78352cf091..0dab016be4 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/initialization.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/initialization.dart @@ -187,7 +187,8 @@ Future initializeEngineServices({ // TODO(yjbanov): technically Flutter flushes microtasks between // onBeginFrame and onDrawFrame. We don't, which hasn't // been an issue yet, but eventually we'll have to - // implement it properly. + // implement it properly. (Also see the to-do in + // `EnginePlatformDispatcher.scheduleWarmUpFrame`). EnginePlatformDispatcher.instance.invokeOnDrawFrame(); } }); diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/platform_dispatcher.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/platform_dispatcher.dart index 158cefe14a..277fe73018 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/platform_dispatcher.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/platform_dispatcher.dart @@ -782,6 +782,19 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { scheduleFrameCallback!(); } + @override + void scheduleWarmUpFrame({required ui.VoidCallback beginFrame, required ui.VoidCallback drawFrame}) { + Timer.run(beginFrame); + // We use timers here to ensure that microtasks flush in between. + // + // TODO(dkwingsmt): This logic was moved from the framework and is different + // from how Web renders a regular frame, which doesn't flush microtasks + // between the callbacks at all (see `initializeEngineServices`). We might + // want to change this. See the to-do in `initializeEngineServices` and + // https://github.com/flutter/engine/pull/50570#discussion_r1496671676 + Timer.run(drawFrame); + } + /// Updates the application's rendering on the GPU with the newly provided /// [Scene]. This function must be called within the scope of the /// [onBeginFrame] or [onDrawFrame] callbacks being invoked. If this function diff --git a/engine/src/flutter/lib/web_ui/test/engine/platform_dispatcher/platform_dispatcher_test.dart b/engine/src/flutter/lib/web_ui/test/engine/platform_dispatcher/platform_dispatcher_test.dart index 89cf4c8388..09d64757b5 100644 --- a/engine/src/flutter/lib/web_ui/test/engine/platform_dispatcher/platform_dispatcher_test.dart +++ b/engine/src/flutter/lib/web_ui/test/engine/platform_dispatcher/platform_dispatcher_test.dart @@ -417,6 +417,23 @@ void testMain() { dispatcher.dispose(); expect(dispatcher.accessibilityPlaceholder.isConnected, isFalse); }); + + test('scheduleWarmupFrame should call both callbacks', () async { + bool beginFrameCalled = false; + final Completer drawFrameCalled = Completer(); + dispatcher.scheduleWarmUpFrame(beginFrame: () { + expect(drawFrameCalled.isCompleted, false); + expect(beginFrameCalled, false); + beginFrameCalled = true; + }, drawFrame: () { + expect(beginFrameCalled, true); + expect(drawFrameCalled.isCompleted, false); + drawFrameCalled.complete(); + }); + await drawFrameCalled.future; + expect(beginFrameCalled, true); + expect(drawFrameCalled.isCompleted, true); + }); }); } diff --git a/engine/src/flutter/runtime/runtime_controller.cc b/engine/src/flutter/runtime/runtime_controller.cc index 71ac779e88..85a3bd9efa 100644 --- a/engine/src/flutter/runtime/runtime_controller.cc +++ b/engine/src/flutter/runtime/runtime_controller.cc @@ -340,6 +340,11 @@ void RuntimeController::ScheduleFrame() { client_.ScheduleFrame(); } +// |PlatformConfigurationClient| +void RuntimeController::EndWarmUpFrame() { + client_.EndWarmUpFrame(); +} + // |PlatformConfigurationClient| void RuntimeController::Render(Scene* scene, double width, double height) { // TODO(dkwingsmt): Currently only supports a single window. diff --git a/engine/src/flutter/runtime/runtime_controller.h b/engine/src/flutter/runtime/runtime_controller.h index e9e1335cbb..b68750df87 100644 --- a/engine/src/flutter/runtime/runtime_controller.h +++ b/engine/src/flutter/runtime/runtime_controller.h @@ -657,6 +657,9 @@ class RuntimeController : public PlatformConfigurationClient { // |PlatformConfigurationClient| void ScheduleFrame() override; + // |PlatformConfigurationClient| + void EndWarmUpFrame() override; + // |PlatformConfigurationClient| void Render(Scene* scene, double width, double height) override; diff --git a/engine/src/flutter/runtime/runtime_delegate.h b/engine/src/flutter/runtime/runtime_delegate.h index bc3de031f4..6d3707d277 100644 --- a/engine/src/flutter/runtime/runtime_delegate.h +++ b/engine/src/flutter/runtime/runtime_delegate.h @@ -25,6 +25,8 @@ class RuntimeDelegate { virtual void ScheduleFrame(bool regenerate_layer_trees = true) = 0; + virtual void EndWarmUpFrame() = 0; + virtual void Render(std::unique_ptr layer_tree, float device_pixel_ratio) = 0; diff --git a/engine/src/flutter/shell/common/animator.cc b/engine/src/flutter/shell/common/animator.cc index 3dd925cee6..ad3cea4d1a 100644 --- a/engine/src/flutter/shell/common/animator.cc +++ b/engine/src/flutter/shell/common/animator.cc @@ -264,6 +264,12 @@ 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::ScheduleSecondaryVsyncCallback(uintptr_t id, const fml::closure& callback) { waiter_->ScheduleSecondaryCallback(id, callback); diff --git a/engine/src/flutter/shell/common/animator.h b/engine/src/flutter/shell/common/animator.h index be15a76b76..51bea1d273 100644 --- a/engine/src/flutter/shell/common/animator.h +++ b/engine/src/flutter/shell/common/animator.h @@ -53,6 +53,22 @@ class Animator final { void RequestFrame(bool regenerate_layer_trees = true); + //-------------------------------------------------------------------------- + /// @brief Tells the Animator that a warm up frame has ended. + /// + /// 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. + /// + /// 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. + /// + /// For more about warm up frames, see + /// `PlatformDispatcher.scheduleWarmUpFrame`. + /// + void EndWarmUpFrame(); + //-------------------------------------------------------------------------- /// @brief Tells the Animator that this frame needs to render another view. /// diff --git a/engine/src/flutter/shell/common/engine.cc b/engine/src/flutter/shell/common/engine.cc index 3cc83228fe..c7af213137 100644 --- a/engine/src/flutter/shell/common/engine.cc +++ b/engine/src/flutter/shell/common/engine.cc @@ -462,6 +462,10 @@ void Engine::ScheduleFrame(bool regenerate_layer_trees) { animator_->RequestFrame(regenerate_layer_trees); } +void Engine::EndWarmUpFrame() { + animator_->EndWarmUpFrame(); +} + void Engine::Render(std::unique_ptr layer_tree, float device_pixel_ratio) { if (!layer_tree) { diff --git a/engine/src/flutter/shell/common/engine.h b/engine/src/flutter/shell/common/engine.h index 4a4b3318b1..0501017737 100644 --- a/engine/src/flutter/shell/common/engine.h +++ b/engine/src/flutter/shell/common/engine.h @@ -837,6 +837,9 @@ class Engine final : public RuntimeDelegate, PointerDataDispatcher::Delegate { /// tree. void ScheduleFrame() { ScheduleFrame(true); } + // |RuntimeDelegate| + void EndWarmUpFrame() override; + // |RuntimeDelegate| FontCollection& GetFontCollection() override; diff --git a/engine/src/flutter/shell/common/engine_unittests.cc b/engine/src/flutter/shell/common/engine_unittests.cc index 8a6cc78345..182d8423ad 100644 --- a/engine/src/flutter/shell/common/engine_unittests.cc +++ b/engine/src/flutter/shell/common/engine_unittests.cc @@ -6,7 +6,10 @@ #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" @@ -19,6 +22,19 @@ 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 MockDelegate : public Engine::Delegate { public: MOCK_METHOD(void, @@ -63,6 +79,7 @@ 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, Render, (std::unique_ptr, float), @@ -117,6 +134,51 @@ 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)); +}; + std::unique_ptr MakePlatformMessage( const std::string& channel, const std::map& values, @@ -185,6 +247,96 @@ 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) { @@ -418,4 +570,69 @@ 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(); +} + } // namespace flutter diff --git a/engine/src/flutter/shell/common/fixtures/shell_test.dart b/engine/src/flutter/shell/common/fixtures/shell_test.dart index 19e91c6386..1b52fabe1e 100644 --- a/engine/src/flutter/shell/common/fixtures/shell_test.dart +++ b/engine/src/flutter/shell/common/fixtures/shell_test.dart @@ -2,11 +2,18 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:convert' show utf8, json; +import 'dart:async' show scheduleMicrotask; +import 'dart:convert' show json, utf8; import 'dart:isolate'; import 'dart:typed_data'; import 'dart:ui'; +void expect(Object? a, Object? b) { + if (a != b) { + throw AssertionError('Expected $a to == $b'); + } +} + void main() {} @pragma('vm:entry-point') @@ -349,11 +356,6 @@ Future toImageSync() async { onBeforeToImageSync(); final Image image = picture.toImageSync(20, 25); - void expect(Object? a, Object? b) { - if (a != b) { - throw 'Expected $a to == $b'; - } - } expect(image.width, 20); expect(image.height, 25); @@ -529,3 +531,31 @@ void testReportViewWidths() { nativeReportViewWidthsCallback(getCurrentViewWidths()); }; } + +@pragma('vm:entry-point') +void renderWarmUpImplicitView() { + bool beginFrameCalled = false; + + PlatformDispatcher.instance.scheduleWarmUpFrame( + beginFrame: () { + expect(beginFrameCalled, false); + beginFrameCalled = true; + }, + drawFrame: () { + expect(beginFrameCalled, true); + + 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); + + scene.dispose(); + picture.dispose(); + }, + ); +} diff --git a/engine/src/flutter/testing/dart/platform_dispatcher_test.dart b/engine/src/flutter/testing/dart/platform_dispatcher_test.dart index 0d3d8802cd..e7ff1f1aa5 100644 --- a/engine/src/flutter/testing/dart/platform_dispatcher_test.dart +++ b/engine/src/flutter/testing/dart/platform_dispatcher_test.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:async'; import 'dart:ui'; import 'package:litetest/litetest.dart'; @@ -46,4 +47,31 @@ void main() { expect(constraints.isSatisfiedBy(const Size(400, 500)), false); expect(constraints / 2, const ViewConstraints(minWidth: 50, maxWidth: 100, minHeight: 150, maxHeight: 200)); }); + + test('scheduleWarmupFrame should call both callbacks and flush microtasks', () async { + bool microtaskFlushed = false; + bool beginFrameCalled = false; + final Completer drawFrameCalled = Completer(); + PlatformDispatcher.instance.scheduleWarmUpFrame(beginFrame: () { + expect(microtaskFlushed, false); + expect(drawFrameCalled.isCompleted, false); + expect(beginFrameCalled, false); + beginFrameCalled = true; + scheduleMicrotask(() { + expect(microtaskFlushed, false); + expect(drawFrameCalled.isCompleted, false); + microtaskFlushed = true; + }); + expect(microtaskFlushed, false); + }, drawFrame: () { + expect(beginFrameCalled, true); + expect(microtaskFlushed, true); + expect(drawFrameCalled.isCompleted, false); + drawFrameCalled.complete(); + }); + await drawFrameCalled.future; + expect(beginFrameCalled, true); + expect(drawFrameCalled.isCompleted, true); + expect(microtaskFlushed, true); + }); }