// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "chrome/browser/chromeos/external_metrics.h" #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/file.h> #include <sys/stat.h> #include <sys/types.h> #include <unistd.h> #include <map> #include <string> #include "base/basictypes.h" #include "base/bind.h" #include "base/file_util.h" #include "base/files/file_path.h" #include "base/metrics/histogram.h" #include "base/metrics/sparse_histogram.h" #include "base/metrics/statistics_recorder.h" #include "base/posix/eintr_wrapper.h" #include "base/sys_info.h" #include "base/time/time.h" #include "base/timer/elapsed_timer.h" #include "chrome/browser/browser_process.h" #include "chrome/browser/metrics/metrics_service.h" #include "content/public/browser/browser_thread.h" #include "content/public/browser/user_metrics.h" using content::BrowserThread; using content::UserMetricsAction; namespace chromeos { namespace { bool CheckValues(const std::string& name, int minimum, int maximum, size_t bucket_count) { if (!base::Histogram::InspectConstructionArguments( name, &minimum, &maximum, &bucket_count)) return false; base::HistogramBase* histogram = base::StatisticsRecorder::FindHistogram(name); if (!histogram) return true; return histogram->HasConstructionArguments(minimum, maximum, bucket_count); } bool CheckLinearValues(const std::string& name, int maximum) { return CheckValues(name, 1, maximum, maximum + 1); } // Establishes field trial for wifi scanning in chromeos. crbug.com/242733. void SetupProgressiveScanFieldTrial() { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); const char name_of_experiment[] = "ProgressiveScan"; const base::FilePath group_file_path( "/home/chronos/.progressive_scan_variation"); const base::FieldTrial::Probability kDivisor = 1000; scoped_refptr<base::FieldTrial> trial = base::FieldTrialList::FactoryGetFieldTrial( name_of_experiment, kDivisor, "Default", 2013, 12, 31, base::FieldTrial::SESSION_RANDOMIZED, NULL); // Announce the groups with 0 percentage; the actual percentages come from // the server configuration. std::map<int, std::string> group_to_char; group_to_char[trial->AppendGroup("FullScan", 0)] = "c"; group_to_char[trial->AppendGroup("33Percent_4MinMax", 0)] = "1"; group_to_char[trial->AppendGroup("50Percent_4MinMax", 0)] = "2"; group_to_char[trial->AppendGroup("50Percent_8MinMax", 0)] = "3"; group_to_char[trial->AppendGroup("100Percent_8MinMax", 0)] = "4"; group_to_char[trial->AppendGroup("100Percent_1MinSeen_A", 0)] = "5"; group_to_char[trial->AppendGroup("100Percent_1MinSeen_B", 0)] = "6"; group_to_char[trial->AppendGroup("100Percent_1Min_4Max", 0)] = "7"; // Announce the experiment to any listeners (especially important is the UMA // software, which will append the group names to UMA statistics). const int group_num = trial->group(); std::string group_char = "x"; if (ContainsKey(group_to_char, group_num)) group_char = group_to_char[group_num]; // Write the group to the file to be read by ChromeOS. int size = static_cast<int>(group_char.length()); if (file_util::WriteFile(group_file_path, group_char.c_str(), size) == size) { VLOG(1) << "Configured in group '" << trial->group_name() << "' ('" << group_char << "') for " << name_of_experiment << " field trial"; } else { LOG(ERROR) << "Couldn't write to " << group_file_path.value(); } } } // namespace // The interval between external metrics collections in seconds static const int kExternalMetricsCollectionIntervalSeconds = 30; ExternalMetrics::ExternalMetrics() : test_recorder_(NULL) {} ExternalMetrics::~ExternalMetrics() {} void ExternalMetrics::Start() { // Register user actions external to the browser. // tools/metrics/actions/extract_actions.py won't understand these lines, so // all of these are explicitly added in that script. // TODO(derat): We shouldn't need to verify actions before reporting them; // remove all of this once http://crosbug.com/11125 is fixed. valid_user_actions_.insert("Cryptohome.PKCS11InitFail"); valid_user_actions_.insert("Updater.ServerCertificateChanged"); valid_user_actions_.insert("Updater.ServerCertificateFailed"); // Initialize here field trials that don't need to read from files. // (None for the moment.) // Initialize any chromeos field trials that need to read from a file (e.g., // those that have an upstart script determine their experimental group for // them) then schedule the data collection. All of this is done on the file // thread. bool task_posted = BrowserThread::PostTask( BrowserThread::FILE, FROM_HERE, base::Bind(&chromeos::ExternalMetrics::SetupFieldTrialsOnFileThread, this)); DCHECK(task_posted); } void ExternalMetrics::RecordActionUI(std::string action_string) { if (valid_user_actions_.count(action_string)) { content::RecordComputedAction(action_string); } else { DLOG(ERROR) << "undefined UMA action: " << action_string; } } void ExternalMetrics::RecordAction(const char* action) { std::string action_string(action); BrowserThread::PostTask( BrowserThread::UI, FROM_HERE, base::Bind(&ExternalMetrics::RecordActionUI, this, action_string)); } void ExternalMetrics::RecordCrashUI(const std::string& crash_kind) { if (g_browser_process && g_browser_process->metrics_service()) { g_browser_process->metrics_service()->LogChromeOSCrash(crash_kind); } } void ExternalMetrics::RecordCrash(const std::string& crash_kind) { BrowserThread::PostTask( BrowserThread::UI, FROM_HERE, base::Bind(&ExternalMetrics::RecordCrashUI, this, crash_kind)); } void ExternalMetrics::RecordHistogram(const char* histogram_data) { int sample, min, max, nbuckets; char name[128]; // length must be consistent with sscanf format below. int n = sscanf(histogram_data, "%127s %d %d %d %d", name, &sample, &min, &max, &nbuckets); if (n != 5) { DLOG(ERROR) << "bad histogram request: " << histogram_data; return; } if (!CheckValues(name, min, max, nbuckets)) { DLOG(ERROR) << "Invalid histogram " << name << ", min=" << min << ", max=" << max << ", nbuckets=" << nbuckets; return; } // Do not use the UMA_HISTOGRAM_... macros here. They cache the Histogram // instance and thus only work if |name| is constant. base::HistogramBase* counter = base::Histogram::FactoryGet( name, min, max, nbuckets, base::Histogram::kUmaTargetedHistogramFlag); counter->Add(sample); } void ExternalMetrics::RecordLinearHistogram(const char* histogram_data) { int sample, max; char name[128]; // length must be consistent with sscanf format below. int n = sscanf(histogram_data, "%127s %d %d", name, &sample, &max); if (n != 3) { DLOG(ERROR) << "bad linear histogram request: " << histogram_data; return; } if (!CheckLinearValues(name, max)) { DLOG(ERROR) << "Invalid linear histogram " << name << ", max=" << max; return; } // Do not use the UMA_HISTOGRAM_... macros here. They cache the Histogram // instance and thus only work if |name| is constant. base::HistogramBase* counter = base::LinearHistogram::FactoryGet( name, 1, max, max + 1, base::Histogram::kUmaTargetedHistogramFlag); counter->Add(sample); } void ExternalMetrics::RecordSparseHistogram(const char* histogram_data) { int sample; char name[128]; // length must be consistent with sscanf format below. int n = sscanf(histogram_data, "%127s %d", name, &sample); if (n != 2) { DLOG(ERROR) << "bad sparse histogram request: " << histogram_data; return; } // Do not use the UMA_HISTOGRAM_... macros here. They cache the Histogram // instance and thus only work if |name| is constant. base::HistogramBase* counter = base::SparseHistogram::FactoryGet( name, base::HistogramBase::kUmaTargetedHistogramFlag); counter->Add(sample); } void ExternalMetrics::CollectEvents() { const char* event_file_path = "/var/log/metrics/uma-events"; struct stat stat_buf; int result; if (!test_path_.empty()) { event_file_path = test_path_.value().c_str(); } result = stat(event_file_path, &stat_buf); if (result < 0) { if (errno != ENOENT) { DPLOG(ERROR) << event_file_path << ": bad metrics file stat"; } // Nothing to collect---try later. return; } if (stat_buf.st_size == 0) { // Also nothing to collect. return; } int fd = open(event_file_path, O_RDWR); if (fd < 0) { DPLOG(ERROR) << event_file_path << ": cannot open"; return; } result = flock(fd, LOCK_EX); if (result < 0) { DPLOG(ERROR) << event_file_path << ": cannot lock"; close(fd); return; } // This processes all messages in the log. Each message starts with a 4-byte // field containing the length of the entire message. The length is followed // by a name-value pair of null-terminated strings. When all messages are // read and processed, or an error occurs, truncate the file to zero size. for (;;) { int32 message_size; result = HANDLE_EINTR(read(fd, &message_size, sizeof(message_size))); if (result < 0) { DPLOG(ERROR) << "reading metrics message header"; break; } if (result == 0) { // This indicates a normal EOF. break; } if (result < static_cast<int>(sizeof(message_size))) { DLOG(ERROR) << "bad read size " << result << ", expecting " << sizeof(message_size); break; } // kMetricsMessageMaxLength applies to the entire message: the 4-byte // length field and the two null-terminated strings. if (message_size < 2 + static_cast<int>(sizeof(message_size)) || message_size > static_cast<int>(kMetricsMessageMaxLength)) { DLOG(ERROR) << "bad message size " << message_size; break; } message_size -= sizeof(message_size); // The message size includes itself. uint8 buffer[kMetricsMessageMaxLength]; result = HANDLE_EINTR(read(fd, buffer, message_size)); if (result < 0) { DPLOG(ERROR) << "reading metrics message body"; break; } if (result < message_size) { DLOG(ERROR) << "message too short: length " << result << ", expected " << message_size; break; } // The buffer should now contain a pair of null-terminated strings. uint8* p = reinterpret_cast<uint8*>(memchr(buffer, '\0', message_size)); uint8* q = NULL; if (p != NULL) { q = reinterpret_cast<uint8*>( memchr(p + 1, '\0', message_size - (p + 1 - buffer))); } if (q == NULL) { DLOG(ERROR) << "bad name-value pair for metrics"; break; } char* name = reinterpret_cast<char*>(buffer); char* value = reinterpret_cast<char*>(p + 1); if (test_recorder_ != NULL) { test_recorder_(name, value); } else if (strcmp(name, "crash") == 0) { RecordCrash(value); } else if (strcmp(name, "histogram") == 0) { RecordHistogram(value); } else if (strcmp(name, "linearhistogram") == 0) { RecordLinearHistogram(value); } else if (strcmp(name, "sparsehistogram") == 0) { RecordSparseHistogram(value); } else if (strcmp(name, "useraction") == 0) { RecordAction(value); } else { DLOG(ERROR) << "invalid event type: " << name; } } result = ftruncate(fd, 0); if (result < 0) { DPLOG(ERROR) << "truncate metrics log"; } result = flock(fd, LOCK_UN); if (result < 0) { DPLOG(ERROR) << "unlock metrics log"; } result = close(fd); if (result < 0) { DPLOG(ERROR) << "close metrics log"; } } void ExternalMetrics::CollectEventsAndReschedule() { base::ElapsedTimer timer; CollectEvents(); UMA_HISTOGRAM_TIMES("UMA.CollectExternalEventsTime", timer.Elapsed()); ScheduleCollector(); } void ExternalMetrics::ScheduleCollector() { bool result; result = BrowserThread::PostDelayedTask( BrowserThread::FILE, FROM_HERE, base::Bind(&chromeos::ExternalMetrics::CollectEventsAndReschedule, this), base::TimeDelta::FromSeconds(kExternalMetricsCollectionIntervalSeconds)); DCHECK(result); } void ExternalMetrics::SetupFieldTrialsOnFileThread() { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); // Field trials that do not read from files can be initialized in // ExternalMetrics::Start() above. SetupProgressiveScanFieldTrial(); ScheduleCollector(); } } // namespace chromeos