1 | // Copyright 2013 The Flutter Authors. All rights reserved. |
2 | // Use of this source code is governed by a BSD-style license that can be |
3 | // found in the LICENSE file. |
4 | |
5 | #include "flutter/shell/common/persistent_cache.h" |
6 | |
7 | #include <future> |
8 | #include <memory> |
9 | #include <string> |
10 | #include <string_view> |
11 | |
12 | #include "rapidjson/document.h" |
13 | #include "third_party/skia/include/utils/SkBase64.h" |
14 | |
15 | #include "flutter/fml/base32.h" |
16 | #include "flutter/fml/file.h" |
17 | #include "flutter/fml/logging.h" |
18 | #include "flutter/fml/make_copyable.h" |
19 | #include "flutter/fml/mapping.h" |
20 | #include "flutter/fml/paths.h" |
21 | #include "flutter/fml/trace_event.h" |
22 | #include "flutter/shell/version/version.h" |
23 | |
24 | namespace flutter { |
25 | |
26 | std::string PersistentCache::cache_base_path_; |
27 | |
28 | std::shared_ptr<AssetManager> PersistentCache::asset_manager_; |
29 | |
30 | std::mutex PersistentCache::instance_mutex_; |
31 | std::unique_ptr<PersistentCache> PersistentCache::gPersistentCache; |
32 | |
33 | std::string PersistentCache::SkKeyToFilePath(const SkData& data) { |
34 | if (data.data() == nullptr || data.size() == 0) { |
35 | return "" ; |
36 | } |
37 | |
38 | std::string_view view(reinterpret_cast<const char*>(data.data()), |
39 | data.size()); |
40 | |
41 | auto encode_result = fml::Base32Encode(view); |
42 | |
43 | if (!encode_result.first) { |
44 | return "" ; |
45 | } |
46 | |
47 | return encode_result.second; |
48 | } |
49 | |
50 | bool PersistentCache::gIsReadOnly = false; |
51 | |
52 | std::atomic<bool> PersistentCache::cache_sksl_ = false; |
53 | std::atomic<bool> PersistentCache::strategy_set_ = false; |
54 | |
55 | void PersistentCache::SetCacheSkSL(bool value) { |
56 | if (strategy_set_ && value != cache_sksl_) { |
57 | FML_LOG(ERROR) << "Cache SkSL can only be set before the " |
58 | "GrContextOptions::fShaderCacheStrategy is set." ; |
59 | return; |
60 | } |
61 | cache_sksl_ = value; |
62 | } |
63 | |
64 | PersistentCache* PersistentCache::GetCacheForProcess() { |
65 | std::scoped_lock lock(instance_mutex_); |
66 | if (gPersistentCache == nullptr) { |
67 | gPersistentCache.reset(new PersistentCache(gIsReadOnly)); |
68 | } |
69 | return gPersistentCache.get(); |
70 | } |
71 | |
72 | void PersistentCache::ResetCacheForProcess() { |
73 | std::scoped_lock lock(instance_mutex_); |
74 | gPersistentCache.reset(new PersistentCache(gIsReadOnly)); |
75 | strategy_set_ = false; |
76 | } |
77 | |
78 | void PersistentCache::SetCacheDirectoryPath(std::string path) { |
79 | cache_base_path_ = path; |
80 | } |
81 | |
82 | bool PersistentCache::Purge() { |
83 | // Make sure that this is called after the worker task runner setup so all the |
84 | // file system modifications would happen on that single thread to avoid |
85 | // racing. |
86 | FML_CHECK(GetWorkerTaskRunner()); |
87 | |
88 | std::promise<bool> removed; |
89 | GetWorkerTaskRunner()->PostTask( |
90 | [&removed, cache_directory = cache_directory_]() { |
91 | if (cache_directory->is_valid()) { |
92 | FML_LOG(INFO) << "Purge persistent cache." ; |
93 | removed.set_value(RemoveFilesInDirectory(*cache_directory)); |
94 | } else { |
95 | removed.set_value(false); |
96 | } |
97 | }); |
98 | return removed.get_future().get(); |
99 | } |
100 | |
101 | namespace { |
102 | |
103 | constexpr char kEngineComponent[] = "flutter_engine" ; |
104 | |
105 | static void FreeOldCacheDirectory(const fml::UniqueFD& cache_base_dir) { |
106 | fml::UniqueFD engine_dir = |
107 | fml::OpenDirectoryReadOnly(cache_base_dir, kEngineComponent); |
108 | if (!engine_dir.is_valid()) { |
109 | return; |
110 | } |
111 | fml::VisitFiles(engine_dir, [](const fml::UniqueFD& directory, |
112 | const std::string& filename) { |
113 | if (filename != GetFlutterEngineVersion()) { |
114 | auto dir = fml::OpenDirectory(directory, filename.c_str(), false, |
115 | fml::FilePermission::kReadWrite); |
116 | if (dir.is_valid()) { |
117 | fml::RemoveDirectoryRecursively(directory, filename.c_str()); |
118 | } |
119 | } |
120 | return true; |
121 | }); |
122 | } |
123 | |
124 | static std::shared_ptr<fml::UniqueFD> MakeCacheDirectory( |
125 | const std::string& global_cache_base_path, |
126 | bool read_only, |
127 | bool cache_sksl) { |
128 | fml::UniqueFD cache_base_dir; |
129 | if (global_cache_base_path.length()) { |
130 | cache_base_dir = fml::OpenDirectory(global_cache_base_path.c_str(), false, |
131 | fml::FilePermission::kRead); |
132 | } else { |
133 | cache_base_dir = fml::paths::GetCachesDirectory(); |
134 | } |
135 | |
136 | if (cache_base_dir.is_valid()) { |
137 | FreeOldCacheDirectory(cache_base_dir); |
138 | std::vector<std::string> components = { |
139 | kEngineComponent, GetFlutterEngineVersion(), "skia" , GetSkiaVersion()}; |
140 | if (cache_sksl) { |
141 | components.push_back(PersistentCache::kSkSLSubdirName); |
142 | } |
143 | return std::make_shared<fml::UniqueFD>( |
144 | CreateDirectory(cache_base_dir, components, |
145 | read_only ? fml::FilePermission::kRead |
146 | : fml::FilePermission::kReadWrite)); |
147 | } else { |
148 | return std::make_shared<fml::UniqueFD>(); |
149 | } |
150 | } |
151 | } // namespace |
152 | |
153 | sk_sp<SkData> ParseBase32(const std::string& input) { |
154 | std::pair<bool, std::string> decode_result = fml::Base32Decode(input); |
155 | if (!decode_result.first) { |
156 | FML_LOG(ERROR) << "Base32 can't decode: " << input; |
157 | return nullptr; |
158 | } |
159 | const std::string& data_string = decode_result.second; |
160 | return SkData::MakeWithCopy(data_string.data(), data_string.length()); |
161 | } |
162 | |
163 | sk_sp<SkData> ParseBase64(const std::string& input) { |
164 | SkBase64 decoder; |
165 | auto error = decoder.decode(input.c_str(), input.length()); |
166 | if (error != SkBase64::Error::kNoError) { |
167 | FML_LOG(ERROR) << "Base64 decode error: " << error; |
168 | FML_LOG(ERROR) << "Base64 can't decode: " << input; |
169 | return nullptr; |
170 | } |
171 | return SkData::MakeWithCopy(decoder.getData(), decoder.getDataSize()); |
172 | } |
173 | |
174 | std::vector<PersistentCache::SkSLCache> PersistentCache::LoadSkSLs() { |
175 | TRACE_EVENT0("flutter" , "PersistentCache::LoadSkSLs" ); |
176 | std::vector<PersistentCache::SkSLCache> result; |
177 | fml::FileVisitor visitor = [&result](const fml::UniqueFD& directory, |
178 | const std::string& filename) { |
179 | sk_sp<SkData> key = ParseBase32(filename); |
180 | sk_sp<SkData> data = LoadFile(directory, filename); |
181 | if (key != nullptr && data != nullptr) { |
182 | result.push_back({key, data}); |
183 | } else { |
184 | FML_LOG(ERROR) << "Failed to load: " << filename; |
185 | } |
186 | return true; |
187 | }; |
188 | |
189 | // Only visit sksl_cache_directory_ if this persistent cache is valid. |
190 | // However, we'd like to continue visit the asset dir even if this persistent |
191 | // cache is invalid. |
192 | if (IsValid()) { |
193 | fml::VisitFiles(*sksl_cache_directory_, visitor); |
194 | } |
195 | |
196 | std::unique_ptr<fml::Mapping> mapping = nullptr; |
197 | if (asset_manager_ != nullptr) { |
198 | mapping = asset_manager_->GetAsMapping(kAssetFileName); |
199 | } |
200 | if (mapping == nullptr) { |
201 | FML_LOG(INFO) << "No sksl asset found." ; |
202 | } else { |
203 | FML_LOG(INFO) << "Found sksl asset. Loading SkSLs from it..." ; |
204 | rapidjson::Document json_doc; |
205 | rapidjson::ParseResult parse_result = |
206 | json_doc.Parse(reinterpret_cast<const char*>(mapping->GetMapping()), |
207 | mapping->GetSize()); |
208 | if (parse_result != rapidjson::ParseErrorCode::kParseErrorNone) { |
209 | FML_LOG(ERROR) << "Failed to parse json file: " << kAssetFileName; |
210 | } else { |
211 | for (auto& item : json_doc["data" ].GetObject()) { |
212 | sk_sp<SkData> key = ParseBase32(item.name.GetString()); |
213 | sk_sp<SkData> sksl = ParseBase64(item.value.GetString()); |
214 | if (key != nullptr && sksl != nullptr) { |
215 | result.push_back({key, sksl}); |
216 | } else { |
217 | FML_LOG(ERROR) << "Failed to load: " << item.name.GetString(); |
218 | } |
219 | } |
220 | } |
221 | } |
222 | |
223 | return result; |
224 | } |
225 | |
226 | PersistentCache::PersistentCache(bool read_only) |
227 | : is_read_only_(read_only), |
228 | cache_directory_(MakeCacheDirectory(cache_base_path_, read_only, false)), |
229 | sksl_cache_directory_( |
230 | MakeCacheDirectory(cache_base_path_, read_only, true)) { |
231 | if (!IsValid()) { |
232 | FML_LOG(WARNING) << "Could not acquire the persistent cache directory. " |
233 | "Caching of GPU resources on disk is disabled." ; |
234 | } |
235 | } |
236 | |
237 | PersistentCache::~PersistentCache() = default; |
238 | |
239 | bool PersistentCache::IsValid() const { |
240 | return cache_directory_ && cache_directory_->is_valid(); |
241 | } |
242 | |
243 | sk_sp<SkData> PersistentCache::LoadFile(const fml::UniqueFD& dir, |
244 | const std::string& file_name) { |
245 | auto file = fml::OpenFileReadOnly(dir, file_name.c_str()); |
246 | if (!file.is_valid()) { |
247 | return nullptr; |
248 | } |
249 | auto mapping = std::make_unique<fml::FileMapping>(file); |
250 | if (mapping->GetSize() == 0) { |
251 | return nullptr; |
252 | } |
253 | return SkData::MakeWithCopy(mapping->GetMapping(), mapping->GetSize()); |
254 | } |
255 | |
256 | // |GrContextOptions::PersistentCache| |
257 | sk_sp<SkData> PersistentCache::load(const SkData& key) { |
258 | TRACE_EVENT0("flutter" , "PersistentCacheLoad" ); |
259 | if (!IsValid()) { |
260 | return nullptr; |
261 | } |
262 | auto file_name = SkKeyToFilePath(key); |
263 | if (file_name.size() == 0) { |
264 | return nullptr; |
265 | } |
266 | auto result = PersistentCache::LoadFile(*cache_directory_, file_name); |
267 | if (result != nullptr) { |
268 | TRACE_EVENT0("flutter" , "PersistentCacheLoadHit" ); |
269 | } |
270 | return result; |
271 | } |
272 | |
273 | static void PersistentCacheStore(fml::RefPtr<fml::TaskRunner> worker, |
274 | std::shared_ptr<fml::UniqueFD> cache_directory, |
275 | std::string key, |
276 | std::unique_ptr<fml::Mapping> value) { |
277 | auto task = |
278 | fml::MakeCopyable([cache_directory, // |
279 | file_name = std::move(key), // |
280 | mapping = std::move(value) // |
281 | ]() mutable { |
282 | TRACE_EVENT0("flutter" , "PersistentCacheStore" ); |
283 | if (!fml::WriteAtomically(*cache_directory, // |
284 | file_name.c_str(), // |
285 | *mapping) // |
286 | ) { |
287 | FML_DLOG(WARNING) |
288 | << "Could not write cache contents to persistent store." ; |
289 | } |
290 | }); |
291 | |
292 | if (!worker) { |
293 | FML_LOG(WARNING) |
294 | << "The persistent cache has no available workers. Performing the task " |
295 | "on the current thread. This slow operation is going to occur on a " |
296 | "frame workload." ; |
297 | task(); |
298 | } else { |
299 | worker->PostTask(std::move(task)); |
300 | } |
301 | } |
302 | |
303 | // |GrContextOptions::PersistentCache| |
304 | void PersistentCache::store(const SkData& key, const SkData& data) { |
305 | stored_new_shaders_ = true; |
306 | |
307 | if (is_read_only_) { |
308 | return; |
309 | } |
310 | |
311 | if (!IsValid()) { |
312 | return; |
313 | } |
314 | |
315 | auto file_name = SkKeyToFilePath(key); |
316 | |
317 | if (file_name.size() == 0) { |
318 | return; |
319 | } |
320 | |
321 | auto mapping = std::make_unique<fml::DataMapping>( |
322 | std::vector<uint8_t>{data.bytes(), data.bytes() + data.size()}); |
323 | |
324 | if (mapping == nullptr || mapping->GetSize() == 0) { |
325 | return; |
326 | } |
327 | |
328 | PersistentCacheStore(GetWorkerTaskRunner(), |
329 | cache_sksl_ ? sksl_cache_directory_ : cache_directory_, |
330 | std::move(file_name), std::move(mapping)); |
331 | } |
332 | |
333 | void PersistentCache::DumpSkp(const SkData& data) { |
334 | if (is_read_only_ || !IsValid()) { |
335 | FML_LOG(ERROR) << "Could not dump SKP from read-only or invalid persistent " |
336 | "cache." ; |
337 | return; |
338 | } |
339 | |
340 | std::stringstream name_stream; |
341 | auto ticks = fml::TimePoint::Now().ToEpochDelta().ToNanoseconds(); |
342 | name_stream << "shader_dump_" << std::to_string(ticks) << ".skp" ; |
343 | std::string file_name = name_stream.str(); |
344 | FML_LOG(INFO) << "Dumping " << file_name; |
345 | auto mapping = std::make_unique<fml::DataMapping>( |
346 | std::vector<uint8_t>{data.bytes(), data.bytes() + data.size()}); |
347 | PersistentCacheStore(GetWorkerTaskRunner(), cache_directory_, |
348 | std::move(file_name), std::move(mapping)); |
349 | } |
350 | |
351 | void PersistentCache::AddWorkerTaskRunner( |
352 | fml::RefPtr<fml::TaskRunner> task_runner) { |
353 | std::scoped_lock lock(worker_task_runners_mutex_); |
354 | worker_task_runners_.insert(task_runner); |
355 | } |
356 | |
357 | void PersistentCache::RemoveWorkerTaskRunner( |
358 | fml::RefPtr<fml::TaskRunner> task_runner) { |
359 | std::scoped_lock lock(worker_task_runners_mutex_); |
360 | auto found = worker_task_runners_.find(task_runner); |
361 | if (found != worker_task_runners_.end()) { |
362 | worker_task_runners_.erase(found); |
363 | } |
364 | } |
365 | |
366 | fml::RefPtr<fml::TaskRunner> PersistentCache::GetWorkerTaskRunner() const { |
367 | fml::RefPtr<fml::TaskRunner> worker; |
368 | |
369 | std::scoped_lock lock(worker_task_runners_mutex_); |
370 | if (!worker_task_runners_.empty()) { |
371 | worker = *worker_task_runners_.begin(); |
372 | } |
373 | |
374 | return worker; |
375 | } |
376 | |
377 | void PersistentCache::SetAssetManager(std::shared_ptr<AssetManager> value) { |
378 | TRACE_EVENT_INSTANT0("flutter" , "PersistentCache::SetAssetManager" ); |
379 | asset_manager_ = value; |
380 | } |
381 | |
382 | } // namespace flutter |
383 | |