blob: 9a1daf2a71c02df603f40e78fef150217609dd7d [file] [log] [blame]
// 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/flow/layers/picture_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 = [](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 = [](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(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