| // 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. |
| |
| #define FML_USED_ON_EMBEDDER |
| |
| #include <functional> |
| #include <future> |
| #include <memory> |
| |
| #include "flutter/flow/layers/layer_tree.h" |
| #include "flutter/flow/layers/transform_layer.h" |
| #include "flutter/fml/command_line.h" |
| #include "flutter/fml/make_copyable.h" |
| #include "flutter/fml/message_loop.h" |
| #include "flutter/fml/synchronization/count_down_latch.h" |
| #include "flutter/fml/synchronization/waitable_event.h" |
| #include "flutter/runtime/dart_vm.h" |
| #include "flutter/shell/common/platform_view.h" |
| #include "flutter/shell/common/rasterizer.h" |
| #include "flutter/shell/common/shell_test.h" |
| #include "flutter/shell/common/switches.h" |
| #include "flutter/shell/common/thread_host.h" |
| #include "flutter/testing/testing.h" |
| |
| namespace flutter { |
| namespace testing { |
| |
| static bool ValidateShell(Shell* shell) { |
| if (!shell) { |
| return false; |
| } |
| |
| if (!shell->IsSetup()) { |
| return false; |
| } |
| |
| ShellTest::PlatformViewNotifyCreated(shell); |
| |
| { |
| fml::AutoResetWaitableEvent latch; |
| fml::TaskRunner::RunNowOrPostTask( |
| shell->GetTaskRunners().GetPlatformTaskRunner(), [shell, &latch]() { |
| shell->GetPlatformView()->NotifyDestroyed(); |
| latch.Signal(); |
| }); |
| latch.Wait(); |
| } |
| |
| return true; |
| } |
| |
| TEST_F(ShellTest, InitializeWithInvalidThreads) { |
| ASSERT_FALSE(DartVMRef::IsInstanceRunning()); |
| Settings settings = CreateSettingsForFixture(); |
| TaskRunners task_runners("test", nullptr, nullptr, nullptr, nullptr); |
| auto shell = CreateShell(std::move(settings), std::move(task_runners)); |
| ASSERT_FALSE(shell); |
| ASSERT_FALSE(DartVMRef::IsInstanceRunning()); |
| } |
| |
| TEST_F(ShellTest, InitializeWithDifferentThreads) { |
| ASSERT_FALSE(DartVMRef::IsInstanceRunning()); |
| Settings settings = CreateSettingsForFixture(); |
| ThreadHost thread_host("io.flutter.test." + GetCurrentTestName() + ".", |
| ThreadHost::Type::Platform | ThreadHost::Type::GPU | |
| ThreadHost::Type::IO | ThreadHost::Type::UI); |
| TaskRunners task_runners("test", thread_host.platform_thread->GetTaskRunner(), |
| thread_host.gpu_thread->GetTaskRunner(), |
| thread_host.ui_thread->GetTaskRunner(), |
| thread_host.io_thread->GetTaskRunner()); |
| auto shell = CreateShell(std::move(settings), std::move(task_runners)); |
| ASSERT_TRUE(ValidateShell(shell.get())); |
| ASSERT_TRUE(DartVMRef::IsInstanceRunning()); |
| shell.reset(); |
| ASSERT_FALSE(DartVMRef::IsInstanceRunning()); |
| } |
| |
| TEST_F(ShellTest, InitializeWithSingleThread) { |
| ASSERT_FALSE(DartVMRef::IsInstanceRunning()); |
| Settings settings = CreateSettingsForFixture(); |
| ThreadHost thread_host("io.flutter.test." + GetCurrentTestName() + ".", |
| ThreadHost::Type::Platform); |
| auto task_runner = thread_host.platform_thread->GetTaskRunner(); |
| TaskRunners task_runners("test", task_runner, task_runner, task_runner, |
| task_runner); |
| auto shell = CreateShell(std::move(settings), std::move(task_runners)); |
| ASSERT_TRUE(DartVMRef::IsInstanceRunning()); |
| ASSERT_TRUE(ValidateShell(shell.get())); |
| shell.reset(); |
| ASSERT_FALSE(DartVMRef::IsInstanceRunning()); |
| } |
| |
| TEST_F(ShellTest, InitializeWithSingleThreadWhichIsTheCallingThread) { |
| ASSERT_FALSE(DartVMRef::IsInstanceRunning()); |
| Settings settings = CreateSettingsForFixture(); |
| fml::MessageLoop::EnsureInitializedForCurrentThread(); |
| auto task_runner = fml::MessageLoop::GetCurrent().GetTaskRunner(); |
| TaskRunners task_runners("test", task_runner, task_runner, task_runner, |
| task_runner); |
| auto shell = CreateShell(std::move(settings), std::move(task_runners)); |
| ASSERT_TRUE(ValidateShell(shell.get())); |
| ASSERT_TRUE(DartVMRef::IsInstanceRunning()); |
| shell.reset(); |
| ASSERT_FALSE(DartVMRef::IsInstanceRunning()); |
| } |
| |
| TEST_F(ShellTest, |
| InitializeWithMultipleThreadButCallingThreadAsPlatformThread) { |
| ASSERT_FALSE(DartVMRef::IsInstanceRunning()); |
| Settings settings = CreateSettingsForFixture(); |
| ThreadHost thread_host( |
| "io.flutter.test." + GetCurrentTestName() + ".", |
| ThreadHost::Type::GPU | ThreadHost::Type::IO | ThreadHost::Type::UI); |
| fml::MessageLoop::EnsureInitializedForCurrentThread(); |
| TaskRunners task_runners("test", |
| fml::MessageLoop::GetCurrent().GetTaskRunner(), |
| thread_host.gpu_thread->GetTaskRunner(), |
| thread_host.ui_thread->GetTaskRunner(), |
| thread_host.io_thread->GetTaskRunner()); |
| auto shell = Shell::Create( |
| std::move(task_runners), settings, |
| [](Shell& shell) { |
| return std::make_unique<ShellTestPlatformView>(shell, |
| shell.GetTaskRunners()); |
| }, |
| [](Shell& shell) { |
| return std::make_unique<Rasterizer>(shell, shell.GetTaskRunners()); |
| }); |
| ASSERT_TRUE(ValidateShell(shell.get())); |
| ASSERT_TRUE(DartVMRef::IsInstanceRunning()); |
| shell.reset(); |
| ASSERT_FALSE(DartVMRef::IsInstanceRunning()); |
| } |
| |
| TEST_F(ShellTest, InitializeWithGPUAndPlatformThreadsTheSame) { |
| ASSERT_FALSE(DartVMRef::IsInstanceRunning()); |
| Settings settings = CreateSettingsForFixture(); |
| ThreadHost thread_host( |
| "io.flutter.test." + GetCurrentTestName() + ".", |
| ThreadHost::Type::Platform | ThreadHost::Type::IO | ThreadHost::Type::UI); |
| TaskRunners task_runners( |
| "test", |
| thread_host.platform_thread->GetTaskRunner(), // platform |
| thread_host.platform_thread->GetTaskRunner(), // gpu |
| thread_host.ui_thread->GetTaskRunner(), // ui |
| thread_host.io_thread->GetTaskRunner() // io |
| ); |
| auto shell = CreateShell(std::move(settings), std::move(task_runners)); |
| ASSERT_TRUE(DartVMRef::IsInstanceRunning()); |
| ASSERT_TRUE(ValidateShell(shell.get())); |
| shell.reset(); |
| ASSERT_FALSE(DartVMRef::IsInstanceRunning()); |
| } |
| |
| TEST_F(ShellTest, FixturesAreFunctional) { |
| ASSERT_FALSE(DartVMRef::IsInstanceRunning()); |
| auto settings = CreateSettingsForFixture(); |
| auto shell = CreateShell(std::move(settings)); |
| ASSERT_TRUE(ValidateShell(shell.get())); |
| |
| auto configuration = RunConfiguration::InferFromSettings(settings); |
| ASSERT_TRUE(configuration.IsValid()); |
| configuration.SetEntrypoint("fixturesAreFunctionalMain"); |
| |
| fml::AutoResetWaitableEvent main_latch; |
| AddNativeCallback( |
| "SayHiFromFixturesAreFunctionalMain", |
| CREATE_NATIVE_ENTRY([&main_latch](auto args) { main_latch.Signal(); })); |
| |
| RunEngine(shell.get(), std::move(configuration)); |
| main_latch.Wait(); |
| ASSERT_TRUE(DartVMRef::IsInstanceRunning()); |
| shell.reset(); |
| ASSERT_FALSE(DartVMRef::IsInstanceRunning()); |
| } |
| |
| TEST_F(ShellTest, SecondaryIsolateBindingsAreSetupViaShellSettings) { |
| ASSERT_FALSE(DartVMRef::IsInstanceRunning()); |
| auto settings = CreateSettingsForFixture(); |
| auto shell = CreateShell(std::move(settings)); |
| ASSERT_TRUE(ValidateShell(shell.get())); |
| |
| auto configuration = RunConfiguration::InferFromSettings(settings); |
| ASSERT_TRUE(configuration.IsValid()); |
| configuration.SetEntrypoint("testCanLaunchSecondaryIsolate"); |
| |
| fml::CountDownLatch latch(2); |
| AddNativeCallback("NotifyNative", CREATE_NATIVE_ENTRY([&latch](auto args) { |
| latch.CountDown(); |
| })); |
| |
| RunEngine(shell.get(), std::move(configuration)); |
| |
| latch.Wait(); |
| |
| ASSERT_TRUE(DartVMRef::IsInstanceRunning()); |
| shell.reset(); |
| ASSERT_FALSE(DartVMRef::IsInstanceRunning()); |
| } |
| |
| TEST(ShellTestNoFixture, EnableMirrorsIsWhitelisted) { |
| if (DartVM::IsRunningPrecompiledCode()) { |
| // This covers profile and release modes which use AOT (where this flag does |
| // not make sense anyway). |
| GTEST_SKIP(); |
| return; |
| } |
| |
| const std::vector<fml::CommandLine::Option> options = { |
| fml::CommandLine::Option("dart-flags", "--enable_mirrors")}; |
| fml::CommandLine command_line("", options, std::vector<std::string>()); |
| flutter::Settings settings = flutter::SettingsFromCommandLine(command_line); |
| EXPECT_EQ(settings.dart_flags.size(), 1u); |
| } |
| |
| TEST_F(ShellTest, BlacklistedDartVMFlag) { |
| // Run this test in a thread-safe manner, otherwise gtest will complain. |
| ::testing::FLAGS_gtest_death_test_style = "threadsafe"; |
| |
| const std::vector<fml::CommandLine::Option> options = { |
| fml::CommandLine::Option("dart-flags", "--verify_after_gc")}; |
| fml::CommandLine command_line("", options, std::vector<std::string>()); |
| |
| #if FLUTTER_RUNTIME_MODE != FLUTTER_RUNTIME_MODE_RELEASE |
| // Upon encountering a non-whitelisted Dart flag the process terminates. |
| const char* expected = |
| "Encountered blacklisted Dart VM flag: --verify_after_gc"; |
| ASSERT_DEATH(flutter::SettingsFromCommandLine(command_line), expected); |
| #else |
| flutter::Settings settings = flutter::SettingsFromCommandLine(command_line); |
| EXPECT_EQ(settings.dart_flags.size(), 0u); |
| #endif |
| } |
| |
| TEST_F(ShellTest, WhitelistedDartVMFlag) { |
| const std::vector<fml::CommandLine::Option> options = { |
| fml::CommandLine::Option("dart-flags", |
| "--max_profile_depth 1,--random_seed 42")}; |
| fml::CommandLine command_line("", options, std::vector<std::string>()); |
| flutter::Settings settings = flutter::SettingsFromCommandLine(command_line); |
| |
| #if FLUTTER_RUNTIME_MODE != FLUTTER_RUNTIME_MODE_RELEASE |
| EXPECT_EQ(settings.dart_flags.size(), 2u); |
| EXPECT_EQ(settings.dart_flags[0], "--max_profile_depth 1"); |
| EXPECT_EQ(settings.dart_flags[1], "--random_seed 42"); |
| #else |
| EXPECT_EQ(settings.dart_flags.size(), 0u); |
| #endif |
| } |
| |
| TEST_F(ShellTest, NoNeedToReportTimingsByDefault) { |
| auto settings = CreateSettingsForFixture(); |
| std::unique_ptr<Shell> shell = CreateShell(std::move(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()); |
| ASSERT_FALSE(GetNeedsReportTimings(shell.get())); |
| |
| // This assertion may or may not be the direct result of needs_report_timings_ |
| // being false. The count could be 0 simply because we just cleared unreported |
| // timings by reporting them. Hence this can't replace the |
| // ASSERT_FALSE(GetNeedsReportTimings(shell.get())) check. We added this |
| // assertion for an additional confidence that we're not pushing back to |
| // unreported timings unnecessarily. |
| // |
| // Conversely, do not assert UnreportedTimingsCount(shell.get()) to be |
| // positive in any tests. Otherwise those tests will be flaky as the clearing |
| // of unreported timings is unpredictive. |
| ASSERT_EQ(UnreportedTimingsCount(shell.get()), 0); |
| } |
| |
| TEST_F(ShellTest, NeedsReportTimingsIsSetWithCallback) { |
| auto settings = CreateSettingsForFixture(); |
| std::unique_ptr<Shell> shell = CreateShell(std::move(settings)); |
| |
| // Create the surface needed by rasterizer |
| PlatformViewNotifyCreated(shell.get()); |
| |
| auto configuration = RunConfiguration::InferFromSettings(settings); |
| configuration.SetEntrypoint("dummyReportTimingsMain"); |
| |
| RunEngine(shell.get(), std::move(configuration)); |
| PumpOneFrame(shell.get()); |
| ASSERT_TRUE(GetNeedsReportTimings(shell.get())); |
| } |
| |
| static void CheckFrameTimings(const std::vector<FrameTiming>& timings, |
| fml::TimePoint start, |
| fml::TimePoint finish) { |
| fml::TimePoint last_frame_start; |
| for (size_t i = 0; i < timings.size(); i += 1) { |
| // Ensure that timings are sorted. |
| ASSERT_TRUE(timings[i].Get(FrameTiming::kPhases[0]) >= last_frame_start); |
| last_frame_start = timings[i].Get(FrameTiming::kPhases[0]); |
| |
| fml::TimePoint last_phase_time; |
| for (auto phase : FrameTiming::kPhases) { |
| ASSERT_TRUE(timings[i].Get(phase) >= start); |
| ASSERT_TRUE(timings[i].Get(phase) <= finish); |
| |
| // phases should have weakly increasing time points |
| ASSERT_TRUE(last_phase_time <= timings[i].Get(phase)); |
| last_phase_time = timings[i].Get(phase); |
| } |
| } |
| } |
| |
| TEST_F(ShellTest, ReportTimingsIsCalled) { |
| fml::TimePoint start = fml::TimePoint::Now(); |
| auto settings = CreateSettingsForFixture(); |
| std::unique_ptr<Shell> shell = CreateShell(std::move(settings)); |
| |
| // Create the surface needed by rasterizer |
| PlatformViewNotifyCreated(shell.get()); |
| |
| auto configuration = RunConfiguration::InferFromSettings(settings); |
| ASSERT_TRUE(configuration.IsValid()); |
| configuration.SetEntrypoint("reportTimingsMain"); |
| fml::AutoResetWaitableEvent reportLatch; |
| std::vector<int64_t> timestamps; |
| auto nativeTimingCallback = [&reportLatch, |
| ×tamps](Dart_NativeArguments args) { |
| Dart_Handle exception = nullptr; |
| timestamps = tonic::DartConverter<std::vector<int64_t>>::FromArguments( |
| args, 0, exception); |
| reportLatch.Signal(); |
| }; |
| AddNativeCallback("NativeReportTimingsCallback", |
| CREATE_NATIVE_ENTRY(nativeTimingCallback)); |
| RunEngine(shell.get(), std::move(configuration)); |
| |
| // Pump many frames so we can trigger the report quickly instead of waiting |
| // for the 1 second threshold. |
| for (int i = 0; i < 200; i += 1) { |
| PumpOneFrame(shell.get()); |
| } |
| |
| reportLatch.Wait(); |
| shell.reset(); |
| |
| fml::TimePoint finish = fml::TimePoint::Now(); |
| ASSERT_TRUE(timestamps.size() > 0); |
| ASSERT_TRUE(timestamps.size() % FrameTiming::kCount == 0); |
| std::vector<FrameTiming> timings(timestamps.size() / FrameTiming::kCount); |
| |
| for (size_t i = 0; i * FrameTiming::kCount < timestamps.size(); i += 1) { |
| for (auto phase : FrameTiming::kPhases) { |
| timings[i].Set( |
| phase, |
| fml::TimePoint::FromEpochDelta(fml::TimeDelta::FromMicroseconds( |
| timestamps[i * FrameTiming::kCount + phase]))); |
| } |
| } |
| CheckFrameTimings(timings, start, finish); |
| } |
| |
| TEST_F(ShellTest, FrameRasterizedCallbackIsCalled) { |
| fml::TimePoint start = fml::TimePoint::Now(); |
| |
| auto settings = CreateSettingsForFixture(); |
| fml::AutoResetWaitableEvent timingLatch; |
| FrameTiming timing; |
| |
| for (auto phase : FrameTiming::kPhases) { |
| timing.Set(phase, fml::TimePoint()); |
| // Check that the time points are initially smaller than start, so |
| // CheckFrameTimings will fail if they're not properly set later. |
| ASSERT_TRUE(timing.Get(phase) < start); |
| } |
| |
| settings.frame_rasterized_callback = [&timing, |
| &timingLatch](const FrameTiming& t) { |
| timing = t; |
| timingLatch.Signal(); |
| }; |
| |
| std::unique_ptr<Shell> shell = CreateShell(std::move(settings)); |
| |
| // Create the surface needed by rasterizer |
| PlatformViewNotifyCreated(shell.get()); |
| |
| auto configuration = RunConfiguration::InferFromSettings(settings); |
| configuration.SetEntrypoint("onBeginFrameMain"); |
| |
| int64_t begin_frame; |
| auto nativeOnBeginFrame = [&begin_frame](Dart_NativeArguments args) { |
| Dart_Handle exception = nullptr; |
| begin_frame = |
| tonic::DartConverter<int64_t>::FromArguments(args, 0, exception); |
| }; |
| AddNativeCallback("NativeOnBeginFrame", |
| CREATE_NATIVE_ENTRY(nativeOnBeginFrame)); |
| |
| RunEngine(shell.get(), std::move(configuration)); |
| |
| PumpOneFrame(shell.get()); |
| |
| // Check that timing is properly set. This implies that |
| // settings.frame_rasterized_callback is called. |
| timingLatch.Wait(); |
| fml::TimePoint finish = fml::TimePoint::Now(); |
| std::vector<FrameTiming> timings = {timing}; |
| CheckFrameTimings(timings, start, finish); |
| |
| // Check that onBeginFrame has the same timestamp as FrameTiming's build start |
| int64_t build_start = |
| timing.Get(FrameTiming::kBuildStart).ToEpochDelta().ToMicroseconds(); |
| ASSERT_EQ(build_start, begin_frame); |
| } |
| |
| TEST(SettingsTest, FrameTimingSetsAndGetsProperly) { |
| // Ensure that all phases are in kPhases. |
| ASSERT_EQ(sizeof(FrameTiming::kPhases), |
| FrameTiming::kCount * sizeof(FrameTiming::Phase)); |
| |
| int lastPhaseIndex = -1; |
| FrameTiming timing; |
| for (auto phase : FrameTiming::kPhases) { |
| ASSERT_TRUE(phase > lastPhaseIndex); // Ensure that kPhases are in order. |
| lastPhaseIndex = phase; |
| auto fake_time = |
| fml::TimePoint::FromEpochDelta(fml::TimeDelta::FromMicroseconds(phase)); |
| timing.Set(phase, fake_time); |
| ASSERT_TRUE(timing.Get(phase) == fake_time); |
| } |
| } |
| |
| #if FLUTTER_RUNTIME_MODE == FLUTTER_RUNTIME_MODE_RELEASE |
| TEST_F(ShellTest, ReportTimingsIsCalledSoonerInNonReleaseMode) { |
| #else |
| TEST_F(ShellTest, ReportTimingsIsCalledLaterInReleaseMode) { |
| #endif |
| fml::TimePoint start = fml::TimePoint::Now(); |
| auto settings = CreateSettingsForFixture(); |
| std::unique_ptr<Shell> shell = CreateShell(std::move(settings)); |
| |
| // Create the surface needed by rasterizer |
| PlatformViewNotifyCreated(shell.get()); |
| |
| auto configuration = RunConfiguration::InferFromSettings(settings); |
| ASSERT_TRUE(configuration.IsValid()); |
| configuration.SetEntrypoint("reportTimingsMain"); |
| fml::AutoResetWaitableEvent reportLatch; |
| std::vector<int64_t> timestamps; |
| auto nativeTimingCallback = [&reportLatch, |
| ×tamps](Dart_NativeArguments args) { |
| Dart_Handle exception = nullptr; |
| timestamps = tonic::DartConverter<std::vector<int64_t>>::FromArguments( |
| args, 0, exception); |
| reportLatch.Signal(); |
| }; |
| AddNativeCallback("NativeReportTimingsCallback", |
| CREATE_NATIVE_ENTRY(nativeTimingCallback)); |
| RunEngine(shell.get(), std::move(configuration)); |
| |
| PumpOneFrame(shell.get()); |
| |
| reportLatch.Wait(); |
| shell.reset(); |
| |
| fml::TimePoint finish = fml::TimePoint::Now(); |
| fml::TimeDelta ellapsed = finish - start; |
| |
| #if FLUTTER_RUNTIME_MODE == FLUTTER_RUNTIME_MODE_RELEASE |
| // Our batch time is 1000ms. Hopefully the 800ms limit is relaxed enough to |
| // make it not too flaky. |
| ASSERT_TRUE(ellapsed >= fml::TimeDelta::FromMilliseconds(800)); |
| #else |
| // Our batch time is 100ms. Hopefully the 500ms limit is relaxed enough to |
| // make it not too flaky. |
| ASSERT_TRUE(ellapsed <= fml::TimeDelta::FromMilliseconds(500)); |
| #endif |
| } |
| |
| } // namespace testing |
| } // namespace flutter |