diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 42517d5b01b75da9c6b2175ee96f06348772e159..1f42e66c3297d3f9dce3039c7b509e851e1d450d 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -186,6 +186,7 @@ FILE: ../../../flutter/fml/platform/win/native_library_win.cc FILE: ../../../flutter/fml/platform/win/paths_win.cc FILE: ../../../flutter/fml/platform/win/wstring_conversion.h FILE: ../../../flutter/fml/size.h +FILE: ../../../flutter/fml/status.h FILE: ../../../flutter/fml/synchronization/atomic_object.h FILE: ../../../flutter/fml/synchronization/count_down_latch.cc FILE: ../../../flutter/fml/synchronization/count_down_latch.h diff --git a/fml/status.h b/fml/status.h new file mode 100644 index 0000000000000000000000000000000000000000..8eed10823f144b479ea2ffe0746560a19eae6727 --- /dev/null +++ b/fml/status.h @@ -0,0 +1,81 @@ +// 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. + +#ifndef FLUTTER_FML_STATUS_H_ +#define FLUTTER_FML_STATUS_H_ + +#include + +namespace fml { + +enum class StatusCode { + kOk, + kCancelled, + kUnknown, + kInvalidArgument, + kDeadlineExceeded, + kNotFound, + kAlreadyExists, + kPermissionDenied, + kResourceExhausted, + kFailedPrecondition, + kAborted, + kOutOfRange, + kUnimplemented, + kInternal, + kUnavailable, + kDataLoss, + kUnauthenticated +}; + +/// Class that represents the resolution of the execution of a procedure. This +/// is used similarly to how exceptions might be used, typically as the return +/// value to a synchronous procedure or an argument to an asynchronous callback. +class Status final { + public: + /// Creates an 'ok' status. + Status(); + + Status(fml::StatusCode code, std::string_view message); + + fml::StatusCode code() const; + + /// A noop that helps with static analysis tools if you decide to ignore an + /// error. + void IgnoreError() const; + + /// @return 'true' when the code is kOk. + bool ok() const; + + std::string_view message() const; + + private: + fml::StatusCode code_; + std::string_view message_; +}; + +inline Status::Status() : code_(fml::StatusCode::kOk), message_() {} + +inline Status::Status(fml::StatusCode code, std::string_view message) + : code_(code), message_(message) {} + +inline fml::StatusCode Status::code() const { + return code_; +} + +inline void Status::IgnoreError() const { + // noop +} + +inline bool Status::ok() const { + return code_ == fml::StatusCode::kOk; +} + +inline std::string_view Status::message() const { + return message_; +} + +} // namespace fml + +#endif // FLUTTER_FML_SIZE_H_ diff --git a/shell/common/shell.cc b/shell/common/shell.cc index 58e7982e4dd467d370d74ca3729f9a0676e43826..ba217e2af74cf7fa011323bc39cc9b4441a02052 100644 --- a/shell/common/shell.cc +++ b/shell/common/shell.cc @@ -454,18 +454,22 @@ void Shell::OnPlatformViewCreated(std::unique_ptr surface) { // This is a synchronous operation because certain platforms depend on // setup/suspension of all activities that may be interacting with the GPU in // a synchronous fashion. - fml::AutoResetWaitableEvent latch; - auto gpu_task = fml::MakeCopyable([rasterizer = rasterizer_->GetWeakPtr(), // - surface = std::move(surface), // - &latch]() mutable { - if (rasterizer) { - rasterizer->Setup(std::move(surface)); - } - // Step 3: All done. Signal the latch that the platform thread is waiting - // on. - latch.Signal(); - }); + auto gpu_task = + fml::MakeCopyable([& waiting_for_first_frame = waiting_for_first_frame_, + rasterizer = rasterizer_->GetWeakPtr(), // + surface = std::move(surface), // + &latch]() mutable { + if (rasterizer) { + rasterizer->Setup(std::move(surface)); + } + + waiting_for_first_frame.store(true); + + // Step 3: All done. Signal the latch that the platform thread is + // waiting on. + latch.Signal(); + }); // The normal flow executed by this method is that the platform thread is // starting the sequence and waiting on the latch. Later the UI thread posts @@ -787,10 +791,17 @@ void Shell::OnAnimatorDraw(fml::RefPtr> pipeline) { FML_DCHECK(is_setup_); task_runners_.GetGPUTaskRunner()->PostTask( - [rasterizer = rasterizer_->GetWeakPtr(), + [& waiting_for_first_frame = waiting_for_first_frame_, + &waiting_for_first_frame_condition = waiting_for_first_frame_condition_, + rasterizer = rasterizer_->GetWeakPtr(), pipeline = std::move(pipeline)]() { if (rasterizer) { rasterizer->Draw(pipeline); + + if (waiting_for_first_frame.load()) { + waiting_for_first_frame.store(false); + waiting_for_first_frame_condition.notify_all(); + } } }); } @@ -1226,4 +1237,26 @@ Rasterizer::Screenshot Shell::Screenshot( return screenshot; } +fml::Status Shell::WaitForFirstFrame(fml::TimeDelta timeout) { + FML_DCHECK(is_setup_); + if (task_runners_.GetUITaskRunner()->RunsTasksOnCurrentThread() || + task_runners_.GetGPUTaskRunner()->RunsTasksOnCurrentThread()) { + return fml::Status(fml::StatusCode::kFailedPrecondition, + "WaitForFirstFrame called from thread that can't wait " + "because it is responsible for generating the frame."); + } + + std::unique_lock lock(waiting_for_first_frame_mutex_); + bool success = waiting_for_first_frame_condition_.wait_for( + lock, std::chrono::milliseconds(timeout.ToMilliseconds()), + [& waiting_for_first_frame = waiting_for_first_frame_] { + return !waiting_for_first_frame.load(); + }); + if (success) { + return fml::Status(); + } else { + return fml::Status(fml::StatusCode::kDeadlineExceeded, "timeout"); + } +} + } // namespace flutter diff --git a/shell/common/shell.h b/shell/common/shell.h index 9e42e93f13b86d0e18f94a45cec8583c55e3963b..2ac4a1b17704808671cd322dc201f30052db51f2 100644 --- a/shell/common/shell.h +++ b/shell/common/shell.h @@ -17,6 +17,7 @@ #include "flutter/fml/memory/ref_ptr.h" #include "flutter/fml/memory/thread_checker.h" #include "flutter/fml/memory/weak_ptr.h" +#include "flutter/fml/status.h" #include "flutter/fml/synchronization/thread_annotations.h" #include "flutter/fml/synchronization/waitable_event.h" #include "flutter/fml/thread.h" @@ -243,6 +244,15 @@ class Shell final : public PlatformView::Delegate, Rasterizer::Screenshot Screenshot(Rasterizer::ScreenshotType type, bool base64_encode); + //---------------------------------------------------------------------------- + /// @brief Pauses the calling thread until the first frame is presented. + /// + /// @return 'kOk' when the first frame has been presented before the timeout + /// successfully, 'kFailedPrecondition' if called from the GPU or UI + /// thread, 'kDeadlineExceeded' if there is a timeout. + /// + fml::Status WaitForFirstFrame(fml::TimeDelta timeout); + private: using ServiceProtocolHandler = std::function waiting_for_first_frame_ = true; + std::mutex waiting_for_first_frame_mutex_; + std::condition_variable waiting_for_first_frame_condition_; // Written in the UI thread and read from the GPU thread. Hence make it // atomic. diff --git a/shell/common/shell_unittests.cc b/shell/common/shell_unittests.cc index 0084850533c00a48a7249df894c48ab8a973cd7e..45d2a658e270764eeadbec9987097172020f2284 100644 --- a/shell/common/shell_unittests.cc +++ b/shell/common/shell_unittests.cc @@ -518,5 +518,87 @@ TEST_F(ShellTest, ReportTimingsIsCalledImmediatelyAfterTheFirstFrame) { ASSERT_EQ(timestamps.size(), FrameTiming::kCount); } +TEST_F(ShellTest, WaitForFirstFrame) { + auto settings = CreateSettingsForFixture(); + std::unique_ptr shell = CreateShell(settings); + + // Create the surface needed by rasterizer + PlatformViewNotifyCreated(shell.get()); + + auto configuration = RunConfiguration::InferFromSettings(settings); + configuration.SetEntrypoint("emptyMain"); + + RunEngine(shell.get(), std::move(configuration)); + PumpOneFrame(shell.get()); + fml::Status result = + shell->WaitForFirstFrame(fml::TimeDelta::FromMilliseconds(1000)); + ASSERT_TRUE(result.ok()); +} + +TEST_F(ShellTest, WaitForFirstFrameTimeout) { + auto settings = CreateSettingsForFixture(); + std::unique_ptr shell = CreateShell(settings); + + // Create the surface needed by rasterizer + PlatformViewNotifyCreated(shell.get()); + + auto configuration = RunConfiguration::InferFromSettings(settings); + configuration.SetEntrypoint("emptyMain"); + + RunEngine(shell.get(), std::move(configuration)); + fml::Status result = + shell->WaitForFirstFrame(fml::TimeDelta::FromMilliseconds(10)); + ASSERT_EQ(result.code(), fml::StatusCode::kDeadlineExceeded); +} + +TEST_F(ShellTest, WaitForFirstFrameMultiple) { + auto settings = CreateSettingsForFixture(); + std::unique_ptr shell = CreateShell(settings); + + // Create the surface needed by rasterizer + PlatformViewNotifyCreated(shell.get()); + + auto configuration = RunConfiguration::InferFromSettings(settings); + configuration.SetEntrypoint("emptyMain"); + + RunEngine(shell.get(), std::move(configuration)); + PumpOneFrame(shell.get()); + fml::Status result = + shell->WaitForFirstFrame(fml::TimeDelta::FromMilliseconds(1000)); + ASSERT_TRUE(result.ok()); + for (int i = 0; i < 100; ++i) { + result = shell->WaitForFirstFrame(fml::TimeDelta::FromMilliseconds(1)); + ASSERT_TRUE(result.ok()); + } +} + +/// Makes sure that WaitForFirstFrame works if we rendered a frame with the +/// single-thread setup. +TEST_F(ShellTest, WaitForFirstFrameInlined) { + Settings settings = CreateSettingsForFixture(); + auto task_runner = GetThreadTaskRunner(); + TaskRunners task_runners("test", task_runner, task_runner, task_runner, + task_runner); + std::unique_ptr shell = + CreateShell(std::move(settings), std::move(task_runners)); + + // Create the surface needed by rasterizer + PlatformViewNotifyCreated(shell.get()); + + auto configuration = RunConfiguration::InferFromSettings(settings); + configuration.SetEntrypoint("emptyMain"); + + RunEngine(shell.get(), std::move(configuration)); + PumpOneFrame(shell.get()); + fml::AutoResetWaitableEvent event; + task_runner->PostTask([&shell, &event] { + fml::Status result = + shell->WaitForFirstFrame(fml::TimeDelta::FromMilliseconds(1000)); + ASSERT_EQ(result.code(), fml::StatusCode::kFailedPrecondition); + event.Signal(); + }); + ASSERT_FALSE(event.WaitWithTimeout(fml::TimeDelta::FromMilliseconds(1000))); +} + } // namespace testing } // namespace flutter diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm index 2d3f4f215dffd463469c0f0a3dd79a8e2417f202..99c1920e4e43f6e3fa659ff738fb54dffba4e7fc 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm @@ -406,8 +406,9 @@ NSNotificationName const FlutterSemanticsUpdateNotification = @"FlutterSemantics // Only recreate surface on subsequent appearances when viewport metrics are known. // First time surface creation is done on viewDidLayoutSubviews. - if (_viewportMetrics.physical_width) + if (_viewportMetrics.physical_width) { [self surfaceUpdated:YES]; + } [[_engine.get() lifecycleChannel] sendMessage:@"AppLifecycleState.inactive"]; [super viewWillAppear:animated]; @@ -698,8 +699,22 @@ static flutter::PointerData::DeviceKind DeviceKindFromTouchType(UITouch* touch) // This must run after updateViewportMetrics so that the surface creation tasks are queued after // the viewport metrics update tasks. - if (firstViewBoundsUpdate) + if (firstViewBoundsUpdate) { [self surfaceUpdated:YES]; + + flutter::Shell& shell = [_engine.get() shell]; + fml::TimeDelta waitTime = +#if FLUTTER_RUNTIME_MODE == FLUTTER_RUNTIME_MODE_DEBUG + fml::TimeDelta::FromMilliseconds(200); +#else + fml::TimeDelta::FromMilliseconds(100); +#endif + if (shell.WaitForFirstFrame(waitTime).code() == fml::StatusCode::kDeadlineExceeded) { + FML_LOG(INFO) << "Timeout waiting for the first frame to render. This may happen in " + << "unoptimized builds. If this is a release build, you should load a less " + << "complex frame to avoid the timeout."; + } + } } - (void)viewSafeAreaInsetsDidChange {