| // Copyright 2018 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| #include "content/browser/code_cache/generated_code_cache_context.h" |
| |
| #include <stdint.h> |
| |
| #include <memory> |
| #include <optional> |
| #include <string> |
| |
| #include "base/barrier_closure.h" |
| #include "base/containers/span.h" |
| #include "base/feature_list.h" |
| #include "base/files/file_path.h" |
| #include "base/files/file_util.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/location.h" |
| #include "base/memory/ref_counted.h" |
| #include "base/memory/scoped_refptr.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/numerics/safe_conversions.h" |
| #include "base/sequence_checker.h" |
| #include "base/system/sys_info.h" |
| #include "base/task/bind_post_task.h" |
| #include "base/task/sequenced_task_runner.h" |
| #include "base/task/single_thread_task_runner_thread_mode.h" |
| #include "base/task/task_traits.h" |
| #include "base/task/thread_pool.h" |
| #include "base/types/expected_macros.h" |
| #include "build/build_config.h" |
| #include "content/browser/code_cache/generated_code_cache.h" |
| #include "content/public/browser/browser_task_traits.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/common/content_features.h" |
| #include "mojo/public/cpp/base/big_buffer.h" |
| #include "net/base/cache_type.h" |
| #include "net/disk_cache/cache_util.h" |
| #include "net/disk_cache/disk_cache.h" |
| #include "net/http/http_cache.h" |
| #include "third_party/blink/public/common/features.h" |
| |
| #if !BUILDFLAG(IS_FUCHSIA) |
| #include "components/persistent_cache/client.h" |
| #include "components/persistent_cache/entry_metadata.h" |
| #include "components/persistent_cache/pending_backend.h" |
| #include "components/persistent_cache/persistent_cache_collection.h" |
| #include "components/persistent_cache/transaction_error.h" |
| #endif |
| |
| namespace content { |
| |
| #if !BUILDFLAG(IS_FUCHSIA) |
| namespace { |
| |
| std::unique_ptr<persistent_cache::PersistentCacheCollection> |
| MakePersistentCacheCollection( |
| int max_bytes, |
| const base::FilePath& disk_cache_path, |
| const base::FilePath& persistent_cache_collection_path) { |
| int64_t disk_cache_max_size = |
| max_bytes > 0 ? max_bytes |
| : disk_cache::PreferredCacheSize( |
| base::SysInfo::AmountOfFreeDiskSpace(disk_cache_path) |
| .value_or(-1), |
| net::GENERATED_BYTE_CODE_CACHE); |
| |
| return std::make_unique<persistent_cache::PersistentCacheCollection>( |
| persistent_cache_collection_path, disk_cache_max_size, |
| persistent_cache::Client::kCodeCache); |
| } |
| |
| } // namespace |
| #endif // !BUILDFLAG(IS_FUCHSIA) |
| |
| // static |
| void GeneratedCodeCacheContext::RunOrPostTask( |
| scoped_refptr<GeneratedCodeCacheContext> context, |
| const base::Location& location, |
| base::OnceClosure task) { |
| if (!context || context->task_runner_->RunsTasksInCurrentSequence()) { |
| std::move(task).Run(); |
| return; |
| } |
| |
| context->task_runner_->PostTask(location, std::move(task)); |
| } |
| |
| // static |
| scoped_refptr<base::SequencedTaskRunner> |
| GeneratedCodeCacheContext::GetTaskRunner( |
| scoped_refptr<GeneratedCodeCacheContext> context) { |
| if (!context) |
| return base::SequencedTaskRunner::GetCurrentDefault(); |
| return context->task_runner_; |
| } |
| |
| GeneratedCodeCacheContext::GeneratedCodeCacheContext() { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| DETACH_FROM_SEQUENCE(sequence_checker_); |
| } |
| |
| void GeneratedCodeCacheContext::Initialize(const base::FilePath& path, |
| int max_bytes) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| CHECK(!task_runner_); // Only initialize once. |
| |
| if (blink::features::IsPersistentCacheForCodeCacheEnabled() || |
| blink::features::IsInlineScriptCacheEnabled()) { |
| // Use a SequencedTaskRunner tied to the path resource. This ensures that if |
| // this StoragePartition is destroyed and recreated with the same path, |
| // operations (including deletions of old files and creation of new ones) |
| // are strictly sequenced, avoiding race conditions between distinct |
| // instances. MayBlock() because disk operations are happening on-thread |
| // under the experiment for now. |
| task_runner_for_resource_ = DedicatedTaskRunnerForResource::Acquire( |
| {base::TaskPriority::USER_BLOCKING, base::MayBlock()}, path); |
| task_runner_ = task_runner_for_resource_.task_runner(); |
| } else { |
| task_runner_ = base::ThreadPool::CreateSingleThreadTaskRunner( |
| {base::TaskPriority::USER_BLOCKING}); |
| } |
| RunOrPostTask(this, FROM_HERE, |
| base::BindOnce(&GeneratedCodeCacheContext::InitializeOnThread, |
| this, path, max_bytes)); |
| } |
| |
| void GeneratedCodeCacheContext::InitializeOnThread(const base::FilePath& path, |
| int max_bytes) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| int max_bytes_js = max_bytes; |
| |
| base::FilePath generated_js_code_cache_path = path.AppendASCII("js"); |
| base::FilePath webui_js_code_cache_path = path.AppendASCII("webui_js"); |
| base::FilePath generated_wasm_code_cache_path = path.AppendASCII("wasm"); |
| #if !BUILDFLAG(IS_FUCHSIA) |
| // Use a short name for the root directory due to max path length limits. |
| base::FilePath persistent_cache_collection_path = path.AppendASCII("pc"); |
| #endif // !BUILDFLAG(IS_FUCHSIA) |
| |
| const bool replace_by_persistent_cache = |
| blink::features::IsPersistentCacheForCodeCacheEnabled(); |
| if (!replace_by_persistent_cache) { |
| if (base::FeatureList::IsEnabled(features::kWebUICodeCache)) { |
| int max_bytes_webui_js = max_bytes; |
| if (max_bytes > 0) { |
| // If a maximum was specified, then we should limit the total JS |
| // bytecode, both from WebUI and from open web sites, to max_bytes. The |
| // larger portion by far should be reserved for open web sites. |
| const int kMaxWebUIPercent = 2; |
| max_bytes_webui_js = |
| std::min(base::saturated_cast<int>(static_cast<int64_t>(max_bytes) * |
| kMaxWebUIPercent / 100), |
| disk_cache::kMaxWebUICodeCacheSize); |
| |
| // The rest is left over for open web JS. |
| max_bytes_js = max_bytes - max_bytes_webui_js; |
| DCHECK_GT(max_bytes_js, max_bytes_webui_js); |
| |
| // Specifying a maximum size of zero means to use heuristics based on |
| // available disk size, which would be the opposite of our intent if the |
| // specified number was so small that the division above truncated to |
| // zero. |
| if (max_bytes_webui_js == 0) { |
| max_bytes_webui_js = 1; |
| } |
| } |
| |
| generated_webui_js_code_cache_ = std::make_unique<GeneratedCodeCache>( |
| webui_js_code_cache_path, max_bytes_webui_js, |
| GeneratedCodeCache::CodeCacheType::kWebUIJavaScript); |
| |
| UMA_HISTOGRAM_BOOLEAN("WebUICodeCache.FeatureEnabled", true); |
| } |
| |
| generated_js_code_cache_ = std::make_unique<GeneratedCodeCache>( |
| generated_js_code_cache_path, max_bytes_js, |
| GeneratedCodeCache::CodeCacheType::kJavaScript); |
| |
| generated_wasm_code_cache_ = std::make_unique<GeneratedCodeCache>( |
| generated_wasm_code_cache_path, max_bytes, |
| GeneratedCodeCache::CodeCacheType::kWebAssembly); |
| |
| #if !BUILDFLAG(IS_FUCHSIA) |
| if (blink::features::IsInlineScriptCacheEnabled()) { |
| persistent_cache_collection_ = MakePersistentCacheCollection( |
| max_bytes_js, path, persistent_cache_collection_path); |
| } else { |
| // Delete the PersistentCache files that won't be used to avoid wasting |
| // space. |
| base::ThreadPool::CreateSequencedTaskRunner({base::MayBlock()}) |
| ->PostTask( |
| FROM_HERE, |
| base::BindOnce(base::IgnoreResult(base::DeletePathRecursively), |
| persistent_cache_collection_path)); |
| } |
| #endif // !BUILDFLAG(IS_FUCHSIA) |
| } else { |
| #if !BUILDFLAG(IS_FUCHSIA) |
| persistent_cache_collection_ = MakePersistentCacheCollection( |
| max_bytes, path, persistent_cache_collection_path); |
| |
| // Delete the GeneratedCodeCache files that won't be used to avoid |
| // wasting space. |
| base::ThreadPool::CreateSequencedTaskRunner({base::MayBlock()}) |
| ->PostTask(FROM_HERE, |
| base::BindOnce( |
| [](const base::FilePath& js_path, |
| const base::FilePath& webui_js_path, |
| const base::FilePath& wasm_path) { |
| base::DeletePathRecursively(js_path); |
| base::DeletePathRecursively(webui_js_path); |
| base::DeletePathRecursively(wasm_path); |
| }, |
| generated_js_code_cache_path, webui_js_code_cache_path, |
| generated_wasm_code_cache_path)); |
| #else // !BUILDFLAG(IS_FUCHSIA) |
| NOTREACHED(); |
| #endif // !BUILDFLAG(IS_FUCHSIA) |
| } |
| } |
| |
| void GeneratedCodeCacheContext::Shutdown() { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| RunOrPostTask(this, FROM_HERE, |
| base::BindOnce(&GeneratedCodeCacheContext::ShutdownOnThread, |
| this, std::move(task_runner_for_resource_))); |
| task_runner_.reset(); |
| } |
| |
| void GeneratedCodeCacheContext::ShutdownForTesting(base::OnceClosure callback) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| auto ui_callback = base::BindPostTaskToCurrentDefault(std::move(callback)); |
| |
| RunOrPostTask( |
| this, FROM_HERE, |
| base::BindOnce(&GeneratedCodeCacheContext::ShutdownOnThreadForTesting, |
| this, std::move(ui_callback), |
| std::move(task_runner_for_resource_))); |
| task_runner_.reset(); |
| } |
| |
| void GeneratedCodeCacheContext::ClearAndDeletePersistentCacheCollection() { |
| #if !BUILDFLAG(IS_FUCHSIA) |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (persistent_cache_collection_) { |
| persistent_cache_collection_->DeleteAllFiles(); |
| } |
| #endif |
| } |
| |
| #if !BUILDFLAG(IS_FUCHSIA) |
| std::optional<persistent_cache::PendingBackend> |
| GeneratedCodeCacheContext::ShareReadOnlyConnection( |
| const std::string& context_key) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (persistent_cache_collection_) { |
| return persistent_cache_collection_->ShareReadOnlyConnection(context_key); |
| } |
| |
| return std::nullopt; |
| } |
| |
| void GeneratedCodeCacheContext::InsertIntoPersistentCacheCollection( |
| const std::string& context_key, |
| base::span<const uint8_t> cache_key, |
| base::span<const uint8_t> content, |
| persistent_cache::EntryMetadata metadata) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (!persistent_cache_collection_) { |
| return; |
| } |
| |
| RETURN_IF_ERROR(persistent_cache_collection_->Insert(context_key, cache_key, |
| content, metadata), |
| [](persistent_cache::TransactionError error) { |
| // TODO(crbug.com/374930286): Handle or at least address |
| // permanent errors. |
| return; |
| }); |
| } |
| |
| std::optional<GeneratedCodeCacheContext::MetadataAndContent> |
| GeneratedCodeCacheContext::FindInPersistentCacheCollection( |
| const std::string& context_key, |
| base::span<const uint8_t> cache_key) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (!persistent_cache_collection_) { |
| return std::nullopt; |
| } |
| |
| mojo_base::BigBuffer content_buffer; |
| |
| // A BufferProvider for PersistentCache that puts a new mojo_base::BigBuffer |
| // in `content_buffer` to hold an entry's content and returns a view into it. |
| auto buffer_provider = [&content_buffer](size_t content_size) { |
| content_buffer = mojo_base::BigBuffer(content_size); |
| return base::span(content_buffer); |
| }; |
| |
| ASSIGN_OR_RETURN(std::optional<persistent_cache::EntryMetadata> metadata, |
| persistent_cache_collection_->Find( |
| context_key, cache_key, std::move(buffer_provider)), |
| // An adapter that is invoked on error. Its return value |
| // percolates up out of this function. |
| [](persistent_cache::TransactionError error) |
| -> std::optional<MetadataAndContent> { |
| // TODO(crbug.com/374930286): Handle or at least address |
| // permanent errors. |
| return std::nullopt; |
| }); |
| |
| if (!metadata.has_value()) { |
| return std::nullopt; // Cache miss. |
| } |
| |
| // Cache hit. |
| return MetadataAndContent{*std::move(metadata), std::move(content_buffer)}; |
| } |
| #endif // !BUILDFLAG(IS_FUCHSIA) |
| |
| void GeneratedCodeCacheContext::ShutdownOnThread( |
| DedicatedTaskRunnerForResource task_runner_for_resource) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| #if !BUILDFLAG(IS_FUCHSIA) |
| persistent_cache_collection_.reset(); |
| #endif // !BUILDFLAG(IS_FUCHSIA) |
| generated_js_code_cache_.reset(); |
| generated_wasm_code_cache_.reset(); |
| generated_webui_js_code_cache_.reset(); |
| } |
| |
| void GeneratedCodeCacheContext::ShutdownOnThreadForTesting( // IN-TEST |
| base::OnceClosure callback, |
| DedicatedTaskRunnerForResource task_runner_for_resource) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| std::vector<GeneratedCodeCache*> caches_to_wait; |
| if (generated_js_code_cache_) { |
| caches_to_wait.push_back(generated_js_code_cache_.get()); |
| } |
| if (generated_wasm_code_cache_) { |
| caches_to_wait.push_back(generated_wasm_code_cache_.get()); |
| } |
| if (generated_webui_js_code_cache_) { |
| caches_to_wait.push_back(generated_webui_js_code_cache_.get()); |
| } |
| |
| if (caches_to_wait.empty()) { |
| ShutdownOnThread(std::move(task_runner_for_resource)); |
| std::move(callback).Run(); |
| return; |
| } |
| |
| #if !BUILDFLAG(IS_FUCHSIA) |
| persistent_cache_collection_.reset(); |
| #endif // !BUILDFLAG(IS_FUCHSIA) |
| |
| // A callback to be run once all GeneratedCodeCache instances have completely |
| // shut down their backends. |
| auto on_cleanup_complete = base::BindOnce( |
| [](scoped_refptr<GeneratedCodeCacheContext> self, |
| base::OnceClosure callback, |
| DedicatedTaskRunnerForResource task_runner_for_resource) { |
| self->ShutdownOnThread(std::move(task_runner_for_resource)); |
| std::move(callback).Run(); |
| }, |
| base::WrapRefCounted(this), std::move(callback), |
| std::move(task_runner_for_resource)); |
| |
| auto barrier = base::BarrierClosure(caches_to_wait.size(), |
| std::move(on_cleanup_complete)); |
| for (auto* cache : caches_to_wait) { |
| cache->ShutdownForTesting(barrier); // IN-TEST |
| } |
| } |
| |
| GeneratedCodeCache* GeneratedCodeCacheContext::generated_js_code_cache() const { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| return generated_js_code_cache_.get(); |
| } |
| |
| GeneratedCodeCache* GeneratedCodeCacheContext::generated_wasm_code_cache() |
| const { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| return generated_wasm_code_cache_.get(); |
| } |
| |
| GeneratedCodeCache* GeneratedCodeCacheContext::generated_webui_js_code_cache() |
| const { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| return generated_webui_js_code_cache_.get(); |
| } |
| |
| GeneratedCodeCacheContext::~GeneratedCodeCacheContext() = default; |
| |
| } // namespace content |