1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
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 "chrome/browser/chromeos/external_metrics.h"
6
7 #include <fcntl.h>
8 #include <stdio.h>
9 #include <stdlib.h>
10 #include <string.h>
11 #include <sys/file.h>
12 #include <sys/stat.h>
13 #include <sys/types.h>
14 #include <unistd.h>
15
16 #include <map>
17 #include <string>
18
19 #include "base/basictypes.h"
20 #include "base/bind.h"
21 #include "base/file_util.h"
22 #include "base/files/file_path.h"
23 #include "base/metrics/histogram.h"
24 #include "base/metrics/sparse_histogram.h"
25 #include "base/metrics/statistics_recorder.h"
26 #include "base/posix/eintr_wrapper.h"
27 #include "base/sys_info.h"
28 #include "base/time/time.h"
29 #include "base/timer/elapsed_timer.h"
30 #include "chrome/browser/browser_process.h"
31 #include "chrome/browser/metrics/metrics_service.h"
32 #include "content/public/browser/browser_thread.h"
33 #include "content/public/browser/user_metrics.h"
34
35 using content::BrowserThread;
36 using content::UserMetricsAction;
37
38 namespace chromeos {
39
40 namespace {
41
CheckValues(const std::string & name,int minimum,int maximum,size_t bucket_count)42 bool CheckValues(const std::string& name,
43 int minimum,
44 int maximum,
45 size_t bucket_count) {
46 if (!base::Histogram::InspectConstructionArguments(
47 name, &minimum, &maximum, &bucket_count))
48 return false;
49 base::HistogramBase* histogram =
50 base::StatisticsRecorder::FindHistogram(name);
51 if (!histogram)
52 return true;
53 return histogram->HasConstructionArguments(minimum, maximum, bucket_count);
54 }
55
CheckLinearValues(const std::string & name,int maximum)56 bool CheckLinearValues(const std::string& name, int maximum) {
57 return CheckValues(name, 1, maximum, maximum + 1);
58 }
59
60 // Establishes field trial for wifi scanning in chromeos. crbug.com/242733.
SetupProgressiveScanFieldTrial()61 void SetupProgressiveScanFieldTrial() {
62 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
63 const char name_of_experiment[] = "ProgressiveScan";
64 const base::FilePath group_file_path(
65 "/home/chronos/.progressive_scan_variation");
66 const base::FieldTrial::Probability kDivisor = 1000;
67 scoped_refptr<base::FieldTrial> trial =
68 base::FieldTrialList::FactoryGetFieldTrial(
69 name_of_experiment, kDivisor, "Default", 2013, 12, 31,
70 base::FieldTrial::SESSION_RANDOMIZED, NULL);
71
72 // Announce the groups with 0 percentage; the actual percentages come from
73 // the server configuration.
74 std::map<int, std::string> group_to_char;
75 group_to_char[trial->AppendGroup("FullScan", 0)] = "c";
76 group_to_char[trial->AppendGroup("33Percent_4MinMax", 0)] = "1";
77 group_to_char[trial->AppendGroup("50Percent_4MinMax", 0)] = "2";
78 group_to_char[trial->AppendGroup("50Percent_8MinMax", 0)] = "3";
79 group_to_char[trial->AppendGroup("100Percent_8MinMax", 0)] = "4";
80 group_to_char[trial->AppendGroup("100Percent_1MinSeen_A", 0)] = "5";
81 group_to_char[trial->AppendGroup("100Percent_1MinSeen_B", 0)] = "6";
82 group_to_char[trial->AppendGroup("100Percent_1Min_4Max", 0)] = "7";
83
84 // Announce the experiment to any listeners (especially important is the UMA
85 // software, which will append the group names to UMA statistics).
86 const int group_num = trial->group();
87 std::string group_char = "x";
88 if (ContainsKey(group_to_char, group_num))
89 group_char = group_to_char[group_num];
90
91 // Write the group to the file to be read by ChromeOS.
92 int size = static_cast<int>(group_char.length());
93 if (file_util::WriteFile(group_file_path, group_char.c_str(), size) == size) {
94 VLOG(1) << "Configured in group '" << trial->group_name()
95 << "' ('" << group_char << "') for "
96 << name_of_experiment << " field trial";
97 } else {
98 LOG(ERROR) << "Couldn't write to " << group_file_path.value();
99 }
100 }
101
102 } // namespace
103
104 // The interval between external metrics collections in seconds
105 static const int kExternalMetricsCollectionIntervalSeconds = 30;
106
ExternalMetrics()107 ExternalMetrics::ExternalMetrics() : test_recorder_(NULL) {}
108
~ExternalMetrics()109 ExternalMetrics::~ExternalMetrics() {}
110
Start()111 void ExternalMetrics::Start() {
112 // Register user actions external to the browser.
113 // tools/metrics/actions/extract_actions.py won't understand these lines, so
114 // all of these are explicitly added in that script.
115 // TODO(derat): We shouldn't need to verify actions before reporting them;
116 // remove all of this once http://crosbug.com/11125 is fixed.
117 valid_user_actions_.insert("Cryptohome.PKCS11InitFail");
118 valid_user_actions_.insert("Updater.ServerCertificateChanged");
119 valid_user_actions_.insert("Updater.ServerCertificateFailed");
120
121 // Initialize here field trials that don't need to read from files.
122 // (None for the moment.)
123
124 // Initialize any chromeos field trials that need to read from a file (e.g.,
125 // those that have an upstart script determine their experimental group for
126 // them) then schedule the data collection. All of this is done on the file
127 // thread.
128 bool task_posted = BrowserThread::PostTask(
129 BrowserThread::FILE,
130 FROM_HERE,
131 base::Bind(&chromeos::ExternalMetrics::SetupFieldTrialsOnFileThread,
132 this));
133 DCHECK(task_posted);
134 }
135
RecordActionUI(std::string action_string)136 void ExternalMetrics::RecordActionUI(std::string action_string) {
137 if (valid_user_actions_.count(action_string)) {
138 content::RecordComputedAction(action_string);
139 } else {
140 DLOG(ERROR) << "undefined UMA action: " << action_string;
141 }
142 }
143
RecordAction(const char * action)144 void ExternalMetrics::RecordAction(const char* action) {
145 std::string action_string(action);
146 BrowserThread::PostTask(
147 BrowserThread::UI, FROM_HERE,
148 base::Bind(&ExternalMetrics::RecordActionUI, this, action_string));
149 }
150
RecordCrashUI(const std::string & crash_kind)151 void ExternalMetrics::RecordCrashUI(const std::string& crash_kind) {
152 if (g_browser_process && g_browser_process->metrics_service()) {
153 g_browser_process->metrics_service()->LogChromeOSCrash(crash_kind);
154 }
155 }
156
RecordCrash(const std::string & crash_kind)157 void ExternalMetrics::RecordCrash(const std::string& crash_kind) {
158 BrowserThread::PostTask(
159 BrowserThread::UI, FROM_HERE,
160 base::Bind(&ExternalMetrics::RecordCrashUI, this, crash_kind));
161 }
162
RecordHistogram(const char * histogram_data)163 void ExternalMetrics::RecordHistogram(const char* histogram_data) {
164 int sample, min, max, nbuckets;
165 char name[128]; // length must be consistent with sscanf format below.
166 int n = sscanf(histogram_data, "%127s %d %d %d %d",
167 name, &sample, &min, &max, &nbuckets);
168 if (n != 5) {
169 DLOG(ERROR) << "bad histogram request: " << histogram_data;
170 return;
171 }
172
173 if (!CheckValues(name, min, max, nbuckets)) {
174 DLOG(ERROR) << "Invalid histogram " << name
175 << ", min=" << min
176 << ", max=" << max
177 << ", nbuckets=" << nbuckets;
178 return;
179 }
180 // Do not use the UMA_HISTOGRAM_... macros here. They cache the Histogram
181 // instance and thus only work if |name| is constant.
182 base::HistogramBase* counter = base::Histogram::FactoryGet(
183 name, min, max, nbuckets, base::Histogram::kUmaTargetedHistogramFlag);
184 counter->Add(sample);
185 }
186
RecordLinearHistogram(const char * histogram_data)187 void ExternalMetrics::RecordLinearHistogram(const char* histogram_data) {
188 int sample, max;
189 char name[128]; // length must be consistent with sscanf format below.
190 int n = sscanf(histogram_data, "%127s %d %d", name, &sample, &max);
191 if (n != 3) {
192 DLOG(ERROR) << "bad linear histogram request: " << histogram_data;
193 return;
194 }
195
196 if (!CheckLinearValues(name, max)) {
197 DLOG(ERROR) << "Invalid linear histogram " << name
198 << ", max=" << max;
199 return;
200 }
201 // Do not use the UMA_HISTOGRAM_... macros here. They cache the Histogram
202 // instance and thus only work if |name| is constant.
203 base::HistogramBase* counter = base::LinearHistogram::FactoryGet(
204 name, 1, max, max + 1, base::Histogram::kUmaTargetedHistogramFlag);
205 counter->Add(sample);
206 }
207
RecordSparseHistogram(const char * histogram_data)208 void ExternalMetrics::RecordSparseHistogram(const char* histogram_data) {
209 int sample;
210 char name[128]; // length must be consistent with sscanf format below.
211 int n = sscanf(histogram_data, "%127s %d", name, &sample);
212 if (n != 2) {
213 DLOG(ERROR) << "bad sparse histogram request: " << histogram_data;
214 return;
215 }
216
217 // Do not use the UMA_HISTOGRAM_... macros here. They cache the Histogram
218 // instance and thus only work if |name| is constant.
219 base::HistogramBase* counter = base::SparseHistogram::FactoryGet(
220 name, base::HistogramBase::kUmaTargetedHistogramFlag);
221 counter->Add(sample);
222 }
223
CollectEvents()224 void ExternalMetrics::CollectEvents() {
225 const char* event_file_path = "/var/log/metrics/uma-events";
226 struct stat stat_buf;
227 int result;
228 if (!test_path_.empty()) {
229 event_file_path = test_path_.value().c_str();
230 }
231 result = stat(event_file_path, &stat_buf);
232 if (result < 0) {
233 if (errno != ENOENT) {
234 DPLOG(ERROR) << event_file_path << ": bad metrics file stat";
235 }
236 // Nothing to collect---try later.
237 return;
238 }
239 if (stat_buf.st_size == 0) {
240 // Also nothing to collect.
241 return;
242 }
243 int fd = open(event_file_path, O_RDWR);
244 if (fd < 0) {
245 DPLOG(ERROR) << event_file_path << ": cannot open";
246 return;
247 }
248 result = flock(fd, LOCK_EX);
249 if (result < 0) {
250 DPLOG(ERROR) << event_file_path << ": cannot lock";
251 close(fd);
252 return;
253 }
254 // This processes all messages in the log. Each message starts with a 4-byte
255 // field containing the length of the entire message. The length is followed
256 // by a name-value pair of null-terminated strings. When all messages are
257 // read and processed, or an error occurs, truncate the file to zero size.
258 for (;;) {
259 int32 message_size;
260 result = HANDLE_EINTR(read(fd, &message_size, sizeof(message_size)));
261 if (result < 0) {
262 DPLOG(ERROR) << "reading metrics message header";
263 break;
264 }
265 if (result == 0) { // This indicates a normal EOF.
266 break;
267 }
268 if (result < static_cast<int>(sizeof(message_size))) {
269 DLOG(ERROR) << "bad read size " << result <<
270 ", expecting " << sizeof(message_size);
271 break;
272 }
273 // kMetricsMessageMaxLength applies to the entire message: the 4-byte
274 // length field and the two null-terminated strings.
275 if (message_size < 2 + static_cast<int>(sizeof(message_size)) ||
276 message_size > static_cast<int>(kMetricsMessageMaxLength)) {
277 DLOG(ERROR) << "bad message size " << message_size;
278 break;
279 }
280 message_size -= sizeof(message_size); // The message size includes itself.
281 uint8 buffer[kMetricsMessageMaxLength];
282 result = HANDLE_EINTR(read(fd, buffer, message_size));
283 if (result < 0) {
284 DPLOG(ERROR) << "reading metrics message body";
285 break;
286 }
287 if (result < message_size) {
288 DLOG(ERROR) << "message too short: length " << result <<
289 ", expected " << message_size;
290 break;
291 }
292 // The buffer should now contain a pair of null-terminated strings.
293 uint8* p = reinterpret_cast<uint8*>(memchr(buffer, '\0', message_size));
294 uint8* q = NULL;
295 if (p != NULL) {
296 q = reinterpret_cast<uint8*>(
297 memchr(p + 1, '\0', message_size - (p + 1 - buffer)));
298 }
299 if (q == NULL) {
300 DLOG(ERROR) << "bad name-value pair for metrics";
301 break;
302 }
303 char* name = reinterpret_cast<char*>(buffer);
304 char* value = reinterpret_cast<char*>(p + 1);
305 if (test_recorder_ != NULL) {
306 test_recorder_(name, value);
307 } else if (strcmp(name, "crash") == 0) {
308 RecordCrash(value);
309 } else if (strcmp(name, "histogram") == 0) {
310 RecordHistogram(value);
311 } else if (strcmp(name, "linearhistogram") == 0) {
312 RecordLinearHistogram(value);
313 } else if (strcmp(name, "sparsehistogram") == 0) {
314 RecordSparseHistogram(value);
315 } else if (strcmp(name, "useraction") == 0) {
316 RecordAction(value);
317 } else {
318 DLOG(ERROR) << "invalid event type: " << name;
319 }
320 }
321
322 result = ftruncate(fd, 0);
323 if (result < 0) {
324 DPLOG(ERROR) << "truncate metrics log";
325 }
326 result = flock(fd, LOCK_UN);
327 if (result < 0) {
328 DPLOG(ERROR) << "unlock metrics log";
329 }
330 result = close(fd);
331 if (result < 0) {
332 DPLOG(ERROR) << "close metrics log";
333 }
334 }
335
CollectEventsAndReschedule()336 void ExternalMetrics::CollectEventsAndReschedule() {
337 base::ElapsedTimer timer;
338 CollectEvents();
339 UMA_HISTOGRAM_TIMES("UMA.CollectExternalEventsTime", timer.Elapsed());
340 ScheduleCollector();
341 }
342
ScheduleCollector()343 void ExternalMetrics::ScheduleCollector() {
344 bool result;
345 result = BrowserThread::PostDelayedTask(
346 BrowserThread::FILE, FROM_HERE,
347 base::Bind(&chromeos::ExternalMetrics::CollectEventsAndReschedule, this),
348 base::TimeDelta::FromSeconds(kExternalMetricsCollectionIntervalSeconds));
349 DCHECK(result);
350 }
351
SetupFieldTrialsOnFileThread()352 void ExternalMetrics::SetupFieldTrialsOnFileThread() {
353 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
354 // Field trials that do not read from files can be initialized in
355 // ExternalMetrics::Start() above.
356 SetupProgressiveScanFieldTrial();
357
358 ScheduleCollector();
359 }
360
361 } // namespace chromeos
362