1 // Copyright 2024 The Chromium Authors
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 "components/metrics/structured/flushed_map.h"
6
7 #include <algorithm>
8 #include <cstdint>
9 #include <optional>
10 #include <unordered_set>
11
12 #include "base/files/file_enumerator.h"
13 #include "base/files/file_util.h"
14 #include "base/functional/bind.h"
15 #include "base/location.h"
16 #include "base/logging.h"
17 #include "base/system/sys_info.h"
18 #include "base/task/bind_post_task.h"
19 #include "base/task/sequenced_task_runner.h"
20 #include "base/task/task_traits.h"
21 #include "base/task/thread_pool.h"
22 #include "base/time/time.h"
23 #include "base/types/expected.h"
24 #include "base/uuid.h"
25 #include "components/metrics/structured/lib/resource_info.h"
26 #include "components/metrics/structured/proto/event_storage.pb.h"
27
28 namespace metrics::structured {
29
FlushedMap(const base::FilePath & flushed_dir,uint64_t max_size_bytes)30 FlushedMap::FlushedMap(const base::FilePath& flushed_dir,
31 uint64_t max_size_bytes)
32 : flushed_dir_(flushed_dir),
33 resource_info_(max_size_bytes),
34 task_runner_(base::ThreadPool::CreateSequencedTaskRunner(
35 {base::TaskPriority::BEST_EFFORT, base::MayBlock(),
36 base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN})) {
37 if (!base::CreateDirectory(flushed_dir_)) {
38 LOG(ERROR) << "Failed to created directory for flushed events: "
39 << flushed_dir_;
40 return;
41 }
42
43 LoadKeysFromDir(flushed_dir_);
44 }
45
46 FlushedMap::~FlushedMap() = default;
47
Purge()48 void FlushedMap::Purge() {
49 for (const FlushedKey& key : keys_) {
50 if (!base::DeleteFile(base::FilePath(key.path))) {
51 LOG(ERROR) << "Failed to delete key: " << key.path;
52 }
53 }
54 keys_.clear();
55 }
56
Flush(EventBuffer<StructuredEventProto> & buffer,FlushedCallback callback)57 void FlushedMap::Flush(EventBuffer<StructuredEventProto>& buffer,
58 FlushedCallback callback) {
59 // Generate the new path to flush the buffer to.
60 const base::FilePath path = GenerateFilePath();
61
62 // Flush the buffer. This depends on the implementation of |buffer|.
63 buffer.Flush(path,
64 base::BindPostTask(task_runner_,
65 base::BindOnce(&FlushedMap::OnFlushed,
66 weak_factory_.GetWeakPtr(),
67 std::move(callback))));
68 }
69
ReadKey(const FlushedKey & key) const70 std::optional<EventsProto> FlushedMap::ReadKey(const FlushedKey& key) const {
71 std::string content;
72 if (!base::ReadFileToString(key.path, &content)) {
73 LOG(ERROR) << "Failed to read flushed key: " << key.path;
74 return std::nullopt;
75 }
76
77 EventsProto events;
78 if (!events.MergeFromString(content)) {
79 LOG(ERROR)
80 << "Failed to load events stored at path: " << key.path
81 << ". This probably means the content isn't an EventsProto proto.";
82 events.Clear();
83 }
84 return events;
85 }
86
DeleteKey(const FlushedKey & key)87 void FlushedMap::DeleteKey(const FlushedKey& key) {
88 // If not on |task_runner_| then post to that task.
89 if (!task_runner_->RunsTasksInCurrentSequence()) {
90 task_runner_->PostTask(FROM_HERE,
91 base::BindOnce(&FlushedMap::DeleteKey,
92 weak_factory_.GetWeakPtr(), key));
93 return;
94 }
95
96 // Lambda is safe because it is used only within this scope.
97 auto elem = std::find_if(
98 keys_.begin(), keys_.end(),
99 [&key](const FlushedKey& elem) { return elem.path == key.path; });
100
101 if (elem == keys_.end()) {
102 LOG(ERROR) << "Attempting to delete a key that doesn't exist: " << key.path;
103 return;
104 }
105
106 keys_.erase(elem);
107 resource_info_.used_size_bytes -= key.size;
108 base::DeleteFile(key.path);
109 }
110
DeleteKeys(const std::vector<FlushedKey> & keys)111 void FlushedMap::DeleteKeys(const std::vector<FlushedKey>& keys) {
112 // If not on |task_runner_| then post to that task.
113 if (!task_runner_->RunsTasksInCurrentSequence()) {
114 task_runner_->PostTask(FROM_HERE,
115 base::BindOnce(&FlushedMap::DeleteKeys,
116 weak_factory_.GetWeakPtr(), keys));
117 return;
118 }
119
120 for (const FlushedKey& key : keys) {
121 DeleteKey(key);
122 }
123 }
124
GenerateFilePath() const125 base::FilePath FlushedMap::GenerateFilePath() const {
126 return flushed_dir_.Append(
127 base::Uuid::GenerateRandomV4().AsLowercaseString());
128 }
129
LoadKeysFromDir(const base::FilePath & dir)130 void FlushedMap::LoadKeysFromDir(const base::FilePath& dir) {
131 task_runner_->PostTask(FROM_HERE,
132 base::BindOnce(&FlushedMap::BuildKeysFromDir,
133 weak_factory_.GetWeakPtr(), dir));
134 }
135
BuildKeysFromDir(const base::FilePath & dir)136 void FlushedMap::BuildKeysFromDir(const base::FilePath& dir) {
137 // This loads all paths of |dir| into memory.
138 base::FileEnumerator file_enumerator(dir, /*recursive=*/false,
139 base::FileEnumerator::FileType::FILES);
140
141 // Iterate over all files in this directory. All files should be
142 // FlushedEvents.
143 file_enumerator.ForEach([this](const base::FilePath& path) {
144 base::File::Info info;
145 if (!base::GetFileInfo(path, &info)) {
146 LOG(ERROR) << "Failed to get file info for " << path;
147 return;
148 }
149
150 FlushedKey key;
151 key.path = path;
152 key.size = info.size;
153 key.creation_time = info.creation_time;
154
155 // Update the amount of space consumed. We should always be below quota. If
156 // not, then the next flush will resolve it.
157 resource_info_.Consume(key.size);
158
159 keys_.push_back(key);
160 });
161
162 std::sort(keys_.begin(), keys_.end(),
163 [](const FlushedKey& l, const FlushedKey& r) {
164 return l.creation_time < r.creation_time;
165 });
166 }
167
OnFlushed(FlushedCallback callback,base::expected<FlushedKey,FlushError> key)168 void FlushedMap::OnFlushed(FlushedCallback callback,
169 base::expected<FlushedKey, FlushError> key) {
170 if (!key.has_value()) {
171 LOG(ERROR) << "Flush failed with error: " << static_cast<int>(key.error());
172 std::move(callback).Run(key);
173 return;
174 }
175
176 // Add the key to |keys_| to keep track of it.
177 keys_.push_back(*key);
178
179 // Check if we have room to contain this data.
180 const bool under_quota = resource_info_.HasRoom(key->size);
181
182 // We have flushed at this point, the resources needs to be consumed.
183 resource_info_.Consume(key->size);
184
185 // Notify the call that this flush makes use exceed the allotted quota.
186 // The key is not returned in this case because it is added to |keys_| above.
187 if (under_quota) {
188 std::move(callback).Run(key);
189 } else {
190 std::move(callback).Run(base::unexpected(kQuotaExceeded));
191 }
192 }
193
194 } // namespace metrics::structured
195