| // 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/common/graphics/persistent_cache.h" |
| |
| #include <memory> |
| |
| #include "flutter/assets/directory_asset_bundle.h" |
| #include "flutter/flow/layers/container_layer.h" |
| #include "flutter/flow/layers/layer.h" |
| #include "flutter/flow/layers/physical_shape_layer.h" |
| #include "flutter/fml/command_line.h" |
| #include "flutter/fml/file.h" |
| #include "flutter/fml/log_settings.h" |
| #include "flutter/fml/unique_fd.h" |
| #include "flutter/shell/common/shell_test.h" |
| #include "flutter/shell/common/switches.h" |
| #include "flutter/shell/version/version.h" |
| #include "flutter/testing/testing.h" |
| #include "include/core/SkPicture.h" |
| |
| namespace flutter { |
| namespace testing { |
| |
| using PersistentCacheTest = ShellTest; |
| |
| static void WaitForIO(Shell* shell) { |
| std::promise<bool> io_task_finished; |
| shell->GetTaskRunners().GetIOTaskRunner()->PostTask( |
| [&io_task_finished]() { io_task_finished.set_value(true); }); |
| io_task_finished.get_future().wait(); |
| } |
| |
| static void WaitForRaster(Shell* shell) { |
| std::promise<bool> raster_task_finished; |
| shell->GetTaskRunners().GetRasterTaskRunner()->PostTask( |
| [&raster_task_finished]() { raster_task_finished.set_value(true); }); |
| raster_task_finished.get_future().wait(); |
| } |
| |
| TEST_F(PersistentCacheTest, CacheSkSLWorks) { |
| // Create a temp dir to store the persistent cache |
| fml::ScopedTemporaryDirectory dir; |
| PersistentCache::SetCacheDirectoryPath(dir.path()); |
| PersistentCache::ResetCacheForProcess(); |
| |
| auto settings = CreateSettingsForFixture(); |
| settings.cache_sksl = true; |
| settings.dump_skp_on_shader_compilation = true; |
| |
| fml::AutoResetWaitableEvent first_frame_latch; |
| settings.frame_rasterized_callback = |
| [&first_frame_latch](const FrameTiming& t) { |
| first_frame_latch.Signal(); |
| }; |
| |
| auto sksl_config = RunConfiguration::InferFromSettings(settings); |
| sksl_config.SetEntrypoint("emptyMain"); |
| std::unique_ptr<Shell> shell = CreateShell(settings); |
| PlatformViewNotifyCreated(shell.get()); |
| RunEngine(shell.get(), std::move(sksl_config)); |
| |
| // Initially, we should have no SkSL cache |
| auto cache = PersistentCache::GetCacheForProcess()->LoadSkSLs(); |
| ASSERT_EQ(cache.size(), 0u); |
| |
| // Draw something to trigger shader compilations. |
| LayerTreeBuilder builder = [](const std::shared_ptr<ContainerLayer>& root) { |
| SkPath path; |
| path.addCircle(50, 50, 20); |
| auto physical_shape_layer = std::make_shared<PhysicalShapeLayer>( |
| SK_ColorRED, SK_ColorBLUE, 1.0f, path, Clip::antiAlias); |
| root->Add(physical_shape_layer); |
| }; |
| PumpOneFrame(shell.get(), 100, 100, builder); |
| first_frame_latch.Wait(); |
| WaitForIO(shell.get()); |
| |
| // Some skp should be dumped due to shader compilations. |
| int skp_count = 0; |
| fml::FileVisitor skp_visitor = [&skp_count](const fml::UniqueFD& directory, |
| const std::string& filename) { |
| if (filename.size() >= 4 && |
| filename.substr(filename.size() - 4, 4) == ".skp") { |
| skp_count += 1; |
| } |
| return true; |
| }; |
| fml::VisitFilesRecursively(dir.fd(), skp_visitor); |
| ASSERT_GT(skp_count, 0); |
| |
| // SkSL cache should be generated by the last run. |
| cache = PersistentCache::GetCacheForProcess()->LoadSkSLs(); |
| ASSERT_GT(cache.size(), 0u); |
| |
| // Run the engine again with cache_sksl = false and check that the previously |
| // generated SkSL cache is used for precompile. |
| PersistentCache::ResetCacheForProcess(); |
| settings.cache_sksl = false; |
| settings.dump_skp_on_shader_compilation = true; |
| auto normal_config = RunConfiguration::InferFromSettings(settings); |
| normal_config.SetEntrypoint("emptyMain"); |
| DestroyShell(std::move(shell)); |
| shell = CreateShell(settings); |
| PlatformViewNotifyCreated(shell.get()); |
| RunEngine(shell.get(), std::move(normal_config)); |
| first_frame_latch.Reset(); |
| PumpOneFrame(shell.get(), 100, 100, builder); |
| first_frame_latch.Wait(); |
| WaitForIO(shell.get()); |
| |
| // Shader precompilation from SkSL is not implemented on the Skia Vulkan |
| // backend so don't run the second half of this test on Vulkan. This can get |
| // removed if SkSL precompilation is implemented in the Skia Vulkan backend. |
| #if !defined(SHELL_ENABLE_VULKAN) |
| // To check that all shaders are precompiled, verify that no new skp is dumped |
| // due to shader compilations. |
| int old_skp_count = skp_count; |
| skp_count = 0; |
| fml::VisitFilesRecursively(dir.fd(), skp_visitor); |
| ASSERT_EQ(skp_count, old_skp_count); |
| #endif // !defined(SHELL_ENABLE_VULKAN) |
| |
| // Remove all files generated |
| fml::RemoveFilesInDirectory(dir.fd()); |
| DestroyShell(std::move(shell)); |
| } |
| |
| TEST_F(PersistentCacheTest, CanPrecompileMetalShaders) { |
| #if !SHELL_ENABLE_METAL |
| GTEST_SKIP(); |
| #endif // !SHELL_ENABLE_METAL |
| fml::ScopedTemporaryDirectory dir; |
| PersistentCache::SetCacheDirectoryPath(dir.path()); |
| PersistentCache::ResetCacheForProcess(); |
| |
| auto settings = CreateSettingsForFixture(); |
| settings.cache_sksl = true; |
| settings.dump_skp_on_shader_compilation = true; |
| |
| fml::AutoResetWaitableEvent first_frame_latch; |
| settings.frame_rasterized_callback = |
| [&first_frame_latch](const FrameTiming& t) { |
| first_frame_latch.Signal(); |
| }; |
| |
| auto sksl_config = RunConfiguration::InferFromSettings(settings); |
| sksl_config.SetEntrypoint("emptyMain"); |
| std::unique_ptr<Shell> shell = |
| CreateShell(settings, // |
| GetTaskRunnersForFixture(), // |
| false, // |
| nullptr, // |
| false, // |
| ShellTestPlatformView::BackendType::kMetalBackend // |
| ); |
| PlatformViewNotifyCreated(shell.get()); |
| RunEngine(shell.get(), std::move(sksl_config)); |
| |
| // Initially, we should have no SkSL cache |
| { |
| auto empty_cache = PersistentCache::GetCacheForProcess()->LoadSkSLs(); |
| ASSERT_EQ(empty_cache.size(), 0u); |
| } |
| |
| // Draw something to trigger shader compilations. |
| LayerTreeBuilder builder = [](const std::shared_ptr<ContainerLayer>& root) { |
| SkPath path; |
| path.addCircle(50, 50, 20); |
| auto physical_shape_layer = std::make_shared<PhysicalShapeLayer>( |
| SK_ColorRED, SK_ColorBLUE, 1.0f, path, Clip::antiAlias); |
| root->Add(physical_shape_layer); |
| }; |
| PumpOneFrame(shell.get(), 100, 100, builder); |
| first_frame_latch.Wait(); |
| WaitForRaster(shell.get()); |
| WaitForIO(shell.get()); |
| |
| // Assert that SkSLs have been generated. |
| auto filled_cache = PersistentCache::GetCacheForProcess()->LoadSkSLs(); |
| ASSERT_GT(filled_cache.size(), 0u); |
| |
| // Remove all files generated. |
| fml::RemoveFilesInDirectory(dir.fd()); |
| DestroyShell(std::move(shell)); |
| } |
| |
| static void CheckTextSkData(const sk_sp<SkData>& data, |
| const std::string& expected) { |
| std::string data_string(reinterpret_cast<const char*>(data->bytes()), |
| data->size()); |
| ASSERT_EQ(data_string, expected); |
| } |
| |
| static void ResetAssetManager() { |
| PersistentCache::SetAssetManager(nullptr); |
| ASSERT_EQ(PersistentCache::GetCacheForProcess()->LoadSkSLs().size(), 0u); |
| } |
| |
| static void CheckTwoSkSLsAreLoaded() { |
| auto shaders = PersistentCache::GetCacheForProcess()->LoadSkSLs(); |
| ASSERT_EQ(shaders.size(), 2u); |
| } |
| |
| TEST_F(PersistentCacheTest, CanLoadSkSLsFromAsset) { |
| // Avoid polluting unit tests output by hiding INFO level logging. |
| fml::LogSettings warning_only = {fml::LOG_WARNING}; |
| fml::ScopedSetLogSettings scoped_set_log_settings(warning_only); |
| |
| // The SkSL key is Base32 encoded. "IE" is the encoding of "A" and "II" is the |
| // encoding of "B". |
| // |
| // The SkSL data is Base64 encoded. "eA==" is the encoding of "x" and "eQ==" |
| // is the encoding of "y". |
| const std::string kTestJson = |
| "{\n" |
| " \"data\": {\n" |
| " \"IE\": \"eA==\",\n" |
| " \"II\": \"eQ==\"\n" |
| " }\n" |
| "}\n"; |
| |
| // Temp dir for the asset. |
| fml::ScopedTemporaryDirectory asset_dir; |
| |
| auto data = std::make_unique<fml::DataMapping>( |
| std::vector<uint8_t>{kTestJson.begin(), kTestJson.end()}); |
| fml::WriteAtomically(asset_dir.fd(), PersistentCache::kAssetFileName, *data); |
| |
| // 1st, test that RunConfiguration::InferFromSettings sets the asset manager. |
| ResetAssetManager(); |
| auto settings = CreateSettingsForFixture(); |
| settings.assets_path = asset_dir.path(); |
| RunConfiguration::InferFromSettings(settings); |
| CheckTwoSkSLsAreLoaded(); |
| |
| // 2nd, test that the RunConfiguration constructor sets the asset manager. |
| // (Android is directly calling that constructor without InferFromSettings.) |
| ResetAssetManager(); |
| auto asset_manager = std::make_shared<AssetManager>(); |
| RunConfiguration config(nullptr, asset_manager); |
| asset_manager->PushBack(std::make_unique<DirectoryAssetBundle>( |
| fml::OpenDirectory(asset_dir.path().c_str(), false, |
| fml::FilePermission::kRead), |
| false)); |
| CheckTwoSkSLsAreLoaded(); |
| |
| // 3rd, test the content of the SkSLs in the asset. |
| { |
| auto shaders = PersistentCache::GetCacheForProcess()->LoadSkSLs(); |
| ASSERT_EQ(shaders.size(), 2u); |
| |
| // Make sure that the 2 shaders are sorted by their keys. Their keys should |
| // be "A" and "B" (decoded from "II" and "IE"). |
| if (shaders[0].key->bytes()[0] == 'B') { |
| std::swap(shaders[0], shaders[1]); |
| } |
| |
| CheckTextSkData(shaders[0].key, "A"); |
| CheckTextSkData(shaders[1].key, "B"); |
| CheckTextSkData(shaders[0].value, "x"); |
| CheckTextSkData(shaders[1].value, "y"); |
| } |
| |
| // Cleanup. |
| fml::UnlinkFile(asset_dir.fd(), PersistentCache::kAssetFileName); |
| } |
| |
| TEST_F(PersistentCacheTest, CanRemoveOldPersistentCache) { |
| fml::ScopedTemporaryDirectory base_dir; |
| ASSERT_TRUE(base_dir.fd().is_valid()); |
| |
| fml::CreateDirectory(base_dir.fd(), |
| {"flutter_engine", GetFlutterEngineVersion(), "skia"}, |
| fml::FilePermission::kReadWrite); |
| |
| constexpr char kOldEngineVersion[] = "old"; |
| auto old_created = fml::CreateDirectory( |
| base_dir.fd(), {"flutter_engine", kOldEngineVersion, "skia"}, |
| fml::FilePermission::kReadWrite); |
| ASSERT_TRUE(old_created.is_valid()); |
| |
| PersistentCache::SetCacheDirectoryPath(base_dir.path()); |
| PersistentCache::ResetCacheForProcess(); |
| |
| auto engine_dir = fml::OpenDirectoryReadOnly(base_dir.fd(), "flutter_engine"); |
| auto current_dir = |
| fml::OpenDirectoryReadOnly(engine_dir, GetFlutterEngineVersion()); |
| auto old_dir = fml::OpenDirectoryReadOnly(engine_dir, kOldEngineVersion); |
| |
| ASSERT_TRUE(engine_dir.is_valid()); |
| ASSERT_TRUE(current_dir.is_valid()); |
| ASSERT_FALSE(old_dir.is_valid()); |
| |
| // Cleanup |
| fml::RemoveFilesInDirectory(base_dir.fd()); |
| } |
| |
| TEST_F(PersistentCacheTest, CanPurgePersistentCache) { |
| fml::ScopedTemporaryDirectory base_dir; |
| ASSERT_TRUE(base_dir.fd().is_valid()); |
| auto cache_dir = fml::CreateDirectory( |
| base_dir.fd(), |
| {"flutter_engine", GetFlutterEngineVersion(), "skia", GetSkiaVersion()}, |
| fml::FilePermission::kReadWrite); |
| PersistentCache::SetCacheDirectoryPath(base_dir.path()); |
| PersistentCache::ResetCacheForProcess(); |
| |
| // Generate a dummy persistent cache. |
| fml::DataMapping test_data(std::string("test")); |
| ASSERT_TRUE(fml::WriteAtomically(cache_dir, "test", test_data)); |
| auto file = fml::OpenFileReadOnly(cache_dir, "test"); |
| ASSERT_TRUE(file.is_valid()); |
| |
| // Run engine with purge_persistent_cache to remove the dummy cache. |
| auto settings = CreateSettingsForFixture(); |
| settings.purge_persistent_cache = true; |
| auto config = RunConfiguration::InferFromSettings(settings); |
| std::unique_ptr<Shell> shell = CreateShell(settings); |
| RunEngine(shell.get(), std::move(config)); |
| |
| // Verify that the dummy is purged. |
| file = fml::OpenFileReadOnly(cache_dir, "test"); |
| ASSERT_FALSE(file.is_valid()); |
| |
| // Cleanup |
| fml::RemoveFilesInDirectory(base_dir.fd()); |
| DestroyShell(std::move(shell)); |
| } |
| |
| TEST_F(PersistentCacheTest, PurgeAllowsFutureSkSLCache) { |
| sk_sp<SkData> shader_key = SkData::MakeWithCString("key"); |
| sk_sp<SkData> shader_value = SkData::MakeWithCString("value"); |
| std::string shader_filename = PersistentCache::SkKeyToFilePath(*shader_key); |
| |
| fml::ScopedTemporaryDirectory base_dir; |
| ASSERT_TRUE(base_dir.fd().is_valid()); |
| PersistentCache::SetCacheDirectoryPath(base_dir.path()); |
| PersistentCache::ResetCacheForProcess(); |
| |
| // Run engine with purge_persistent_cache and cache_sksl. |
| auto settings = CreateSettingsForFixture(); |
| settings.purge_persistent_cache = true; |
| settings.cache_sksl = true; |
| auto config = RunConfiguration::InferFromSettings(settings); |
| std::unique_ptr<Shell> shell = CreateShell(settings); |
| RunEngine(shell.get(), std::move(config)); |
| auto persistent_cache = PersistentCache::GetCacheForProcess(); |
| ASSERT_EQ(persistent_cache->LoadSkSLs().size(), 0u); |
| |
| // Store the cache and verify it's valid. |
| StorePersistentCache(persistent_cache, *shader_key, *shader_value); |
| std::promise<bool> io_flushed; |
| shell->GetTaskRunners().GetIOTaskRunner()->PostTask( |
| [&io_flushed]() { io_flushed.set_value(true); }); |
| io_flushed.get_future().get(); // Wait for the IO thread to flush the file. |
| ASSERT_GT(persistent_cache->LoadSkSLs().size(), 0u); |
| |
| // Cleanup |
| fml::RemoveFilesInDirectory(base_dir.fd()); |
| DestroyShell(std::move(shell)); |
| } |
| |
| } // namespace testing |
| } // namespace flutter |