• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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