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