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