blob: 50d09bb8fcbc6d4c93d654004085d1cd8d1884ea [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 <future>
#include <memory>
#include <string>
#include <string_view>
#include <utility>
#include "flutter/fml/base32.h"
#include "flutter/fml/file.h"
#include "flutter/fml/hex_codec.h"
#include "flutter/fml/logging.h"
#include "flutter/fml/make_copyable.h"
#include "flutter/fml/mapping.h"
#include "flutter/fml/paths.h"
#include "flutter/fml/trace_event.h"
#include "flutter/shell/version/version.h"
#include "openssl/sha.h"
#include "rapidjson/document.h"
#include "third_party/skia/include/gpu/GrDirectContext.h"
#include "third_party/skia/include/utils/SkBase64.h"
namespace flutter {
std::string PersistentCache::cache_base_path_;
std::shared_ptr<AssetManager> PersistentCache::asset_manager_;
std::mutex PersistentCache::instance_mutex_;
std::unique_ptr<PersistentCache> PersistentCache::gPersistentCache;
std::string PersistentCache::SkKeyToFilePath(const SkData& key) {
if (key.data() == nullptr || key.size() == 0) {
return "";
}
uint8_t sha_digest[SHA_DIGEST_LENGTH];
SHA1(static_cast<const uint8_t*>(key.data()), key.size(), sha_digest);
std::string_view view(reinterpret_cast<const char*>(sha_digest),
SHA_DIGEST_LENGTH);
return fml::HexEncode(view);
}
bool PersistentCache::gIsReadOnly = false;
std::atomic<bool> PersistentCache::cache_sksl_ = false;
std::atomic<bool> PersistentCache::strategy_set_ = false;
void PersistentCache::SetCacheSkSL(bool value) {
if (strategy_set_ && value != cache_sksl_) {
FML_LOG(ERROR) << "Cache SkSL can only be set before the "
"GrContextOptions::fShaderCacheStrategy is set.";
return;
}
cache_sksl_ = value;
}
PersistentCache* PersistentCache::GetCacheForProcess() {
std::scoped_lock lock(instance_mutex_);
if (gPersistentCache == nullptr) {
gPersistentCache.reset(new PersistentCache(gIsReadOnly));
}
return gPersistentCache.get();
}
void PersistentCache::ResetCacheForProcess() {
std::scoped_lock lock(instance_mutex_);
gPersistentCache.reset(new PersistentCache(gIsReadOnly));
strategy_set_ = false;
}
void PersistentCache::SetCacheDirectoryPath(std::string path) {
cache_base_path_ = std::move(path);
}
bool PersistentCache::Purge() {
// Make sure that this is called after the worker task runner setup so all the
// file system modifications would happen on that single thread to avoid
// racing.
FML_CHECK(GetWorkerTaskRunner());
std::promise<bool> removed;
GetWorkerTaskRunner()->PostTask([&removed,
cache_directory = cache_directory_]() {
if (cache_directory->is_valid()) {
// Only remove files but not directories.
FML_LOG(INFO) << "Purge persistent cache.";
fml::FileVisitor delete_file = [](const fml::UniqueFD& directory,
const std::string& filename) {
// Do not delete directories. Return true to continue with other files.
if (fml::IsDirectory(directory, filename.c_str())) {
return true;
}
return fml::UnlinkFile(directory, filename.c_str());
};
removed.set_value(VisitFilesRecursively(*cache_directory, delete_file));
} else {
removed.set_value(false);
}
});
return removed.get_future().get();
}
namespace {
constexpr char kEngineComponent[] = "flutter_engine";
static void FreeOldCacheDirectory(const fml::UniqueFD& cache_base_dir) {
fml::UniqueFD engine_dir =
fml::OpenDirectoryReadOnly(cache_base_dir, kEngineComponent);
if (!engine_dir.is_valid()) {
return;
}
fml::VisitFiles(engine_dir, [](const fml::UniqueFD& directory,
const std::string& filename) {
if (filename != GetFlutterEngineVersion()) {
auto dir = fml::OpenDirectory(directory, filename.c_str(), false,
fml::FilePermission::kReadWrite);
if (dir.is_valid()) {
fml::RemoveDirectoryRecursively(directory, filename.c_str());
}
}
return true;
});
}
static std::shared_ptr<fml::UniqueFD> MakeCacheDirectory(
const std::string& global_cache_base_path,
bool read_only,
bool cache_sksl) {
fml::UniqueFD cache_base_dir;
if (global_cache_base_path.length()) {
cache_base_dir = fml::OpenDirectory(global_cache_base_path.c_str(), false,
fml::FilePermission::kRead);
} else {
cache_base_dir = fml::paths::GetCachesDirectory();
}
if (cache_base_dir.is_valid()) {
FreeOldCacheDirectory(cache_base_dir);
std::vector<std::string> components = {
kEngineComponent, GetFlutterEngineVersion(), "skia", GetSkiaVersion()};
if (cache_sksl) {
components.push_back(PersistentCache::kSkSLSubdirName);
}
return std::make_shared<fml::UniqueFD>(
CreateDirectory(cache_base_dir, components,
read_only ? fml::FilePermission::kRead
: fml::FilePermission::kReadWrite));
} else {
return std::make_shared<fml::UniqueFD>();
}
}
} // namespace
sk_sp<SkData> ParseBase32(const std::string& input) {
std::pair<bool, std::string> decode_result = fml::Base32Decode(input);
if (!decode_result.first) {
FML_LOG(ERROR) << "Base32 can't decode: " << input;
return nullptr;
}
const std::string& data_string = decode_result.second;
return SkData::MakeWithCopy(data_string.data(), data_string.length());
}
sk_sp<SkData> ParseBase64(const std::string& input) {
SkBase64::Error error;
size_t output_len;
error = SkBase64::Decode(input.c_str(), input.length(), nullptr, &output_len);
if (error != SkBase64::Error::kNoError) {
FML_LOG(ERROR) << "Base64 decode error: " << error;
FML_LOG(ERROR) << "Base64 can't decode: " << input;
return nullptr;
}
sk_sp<SkData> data = SkData::MakeUninitialized(output_len);
void* output = data->writable_data();
error = SkBase64::Decode(input.c_str(), input.length(), output, &output_len);
if (error != SkBase64::Error::kNoError) {
FML_LOG(ERROR) << "Base64 decode error: " << error;
FML_LOG(ERROR) << "Base64 can't decode: " << input;
return nullptr;
}
return data;
}
size_t PersistentCache::PrecompileKnownSkSLs(GrDirectContext* context) const {
// clang-tidy has trouble reasoning about some of the complicated array and
// pointer-arithmetic code in rapidjson.
// NOLINTNEXTLINE(clang-analyzer-cplusplus.PlacementNew)
auto known_sksls = LoadSkSLs();
// A trace must be present even if no precompilations have been completed.
FML_TRACE_EVENT("flutter", "PersistentCache::PrecompileKnownSkSLs", "count",
known_sksls.size());
if (context == nullptr) {
return 0;
}
size_t precompiled_count = 0;
for (const auto& sksl : known_sksls) {
TRACE_EVENT0("flutter", "PrecompilingSkSL");
if (context->precompileShader(*sksl.key, *sksl.value)) {
precompiled_count++;
}
}
FML_TRACE_COUNTER("flutter", "PersistentCache::PrecompiledSkSLs",
reinterpret_cast<int64_t>(this), // Trace Counter ID
"Successful", precompiled_count);
return precompiled_count;
}
std::vector<PersistentCache::SkSLCache> PersistentCache::LoadSkSLs() const {
TRACE_EVENT0("flutter", "PersistentCache::LoadSkSLs");
std::vector<PersistentCache::SkSLCache> result;
fml::FileVisitor visitor = [&result](const fml::UniqueFD& directory,
const std::string& filename) {
SkSLCache cache = LoadFile(directory, filename, true);
if (cache.key != nullptr && cache.value != nullptr) {
result.push_back(cache);
} else {
FML_LOG(ERROR) << "Failed to load: " << filename;
}
return true;
};
// Only visit sksl_cache_directory_ if this persistent cache is valid.
// However, we'd like to continue visit the asset dir even if this persistent
// cache is invalid.
if (IsValid()) {
// In case `rewinddir` doesn't work reliably, load SkSLs from a freshly
// opened directory (https://github.com/flutter/flutter/issues/65258).
fml::UniqueFD fresh_dir =
fml::OpenDirectoryReadOnly(*cache_directory_, kSkSLSubdirName);
if (fresh_dir.is_valid()) {
fml::VisitFiles(fresh_dir, visitor);
}
}
std::unique_ptr<fml::Mapping> mapping = nullptr;
if (asset_manager_ != nullptr) {
mapping = asset_manager_->GetAsMapping(kAssetFileName);
}
if (mapping == nullptr) {
FML_LOG(INFO) << "No sksl asset found.";
} else {
FML_LOG(INFO) << "Found sksl asset. Loading SkSLs from it...";
rapidjson::Document json_doc;
rapidjson::ParseResult parse_result =
json_doc.Parse(reinterpret_cast<const char*>(mapping->GetMapping()),
mapping->GetSize());
if (parse_result.IsError()) {
FML_LOG(ERROR) << "Failed to parse json file: " << kAssetFileName;
} else {
for (auto& item : json_doc["data"].GetObject()) {
sk_sp<SkData> key = ParseBase32(item.name.GetString());
sk_sp<SkData> sksl = ParseBase64(item.value.GetString());
if (key != nullptr && sksl != nullptr) {
result.push_back({key, sksl});
} else {
FML_LOG(ERROR) << "Failed to load: " << item.name.GetString();
}
}
}
}
return result;
}
PersistentCache::PersistentCache(bool read_only)
: is_read_only_(read_only),
cache_directory_(MakeCacheDirectory(cache_base_path_, read_only, false)),
sksl_cache_directory_(
MakeCacheDirectory(cache_base_path_, read_only, true)) {
if (!IsValid()) {
FML_LOG(WARNING) << "Could not acquire the persistent cache directory. "
"Caching of GPU resources on disk is disabled.";
}
}
PersistentCache::~PersistentCache() = default;
bool PersistentCache::IsValid() const {
return cache_directory_ && cache_directory_->is_valid();
}
PersistentCache::SkSLCache PersistentCache::LoadFile(
const fml::UniqueFD& dir,
const std::string& file_name,
bool need_key) {
SkSLCache result;
auto file = fml::OpenFileReadOnly(dir, file_name.c_str());
if (!file.is_valid()) {
return result;
}
auto mapping = std::make_unique<fml::FileMapping>(file);
if (mapping->GetSize() < sizeof(CacheObjectHeader)) {
return result;
}
const CacheObjectHeader* header =
reinterpret_cast<const CacheObjectHeader*>(mapping->GetMapping());
if (header->signature != CacheObjectHeader::kSignature ||
header->version != CacheObjectHeader::kVersion1) {
FML_LOG(INFO) << "Persistent cache header is corrupt: " << file_name;
return result;
}
if (mapping->GetSize() < sizeof(CacheObjectHeader) + header->key_size) {
FML_LOG(INFO) << "Persistent cache size is corrupt: " << file_name;
return result;
}
if (need_key) {
result.key = SkData::MakeWithCopy(
mapping->GetMapping() + sizeof(CacheObjectHeader), header->key_size);
}
size_t value_offset = sizeof(CacheObjectHeader) + header->key_size;
result.value = SkData::MakeWithCopy(mapping->GetMapping() + value_offset,
mapping->GetSize() - value_offset);
return result;
}
// |GrContextOptions::PersistentCache|
sk_sp<SkData> PersistentCache::load(const SkData& key) {
TRACE_EVENT0("flutter", "PersistentCacheLoad");
if (!IsValid()) {
return nullptr;
}
auto file_name = SkKeyToFilePath(key);
if (file_name.empty()) {
return nullptr;
}
auto result =
PersistentCache::LoadFile(*cache_directory_, file_name, false).value;
if (result != nullptr) {
TRACE_EVENT0("flutter", "PersistentCacheLoadHit");
}
return result;
}
static void PersistentCacheStore(
const fml::RefPtr<fml::TaskRunner>& worker,
const std::shared_ptr<fml::UniqueFD>& cache_directory,
std::string key,
std::unique_ptr<fml::Mapping> value) {
// The static leak checker gets confused by the use of fml::MakeCopyable.
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks)
auto task = fml::MakeCopyable([cache_directory, //
file_name = std::move(key), //
mapping = std::move(value) //
]() mutable {
TRACE_EVENT0("flutter", "PersistentCacheStore");
if (!fml::WriteAtomically(*cache_directory, //
file_name.c_str(), //
*mapping) //
) {
FML_LOG(WARNING) << "Could not write cache contents to persistent store.";
}
});
if (!worker) {
FML_LOG(WARNING)
<< "The persistent cache has no available workers. Performing the task "
"on the current thread. This slow operation is going to occur on a "
"frame workload.";
task();
} else {
worker->PostTask(std::move(task));
}
}
std::unique_ptr<fml::MallocMapping> PersistentCache::BuildCacheObject(
const SkData& key,
const SkData& data) {
size_t total_size = sizeof(CacheObjectHeader) + key.size() + data.size();
uint8_t* mapping_buf = reinterpret_cast<uint8_t*>(malloc(total_size));
if (!mapping_buf) {
return nullptr;
}
auto mapping = std::make_unique<fml::MallocMapping>(mapping_buf, total_size);
CacheObjectHeader header(key.size());
memcpy(mapping_buf, &header, sizeof(CacheObjectHeader));
mapping_buf += sizeof(CacheObjectHeader);
memcpy(mapping_buf, key.data(), key.size());
mapping_buf += key.size();
memcpy(mapping_buf, data.data(), data.size());
return mapping;
}
// |GrContextOptions::PersistentCache|
void PersistentCache::store(const SkData& key, const SkData& data) {
stored_new_shaders_ = true;
if (is_read_only_) {
return;
}
if (!IsValid()) {
return;
}
auto file_name = SkKeyToFilePath(key);
if (file_name.empty()) {
return;
}
std::unique_ptr<fml::MallocMapping> mapping = BuildCacheObject(key, data);
if (!mapping) {
return;
}
PersistentCacheStore(GetWorkerTaskRunner(),
cache_sksl_ ? sksl_cache_directory_ : cache_directory_,
std::move(file_name), std::move(mapping));
}
void PersistentCache::DumpSkp(const SkData& data) {
if (is_read_only_ || !IsValid()) {
FML_LOG(ERROR) << "Could not dump SKP from read-only or invalid persistent "
"cache.";
return;
}
std::stringstream name_stream;
auto ticks = fml::TimePoint::Now().ToEpochDelta().ToNanoseconds();
name_stream << "shader_dump_" << std::to_string(ticks) << ".skp";
std::string file_name = name_stream.str();
FML_LOG(INFO) << "Dumping " << file_name;
auto mapping = std::make_unique<fml::DataMapping>(
std::vector<uint8_t>{data.bytes(), data.bytes() + data.size()});
PersistentCacheStore(GetWorkerTaskRunner(), cache_directory_,
std::move(file_name), std::move(mapping));
}
void PersistentCache::AddWorkerTaskRunner(
const fml::RefPtr<fml::TaskRunner>& task_runner) {
std::scoped_lock lock(worker_task_runners_mutex_);
worker_task_runners_.insert(task_runner);
}
void PersistentCache::RemoveWorkerTaskRunner(
const fml::RefPtr<fml::TaskRunner>& task_runner) {
std::scoped_lock lock(worker_task_runners_mutex_);
auto found = worker_task_runners_.find(task_runner);
if (found != worker_task_runners_.end()) {
worker_task_runners_.erase(found);
}
}
fml::RefPtr<fml::TaskRunner> PersistentCache::GetWorkerTaskRunner() const {
fml::RefPtr<fml::TaskRunner> worker;
std::scoped_lock lock(worker_task_runners_mutex_);
if (!worker_task_runners_.empty()) {
worker = *worker_task_runners_.begin();
}
return worker;
}
void PersistentCache::SetAssetManager(std::shared_ptr<AssetManager> value) {
TRACE_EVENT_INSTANT0("flutter", "PersistentCache::SetAssetManager");
asset_manager_ = std::move(value);
}
std::vector<std::unique_ptr<fml::Mapping>>
PersistentCache::GetSkpsFromAssetManager() const {
if (!asset_manager_) {
FML_LOG(ERROR)
<< "PersistentCache::GetSkpsFromAssetManager: Asset manager not set!";
return std::vector<std::unique_ptr<fml::Mapping>>();
}
return asset_manager_->GetAsMappings(".*\\.skp$", "shaders");
}
} // namespace flutter