// Copyright 2024 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "components/metrics/structured/flushed_map.h" #include #include #include #include #include "base/files/file_enumerator.h" #include "base/files/file_util.h" #include "base/functional/bind.h" #include "base/location.h" #include "base/logging.h" #include "base/system/sys_info.h" #include "base/task/bind_post_task.h" #include "base/task/sequenced_task_runner.h" #include "base/task/task_traits.h" #include "base/task/thread_pool.h" #include "base/time/time.h" #include "base/types/expected.h" #include "base/uuid.h" #include "components/metrics/structured/lib/resource_info.h" #include "components/metrics/structured/proto/event_storage.pb.h" namespace metrics::structured { FlushedMap::FlushedMap(const base::FilePath& flushed_dir, uint64_t max_size_bytes) : flushed_dir_(flushed_dir), resource_info_(max_size_bytes), task_runner_(base::ThreadPool::CreateSequencedTaskRunner( {base::TaskPriority::BEST_EFFORT, base::MayBlock(), base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN})) { if (!base::CreateDirectory(flushed_dir_)) { LOG(ERROR) << "Failed to created directory for flushed events: " << flushed_dir_; return; } LoadKeysFromDir(flushed_dir_); } FlushedMap::~FlushedMap() = default; void FlushedMap::Purge() { for (const FlushedKey& key : keys_) { if (!base::DeleteFile(base::FilePath(key.path))) { LOG(ERROR) << "Failed to delete key: " << key.path; } } keys_.clear(); } void FlushedMap::Flush(EventBuffer& buffer, FlushedCallback callback) { // Generate the new path to flush the buffer to. const base::FilePath path = GenerateFilePath(); // Flush the buffer. This depends on the implementation of |buffer|. buffer.Flush(path, base::BindPostTask(task_runner_, base::BindOnce(&FlushedMap::OnFlushed, weak_factory_.GetWeakPtr(), std::move(callback)))); } std::optional FlushedMap::ReadKey(const FlushedKey& key) const { std::string content; if (!base::ReadFileToString(key.path, &content)) { LOG(ERROR) << "Failed to read flushed key: " << key.path; return std::nullopt; } EventsProto events; if (!events.MergeFromString(content)) { LOG(ERROR) << "Failed to load events stored at path: " << key.path << ". This probably means the content isn't an EventsProto proto."; events.Clear(); } return events; } void FlushedMap::DeleteKey(const FlushedKey& key) { // If not on |task_runner_| then post to that task. if (!task_runner_->RunsTasksInCurrentSequence()) { task_runner_->PostTask(FROM_HERE, base::BindOnce(&FlushedMap::DeleteKey, weak_factory_.GetWeakPtr(), key)); return; } // Lambda is safe because it is used only within this scope. auto elem = std::find_if( keys_.begin(), keys_.end(), [&key](const FlushedKey& elem) { return elem.path == key.path; }); if (elem == keys_.end()) { LOG(ERROR) << "Attempting to delete a key that doesn't exist: " << key.path; return; } keys_.erase(elem); resource_info_.used_size_bytes -= key.size; base::DeleteFile(key.path); } void FlushedMap::DeleteKeys(const std::vector& keys) { // If not on |task_runner_| then post to that task. if (!task_runner_->RunsTasksInCurrentSequence()) { task_runner_->PostTask(FROM_HERE, base::BindOnce(&FlushedMap::DeleteKeys, weak_factory_.GetWeakPtr(), keys)); return; } for (const FlushedKey& key : keys) { DeleteKey(key); } } base::FilePath FlushedMap::GenerateFilePath() const { return flushed_dir_.Append( base::Uuid::GenerateRandomV4().AsLowercaseString()); } void FlushedMap::LoadKeysFromDir(const base::FilePath& dir) { task_runner_->PostTask(FROM_HERE, base::BindOnce(&FlushedMap::BuildKeysFromDir, weak_factory_.GetWeakPtr(), dir)); } void FlushedMap::BuildKeysFromDir(const base::FilePath& dir) { // This loads all paths of |dir| into memory. base::FileEnumerator file_enumerator(dir, /*recursive=*/false, base::FileEnumerator::FileType::FILES); // Iterate over all files in this directory. All files should be // FlushedEvents. file_enumerator.ForEach([this](const base::FilePath& path) { base::File::Info info; if (!base::GetFileInfo(path, &info)) { LOG(ERROR) << "Failed to get file info for " << path; return; } FlushedKey key; key.path = path; key.size = info.size; key.creation_time = info.creation_time; // Update the amount of space consumed. We should always be below quota. If // not, then the next flush will resolve it. resource_info_.Consume(key.size); keys_.push_back(key); }); std::sort(keys_.begin(), keys_.end(), [](const FlushedKey& l, const FlushedKey& r) { return l.creation_time < r.creation_time; }); } void FlushedMap::OnFlushed(FlushedCallback callback, base::expected key) { if (!key.has_value()) { LOG(ERROR) << "Flush failed with error: " << static_cast(key.error()); std::move(callback).Run(key); return; } // Add the key to |keys_| to keep track of it. keys_.push_back(*key); // Check if we have room to contain this data. const bool under_quota = resource_info_.HasRoom(key->size); // We have flushed at this point, the resources needs to be consumed. resource_info_.Consume(key->size); // Notify the call that this flush makes use exceed the allotted quota. // The key is not returned in this case because it is added to |keys_| above. if (under_quota) { std::move(callback).Run(key); } else { std::move(callback).Run(base::unexpected(kQuotaExceeded)); } } } // namespace metrics::structured