1 // Copyright 2021 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/external_metrics.h"
6
7 #include <errno.h>
8 #include <sys/file.h>
9 #include <sys/stat.h>
10
11 #include <string_view>
12
13 #include "base/containers/fixed_flat_set.h"
14 #include "base/files/dir_reader_posix.h"
15 #include "base/files/file.h"
16 #include "base/files/file_util.h"
17 #include "base/files/scoped_file.h"
18 #include "base/logging.h"
19 #include "base/strings/string_number_conversions.h"
20 #include "base/strings/string_split.h"
21 #include "base/task/sequenced_task_runner.h"
22 #include "base/task/task_traits.h"
23 #include "base/task/thread_pool.h"
24 #include "base/threading/scoped_blocking_call.h"
25 #include "components/metrics/structured/histogram_util.h"
26 #include "components/metrics/structured/proto/event_storage.pb.h"
27 #include "components/metrics/structured/structured_metrics_features.h"
28
29 namespace metrics::structured {
30 namespace {
31
FilterEvents(google::protobuf::RepeatedPtrField<metrics::StructuredEventProto> * events,const base::flat_set<uint64_t> & disallowed_projects)32 void FilterEvents(
33 google::protobuf::RepeatedPtrField<metrics::StructuredEventProto>* events,
34 const base::flat_set<uint64_t>& disallowed_projects) {
35 auto it = events->begin();
36 while (it != events->end()) {
37 if (disallowed_projects.contains(it->project_name_hash())) {
38 it = events->erase(it);
39 } else {
40 ++it;
41 }
42 }
43 }
44
45 // This function assumes that a LOCK_EX has been obtained for file descriptor at
46 // |path|.
DeleteFileAndUnlock(const base::FilePath & path,const base::ScopedFD & fd)47 void DeleteFileAndUnlock(const base::FilePath& path, const base::ScopedFD& fd) {
48 bool delete_result = base::DeleteFile(path);
49 if (!delete_result) {
50 LOG(ERROR) << "Failed to unlink event file " << path.value();
51 }
52 int result = flock(fd.get(), LOCK_UN);
53 if (result < 0) {
54 PLOG(ERROR) << "Failed to unlock for event file " << path.value();
55 }
56 }
57
Platform2ProjectName(uint64_t project_name_hash)58 std::string_view Platform2ProjectName(uint64_t project_name_hash) {
59 switch (project_name_hash) {
60 case UINT64_C(827233605053062635):
61 return "AudioPeripheral";
62 case UINT64_C(524369188505453537):
63 return "AudioPeripheralInfo";
64 case UINT64_C(9074739597929991885):
65 return "Bluetooth";
66 case UINT64_C(1745381000935843040):
67 return "BluetoothDevice";
68 case UINT64_C(11181229631788078243):
69 return "BluetoothChipset";
70 case UINT64_C(8206859287963243715):
71 return "Cellular";
72 case UINT64_C(11294265225635075664):
73 return "HardwareVerifier";
74 case UINT64_C(4905803635010729907):
75 return "RollbackEnterprise";
76 case UINT64_C(9675127341789951965):
77 return "Rmad";
78 case UINT64_C(4690103929823698613):
79 return "WiFiChipset";
80 case UINT64_C(17922303533051575891):
81 return "UsbDevice";
82 case UINT64_C(1370722622176744014):
83 return "UsbError";
84 case UINT64_C(17319042894491683836):
85 return "UsbPdDevice";
86 case UINT64_C(6962789877417678651):
87 return "UsbSession";
88 case UINT64_C(4320592646346933548):
89 return "WiFi";
90 case UINT64_C(7302676440391025918):
91 return "WiFiAP";
92 default:
93 return "UNKNOWN";
94 }
95 }
96
IncrementProjectCount(base::flat_map<uint64_t,int> & project_count_map,uint64_t project_name_hash)97 void IncrementProjectCount(base::flat_map<uint64_t, int>& project_count_map,
98 uint64_t project_name_hash) {
99 if (project_count_map.contains(project_name_hash)) {
100 project_count_map[project_name_hash] += 1;
101 } else {
102 project_count_map[project_name_hash] = 1;
103 }
104 }
105
ProcessEventProtosProjectCounts(base::flat_map<uint64_t,int> & project_count_map,const EventsProto & proto)106 void ProcessEventProtosProjectCounts(
107 base::flat_map<uint64_t, int>& project_count_map,
108 const EventsProto& proto) {
109 // Process all events that were packed in the proto.
110 for (const auto& event : proto.uma_events()) {
111 IncrementProjectCount(project_count_map, event.project_name_hash());
112 }
113
114 for (const auto& event : proto.events()) {
115 IncrementProjectCount(project_count_map, event.project_name_hash());
116 }
117 }
118
FilterProto(EventsProto * proto,const base::flat_set<uint64_t> & disallowed_projects)119 bool FilterProto(EventsProto* proto,
120 const base::flat_set<uint64_t>& disallowed_projects) {
121 FilterEvents(proto->mutable_uma_events(), disallowed_projects);
122 FilterEvents(proto->mutable_events(), disallowed_projects);
123 return proto->uma_events_size() > 0 || proto->events_size() > 0;
124 }
125
126 // See header comments on CollectEvents() for more details.
ReadAndDeleteEvents(const base::FilePath & directory,const base::flat_set<uint64_t> & disallowed_projects,bool recording_enabled)127 EventsProto ReadAndDeleteEvents(
128 const base::FilePath& directory,
129 const base::flat_set<uint64_t>& disallowed_projects,
130 bool recording_enabled) {
131 base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
132 base::BlockingType::MAY_BLOCK);
133 EventsProto result;
134 if (!base::DirectoryExists(directory)) {
135 return result;
136 }
137
138 base::DirReaderPosix dir_reader(directory.value().c_str());
139 if (!dir_reader.IsValid()) {
140 VLOG(2) << "Failed to load External Metrics directory: " << directory;
141 return result;
142 }
143
144 int file_counter = 0;
145 int dropped_events = 0;
146 base::flat_map<uint64_t, int> dropped_projects_count, produced_projects_count;
147
148 while (dir_reader.Next()) {
149 base::FilePath path = directory.Append(dir_reader.name());
150 base::File file(path, base::File::FLAG_OPEN | base::File::FLAG_READ);
151
152 // This needs to be checked before calling GetInfo to prevent a crash.
153 if (!file.IsValid()) {
154 continue;
155 }
156
157 // Fetches file metadata.
158 base::File::Info info;
159 if (!file.GetInfo(&info)) {
160 continue;
161 }
162
163 if (info.is_directory) {
164 continue;
165 }
166
167 base::ScopedFD fd(open(path.value().c_str(), O_RDWR));
168 if (fd.get() < 0) {
169 LOG(ERROR) << "Failed to open event file " << path.value();
170 continue;
171 }
172
173 // Obtain the file lock.
174 int err = flock(fd.get(), LOCK_EX);
175 if (err < 0) {
176 PLOG(ERROR) << "Failed to get lock for event file " << path.value();
177 continue;
178 }
179
180 // If recording is disabled, delete the file before reading.
181 if (!recording_enabled) {
182 DeleteFileAndUnlock(path, fd);
183 continue;
184 }
185
186 ++file_counter;
187
188 std::string proto_str;
189 EventsProto proto;
190
191 LogEventFileSizeKB(static_cast<int>(info.size / 1024));
192
193 // If the file_size exceeds the limit, drop the payload.
194 if (info.size > GetFileSizeByteLimit()) {
195 LOG(ERROR)
196 << "Event file size exceeds the limit. Dropping events at file "
197 << path.value();
198 DeleteFileAndUnlock(path, fd);
199 continue;
200 }
201
202 bool read_ok = base::ReadFileToString(path, &proto_str) &&
203 proto.ParseFromString(proto_str);
204
205 // Delete the file regardless of whether the read succeeded or failed.
206 DeleteFileAndUnlock(path, fd);
207 if (!read_ok) {
208 LOG(ERROR) << "Failed to read and parse the file " << path.value();
209 continue;
210 }
211
212 // Process all events that were packed in the proto.
213 ProcessEventProtosProjectCounts(produced_projects_count, proto);
214
215 // There may be too many messages in the directory to hold in-memory.
216 // This could happen if the process in which Structured metrics resides
217 // is either crash-looping or taking too long to process externally
218 // recorded events.
219 if (file_counter > GetFileLimitPerScan()) {
220 ++dropped_events;
221
222 // Process all events that were packed in the proto.
223 ProcessEventProtosProjectCounts(dropped_projects_count, proto);
224 continue;
225 }
226
227 // Events will also be dropped if the project is not allowed to be recorded.
228 // FilterProto will return false if all events have been filtered out.
229 if (!FilterProto(&proto, disallowed_projects)) {
230 continue;
231 }
232
233 // MergeFrom performs a copy that could be a move if done manually. But
234 // all the protos here are expected to be small, so let's keep it simple.
235 result.mutable_uma_events()->MergeFrom(proto.uma_events());
236 result.mutable_events()->MergeFrom(proto.events());
237 }
238
239 if (recording_enabled) {
240 LogDroppedExternalMetrics(dropped_events);
241
242 // Log histograms for each project with their appropriate counts.
243 // If a project isn't seen then it will not be logged.
244 for (const auto& project_counts : produced_projects_count) {
245 LogProducedProjectExternalMetrics(
246 Platform2ProjectName(project_counts.first), project_counts.second);
247 }
248
249 for (const auto& project_counts : dropped_projects_count) {
250 LogDroppedProjectExternalMetrics(
251 Platform2ProjectName(project_counts.first), project_counts.second);
252 }
253 }
254
255 LogNumFilesPerExternalMetricsScan(file_counter);
256 return result;
257 }
258
259 } // namespace
260
ExternalMetrics(const base::FilePath & events_directory,const base::TimeDelta & collection_interval,MetricsCollectedCallback callback)261 ExternalMetrics::ExternalMetrics(const base::FilePath& events_directory,
262 const base::TimeDelta& collection_interval,
263 MetricsCollectedCallback callback)
264 : events_directory_(events_directory),
265 collection_interval_(collection_interval),
266 callback_(std::move(callback)),
267 task_runner_(base::ThreadPool::CreateSequencedTaskRunner(
268 {base::TaskPriority::BEST_EFFORT, base::MayBlock(),
269 base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN})) {
270 ScheduleCollector();
271 CacheDisallowedProjectsSet();
272 }
273
274 ExternalMetrics::~ExternalMetrics() = default;
275
CollectEventsAndReschedule()276 void ExternalMetrics::CollectEventsAndReschedule() {
277 CollectEvents();
278 ScheduleCollector();
279 }
280
ScheduleCollector()281 void ExternalMetrics::ScheduleCollector() {
282 base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
283 FROM_HERE,
284 base::BindOnce(&ExternalMetrics::CollectEventsAndReschedule,
285 weak_factory_.GetWeakPtr()),
286 collection_interval_);
287 }
288
CollectEvents()289 void ExternalMetrics::CollectEvents() {
290 task_runner_->PostTaskAndReplyWithResult(
291 FROM_HERE,
292 base::BindOnce(&ReadAndDeleteEvents, events_directory_,
293 disallowed_projects_, recording_enabled_),
294 base::BindOnce(callback_));
295 }
296
CacheDisallowedProjectsSet()297 void ExternalMetrics::CacheDisallowedProjectsSet() {
298 const std::string& disallowed_list = GetDisabledProjects();
299 if (disallowed_list.empty()) {
300 return;
301 }
302
303 for (const auto& value :
304 base::SplitString(disallowed_list, ",", base::TRIM_WHITESPACE,
305 base::SPLIT_WANT_NONEMPTY)) {
306 uint64_t project_name_hash;
307 // Parse the string and keep only perfect conversions.
308 if (base::StringToUint64(value, &project_name_hash)) {
309 disallowed_projects_.insert(project_name_hash);
310 }
311 }
312 }
313
AddDisallowedProjectForTest(uint64_t project_name_hash)314 void ExternalMetrics::AddDisallowedProjectForTest(uint64_t project_name_hash) {
315 disallowed_projects_.insert(project_name_hash);
316 }
317
EnableRecording()318 void ExternalMetrics::EnableRecording() {
319 recording_enabled_ = true;
320 }
321
DisableRecording()322 void ExternalMetrics::DisableRecording() {
323 recording_enabled_ = false;
324 }
325
326 } // namespace metrics::structured
327