diff options
author | Bill Yi <byi@google.com> | 2015-07-28 09:35:20 -0700 |
---|---|---|
committer | Bill Yi <byi@google.com> | 2015-07-28 09:35:42 -0700 |
commit | 15b8a778760707b33bffd97491ccc80854d994ce (patch) | |
tree | 9e7ea9bc9c3402ee303dffb2ff269e64e2df750e | |
parent | 5cab52906c1e7d7dd0adc0b03b8eb8994e5d3e9c (diff) | |
parent | c95440e9b62b82591d4b03513958ab293fe187e6 (diff) | |
download | core-15b8a778760707b33bffd97491ccc80854d994ce.tar.gz core-15b8a778760707b33bffd97491ccc80854d994ce.tar.bz2 core-15b8a778760707b33bffd97491ccc80854d994ce.zip |
Merge branch 'rewrite-metrics' into merge-metrics
BUG:22773266
61 files changed, 7901 insertions, 0 deletions
diff --git a/metrics/OWNERS b/metrics/OWNERS new file mode 100644 index 000000000..7f5e50dab --- /dev/null +++ b/metrics/OWNERS @@ -0,0 +1,3 @@ +semenzato@chromium.org +derat@chromium.org +bsimonnet@chromium.org diff --git a/metrics/README b/metrics/README new file mode 100644 index 000000000..4b92af35f --- /dev/null +++ b/metrics/README @@ -0,0 +1,138 @@ +Copyright (c) 2010 The Chromium OS Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. + +The Chrome OS "metrics" package contains utilities for client-side user metric +collection. +When Chrome is installed, Chrome will take care of aggregating and uploading the +metrics to the UMA server. +When Chrome is not installed (embedded build) and the metrics_uploader USE flag +is set, metrics_daemon will aggregate and upload the metrics itself. + + +================================================================================ +The Metrics Library: libmetrics +================================================================================ + +libmetrics is a small library that implements the basic C and C++ API for +metrics collection. All metrics collection is funneled through this library. The +easiest and recommended way for a client-side module to collect user metrics is +to link libmetrics and use its APIs to send metrics to Chrome for transport to +UMA. In order to use the library in a module, you need to do the following: + +- Add a dependence (DEPEND and RDEPEND) on chromeos-base/metrics to the module's + ebuild. + +- Link the module with libmetrics (for example, by passing -lmetrics to the + module's link command). Both libmetrics.so and libmetrics.a are built and + installed under $SYSROOT/usr/lib/. Note that by default -lmetrics will link + against libmetrics.so, which is preferred. + +- To access the metrics library API in the module, include the + <metrics/metrics_library.h> header file. The file is installed in + $SYSROOT/usr/include/ when the metrics library is built and installed. + +- The API is documented in metrics_library.h under src/platform/metrics/. Before + using the API methods, a MetricsLibrary object needs to be constructed and + initialized through its Init method. + + For more information on the C API see c_metrics_library.h. + +- Samples are sent to Chrome only if the "/home/chronos/Consent To Send Stats" + file exists or the metrics are declared enabled in the policy file (see the + AreMetricsEnabled API method). + +- On the target platform, shortly after the sample is sent, it should be visible + in Chrome through "about:histograms". + + +================================================================================ +Histogram Naming Convention +================================================================================ + +Use TrackerArea.MetricName. For example: + +Platform.DailyUseTime +Network.TimeToDrop + + +================================================================================ +Server Side +================================================================================ + +If the histogram data is visible in about:histograms, it will be sent by an +official Chrome build to UMA, assuming the user has opted into metrics +collection. To make the histogram visible on "chromedashboard", the histogram +description XML file needs to be updated (steps 2 and 3 after following the +"Details on how to add your own histograms" link under the Histograms tab). +Include the string "Chrome OS" in the histogram description so that it's easier +to distinguish Chrome OS specific metrics from general Chrome histograms. + +The UMA server logs and keeps the collected field data even if the metric's name +is not added to the histogram XML. However, the dashboard histogram for that +metric will show field data as of the histogram XML update date; it will not +include data for older dates. If past data needs to be displayed, manual +server-side intervention is required. In other words, one should assume that +field data collection starts only after the histogram XML has been updated. + + +================================================================================ +The Metrics Client: metrics_client +================================================================================ + +metrics_client is a simple shell command-line utility for sending histogram +samples and user actions. It's installed under /usr/bin on the target platform +and uses libmetrics to send the data to Chrome. The utility is useful for +generating metrics from shell scripts. + +For usage information and command-line options, run "metrics_client" on the +target platform or look for "Usage:" in metrics_client.cc. + + +================================================================================ +The Metrics Daemon: metrics_daemon +================================================================================ + +metrics_daemon is a daemon that runs in the background on the target platform +and is intended for passive or ongoing metrics collection, or metrics collection +requiring feedback from multiple modules. For example, it listens to D-Bus +signals related to the user session and screen saver states to determine if the +user is actively using the device or not and generates the corresponding +data. The metrics daemon uses libmetrics to send the data to Chrome. + +The recommended way to generate metrics data from a module is to link and use +libmetrics directly. However, the module could instead send signals to or +communicate in some alternative way with the metrics daemon. Then the metrics +daemon needs to monitor for the relevant events and take appropriate action -- +for example, aggregate data and send the histogram samples. + + +================================================================================ +FAQ +================================================================================ + +Q. What should my histogram's |min| and |max| values be set at? + +A. You should set the values to a range that covers the vast majority of samples + that would appear in the field. Note that samples below the |min| will still + be collected in the underflow bucket and samples above the |max| will end up + in the overflow bucket. Also, the reported mean of the data will be correct + regardless of the range. + +Q. How many buckets should I use in my histogram? + +A. You should allocate as many buckets as necessary to perform proper analysis + on the collected data. Note, however, that the memory allocated in Chrome for + each histogram is proportional to the number of buckets. Therefore, it is + strongly recommended to keep this number low (e.g., 50 is normal, while 100 + is probably high). + +Q. When should I use an enumeration (linear) histogram vs. a regular + (exponential) histogram? + +A. Enumeration histograms should really be used only for sampling enumerated + events and, in some cases, percentages. Normally, you should use a regular + histogram with exponential bucket layout that provides higher resolution at + the low end of the range and lower resolution at the high end. Regular + histograms are generally used for collecting performance data (e.g., timing, + memory usage, power) as well as aggregated event counts. diff --git a/metrics/WATCHLISTS b/metrics/WATCHLISTS new file mode 100644 index 000000000..a051f350f --- /dev/null +++ b/metrics/WATCHLISTS @@ -0,0 +1,16 @@ +# See http://dev.chromium.org/developers/contributing-code/watchlists for +# a description of this file's format. +# Please keep these keys in alphabetical order. + +{ + 'WATCHLIST_DEFINITIONS': { + 'all': { + 'filepath': '.', + }, + }, + 'WATCHLISTS': { + 'all': ['petkov@chromium.org', + 'semenzato@chromium.org', + 'sosa@chromium.org'] + }, +} diff --git a/metrics/c_metrics_library.cc b/metrics/c_metrics_library.cc new file mode 100644 index 000000000..90a2d59c9 --- /dev/null +++ b/metrics/c_metrics_library.cc @@ -0,0 +1,78 @@ +// Copyright (c) 2010 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// +// C wrapper to libmetrics +// + +#include "metrics/c_metrics_library.h" + +#include <string> + +#include "metrics/metrics_library.h" + +extern "C" CMetricsLibrary CMetricsLibraryNew(void) { + MetricsLibrary* lib = new MetricsLibrary; + return reinterpret_cast<CMetricsLibrary>(lib); +} + +extern "C" void CMetricsLibraryDelete(CMetricsLibrary handle) { + MetricsLibrary* lib = reinterpret_cast<MetricsLibrary*>(handle); + delete lib; +} + +extern "C" void CMetricsLibraryInit(CMetricsLibrary handle) { + MetricsLibrary* lib = reinterpret_cast<MetricsLibrary*>(handle); + if (lib != NULL) + lib->Init(); +} + +extern "C" int CMetricsLibrarySendToUMA(CMetricsLibrary handle, + const char* name, int sample, + int min, int max, int nbuckets) { + MetricsLibrary* lib = reinterpret_cast<MetricsLibrary*>(handle); + if (lib == NULL) + return 0; + return lib->SendToUMA(std::string(name), sample, min, max, nbuckets); +} + +extern "C" int CMetricsLibrarySendEnumToUMA(CMetricsLibrary handle, + const char* name, int sample, + int max) { + MetricsLibrary* lib = reinterpret_cast<MetricsLibrary*>(handle); + if (lib == NULL) + return 0; + return lib->SendEnumToUMA(std::string(name), sample, max); +} + +extern "C" int CMetricsLibrarySendSparseToUMA(CMetricsLibrary handle, + const char* name, int sample) { + MetricsLibrary* lib = reinterpret_cast<MetricsLibrary*>(handle); + if (lib == NULL) + return 0; + return lib->SendSparseToUMA(std::string(name), sample); +} + +extern "C" int CMetricsLibrarySendUserActionToUMA(CMetricsLibrary handle, + const char* action) { + MetricsLibrary* lib = reinterpret_cast<MetricsLibrary*>(handle); + if (lib == NULL) + return 0; + return lib->SendUserActionToUMA(std::string(action)); +} + +extern "C" int CMetricsLibrarySendCrashToUMA(CMetricsLibrary handle, + const char* crash_kind) { + MetricsLibrary* lib = reinterpret_cast<MetricsLibrary*>(handle); + if (lib == NULL) + return 0; + return lib->SendCrashToUMA(crash_kind); +} + +extern "C" int CMetricsLibraryAreMetricsEnabled(CMetricsLibrary handle) { + MetricsLibrary* lib = reinterpret_cast<MetricsLibrary*>(handle); + if (lib == NULL) + return 0; + return lib->AreMetricsEnabled(); +} diff --git a/metrics/c_metrics_library.h b/metrics/c_metrics_library.h new file mode 100644 index 000000000..7f78e43a7 --- /dev/null +++ b/metrics/c_metrics_library.h @@ -0,0 +1,49 @@ +// Copyright (c) 2010 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef METRICS_C_METRICS_LIBRARY_H_ +#define METRICS_C_METRICS_LIBRARY_H_ + +#if defined(__cplusplus) +extern "C" { +#endif +typedef struct CMetricsLibraryOpaque* CMetricsLibrary; + +// C wrapper for MetricsLibrary::MetricsLibrary. +CMetricsLibrary CMetricsLibraryNew(void); + +// C wrapper for MetricsLibrary::~MetricsLibrary. +void CMetricsLibraryDelete(CMetricsLibrary handle); + +// C wrapper for MetricsLibrary::Init. +void CMetricsLibraryInit(CMetricsLibrary handle); + +// C wrapper for MetricsLibrary::SendToUMA. +int CMetricsLibrarySendToUMA(CMetricsLibrary handle, + const char* name, int sample, + int min, int max, int nbuckets); + +// C wrapper for MetricsLibrary::SendEnumToUMA. +int CMetricsLibrarySendEnumToUMA(CMetricsLibrary handle, + const char* name, int sample, int max); + +// C wrapper for MetricsLibrary::SendSparseToUMA. +int CMetricsLibrarySendSparseToUMA(CMetricsLibrary handle, + const char* name, int sample); + +// C wrapper for MetricsLibrary::SendUserActionToUMA. +int CMetricsLibrarySendUserActionToUMA(CMetricsLibrary handle, + const char* action); + +// C wrapper for MetricsLibrary::SendCrashToUMA. +int CMetricsLibrarySendCrashToUMA(CMetricsLibrary handle, + const char* crash_kind); + +// C wrapper for MetricsLibrary::AreMetricsEnabled. +int CMetricsLibraryAreMetricsEnabled(CMetricsLibrary handle); + +#if defined(__cplusplus) +} +#endif +#endif // METRICS_C_METRICS_LIBRARY_H_ diff --git a/metrics/init/metrics_daemon.conf b/metrics/init/metrics_daemon.conf new file mode 100644 index 000000000..e6932cf99 --- /dev/null +++ b/metrics/init/metrics_daemon.conf @@ -0,0 +1,18 @@ +# Copyright (c) 2014 The Chromium OS Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +description "Metrics collection daemon" +author "chromium-os-dev@chromium.org" + +# The metrics daemon is responsible for receiving and forwarding to +# chrome UMA statistics not produced by chrome. +start on starting system-services +stop on stopping system-services +respawn + +# metrics will update the next line to add -uploader for embedded builds. +env DAEMON_FLAGS="" + +expect fork +exec metrics_daemon ${DAEMON_FLAGS} diff --git a/metrics/init/metrics_library.conf b/metrics/init/metrics_library.conf new file mode 100644 index 000000000..03016d129 --- /dev/null +++ b/metrics/init/metrics_library.conf @@ -0,0 +1,25 @@ +# Copyright (c) 2014 The Chromium OS Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +description "Metrics Library upstart file" +author "chromium-os-dev@chromium.org" + +# The metrics library is used by several programs (daemons and others) +# to send UMA stats. +start on starting boot-services + +pre-start script + # Create the file used as communication endpoint for metrics. + METRICS_DIR=/var/lib/metrics + EVENTS_FILE=${METRICS_DIR}/uma-events + mkdir -p ${METRICS_DIR} + touch ${EVENTS_FILE} + chown chronos.chronos ${EVENTS_FILE} + chmod 666 ${EVENTS_FILE} + # TRANSITION ONLY. + # TODO(semenzato) Remove after Chrome change, see issue 447256. + # Let Chrome read the metrics file from the old location. + mkdir -p /var/run/metrics + ln -sf ${EVENTS_FILE} /var/run/metrics +end script diff --git a/metrics/libmetrics-334380.gyp b/metrics/libmetrics-334380.gyp new file mode 100644 index 000000000..9771821fb --- /dev/null +++ b/metrics/libmetrics-334380.gyp @@ -0,0 +1,8 @@ +{ + 'variables': { + 'libbase_ver': 334380, + }, + 'includes': [ + 'libmetrics.gypi', + ], +} diff --git a/metrics/libmetrics.gypi b/metrics/libmetrics.gypi new file mode 100644 index 000000000..5b90a550c --- /dev/null +++ b/metrics/libmetrics.gypi @@ -0,0 +1,33 @@ +{ + 'target_defaults': { + 'variables': { + 'deps': [ + 'libchrome-<(libbase_ver)', + 'libchromeos-<(libbase_ver)', + ] + }, + 'cflags_cc': [ + '-fno-exceptions', + ], + }, + 'targets': [ + { + 'target_name': 'libmetrics-<(libbase_ver)', + 'type': 'shared_library', + 'cflags': [ + '-fvisibility=default', + ], + 'libraries+': [ + '-lpolicy-<(libbase_ver)', + ], + 'sources': [ + 'c_metrics_library.cc', + 'metrics_library.cc', + 'serialization/metric_sample.cc', + 'serialization/serialization_utils.cc', + 'timer.cc', + ], + 'include_dirs': ['.'], + }, + ], +} diff --git a/metrics/libmetrics.pc.in b/metrics/libmetrics.pc.in new file mode 100644 index 000000000..233f3181a --- /dev/null +++ b/metrics/libmetrics.pc.in @@ -0,0 +1,7 @@ +bslot=@BSLOT@ + +Name: libmetrics +Description: Chrome OS metrics library +Version: ${bslot} +Requires.private: libchrome-${bslot} +Libs: -lmetrics-${bslot} diff --git a/metrics/make_tests.sh b/metrics/make_tests.sh new file mode 100755 index 000000000..9dcc80475 --- /dev/null +++ b/metrics/make_tests.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +# Copyright (c) 2010 The Chromium OS Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +# Builds tests. + +set -e +make tests +mkdir -p "${OUT_DIR}" +cp *_test "${OUT_DIR}" diff --git a/metrics/metrics.gyp b/metrics/metrics.gyp new file mode 100644 index 000000000..276ec7876 --- /dev/null +++ b/metrics/metrics.gyp @@ -0,0 +1,184 @@ +{ + 'target_defaults': { + 'variables': { + 'deps': [ + 'dbus-1', + 'libchrome-<(libbase_ver)', + 'libchromeos-<(libbase_ver)', + ] + }, + 'cflags_cc': [ + '-fno-exceptions', + ], + }, + 'targets': [ + { + 'target_name': 'libmetrics_daemon', + 'type': 'static_library', + 'dependencies': [ + '../metrics/libmetrics-<(libbase_ver).gyp:libmetrics-<(libbase_ver)', + 'libupload_service', + 'metrics_proto', + ], + 'link_settings': { + 'libraries': [ + '-lrootdev', + ], + }, + 'sources': [ + 'persistent_integer.cc', + 'metrics_daemon.cc', + 'metrics_daemon_main.cc', + ], + 'include_dirs': ['.'], + }, + { + 'target_name': 'metrics_client', + 'type': 'executable', + 'dependencies': [ + '../metrics/libmetrics-<(libbase_ver).gyp:libmetrics-<(libbase_ver)', + ], + 'sources': [ + 'metrics_client.cc', + ] + }, + { + 'target_name': 'libupload_service', + 'type': 'static_library', + 'dependencies': [ + 'metrics_proto', + '../metrics/libmetrics-<(libbase_ver).gyp:libmetrics-<(libbase_ver)', + ], + 'link_settings': { + 'libraries': [ + '-lvboot_host', + ], + }, + 'variables': { + 'exported_deps': [ + 'protobuf-lite', + ], + 'deps': [ + '<@(exported_deps)', + ], + }, + 'all_dependent_settings': { + 'variables': { + 'deps+': [ + '<@(exported_deps)', + ], + }, + }, + 'sources': [ + 'uploader/upload_service.cc', + 'uploader/metrics_hashes.cc', + 'uploader/metrics_log.cc', + 'uploader/metrics_log_base.cc', + 'uploader/system_profile_cache.cc', + 'uploader/sender_http.cc', + ], + 'include_dirs': ['.'] + }, + { + 'target_name': 'metrics_proto', + 'type': 'static_library', + 'variables': { + 'proto_in_dir': 'uploader/proto', + 'proto_out_dir': 'include/metrics/uploader/proto', + }, + 'sources': [ + '<(proto_in_dir)/chrome_user_metrics_extension.proto', + '<(proto_in_dir)/histogram_event.proto', + '<(proto_in_dir)/system_profile.proto', + '<(proto_in_dir)/user_action_event.proto', + ], + 'includes': [ + '../common-mk/protoc.gypi' + ], + }, + ], + 'conditions': [ + ['USE_passive_metrics == 1', { + 'targets': [ + { + 'target_name': 'metrics_daemon', + 'type': 'executable', + 'dependencies': ['libmetrics_daemon'], + }, + ], + }], + ['USE_test == 1', { + 'targets': [ + { + 'target_name': 'persistent_integer_test', + 'type': 'executable', + 'includes': ['../common-mk/common_test.gypi'], + 'sources': [ + 'persistent_integer.cc', + 'persistent_integer_test.cc', + ] + }, + { + 'target_name': 'metrics_library_test', + 'type': 'executable', + 'dependencies': [ + '../metrics/libmetrics-<(libbase_ver).gyp:libmetrics-<(libbase_ver)', + ], + 'includes': ['../common-mk/common_test.gypi'], + 'sources': [ + 'metrics_library_test.cc', + 'serialization/serialization_utils_unittest.cc', + ], + 'link_settings': { + 'libraries': [ + '-lpolicy-<(libbase_ver)', + ] + } + }, + { + 'target_name': 'timer_test', + 'type': 'executable', + 'includes': ['../common-mk/common_test.gypi'], + 'sources': [ + 'timer.cc', + 'timer_test.cc', + ] + }, + { + 'target_name': 'upload_service_test', + 'type': 'executable', + 'sources': [ + 'persistent_integer.cc', + 'uploader/metrics_hashes_unittest.cc', + 'uploader/metrics_log_base_unittest.cc', + 'uploader/mock/sender_mock.cc', + 'uploader/upload_service_test.cc', + ], + 'dependencies': [ + 'libupload_service', + ], + 'includes':[ + '../common-mk/common_test.gypi', + ], + 'include_dirs': ['.'] + }, + ], + }], + ['USE_passive_metrics == 1 and USE_test == 1', { + 'targets': [ + { + 'target_name': 'metrics_daemon_test', + 'type': 'executable', + 'dependencies': [ + 'libmetrics_daemon', + ], + 'includes': ['../common-mk/common_test.gypi'], + 'sources': [ + 'metrics_daemon_test.cc', + ], + 'include_dirs': ['.'], + }, + ], + }], + ] +} diff --git a/metrics/metrics_client.cc b/metrics/metrics_client.cc new file mode 100644 index 000000000..bbe9dcda2 --- /dev/null +++ b/metrics/metrics_client.cc @@ -0,0 +1,221 @@ +// Copyright (c) 2011 The Chromium OS 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 <cstdio> +#include <cstdlib> + +#include "metrics/metrics_library.h" + +enum Mode { + kModeSendSample, + kModeSendEnumSample, + kModeSendSparseSample, + kModeSendUserAction, + kModeSendCrosEvent, + kModeHasConsent, + kModeIsGuestMode, +}; + +void ShowUsage() { + fprintf(stderr, + "Usage: metrics_client [-ab] [-t] name sample min max nbuckets\n" + " metrics_client [-ab] -e name sample max\n" + " metrics_client [-ab] -s name sample\n" + " metrics_client [-ab] -v event\n" + " metrics_client -u action\n" + " metrics_client [-cg]\n" + "\n" + " default: send metric with integer values to Chrome only\n" + " |min| > 0, |min| <= sample < |max|\n" + " -a: send metric (name/sample) to Autotest only\n" + " -b: send metric to both Chrome and Autotest\n" + " -c: return exit status 0 if user consents to stats, 1 otherwise,\n" + " in guest mode always return 1\n" + " -e: send linear/enumeration histogram data\n" + " -g: return exit status 0 if machine in guest mode, 1 otherwise\n" + " -s: send a sparse histogram sample\n" + " -t: convert sample from double seconds to int milliseconds\n" + " -u: send a user action to Chrome\n" + " -v: send a Platform.CrOSEvent enum histogram sample\n"); + exit(1); +} + +static int ParseInt(const char *arg) { + char *endptr; + int value = strtol(arg, &endptr, 0); + if (*endptr != '\0') { + fprintf(stderr, "metrics client: bad integer \"%s\"\n", arg); + ShowUsage(); + } + return value; +} + +static double ParseDouble(const char *arg) { + char *endptr; + double value = strtod(arg, &endptr); + if (*endptr != '\0') { + fprintf(stderr, "metrics client: bad double \"%s\"\n", arg); + ShowUsage(); + } + return value; +} + +static int SendStats(char* argv[], + int name_index, + enum Mode mode, + bool secs_to_msecs, + bool send_to_autotest, + bool send_to_chrome) { + const char* name = argv[name_index]; + int sample; + if (secs_to_msecs) { + sample = static_cast<int>(ParseDouble(argv[name_index + 1]) * 1000.0); + } else { + sample = ParseInt(argv[name_index + 1]); + } + + // Send metrics + if (send_to_autotest) { + MetricsLibrary::SendToAutotest(name, sample); + } + + if (send_to_chrome) { + MetricsLibrary metrics_lib; + metrics_lib.Init(); + if (mode == kModeSendSparseSample) { + metrics_lib.SendSparseToUMA(name, sample); + } else if (mode == kModeSendEnumSample) { + int max = ParseInt(argv[name_index + 2]); + metrics_lib.SendEnumToUMA(name, sample, max); + } else { + int min = ParseInt(argv[name_index + 2]); + int max = ParseInt(argv[name_index + 3]); + int nbuckets = ParseInt(argv[name_index + 4]); + metrics_lib.SendToUMA(name, sample, min, max, nbuckets); + } + } + return 0; +} + +static int SendUserAction(char* argv[], int action_index) { + const char* action = argv[action_index]; + MetricsLibrary metrics_lib; + metrics_lib.Init(); + metrics_lib.SendUserActionToUMA(action); + return 0; +} + +static int SendCrosEvent(char* argv[], int action_index) { + const char* event = argv[action_index]; + bool result; + MetricsLibrary metrics_lib; + metrics_lib.Init(); + result = metrics_lib.SendCrosEventToUMA(event); + if (!result) { + fprintf(stderr, "metrics_client: could not send event %s\n", event); + return 1; + } + return 0; +} + +static int HasConsent() { + MetricsLibrary metrics_lib; + metrics_lib.Init(); + return metrics_lib.AreMetricsEnabled() ? 0 : 1; +} + +static int IsGuestMode() { + MetricsLibrary metrics_lib; + metrics_lib.Init(); + return metrics_lib.IsGuestMode() ? 0 : 1; +} + +int main(int argc, char** argv) { + enum Mode mode = kModeSendSample; + bool send_to_autotest = false; + bool send_to_chrome = true; + bool secs_to_msecs = false; + + // Parse arguments + int flag; + while ((flag = getopt(argc, argv, "abcegstuv")) != -1) { + switch (flag) { + case 'a': + send_to_autotest = true; + send_to_chrome = false; + break; + case 'b': + send_to_chrome = true; + send_to_autotest = true; + break; + case 'c': + mode = kModeHasConsent; + break; + case 'e': + mode = kModeSendEnumSample; + break; + case 'g': + mode = kModeIsGuestMode; + break; + case 's': + mode = kModeSendSparseSample; + break; + case 't': + secs_to_msecs = true; + break; + case 'u': + mode = kModeSendUserAction; + break; + case 'v': + mode = kModeSendCrosEvent; + break; + default: + ShowUsage(); + break; + } + } + int arg_index = optind; + + int expected_args = 0; + if (mode == kModeSendSample) + expected_args = 5; + else if (mode == kModeSendEnumSample) + expected_args = 3; + else if (mode == kModeSendSparseSample) + expected_args = 2; + else if (mode == kModeSendUserAction) + expected_args = 1; + else if (mode == kModeSendCrosEvent) + expected_args = 1; + + if ((arg_index + expected_args) != argc) { + ShowUsage(); + } + + switch (mode) { + case kModeSendSample: + case kModeSendEnumSample: + case kModeSendSparseSample: + if ((mode != kModeSendSample) && secs_to_msecs) { + ShowUsage(); + } + return SendStats(argv, + arg_index, + mode, + secs_to_msecs, + send_to_autotest, + send_to_chrome); + case kModeSendUserAction: + return SendUserAction(argv, arg_index); + case kModeSendCrosEvent: + return SendCrosEvent(argv, arg_index); + case kModeHasConsent: + return HasConsent(); + case kModeIsGuestMode: + return IsGuestMode(); + default: + ShowUsage(); + return 0; + } +} diff --git a/metrics/metrics_daemon.cc b/metrics/metrics_daemon.cc new file mode 100644 index 000000000..880e90ccf --- /dev/null +++ b/metrics/metrics_daemon.cc @@ -0,0 +1,1167 @@ +// Copyright (c) 2011 The Chromium OS 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 "metrics/metrics_daemon.h" + +#include <fcntl.h> +#include <inttypes.h> +#include <math.h> +#include <string.h> +#include <sysexits.h> +#include <time.h> + +#include <base/files/file_path.h> +#include <base/files/file_util.h> +#include <base/hash.h> +#include <base/logging.h> +#include <base/strings/string_number_conversions.h> +#include <base/strings/string_split.h> +#include <base/strings/string_util.h> +#include <base/strings/stringprintf.h> +#include <base/sys_info.h> +#include <chromeos/dbus/service_constants.h> +#include <dbus/dbus.h> +#include <dbus/message.h> +#include "uploader/upload_service.h" + +using base::FilePath; +using base::StringPrintf; +using base::Time; +using base::TimeDelta; +using base::TimeTicks; +using chromeos_metrics::PersistentInteger; +using std::map; +using std::string; +using std::vector; + +namespace { + +#define SAFE_MESSAGE(e) (e.message ? e.message : "unknown error") + +const char kCrashReporterInterface[] = "org.chromium.CrashReporter"; +const char kCrashReporterUserCrashSignal[] = "UserCrash"; +const char kCrashReporterMatchRule[] = + "type='signal',interface='%s',path='/',member='%s'"; + +// Build type of an official build. +// See src/third_party/chromiumos-overlay/chromeos/scripts/cros_set_lsb_release. +const char kOfficialBuild[] = "Official Build"; + +const int kSecondsPerMinute = 60; +const int kMinutesPerHour = 60; +const int kHoursPerDay = 24; +const int kMinutesPerDay = kHoursPerDay * kMinutesPerHour; +const int kSecondsPerDay = kSecondsPerMinute * kMinutesPerDay; +const int kDaysPerWeek = 7; +const int kSecondsPerWeek = kSecondsPerDay * kDaysPerWeek; + +// Interval between calls to UpdateStats(). +const uint32_t kUpdateStatsIntervalMs = 300000; + +const char kKernelCrashDetectedFile[] = "/var/run/kernel-crash-detected"; +const char kUncleanShutdownDetectedFile[] = + "/var/run/unclean-shutdown-detected"; + +} // namespace + +// disk stats metrics + +// The {Read,Write}Sectors numbers are in sectors/second. +// A sector is usually 512 bytes. + +const char MetricsDaemon::kMetricReadSectorsLongName[] = + "Platform.ReadSectorsLong"; +const char MetricsDaemon::kMetricWriteSectorsLongName[] = + "Platform.WriteSectorsLong"; +const char MetricsDaemon::kMetricReadSectorsShortName[] = + "Platform.ReadSectorsShort"; +const char MetricsDaemon::kMetricWriteSectorsShortName[] = + "Platform.WriteSectorsShort"; + +const int MetricsDaemon::kMetricStatsShortInterval = 1; // seconds +const int MetricsDaemon::kMetricStatsLongInterval = 30; // seconds + +const int MetricsDaemon::kMetricMeminfoInterval = 30; // seconds + +// Assume a max rate of 250Mb/s for reads (worse for writes) and 512 byte +// sectors. +const int MetricsDaemon::kMetricSectorsIOMax = 500000; // sectors/second +const int MetricsDaemon::kMetricSectorsBuckets = 50; // buckets +// Page size is 4k, sector size is 0.5k. We're not interested in page fault +// rates that the disk cannot sustain. +const int MetricsDaemon::kMetricPageFaultsMax = kMetricSectorsIOMax / 8; +const int MetricsDaemon::kMetricPageFaultsBuckets = 50; + +// Major page faults, i.e. the ones that require data to be read from disk. + +const char MetricsDaemon::kMetricPageFaultsLongName[] = + "Platform.PageFaultsLong"; +const char MetricsDaemon::kMetricPageFaultsShortName[] = + "Platform.PageFaultsShort"; + +// Swap in and Swap out + +const char MetricsDaemon::kMetricSwapInLongName[] = + "Platform.SwapInLong"; +const char MetricsDaemon::kMetricSwapInShortName[] = + "Platform.SwapInShort"; + +const char MetricsDaemon::kMetricSwapOutLongName[] = + "Platform.SwapOutLong"; +const char MetricsDaemon::kMetricSwapOutShortName[] = + "Platform.SwapOutShort"; + +const char MetricsDaemon::kMetricsProcStatFileName[] = "/proc/stat"; +const int MetricsDaemon::kMetricsProcStatFirstLineItemsCount = 11; + +// Thermal CPU throttling. + +const char MetricsDaemon::kMetricScaledCpuFrequencyName[] = + "Platform.CpuFrequencyThermalScaling"; + +// Zram sysfs entries. + +const char MetricsDaemon::kComprDataSizeName[] = "compr_data_size"; +const char MetricsDaemon::kOrigDataSizeName[] = "orig_data_size"; +const char MetricsDaemon::kZeroPagesName[] = "zero_pages"; + +// Memory use stats collection intervals. We collect some memory use interval +// at these intervals after boot, and we stop collecting after the last one, +// with the assumption that in most cases the memory use won't change much +// after that. +static const int kMemuseIntervals[] = { + 1 * kSecondsPerMinute, // 1 minute mark + 4 * kSecondsPerMinute, // 5 minute mark + 25 * kSecondsPerMinute, // 0.5 hour mark + 120 * kSecondsPerMinute, // 2.5 hour mark + 600 * kSecondsPerMinute, // 12.5 hour mark +}; + +MetricsDaemon::MetricsDaemon() + : memuse_final_time_(0), + memuse_interval_index_(0), + read_sectors_(0), + write_sectors_(0), + vmstats_(), + stats_state_(kStatsShort), + stats_initial_time_(0), + ticks_per_second_(0), + latest_cpu_use_ticks_(0) {} + +MetricsDaemon::~MetricsDaemon() { +} + +double MetricsDaemon::GetActiveTime() { + struct timespec ts; + int r = clock_gettime(CLOCK_MONOTONIC, &ts); + if (r < 0) { + PLOG(WARNING) << "clock_gettime(CLOCK_MONOTONIC) failed"; + return 0; + } else { + return ts.tv_sec + static_cast<double>(ts.tv_nsec) / (1000 * 1000 * 1000); + } +} + +int MetricsDaemon::Run() { + if (CheckSystemCrash(kKernelCrashDetectedFile)) { + ProcessKernelCrash(); + } + + if (CheckSystemCrash(kUncleanShutdownDetectedFile)) { + ProcessUncleanShutdown(); + } + + // On OS version change, clear version stats (which are reported daily). + int32_t version = GetOsVersionHash(); + if (version_cycle_->Get() != version) { + version_cycle_->Set(version); + kernel_crashes_version_count_->Set(0); + version_cumulative_active_use_->Set(0); + version_cumulative_cpu_use_->Set(0); + } + + return chromeos::DBusDaemon::Run(); +} + +void MetricsDaemon::RunUploaderTest() { + upload_service_.reset(new UploadService(new SystemProfileCache(true, + config_root_), + metrics_lib_, + server_)); + upload_service_->Init(upload_interval_, metrics_file_); + upload_service_->UploadEvent(); +} + +uint32_t MetricsDaemon::GetOsVersionHash() { + static uint32_t cached_version_hash = 0; + static bool version_hash_is_cached = false; + if (version_hash_is_cached) + return cached_version_hash; + version_hash_is_cached = true; + std::string version; + if (base::SysInfo::GetLsbReleaseValue("CHROMEOS_RELEASE_VERSION", &version)) { + cached_version_hash = base::Hash(version); + } else if (testing_) { + cached_version_hash = 42; // return any plausible value for the hash + } else { + LOG(FATAL) << "could not find CHROMEOS_RELEASE_VERSION"; + } + return cached_version_hash; +} + +bool MetricsDaemon::IsOnOfficialBuild() const { + std::string build_type; + return (base::SysInfo::GetLsbReleaseValue("CHROMEOS_RELEASE_BUILD_TYPE", + &build_type) && + build_type == kOfficialBuild); +} + +void MetricsDaemon::Init(bool testing, + bool uploader_active, + MetricsLibraryInterface* metrics_lib, + const string& diskstats_path, + const string& vmstats_path, + const string& scaling_max_freq_path, + const string& cpuinfo_max_freq_path, + const base::TimeDelta& upload_interval, + const string& server, + const string& metrics_file, + const string& config_root) { + testing_ = testing; + uploader_active_ = uploader_active; + config_root_ = config_root; + DCHECK(metrics_lib != nullptr); + metrics_lib_ = metrics_lib; + + upload_interval_ = upload_interval; + server_ = server; + metrics_file_ = metrics_file; + + // Get ticks per second (HZ) on this system. + // Sysconf cannot fail, so no sanity checks are needed. + ticks_per_second_ = sysconf(_SC_CLK_TCK); + + daily_active_use_.reset( + new PersistentInteger("Platform.DailyUseTime")); + version_cumulative_active_use_.reset( + new PersistentInteger("Platform.CumulativeDailyUseTime")); + version_cumulative_cpu_use_.reset( + new PersistentInteger("Platform.CumulativeCpuTime")); + + kernel_crash_interval_.reset( + new PersistentInteger("Platform.KernelCrashInterval")); + unclean_shutdown_interval_.reset( + new PersistentInteger("Platform.UncleanShutdownInterval")); + user_crash_interval_.reset( + new PersistentInteger("Platform.UserCrashInterval")); + + any_crashes_daily_count_.reset( + new PersistentInteger("Platform.AnyCrashesDaily")); + any_crashes_weekly_count_.reset( + new PersistentInteger("Platform.AnyCrashesWeekly")); + user_crashes_daily_count_.reset( + new PersistentInteger("Platform.UserCrashesDaily")); + user_crashes_weekly_count_.reset( + new PersistentInteger("Platform.UserCrashesWeekly")); + kernel_crashes_daily_count_.reset( + new PersistentInteger("Platform.KernelCrashesDaily")); + kernel_crashes_weekly_count_.reset( + new PersistentInteger("Platform.KernelCrashesWeekly")); + kernel_crashes_version_count_.reset( + new PersistentInteger("Platform.KernelCrashesSinceUpdate")); + unclean_shutdowns_daily_count_.reset( + new PersistentInteger("Platform.UncleanShutdownsDaily")); + unclean_shutdowns_weekly_count_.reset( + new PersistentInteger("Platform.UncleanShutdownsWeekly")); + + daily_cycle_.reset(new PersistentInteger("daily.cycle")); + weekly_cycle_.reset(new PersistentInteger("weekly.cycle")); + version_cycle_.reset(new PersistentInteger("version.cycle")); + + diskstats_path_ = diskstats_path; + vmstats_path_ = vmstats_path; + scaling_max_freq_path_ = scaling_max_freq_path; + cpuinfo_max_freq_path_ = cpuinfo_max_freq_path; + + // If testing, initialize Stats Reporter without connecting DBus + if (testing_) + StatsReporterInit(); +} + +int MetricsDaemon::OnInit() { + int return_code = chromeos::DBusDaemon::OnInit(); + if (return_code != EX_OK) + return return_code; + + StatsReporterInit(); + + // Start collecting meminfo stats. + ScheduleMeminfoCallback(kMetricMeminfoInterval); + memuse_final_time_ = GetActiveTime() + kMemuseIntervals[0]; + ScheduleMemuseCallback(kMemuseIntervals[0]); + + if (testing_) + return EX_OK; + + bus_->AssertOnDBusThread(); + CHECK(bus_->SetUpAsyncOperations()); + + if (bus_->is_connected()) { + const std::string match_rule = + base::StringPrintf(kCrashReporterMatchRule, + kCrashReporterInterface, + kCrashReporterUserCrashSignal); + + bus_->AddFilterFunction(&MetricsDaemon::MessageFilter, this); + + DBusError error; + dbus_error_init(&error); + bus_->AddMatch(match_rule, &error); + + if (dbus_error_is_set(&error)) { + LOG(ERROR) << "Failed to add match rule \"" << match_rule << "\". Got " + << error.name << ": " << error.message; + return EX_SOFTWARE; + } + } else { + LOG(ERROR) << "DBus isn't connected."; + return EX_UNAVAILABLE; + } + + base::MessageLoop::current()->PostDelayedTask(FROM_HERE, + base::Bind(&MetricsDaemon::HandleUpdateStatsTimeout, + base::Unretained(this)), + base::TimeDelta::FromMilliseconds(kUpdateStatsIntervalMs)); + + if (uploader_active_) { + if (IsOnOfficialBuild()) { + LOG(INFO) << "uploader enabled"; + upload_service_.reset( + new UploadService(new SystemProfileCache(), metrics_lib_, server_)); + upload_service_->Init(upload_interval_, metrics_file_); + } else { + LOG(INFO) << "uploader disabled on non-official build"; + } + } + + return EX_OK; +} + +void MetricsDaemon::OnShutdown(int* return_code) { + if (!testing_ && bus_->is_connected()) { + const std::string match_rule = + base::StringPrintf(kCrashReporterMatchRule, + kCrashReporterInterface, + kCrashReporterUserCrashSignal); + + bus_->RemoveFilterFunction(&MetricsDaemon::MessageFilter, this); + + DBusError error; + dbus_error_init(&error); + bus_->RemoveMatch(match_rule, &error); + + if (dbus_error_is_set(&error)) { + LOG(ERROR) << "Failed to remove match rule \"" << match_rule << "\". Got " + << error.name << ": " << error.message; + } + } + chromeos::DBusDaemon::OnShutdown(return_code); +} + +// static +DBusHandlerResult MetricsDaemon::MessageFilter(DBusConnection* connection, + DBusMessage* message, + void* user_data) { + int message_type = dbus_message_get_type(message); + if (message_type != DBUS_MESSAGE_TYPE_SIGNAL) { + DLOG(WARNING) << "unexpected message type " << message_type; + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } + + // Signal messages always have interfaces. + const std::string interface(dbus_message_get_interface(message)); + const std::string member(dbus_message_get_member(message)); + DLOG(INFO) << "Got " << interface << "." << member << " D-Bus signal"; + + MetricsDaemon* daemon = static_cast<MetricsDaemon*>(user_data); + + DBusMessageIter iter; + dbus_message_iter_init(message, &iter); + if (interface == kCrashReporterInterface) { + CHECK_EQ(member, kCrashReporterUserCrashSignal); + daemon->ProcessUserCrash(); + } else { + // Ignore messages from the bus itself. + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } + + return DBUS_HANDLER_RESULT_HANDLED; +} + +// One might argue that parts of this should go into +// chromium/src/base/sys_info_chromeos.c instead, but put it here for now. + +TimeDelta MetricsDaemon::GetIncrementalCpuUse() { + FilePath proc_stat_path = FilePath(kMetricsProcStatFileName); + std::string proc_stat_string; + if (!base::ReadFileToString(proc_stat_path, &proc_stat_string)) { + LOG(WARNING) << "cannot open " << kMetricsProcStatFileName; + return TimeDelta(); + } + + std::vector<std::string> proc_stat_lines; + base::SplitString(proc_stat_string, '\n', &proc_stat_lines); + if (proc_stat_lines.empty()) { + LOG(WARNING) << "cannot parse " << kMetricsProcStatFileName + << ": " << proc_stat_string; + return TimeDelta(); + } + std::vector<std::string> proc_stat_totals; + base::SplitStringAlongWhitespace(proc_stat_lines[0], &proc_stat_totals); + + uint64_t user_ticks, user_nice_ticks, system_ticks; + if (proc_stat_totals.size() != kMetricsProcStatFirstLineItemsCount || + proc_stat_totals[0] != "cpu" || + !base::StringToUint64(proc_stat_totals[1], &user_ticks) || + !base::StringToUint64(proc_stat_totals[2], &user_nice_ticks) || + !base::StringToUint64(proc_stat_totals[3], &system_ticks)) { + LOG(WARNING) << "cannot parse first line: " << proc_stat_lines[0]; + return TimeDelta(base::TimeDelta::FromSeconds(0)); + } + + uint64_t total_cpu_use_ticks = user_ticks + user_nice_ticks + system_ticks; + + // Sanity check. + if (total_cpu_use_ticks < latest_cpu_use_ticks_) { + LOG(WARNING) << "CPU time decreasing from " << latest_cpu_use_ticks_ + << " to " << total_cpu_use_ticks; + return TimeDelta(); + } + + uint64_t diff = total_cpu_use_ticks - latest_cpu_use_ticks_; + latest_cpu_use_ticks_ = total_cpu_use_ticks; + // Use microseconds to avoid significant truncations. + return base::TimeDelta::FromMicroseconds( + diff * 1000 * 1000 / ticks_per_second_); +} + +void MetricsDaemon::ProcessUserCrash() { + // Counts the active time up to now. + UpdateStats(TimeTicks::Now(), Time::Now()); + + // Reports the active use time since the last crash and resets it. + SendCrashIntervalSample(user_crash_interval_); + + any_crashes_daily_count_->Add(1); + any_crashes_weekly_count_->Add(1); + user_crashes_daily_count_->Add(1); + user_crashes_weekly_count_->Add(1); +} + +void MetricsDaemon::ProcessKernelCrash() { + // Counts the active time up to now. + UpdateStats(TimeTicks::Now(), Time::Now()); + + // Reports the active use time since the last crash and resets it. + SendCrashIntervalSample(kernel_crash_interval_); + + any_crashes_daily_count_->Add(1); + any_crashes_weekly_count_->Add(1); + kernel_crashes_daily_count_->Add(1); + kernel_crashes_weekly_count_->Add(1); + + kernel_crashes_version_count_->Add(1); +} + +void MetricsDaemon::ProcessUncleanShutdown() { + // Counts the active time up to now. + UpdateStats(TimeTicks::Now(), Time::Now()); + + // Reports the active use time since the last crash and resets it. + SendCrashIntervalSample(unclean_shutdown_interval_); + + unclean_shutdowns_daily_count_->Add(1); + unclean_shutdowns_weekly_count_->Add(1); + any_crashes_daily_count_->Add(1); + any_crashes_weekly_count_->Add(1); +} + +bool MetricsDaemon::CheckSystemCrash(const string& crash_file) { + FilePath crash_detected(crash_file); + if (!base::PathExists(crash_detected)) + return false; + + // Deletes the crash-detected file so that the daemon doesn't report + // another kernel crash in case it's restarted. + base::DeleteFile(crash_detected, false); // not recursive + return true; +} + +void MetricsDaemon::StatsReporterInit() { + DiskStatsReadStats(&read_sectors_, &write_sectors_); + VmStatsReadStats(&vmstats_); + // The first time around just run the long stat, so we don't delay boot. + stats_state_ = kStatsLong; + stats_initial_time_ = GetActiveTime(); + if (stats_initial_time_ < 0) { + LOG(WARNING) << "not collecting disk stats"; + } else { + ScheduleStatsCallback(kMetricStatsLongInterval); + } +} + +void MetricsDaemon::ScheduleStatsCallback(int wait) { + if (testing_) { + return; + } + base::MessageLoop::current()->PostDelayedTask(FROM_HERE, + base::Bind(&MetricsDaemon::StatsCallback, base::Unretained(this)), + base::TimeDelta::FromSeconds(wait)); +} + +bool MetricsDaemon::DiskStatsReadStats(uint64_t* read_sectors, + uint64_t* write_sectors) { + int nchars; + int nitems; + bool success = false; + char line[200]; + if (diskstats_path_.empty()) { + return false; + } + int file = HANDLE_EINTR(open(diskstats_path_.c_str(), O_RDONLY)); + if (file < 0) { + PLOG(WARNING) << "cannot open " << diskstats_path_; + return false; + } + nchars = HANDLE_EINTR(read(file, line, sizeof(line))); + if (nchars < 0) { + PLOG(WARNING) << "cannot read from " << diskstats_path_; + return false; + } else { + LOG_IF(WARNING, nchars == sizeof(line)) + << "line too long in " << diskstats_path_; + line[nchars] = '\0'; + nitems = sscanf(line, "%*d %*d %" PRIu64 " %*d %*d %*d %" PRIu64, + read_sectors, write_sectors); + if (nitems == 2) { + success = true; + } else { + LOG(WARNING) << "found " << nitems << " items in " + << diskstats_path_ << ", expected 2"; + } + } + IGNORE_EINTR(close(file)); + return success; +} + +bool MetricsDaemon::VmStatsParseStats(const char* stats, + struct VmstatRecord* record) { + // a mapping of string name to field in VmstatRecord and whether we found it + struct mapping { + const string name; + uint64_t* value_p; + bool found; + } map[] = + { { .name = "pgmajfault", + .value_p = &record->page_faults_, + .found = false }, + { .name = "pswpin", + .value_p = &record->swap_in_, + .found = false }, + { .name = "pswpout", + .value_p = &record->swap_out_, + .found = false }, }; + + // Each line in the file has the form + // <ID> <VALUE> + // for instance: + // nr_free_pages 213427 + vector<string> lines; + Tokenize(stats, "\n", &lines); + for (vector<string>::iterator it = lines.begin(); + it != lines.end(); ++it) { + vector<string> tokens; + base::SplitString(*it, ' ', &tokens); + if (tokens.size() == 2) { + for (unsigned int i = 0; i < sizeof(map)/sizeof(struct mapping); i++) { + if (!tokens[0].compare(map[i].name)) { + if (!base::StringToUint64(tokens[1], map[i].value_p)) + return false; + map[i].found = true; + } + } + } else { + LOG(WARNING) << "unexpected vmstat format"; + } + } + // make sure we got all the stats + for (unsigned i = 0; i < sizeof(map)/sizeof(struct mapping); i++) { + if (map[i].found == false) { + LOG(WARNING) << "vmstat missing " << map[i].name; + return false; + } + } + return true; +} + +bool MetricsDaemon::VmStatsReadStats(struct VmstatRecord* stats) { + string value_string; + FilePath* path = new FilePath(vmstats_path_); + if (!base::ReadFileToString(*path, &value_string)) { + delete path; + LOG(WARNING) << "cannot read " << vmstats_path_; + return false; + } + delete path; + return VmStatsParseStats(value_string.c_str(), stats); +} + +bool MetricsDaemon::ReadFreqToInt(const string& sysfs_file_name, int* value) { + const FilePath sysfs_path(sysfs_file_name); + string value_string; + if (!base::ReadFileToString(sysfs_path, &value_string)) { + LOG(WARNING) << "cannot read " << sysfs_path.value().c_str(); + return false; + } + if (!base::RemoveChars(value_string, "\n", &value_string)) { + LOG(WARNING) << "no newline in " << value_string; + // Continue even though the lack of newline is suspicious. + } + if (!base::StringToInt(value_string, value)) { + LOG(WARNING) << "cannot convert " << value_string << " to int"; + return false; + } + return true; +} + +void MetricsDaemon::SendCpuThrottleMetrics() { + // |max_freq| is 0 only the first time through. + static int max_freq = 0; + if (max_freq == -1) + // Give up, as sysfs did not report max_freq correctly. + return; + if (max_freq == 0 || testing_) { + // One-time initialization of max_freq. (Every time when testing.) + if (!ReadFreqToInt(cpuinfo_max_freq_path_, &max_freq)) { + max_freq = -1; + return; + } + if (max_freq == 0) { + LOG(WARNING) << "sysfs reports 0 max CPU frequency\n"; + max_freq = -1; + return; + } + if (max_freq % 10000 == 1000) { + // Special case: system has turbo mode, and max non-turbo frequency is + // max_freq - 1000. This relies on "normal" (non-turbo) frequencies + // being multiples of (at least) 10 MHz. Although there is no guarantee + // of this, it seems a fairly reasonable assumption. Otherwise we should + // read scaling_available_frequencies, sort the frequencies, compare the + // two highest ones, and check if they differ by 1000 (kHz) (and that's a + // hack too, no telling when it will change). + max_freq -= 1000; + } + } + int scaled_freq = 0; + if (!ReadFreqToInt(scaling_max_freq_path_, &scaled_freq)) + return; + // Frequencies are in kHz. If scaled_freq > max_freq, turbo is on, but + // scaled_freq is not the actual turbo frequency. We indicate this situation + // with a 101% value. + int percent = scaled_freq > max_freq ? 101 : scaled_freq / (max_freq / 100); + SendLinearSample(kMetricScaledCpuFrequencyName, percent, 101, 102); +} + +// Collects disk and vm stats alternating over a short and a long interval. + +void MetricsDaemon::StatsCallback() { + uint64_t read_sectors_now, write_sectors_now; + struct VmstatRecord vmstats_now; + double time_now = GetActiveTime(); + double delta_time = time_now - stats_initial_time_; + if (testing_) { + // Fake the time when testing. + delta_time = stats_state_ == kStatsShort ? + kMetricStatsShortInterval : kMetricStatsLongInterval; + } + bool diskstats_success = DiskStatsReadStats(&read_sectors_now, + &write_sectors_now); + int delta_read = read_sectors_now - read_sectors_; + int delta_write = write_sectors_now - write_sectors_; + int read_sectors_per_second = delta_read / delta_time; + int write_sectors_per_second = delta_write / delta_time; + bool vmstats_success = VmStatsReadStats(&vmstats_now); + uint64_t delta_faults = vmstats_now.page_faults_ - vmstats_.page_faults_; + uint64_t delta_swap_in = vmstats_now.swap_in_ - vmstats_.swap_in_; + uint64_t delta_swap_out = vmstats_now.swap_out_ - vmstats_.swap_out_; + uint64_t page_faults_per_second = delta_faults / delta_time; + uint64_t swap_in_per_second = delta_swap_in / delta_time; + uint64_t swap_out_per_second = delta_swap_out / delta_time; + + switch (stats_state_) { + case kStatsShort: + if (diskstats_success) { + SendSample(kMetricReadSectorsShortName, + read_sectors_per_second, + 1, + kMetricSectorsIOMax, + kMetricSectorsBuckets); + SendSample(kMetricWriteSectorsShortName, + write_sectors_per_second, + 1, + kMetricSectorsIOMax, + kMetricSectorsBuckets); + } + if (vmstats_success) { + SendSample(kMetricPageFaultsShortName, + page_faults_per_second, + 1, + kMetricPageFaultsMax, + kMetricPageFaultsBuckets); + SendSample(kMetricSwapInShortName, + swap_in_per_second, + 1, + kMetricPageFaultsMax, + kMetricPageFaultsBuckets); + SendSample(kMetricSwapOutShortName, + swap_out_per_second, + 1, + kMetricPageFaultsMax, + kMetricPageFaultsBuckets); + } + // Schedule long callback. + stats_state_ = kStatsLong; + ScheduleStatsCallback(kMetricStatsLongInterval - + kMetricStatsShortInterval); + break; + case kStatsLong: + if (diskstats_success) { + SendSample(kMetricReadSectorsLongName, + read_sectors_per_second, + 1, + kMetricSectorsIOMax, + kMetricSectorsBuckets); + SendSample(kMetricWriteSectorsLongName, + write_sectors_per_second, + 1, + kMetricSectorsIOMax, + kMetricSectorsBuckets); + // Reset sector counters. + read_sectors_ = read_sectors_now; + write_sectors_ = write_sectors_now; + } + if (vmstats_success) { + SendSample(kMetricPageFaultsLongName, + page_faults_per_second, + 1, + kMetricPageFaultsMax, + kMetricPageFaultsBuckets); + SendSample(kMetricSwapInLongName, + swap_in_per_second, + 1, + kMetricPageFaultsMax, + kMetricPageFaultsBuckets); + SendSample(kMetricSwapOutLongName, + swap_out_per_second, + 1, + kMetricPageFaultsMax, + kMetricPageFaultsBuckets); + + vmstats_ = vmstats_now; + } + SendCpuThrottleMetrics(); + // Set start time for new cycle. + stats_initial_time_ = time_now; + // Schedule short callback. + stats_state_ = kStatsShort; + ScheduleStatsCallback(kMetricStatsShortInterval); + break; + default: + LOG(FATAL) << "Invalid stats state"; + } +} + +void MetricsDaemon::ScheduleMeminfoCallback(int wait) { + if (testing_) { + return; + } + base::TimeDelta waitDelta = base::TimeDelta::FromSeconds(wait); + base::MessageLoop::current()->PostDelayedTask(FROM_HERE, + base::Bind(&MetricsDaemon::MeminfoCallback, base::Unretained(this), + waitDelta), + waitDelta); +} + +void MetricsDaemon::MeminfoCallback(base::TimeDelta wait) { + string meminfo_raw; + const FilePath meminfo_path("/proc/meminfo"); + if (!base::ReadFileToString(meminfo_path, &meminfo_raw)) { + LOG(WARNING) << "cannot read " << meminfo_path.value().c_str(); + return; + } + // Make both calls even if the first one fails. + bool success = ProcessMeminfo(meminfo_raw); + bool reschedule = + ReportZram(base::FilePath(FILE_PATH_LITERAL("/sys/block/zram0"))) && + success; + if (reschedule) { + base::MessageLoop::current()->PostDelayedTask(FROM_HERE, + base::Bind(&MetricsDaemon::MeminfoCallback, base::Unretained(this), + wait), + wait); + } +} + +// static +bool MetricsDaemon::ReadFileToUint64(const base::FilePath& path, + uint64_t* value) { + std::string content; + if (!base::ReadFileToString(path, &content)) { + PLOG(WARNING) << "cannot read " << path.MaybeAsASCII(); + return false; + } + // Remove final newline. + base::TrimWhitespaceASCII(content, base::TRIM_TRAILING, &content); + if (!base::StringToUint64(content, value)) { + LOG(WARNING) << "invalid integer: " << content; + return false; + } + return true; +} + +bool MetricsDaemon::ReportZram(const base::FilePath& zram_dir) { + // Data sizes are in bytes. |zero_pages| is in number of pages. + uint64_t compr_data_size, orig_data_size, zero_pages; + const size_t page_size = 4096; + + if (!ReadFileToUint64(zram_dir.Append(kComprDataSizeName), + &compr_data_size) || + !ReadFileToUint64(zram_dir.Append(kOrigDataSizeName), &orig_data_size) || + !ReadFileToUint64(zram_dir.Append(kZeroPagesName), &zero_pages)) { + return false; + } + + // |orig_data_size| does not include zero-filled pages. + orig_data_size += zero_pages * page_size; + + const int compr_data_size_mb = compr_data_size >> 20; + const int savings_mb = (orig_data_size - compr_data_size) >> 20; + const int zero_ratio_percent = zero_pages * page_size * 100 / orig_data_size; + + // Report compressed size in megabytes. 100 MB or less has little impact. + SendSample("Platform.ZramCompressedSize", compr_data_size_mb, 100, 4000, 50); + SendSample("Platform.ZramSavings", savings_mb, 100, 4000, 50); + // The compression ratio is multiplied by 100 for better resolution. The + // ratios of interest are between 1 and 6 (100% and 600% as reported). We + // don't want samples when very little memory is being compressed. + if (compr_data_size_mb >= 1) { + SendSample("Platform.ZramCompressionRatioPercent", + orig_data_size * 100 / compr_data_size, 100, 600, 50); + } + // The values of interest for zero_pages are between 1MB and 1GB. The units + // are number of pages. + SendSample("Platform.ZramZeroPages", zero_pages, 256, 256 * 1024, 50); + SendSample("Platform.ZramZeroRatioPercent", zero_ratio_percent, 1, 50, 50); + + return true; +} + +bool MetricsDaemon::ProcessMeminfo(const string& meminfo_raw) { + static const MeminfoRecord fields_array[] = { + { "MemTotal", "MemTotal" }, // SPECIAL CASE: total system memory + { "MemFree", "MemFree" }, + { "Buffers", "Buffers" }, + { "Cached", "Cached" }, + // { "SwapCached", "SwapCached" }, + { "Active", "Active" }, + { "Inactive", "Inactive" }, + { "ActiveAnon", "Active(anon)" }, + { "InactiveAnon", "Inactive(anon)" }, + { "ActiveFile" , "Active(file)" }, + { "InactiveFile", "Inactive(file)" }, + { "Unevictable", "Unevictable", kMeminfoOp_HistLog }, + // { "Mlocked", "Mlocked" }, + { "SwapTotal", "SwapTotal", kMeminfoOp_SwapTotal }, + { "SwapFree", "SwapFree", kMeminfoOp_SwapFree }, + // { "Dirty", "Dirty" }, + // { "Writeback", "Writeback" }, + { "AnonPages", "AnonPages" }, + { "Mapped", "Mapped" }, + { "Shmem", "Shmem", kMeminfoOp_HistLog }, + { "Slab", "Slab", kMeminfoOp_HistLog }, + // { "SReclaimable", "SReclaimable" }, + // { "SUnreclaim", "SUnreclaim" }, + }; + vector<MeminfoRecord> fields(fields_array, + fields_array + arraysize(fields_array)); + if (!FillMeminfo(meminfo_raw, &fields)) { + return false; + } + int total_memory = fields[0].value; + if (total_memory == 0) { + // this "cannot happen" + LOG(WARNING) << "borked meminfo parser"; + return false; + } + int swap_total = 0; + int swap_free = 0; + // Send all fields retrieved, except total memory. + for (unsigned int i = 1; i < fields.size(); i++) { + string metrics_name = base::StringPrintf("Platform.Meminfo%s", + fields[i].name); + int percent; + switch (fields[i].op) { + case kMeminfoOp_HistPercent: + // report value as percent of total memory + percent = fields[i].value * 100 / total_memory; + SendLinearSample(metrics_name, percent, 100, 101); + break; + case kMeminfoOp_HistLog: + // report value in kbytes, log scale, 4Gb max + SendSample(metrics_name, fields[i].value, 1, 4 * 1000 * 1000, 100); + break; + case kMeminfoOp_SwapTotal: + swap_total = fields[i].value; + case kMeminfoOp_SwapFree: + swap_free = fields[i].value; + break; + } + } + if (swap_total > 0) { + int swap_used = swap_total - swap_free; + int swap_used_percent = swap_used * 100 / swap_total; + SendSample("Platform.MeminfoSwapUsed", swap_used, 1, 8 * 1000 * 1000, 100); + SendLinearSample("Platform.MeminfoSwapUsedPercent", swap_used_percent, + 100, 101); + } + return true; +} + +bool MetricsDaemon::FillMeminfo(const string& meminfo_raw, + vector<MeminfoRecord>* fields) { + vector<string> lines; + unsigned int nlines = Tokenize(meminfo_raw, "\n", &lines); + + // Scan meminfo output and collect field values. Each field name has to + // match a meminfo entry (case insensitive) after removing non-alpha + // characters from the entry. + unsigned int ifield = 0; + for (unsigned int iline = 0; + iline < nlines && ifield < fields->size(); + iline++) { + vector<string> tokens; + Tokenize(lines[iline], ": ", &tokens); + if (strcmp((*fields)[ifield].match, tokens[0].c_str()) == 0) { + // Name matches. Parse value and save. + char* rest; + (*fields)[ifield].value = + static_cast<int>(strtol(tokens[1].c_str(), &rest, 10)); + if (*rest != '\0') { + LOG(WARNING) << "missing meminfo value"; + return false; + } + ifield++; + } + } + if (ifield < fields->size()) { + // End of input reached while scanning. + LOG(WARNING) << "cannot find field " << (*fields)[ifield].match + << " and following"; + return false; + } + return true; +} + +void MetricsDaemon::ScheduleMemuseCallback(double interval) { + if (testing_) { + return; + } + base::MessageLoop::current()->PostDelayedTask(FROM_HERE, + base::Bind(&MetricsDaemon::MemuseCallback, base::Unretained(this)), + base::TimeDelta::FromSeconds(interval)); +} + +void MetricsDaemon::MemuseCallback() { + // Since we only care about active time (i.e. uptime minus sleep time) but + // the callbacks are driven by real time (uptime), we check if we should + // reschedule this callback due to intervening sleep periods. + double now = GetActiveTime(); + // Avoid intervals of less than one second. + double remaining_time = ceil(memuse_final_time_ - now); + if (remaining_time > 0) { + ScheduleMemuseCallback(remaining_time); + } else { + // Report stats and advance the measurement interval unless there are + // errors or we've completed the last interval. + if (MemuseCallbackWork() && + memuse_interval_index_ < arraysize(kMemuseIntervals)) { + double interval = kMemuseIntervals[memuse_interval_index_++]; + memuse_final_time_ = now + interval; + ScheduleMemuseCallback(interval); + } + } +} + +bool MetricsDaemon::MemuseCallbackWork() { + string meminfo_raw; + const FilePath meminfo_path("/proc/meminfo"); + if (!base::ReadFileToString(meminfo_path, &meminfo_raw)) { + LOG(WARNING) << "cannot read " << meminfo_path.value().c_str(); + return false; + } + return ProcessMemuse(meminfo_raw); +} + +bool MetricsDaemon::ProcessMemuse(const string& meminfo_raw) { + static const MeminfoRecord fields_array[] = { + { "MemTotal", "MemTotal" }, // SPECIAL CASE: total system memory + { "ActiveAnon", "Active(anon)" }, + { "InactiveAnon", "Inactive(anon)" }, + }; + vector<MeminfoRecord> fields(fields_array, + fields_array + arraysize(fields_array)); + if (!FillMeminfo(meminfo_raw, &fields)) { + return false; + } + int total = fields[0].value; + int active_anon = fields[1].value; + int inactive_anon = fields[2].value; + if (total == 0) { + // this "cannot happen" + LOG(WARNING) << "borked meminfo parser"; + return false; + } + string metrics_name = base::StringPrintf("Platform.MemuseAnon%d", + memuse_interval_index_); + SendLinearSample(metrics_name, (active_anon + inactive_anon) * 100 / total, + 100, 101); + return true; +} + +void MetricsDaemon::SendSample(const string& name, int sample, + int min, int max, int nbuckets) { + metrics_lib_->SendToUMA(name, sample, min, max, nbuckets); +} + +void MetricsDaemon::SendKernelCrashesCumulativeCountStats() { + // Report the number of crashes for this OS version, but don't clear the + // counter. It is cleared elsewhere on version change. + int64_t crashes_count = kernel_crashes_version_count_->Get(); + SendSample(kernel_crashes_version_count_->Name(), + crashes_count, + 1, // value of first bucket + 500, // value of last bucket + 100); // number of buckets + + + int64_t cpu_use_ms = version_cumulative_cpu_use_->Get(); + SendSample(version_cumulative_cpu_use_->Name(), + cpu_use_ms / 1000, // stat is in seconds + 1, // device may be used very little... + 8 * 1000 * 1000, // ... or a lot (a little over 90 days) + 100); + + // On the first run after an autoupdate, cpu_use_ms and active_use_seconds + // can be zero. Avoid division by zero. + if (cpu_use_ms > 0) { + // Send the crash frequency since update in number of crashes per CPU year. + SendSample("Logging.KernelCrashesPerCpuYear", + crashes_count * kSecondsPerDay * 365 * 1000 / cpu_use_ms, + 1, + 1000 * 1000, // about one crash every 30s of CPU time + 100); + } + + int64_t active_use_seconds = version_cumulative_active_use_->Get(); + if (active_use_seconds > 0) { + SendSample(version_cumulative_active_use_->Name(), + active_use_seconds / 1000, // stat is in seconds + 1, // device may be used very little... + 8 * 1000 * 1000, // ... or a lot (about 90 days) + 100); + // Same as above, but per year of active time. + SendSample("Logging.KernelCrashesPerActiveYear", + crashes_count * kSecondsPerDay * 365 / active_use_seconds, + 1, + 1000 * 1000, // about one crash every 30s of active time + 100); + } +} + +void MetricsDaemon::SendDailyUseSample( + const scoped_ptr<PersistentInteger>& use) { + SendSample(use->Name(), + use->GetAndClear(), + 1, // value of first bucket + kSecondsPerDay, // value of last bucket + 50); // number of buckets +} + +void MetricsDaemon::SendCrashIntervalSample( + const scoped_ptr<PersistentInteger>& interval) { + SendSample(interval->Name(), + interval->GetAndClear(), + 1, // value of first bucket + 4 * kSecondsPerWeek, // value of last bucket + 50); // number of buckets +} + +void MetricsDaemon::SendCrashFrequencySample( + const scoped_ptr<PersistentInteger>& frequency) { + SendSample(frequency->Name(), + frequency->GetAndClear(), + 1, // value of first bucket + 100, // value of last bucket + 50); // number of buckets +} + +void MetricsDaemon::SendLinearSample(const string& name, int sample, + int max, int nbuckets) { + // TODO(semenzato): add a proper linear histogram to the Chrome external + // metrics API. + LOG_IF(FATAL, nbuckets != max + 1) << "unsupported histogram scale"; + metrics_lib_->SendEnumToUMA(name, sample, max); +} + +void MetricsDaemon::UpdateStats(TimeTicks now_ticks, + Time now_wall_time) { + const int elapsed_seconds = (now_ticks - last_update_stats_time_).InSeconds(); + daily_active_use_->Add(elapsed_seconds); + version_cumulative_active_use_->Add(elapsed_seconds); + user_crash_interval_->Add(elapsed_seconds); + kernel_crash_interval_->Add(elapsed_seconds); + version_cumulative_cpu_use_->Add(GetIncrementalCpuUse().InMilliseconds()); + last_update_stats_time_ = now_ticks; + + const TimeDelta since_epoch = now_wall_time - Time::UnixEpoch(); + const int day = since_epoch.InDays(); + const int week = day / 7; + + if (daily_cycle_->Get() != day) { + daily_cycle_->Set(day); + SendDailyUseSample(daily_active_use_); + SendDailyUseSample(version_cumulative_active_use_); + SendCrashFrequencySample(any_crashes_daily_count_); + SendCrashFrequencySample(user_crashes_daily_count_); + SendCrashFrequencySample(kernel_crashes_daily_count_); + SendCrashFrequencySample(unclean_shutdowns_daily_count_); + SendKernelCrashesCumulativeCountStats(); + } + + if (weekly_cycle_->Get() != week) { + weekly_cycle_->Set(week); + SendCrashFrequencySample(any_crashes_weekly_count_); + SendCrashFrequencySample(user_crashes_weekly_count_); + SendCrashFrequencySample(kernel_crashes_weekly_count_); + SendCrashFrequencySample(unclean_shutdowns_weekly_count_); + } +} + +void MetricsDaemon::HandleUpdateStatsTimeout() { + UpdateStats(TimeTicks::Now(), Time::Now()); + base::MessageLoop::current()->PostDelayedTask(FROM_HERE, + base::Bind(&MetricsDaemon::HandleUpdateStatsTimeout, + base::Unretained(this)), + base::TimeDelta::FromMilliseconds(kUpdateStatsIntervalMs)); +} diff --git a/metrics/metrics_daemon.h b/metrics/metrics_daemon.h new file mode 100644 index 000000000..b1b2d11c1 --- /dev/null +++ b/metrics/metrics_daemon.h @@ -0,0 +1,371 @@ +// Copyright (c) 2010 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef METRICS_METRICS_DAEMON_H_ +#define METRICS_METRICS_DAEMON_H_ + +#include <stdint.h> + +#include <map> +#include <string> +#include <vector> + +#include <base/files/file_path.h> +#include <base/memory/scoped_ptr.h> +#include <base/time/time.h> +#include <chromeos/daemons/dbus_daemon.h> +#include <gtest/gtest_prod.h> // for FRIEND_TEST + +#include "metrics/metrics_library.h" +#include "metrics/persistent_integer.h" +#include "uploader/upload_service.h" + +using chromeos_metrics::PersistentInteger; + +class MetricsDaemon : public chromeos::DBusDaemon { + public: + MetricsDaemon(); + ~MetricsDaemon(); + + // Initializes metrics class variables. + void Init(bool testing, + bool uploader_active, + MetricsLibraryInterface* metrics_lib, + const std::string& diskstats_path, + const std::string& vmstats_path, + const std::string& cpuinfo_max_freq_path, + const std::string& scaling_max_freq_path, + const base::TimeDelta& upload_interval, + const std::string& server, + const std::string& metrics_file, + const std::string& config_root); + + // Initializes DBus and MessageLoop variables before running the MessageLoop. + int OnInit() override; + + // Clean up data set up in OnInit before shutting down message loop. + void OnShutdown(int* return_code) override; + + // Does all the work. + int Run() override; + + // Triggers an upload event and exit. (Used to test UploadService) + void RunUploaderTest(); + + protected: + // Used also by the unit tests. + static const char kComprDataSizeName[]; + static const char kOrigDataSizeName[]; + static const char kZeroPagesName[]; + + private: + friend class MetricsDaemonTest; + FRIEND_TEST(MetricsDaemonTest, CheckSystemCrash); + FRIEND_TEST(MetricsDaemonTest, ComputeEpochNoCurrent); + FRIEND_TEST(MetricsDaemonTest, ComputeEpochNoLast); + FRIEND_TEST(MetricsDaemonTest, GetHistogramPath); + FRIEND_TEST(MetricsDaemonTest, IsNewEpoch); + FRIEND_TEST(MetricsDaemonTest, MessageFilter); + FRIEND_TEST(MetricsDaemonTest, ParseVmStats); + FRIEND_TEST(MetricsDaemonTest, ProcessKernelCrash); + FRIEND_TEST(MetricsDaemonTest, ProcessMeminfo); + FRIEND_TEST(MetricsDaemonTest, ProcessMeminfo2); + FRIEND_TEST(MetricsDaemonTest, ProcessUncleanShutdown); + FRIEND_TEST(MetricsDaemonTest, ProcessUserCrash); + FRIEND_TEST(MetricsDaemonTest, ReportCrashesDailyFrequency); + FRIEND_TEST(MetricsDaemonTest, ReadFreqToInt); + FRIEND_TEST(MetricsDaemonTest, ReportDiskStats); + FRIEND_TEST(MetricsDaemonTest, ReportKernelCrashInterval); + FRIEND_TEST(MetricsDaemonTest, ReportUncleanShutdownInterval); + FRIEND_TEST(MetricsDaemonTest, ReportUserCrashInterval); + FRIEND_TEST(MetricsDaemonTest, SendSample); + FRIEND_TEST(MetricsDaemonTest, SendCpuThrottleMetrics); + FRIEND_TEST(MetricsDaemonTest, SendZramMetrics); + + // State for disk stats collector callback. + enum StatsState { + kStatsShort, // short wait before short interval collection + kStatsLong, // final wait before new collection + }; + + // Data record for aggregating daily usage. + class UseRecord { + public: + UseRecord() : day_(0), seconds_(0) {} + int day_; + int seconds_; + }; + + // Type of scale to use for meminfo histograms. For most of them we use + // percent of total RAM, but for some we use absolute numbers, usually in + // megabytes, on a log scale from 0 to 4000, and 0 to 8000 for compressed + // swap (since it can be larger than total RAM). + enum MeminfoOp { + kMeminfoOp_HistPercent = 0, + kMeminfoOp_HistLog, + kMeminfoOp_SwapTotal, + kMeminfoOp_SwapFree, + }; + + // Record for retrieving and reporting values from /proc/meminfo. + struct MeminfoRecord { + const char* name; // print name + const char* match; // string to match in output of /proc/meminfo + MeminfoOp op; // histogram scale selector, or other operator + int value; // value from /proc/meminfo + }; + + // Record for retrieving and reporting values from /proc/vmstat + struct VmstatRecord { + uint64_t page_faults_; // major faults + uint64_t swap_in_; // pages swapped in + uint64_t swap_out_; // pages swapped out + }; + + // Metric parameters. + static const char kMetricReadSectorsLongName[]; + static const char kMetricReadSectorsShortName[]; + static const char kMetricWriteSectorsLongName[]; + static const char kMetricWriteSectorsShortName[]; + static const char kMetricPageFaultsShortName[]; + static const char kMetricPageFaultsLongName[]; + static const char kMetricSwapInLongName[]; + static const char kMetricSwapInShortName[]; + static const char kMetricSwapOutLongName[]; + static const char kMetricSwapOutShortName[]; + static const char kMetricScaledCpuFrequencyName[]; + static const int kMetricStatsShortInterval; + static const int kMetricStatsLongInterval; + static const int kMetricMeminfoInterval; + static const int kMetricSectorsIOMax; + static const int kMetricSectorsBuckets; + static const int kMetricPageFaultsMax; + static const int kMetricPageFaultsBuckets; + static const char kMetricsDiskStatsPath[]; + static const char kMetricsVmStatsPath[]; + static const char kMetricsProcStatFileName[]; + static const int kMetricsProcStatFirstLineItemsCount; + + // Returns the active time since boot (uptime minus sleep time) in seconds. + double GetActiveTime(); + + // D-Bus filter callback. + static DBusHandlerResult MessageFilter(DBusConnection* connection, + DBusMessage* message, + void* user_data); + + // Updates the daily usage file, if necessary, by adding |seconds| + // of active use to the |day| since Epoch. If there's usage data for + // day in the past in the usage file, that data is sent to UMA and + // removed from the file. If there's already usage data for |day| in + // the usage file, the |seconds| are accumulated. + void LogDailyUseRecord(int day, int seconds); + + // Updates the active use time and logs time between user-space + // process crashes. + void ProcessUserCrash(); + + // Updates the active use time and logs time between kernel crashes. + void ProcessKernelCrash(); + + // Updates the active use time and logs time between unclean shutdowns. + void ProcessUncleanShutdown(); + + // Checks if a kernel crash has been detected and returns true if + // so. The method assumes that a kernel crash has happened if + // |crash_file| exists. It removes the file immediately if it + // exists, so it must not be called more than once. + bool CheckSystemCrash(const std::string& crash_file); + + // Sends a regular (exponential) histogram sample to Chrome for + // transport to UMA. See MetricsLibrary::SendToUMA in + // metrics_library.h for a description of the arguments. + void SendSample(const std::string& name, int sample, + int min, int max, int nbuckets); + + // Sends a linear histogram sample to Chrome for transport to UMA. See + // MetricsLibrary::SendToUMA in metrics_library.h for a description of the + // arguments. + void SendLinearSample(const std::string& name, int sample, + int max, int nbuckets); + + // Sends various cumulative kernel crash-related stats, for instance the + // total number of kernel crashes since the last version update. + void SendKernelCrashesCumulativeCountStats(); + + // Returns the total (system-wide) CPU usage between the time of the most + // recent call to this function and now. + base::TimeDelta GetIncrementalCpuUse(); + + // Sends a sample representing the number of seconds of active use + // for a 24-hour period. + void SendDailyUseSample(const scoped_ptr<PersistentInteger>& use); + + // Sends a sample representing a time interval between two crashes of the + // same type. + void SendCrashIntervalSample(const scoped_ptr<PersistentInteger>& interval); + + // Sends a sample representing a frequency of crashes of some type. + void SendCrashFrequencySample(const scoped_ptr<PersistentInteger>& frequency); + + // Initializes vm and disk stats reporting. + void StatsReporterInit(); + + // Schedules a callback for the next vm and disk stats collection. + void ScheduleStatsCallback(int wait); + + // Reads cumulative disk statistics from sysfs. Returns true for success. + bool DiskStatsReadStats(uint64_t* read_sectors, uint64_t* write_sectors); + + // Reads cumulative vm statistics from procfs. Returns true for success. + bool VmStatsReadStats(struct VmstatRecord* stats); + + // Parse cumulative vm statistics from a C string. Returns true for success. + bool VmStatsParseStats(const char* stats, struct VmstatRecord* record); + + // Reports disk and vm statistics. + void StatsCallback(); + + // Schedules meminfo collection callback. + void ScheduleMeminfoCallback(int wait); + + // Reports memory statistics. Reschedules callback on success. + void MeminfoCallback(base::TimeDelta wait); + + // Parses content of /proc/meminfo and sends fields of interest to UMA. + // Returns false on errors. |meminfo_raw| contains the content of + // /proc/meminfo. + bool ProcessMeminfo(const std::string& meminfo_raw); + + // Parses meminfo data from |meminfo_raw|. |fields| is a vector containing + // the fields of interest. The order of the fields must be the same in which + // /proc/meminfo prints them. The result of parsing fields[i] is placed in + // fields[i].value. + bool FillMeminfo(const std::string& meminfo_raw, + std::vector<MeminfoRecord>* fields); + + // Schedule a memory use callback in |interval| seconds. + void ScheduleMemuseCallback(double interval); + + // Calls MemuseCallbackWork, and possibly schedules next callback, if enough + // active time has passed. Otherwise reschedules itself to simulate active + // time callbacks (i.e. wall clock time minus sleep time). + void MemuseCallback(); + + // Reads /proc/meminfo and sends total anonymous memory usage to UMA. + bool MemuseCallbackWork(); + + // Parses meminfo data and sends it to UMA. + bool ProcessMemuse(const std::string& meminfo_raw); + + // Sends stats for thermal CPU throttling. + void SendCpuThrottleMetrics(); + + // Reads an integer CPU frequency value from sysfs. + bool ReadFreqToInt(const std::string& sysfs_file_name, int* value); + + // Reads the current OS version from /etc/lsb-release and hashes it + // to a unsigned 32-bit int. + uint32_t GetOsVersionHash(); + + // Returns true if the system is using an official build. + bool IsOnOfficialBuild() const; + + // Updates stats, additionally sending them to UMA if enough time has elapsed + // since the last report. + void UpdateStats(base::TimeTicks now_ticks, base::Time now_wall_time); + + // Invoked periodically by |update_stats_timeout_id_| to call UpdateStats(). + void HandleUpdateStatsTimeout(); + + // Reports zram statistics. + bool ReportZram(const base::FilePath& zram_dir); + + // Reads a string from a file and converts it to uint64_t. + static bool ReadFileToUint64(const base::FilePath& path, uint64_t* value); + + // VARIABLES + + // Test mode. + bool testing_; + + // Whether the uploader is enabled or disabled. + bool uploader_active_; + + // Root of the configuration files to use. + std::string config_root_; + + // The metrics library handle. + MetricsLibraryInterface* metrics_lib_; + + // Timestamps last network state update. This timestamp is used to + // sample the time from the network going online to going offline so + // TimeTicks ensures a monotonically increasing TimeDelta. + base::TimeTicks network_state_last_; + + // The last time that UpdateStats() was called. + base::TimeTicks last_update_stats_time_; + + // End time of current memuse stat collection interval. + double memuse_final_time_; + + // Selects the wait time for the next memory use callback. + unsigned int memuse_interval_index_; + + // Contain the most recent disk and vm cumulative stats. + uint64_t read_sectors_; + uint64_t write_sectors_; + struct VmstatRecord vmstats_; + + StatsState stats_state_; + double stats_initial_time_; + + // The system "HZ", or frequency of ticks. Some system data uses ticks as a + // unit, and this is used to convert to standard time units. + uint32_t ticks_per_second_; + // Used internally by GetIncrementalCpuUse() to return the CPU utilization + // between calls. + uint64_t latest_cpu_use_ticks_; + + // Persistent values and accumulators for crash statistics. + scoped_ptr<PersistentInteger> daily_cycle_; + scoped_ptr<PersistentInteger> weekly_cycle_; + scoped_ptr<PersistentInteger> version_cycle_; + + // Active use accumulated in a day. + scoped_ptr<PersistentInteger> daily_active_use_; + // Active use accumulated since the latest version update. + scoped_ptr<PersistentInteger> version_cumulative_active_use_; + + // The CPU time accumulator. This contains the CPU time, in milliseconds, + // used by the system since the most recent OS version update. + scoped_ptr<PersistentInteger> version_cumulative_cpu_use_; + + scoped_ptr<PersistentInteger> user_crash_interval_; + scoped_ptr<PersistentInteger> kernel_crash_interval_; + scoped_ptr<PersistentInteger> unclean_shutdown_interval_; + + scoped_ptr<PersistentInteger> any_crashes_daily_count_; + scoped_ptr<PersistentInteger> any_crashes_weekly_count_; + scoped_ptr<PersistentInteger> user_crashes_daily_count_; + scoped_ptr<PersistentInteger> user_crashes_weekly_count_; + scoped_ptr<PersistentInteger> kernel_crashes_daily_count_; + scoped_ptr<PersistentInteger> kernel_crashes_weekly_count_; + scoped_ptr<PersistentInteger> kernel_crashes_version_count_; + scoped_ptr<PersistentInteger> unclean_shutdowns_daily_count_; + scoped_ptr<PersistentInteger> unclean_shutdowns_weekly_count_; + + std::string diskstats_path_; + std::string vmstats_path_; + std::string scaling_max_freq_path_; + std::string cpuinfo_max_freq_path_; + + base::TimeDelta upload_interval_; + std::string server_; + std::string metrics_file_; + + scoped_ptr<UploadService> upload_service_; +}; + +#endif // METRICS_METRICS_DAEMON_H_ diff --git a/metrics/metrics_daemon_main.cc b/metrics/metrics_daemon_main.cc new file mode 100644 index 000000000..1f64ef341 --- /dev/null +++ b/metrics/metrics_daemon_main.cc @@ -0,0 +1,102 @@ +// Copyright (c) 2009 The Chromium OS 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 <base/at_exit.h> +#include <base/command_line.h> +#include <base/logging.h> +#include <base/strings/string_util.h> +#include <chromeos/flag_helper.h> +#include <chromeos/syslog_logging.h> +#include <rootdev/rootdev.h> + +#include "metrics/metrics_daemon.h" + +const char kScalingMaxFreqPath[] = + "/sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq"; +const char kCpuinfoMaxFreqPath[] = + "/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq"; + +// Returns the path to the disk stats in the sysfs. Returns the null string if +// it cannot find the disk stats file. +static +const std::string MetricsMainDiskStatsPath() { + char dev_path_cstr[PATH_MAX]; + std::string dev_prefix = "/dev/"; + std::string dev_path; + std::string dev_name; + + int ret = rootdev(dev_path_cstr, sizeof(dev_path_cstr), true, true); + if (ret != 0) { + LOG(WARNING) << "error " << ret << " determining root device"; + return ""; + } + dev_path = dev_path_cstr; + // Check that rootdev begins with "/dev/". + if (!base::StartsWithASCII(dev_path, dev_prefix, false)) { + LOG(WARNING) << "unexpected root device " << dev_path; + return ""; + } + // Get the device name, e.g. "sda" from "/dev/sda". + dev_name = dev_path.substr(dev_prefix.length()); + return "/sys/class/block/" + dev_name + "/stat"; +} + +int main(int argc, char** argv) { + DEFINE_bool(daemon, true, "run as daemon (use -nodaemon for debugging)"); + + // The uploader is disabled by default on ChromeOS as Chrome is responsible + // for sending the metrics. + DEFINE_bool(uploader, false, "activate the uploader"); + + // Upload the metrics once and exit. (used for testing) + DEFINE_bool(uploader_test, + false, + "run the uploader once and exit"); + + // Upload Service flags. + DEFINE_int32(upload_interval_secs, + 1800, + "Interval at which metrics_daemon sends the metrics. (needs " + "-uploader)"); + DEFINE_string(server, + "https://clients4.google.com/uma/v2", + "Server to upload the metrics to. (needs -uploader)"); + DEFINE_string(metrics_file, + "/var/lib/metrics/uma-events", + "File to use as a proxy for uploading the metrics"); + DEFINE_string(config_root, + "/", "Root of the configuration files (testing only)"); + + chromeos::FlagHelper::Init(argc, argv, "Chromium OS Metrics Daemon"); + + // Also log to stderr when not running as daemon. + chromeos::InitLog(chromeos::kLogToSyslog | chromeos::kLogHeader | + (FLAGS_daemon ? 0 : chromeos::kLogToStderr)); + + if (FLAGS_daemon && daemon(0, 0) != 0) { + return errno; + } + + MetricsLibrary metrics_lib; + metrics_lib.Init(); + MetricsDaemon daemon; + daemon.Init(FLAGS_uploader_test, + FLAGS_uploader | FLAGS_uploader_test, + &metrics_lib, + MetricsMainDiskStatsPath(), + "/proc/vmstat", + kScalingMaxFreqPath, + kCpuinfoMaxFreqPath, + base::TimeDelta::FromSeconds(FLAGS_upload_interval_secs), + FLAGS_server, + FLAGS_metrics_file, + FLAGS_config_root); + + if (FLAGS_uploader_test) { + daemon.RunUploaderTest(); + return 0; + } + + daemon.Run(); +} diff --git a/metrics/metrics_daemon_test.cc b/metrics/metrics_daemon_test.cc new file mode 100644 index 000000000..7dafbd689 --- /dev/null +++ b/metrics/metrics_daemon_test.cc @@ -0,0 +1,390 @@ +// Copyright (c) 2010 The Chromium OS 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 <inttypes.h> +#include <utime.h> + +#include <string> +#include <vector> + +#include <base/at_exit.h> +#include <base/files/file_util.h> +#include <base/strings/string_number_conversions.h> +#include <base/strings/stringprintf.h> +#include <chromeos/dbus/service_constants.h> +#include <gtest/gtest.h> + +#include "metrics/metrics_daemon.h" +#include "metrics/metrics_library_mock.h" +#include "metrics/persistent_integer_mock.h" + +using base::FilePath; +using base::StringPrintf; +using base::Time; +using base::TimeDelta; +using base::TimeTicks; +using std::string; +using std::vector; +using ::testing::_; +using ::testing::AnyNumber; +using ::testing::AtLeast; +using ::testing::Return; +using ::testing::StrictMock; +using chromeos_metrics::PersistentIntegerMock; + +static const char kFakeDiskStatsName[] = "fake-disk-stats"; +static const char kFakeDiskStatsFormat[] = + " 1793 1788 %" PRIu64 " 105580 " + " 196 175 %" PRIu64 " 30290 " + " 0 44060 135850\n"; +static const uint64_t kFakeReadSectors[] = {80000, 100000}; +static const uint64_t kFakeWriteSectors[] = {3000, 4000}; + +static const char kFakeVmStatsName[] = "fake-vm-stats"; +static const char kFakeScalingMaxFreqPath[] = "fake-scaling-max-freq"; +static const char kFakeCpuinfoMaxFreqPath[] = "fake-cpuinfo-max-freq"; +static const char kMetricsServer[] = "https://clients4.google.com/uma/v2"; +static const char kMetricsFilePath[] = "/var/lib/metrics/uma-events"; + +class MetricsDaemonTest : public testing::Test { + protected: + std::string kFakeDiskStats0; + std::string kFakeDiskStats1; + + virtual void SetUp() { + kFakeDiskStats0 = base::StringPrintf(kFakeDiskStatsFormat, + kFakeReadSectors[0], + kFakeWriteSectors[0]); + kFakeDiskStats1 = base::StringPrintf(kFakeDiskStatsFormat, + kFakeReadSectors[1], + kFakeWriteSectors[1]); + CreateFakeDiskStatsFile(kFakeDiskStats0.c_str()); + CreateUint64ValueFile(base::FilePath(kFakeCpuinfoMaxFreqPath), 10000000); + CreateUint64ValueFile(base::FilePath(kFakeScalingMaxFreqPath), 10000000); + + chromeos_metrics::PersistentInteger::SetTestingMode(true); + daemon_.Init(true, + false, + &metrics_lib_, + kFakeDiskStatsName, + kFakeVmStatsName, + kFakeScalingMaxFreqPath, + kFakeCpuinfoMaxFreqPath, + base::TimeDelta::FromMinutes(30), + kMetricsServer, + kMetricsFilePath, + "/"); + + // Replace original persistent values with mock ones. + daily_active_use_mock_ = + new StrictMock<PersistentIntegerMock>("1.mock"); + daemon_.daily_active_use_.reset(daily_active_use_mock_); + + kernel_crash_interval_mock_ = + new StrictMock<PersistentIntegerMock>("2.mock"); + daemon_.kernel_crash_interval_.reset(kernel_crash_interval_mock_); + + user_crash_interval_mock_ = + new StrictMock<PersistentIntegerMock>("3.mock"); + daemon_.user_crash_interval_.reset(user_crash_interval_mock_); + + unclean_shutdown_interval_mock_ = + new StrictMock<PersistentIntegerMock>("4.mock"); + daemon_.unclean_shutdown_interval_.reset(unclean_shutdown_interval_mock_); + } + + virtual void TearDown() { + EXPECT_EQ(0, unlink(kFakeDiskStatsName)); + EXPECT_EQ(0, unlink(kFakeScalingMaxFreqPath)); + EXPECT_EQ(0, unlink(kFakeCpuinfoMaxFreqPath)); + } + + // Adds active use aggregation counters update expectations that the + // specified count will be added. + void ExpectActiveUseUpdate(int count) { + EXPECT_CALL(*daily_active_use_mock_, Add(count)) + .Times(1) + .RetiresOnSaturation(); + EXPECT_CALL(*kernel_crash_interval_mock_, Add(count)) + .Times(1) + .RetiresOnSaturation(); + EXPECT_CALL(*user_crash_interval_mock_, Add(count)) + .Times(1) + .RetiresOnSaturation(); + } + + // As above, but ignore values of counter updates. + void IgnoreActiveUseUpdate() { + EXPECT_CALL(*daily_active_use_mock_, Add(_)) + .Times(1) + .RetiresOnSaturation(); + EXPECT_CALL(*kernel_crash_interval_mock_, Add(_)) + .Times(1) + .RetiresOnSaturation(); + EXPECT_CALL(*user_crash_interval_mock_, Add(_)) + .Times(1) + .RetiresOnSaturation(); + } + + // Adds a metrics library mock expectation that the specified metric + // will be generated. + void ExpectSample(const std::string& name, int sample) { + EXPECT_CALL(metrics_lib_, SendToUMA(name, sample, _, _, _)) + .Times(1) + .WillOnce(Return(true)) + .RetiresOnSaturation(); + } + + // Creates a new DBus signal message with zero or more string arguments. + // The message can be deallocated through DeleteDBusMessage. + // + // |path| is the object emitting the signal. + // |interface| is the interface the signal is emitted from. + // |name| is the name of the signal. + // |arg_values| contains the values of the string arguments. + DBusMessage* NewDBusSignalString(const string& path, + const string& interface, + const string& name, + const vector<string>& arg_values) { + DBusMessage* msg = dbus_message_new_signal(path.c_str(), + interface.c_str(), + name.c_str()); + DBusMessageIter iter; + dbus_message_iter_init_append(msg, &iter); + for (vector<string>::const_iterator it = arg_values.begin(); + it != arg_values.end(); ++it) { + const char* str_value = it->c_str(); + dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, &str_value); + } + return msg; + } + + // Deallocates the DBus message |msg| previously allocated through + // dbus_message_new*. + void DeleteDBusMessage(DBusMessage* msg) { + dbus_message_unref(msg); + } + + // Creates or overwrites an input file containing fake disk stats. + void CreateFakeDiskStatsFile(const char* fake_stats) { + if (unlink(kFakeDiskStatsName) < 0) { + EXPECT_EQ(errno, ENOENT); + } + FILE* f = fopen(kFakeDiskStatsName, "w"); + EXPECT_EQ(1, fwrite(fake_stats, strlen(fake_stats), 1, f)); + EXPECT_EQ(0, fclose(f)); + } + + // Creates or overwrites the file in |path| so that it contains the printable + // representation of |value|. + void CreateUint64ValueFile(const base::FilePath& path, uint64_t value) { + base::DeleteFile(path, false); + std::string value_string = base::Uint64ToString(value); + ASSERT_EQ(value_string.length(), + base::WriteFile(path, value_string.c_str(), + value_string.length())); + } + + // The MetricsDaemon under test. + MetricsDaemon daemon_; + + // Mocks. They are strict mock so that all unexpected + // calls are marked as failures. + StrictMock<MetricsLibraryMock> metrics_lib_; + StrictMock<PersistentIntegerMock>* daily_active_use_mock_; + StrictMock<PersistentIntegerMock>* kernel_crash_interval_mock_; + StrictMock<PersistentIntegerMock>* user_crash_interval_mock_; + StrictMock<PersistentIntegerMock>* unclean_shutdown_interval_mock_; +}; + +TEST_F(MetricsDaemonTest, CheckSystemCrash) { + static const char kKernelCrashDetected[] = "test-kernel-crash-detected"; + EXPECT_FALSE(daemon_.CheckSystemCrash(kKernelCrashDetected)); + + base::FilePath crash_detected(kKernelCrashDetected); + base::WriteFile(crash_detected, "", 0); + EXPECT_TRUE(base::PathExists(crash_detected)); + EXPECT_TRUE(daemon_.CheckSystemCrash(kKernelCrashDetected)); + EXPECT_FALSE(base::PathExists(crash_detected)); + EXPECT_FALSE(daemon_.CheckSystemCrash(kKernelCrashDetected)); + EXPECT_FALSE(base::PathExists(crash_detected)); + base::DeleteFile(crash_detected, false); +} + +TEST_F(MetricsDaemonTest, MessageFilter) { + // Ignore calls to SendToUMA. + EXPECT_CALL(metrics_lib_, SendToUMA(_, _, _, _, _)).Times(AnyNumber()); + + DBusMessage* msg = dbus_message_new(DBUS_MESSAGE_TYPE_METHOD_CALL); + DBusHandlerResult res = + MetricsDaemon::MessageFilter(/* connection */ nullptr, msg, &daemon_); + EXPECT_EQ(DBUS_HANDLER_RESULT_NOT_YET_HANDLED, res); + DeleteDBusMessage(msg); + + IgnoreActiveUseUpdate(); + vector<string> signal_args; + msg = NewDBusSignalString("/", + "org.chromium.CrashReporter", + "UserCrash", + signal_args); + res = MetricsDaemon::MessageFilter(/* connection */ nullptr, msg, &daemon_); + EXPECT_EQ(DBUS_HANDLER_RESULT_HANDLED, res); + DeleteDBusMessage(msg); + + signal_args.clear(); + signal_args.push_back("randomstate"); + signal_args.push_back("bob"); // arbitrary username + msg = NewDBusSignalString("/", + "org.chromium.UnknownService.Manager", + "StateChanged", + signal_args); + res = MetricsDaemon::MessageFilter(/* connection */ nullptr, msg, &daemon_); + EXPECT_EQ(DBUS_HANDLER_RESULT_NOT_YET_HANDLED, res); + DeleteDBusMessage(msg); +} + +TEST_F(MetricsDaemonTest, SendSample) { + ExpectSample("Dummy.Metric", 3); + daemon_.SendSample("Dummy.Metric", /* sample */ 3, + /* min */ 1, /* max */ 100, /* buckets */ 50); +} + +TEST_F(MetricsDaemonTest, ReportDiskStats) { + uint64_t read_sectors_now, write_sectors_now; + CreateFakeDiskStatsFile(kFakeDiskStats1.c_str()); + daemon_.DiskStatsReadStats(&read_sectors_now, &write_sectors_now); + EXPECT_EQ(read_sectors_now, kFakeReadSectors[1]); + EXPECT_EQ(write_sectors_now, kFakeWriteSectors[1]); + + MetricsDaemon::StatsState s_state = daemon_.stats_state_; + EXPECT_CALL(metrics_lib_, + SendToUMA(_, (kFakeReadSectors[1] - kFakeReadSectors[0]) / 30, + _, _, _)); + EXPECT_CALL(metrics_lib_, + SendToUMA(_, (kFakeWriteSectors[1] - kFakeWriteSectors[0]) / 30, + _, _, _)); + EXPECT_CALL(metrics_lib_, SendEnumToUMA(_, _, _)); // SendCpuThrottleMetrics + daemon_.StatsCallback(); + EXPECT_TRUE(s_state != daemon_.stats_state_); +} + +TEST_F(MetricsDaemonTest, ProcessMeminfo) { + string meminfo = + "MemTotal: 2000000 kB\nMemFree: 500000 kB\n" + "Buffers: 1000000 kB\nCached: 213652 kB\n" + "SwapCached: 0 kB\nActive: 133400 kB\n" + "Inactive: 183396 kB\nActive(anon): 92984 kB\n" + "Inactive(anon): 58860 kB\nActive(file): 40416 kB\n" + "Inactive(file): 124536 kB\nUnevictable: 0 kB\n" + "Mlocked: 0 kB\nSwapTotal: 0 kB\n" + "SwapFree: 0 kB\nDirty: 40 kB\n" + "Writeback: 0 kB\nAnonPages: 92652 kB\n" + "Mapped: 59716 kB\nShmem: 59196 kB\n" + "Slab: 16656 kB\nSReclaimable: 6132 kB\n" + "SUnreclaim: 10524 kB\nKernelStack: 1648 kB\n" + "PageTables: 2780 kB\nNFS_Unstable: 0 kB\n" + "Bounce: 0 kB\nWritebackTmp: 0 kB\n" + "CommitLimit: 970656 kB\nCommitted_AS: 1260528 kB\n" + "VmallocTotal: 122880 kB\nVmallocUsed: 12144 kB\n" + "VmallocChunk: 103824 kB\nDirectMap4k: 9636 kB\n" + "DirectMap2M: 1955840 kB\n"; + + // All enum calls must report percents. + EXPECT_CALL(metrics_lib_, SendEnumToUMA(_, _, 100)).Times(AtLeast(1)); + // Check that MemFree is correctly computed at 25%. + EXPECT_CALL(metrics_lib_, SendEnumToUMA("Platform.MeminfoMemFree", 25, 100)) + .Times(AtLeast(1)); + // Check that we call SendToUma at least once (log histogram). + EXPECT_CALL(metrics_lib_, SendToUMA(_, _, _, _, _)) + .Times(AtLeast(1)); + // Make sure we don't report fields not in the list. + EXPECT_CALL(metrics_lib_, SendToUMA("Platform.MeminfoMlocked", _, _, _, _)) + .Times(0); + EXPECT_CALL(metrics_lib_, SendEnumToUMA("Platform.MeminfoMlocked", _, _)) + .Times(0); + EXPECT_TRUE(daemon_.ProcessMeminfo(meminfo)); +} + +TEST_F(MetricsDaemonTest, ProcessMeminfo2) { + string meminfo = "MemTotal: 2000000 kB\nMemFree: 1000000 kB\n"; + // Not enough fields. + EXPECT_FALSE(daemon_.ProcessMeminfo(meminfo)); +} + +TEST_F(MetricsDaemonTest, ParseVmStats) { + static char kVmStats[] = "pswpin 1345\npswpout 8896\n" + "foo 100\nbar 200\npgmajfault 42\netcetc 300\n"; + struct MetricsDaemon::VmstatRecord stats; + EXPECT_TRUE(daemon_.VmStatsParseStats(kVmStats, &stats)); + EXPECT_EQ(stats.page_faults_, 42); + EXPECT_EQ(stats.swap_in_, 1345); + EXPECT_EQ(stats.swap_out_, 8896); +} + +TEST_F(MetricsDaemonTest, ReadFreqToInt) { + const int fake_scaled_freq = 1666999; + const int fake_max_freq = 2000000; + int scaled_freq = 0; + int max_freq = 0; + CreateUint64ValueFile(base::FilePath(kFakeScalingMaxFreqPath), + fake_scaled_freq); + CreateUint64ValueFile(base::FilePath(kFakeCpuinfoMaxFreqPath), fake_max_freq); + EXPECT_TRUE(daemon_.testing_); + EXPECT_TRUE(daemon_.ReadFreqToInt(kFakeScalingMaxFreqPath, &scaled_freq)); + EXPECT_TRUE(daemon_.ReadFreqToInt(kFakeCpuinfoMaxFreqPath, &max_freq)); + EXPECT_EQ(fake_scaled_freq, scaled_freq); + EXPECT_EQ(fake_max_freq, max_freq); +} + +TEST_F(MetricsDaemonTest, SendCpuThrottleMetrics) { + CreateUint64ValueFile(base::FilePath(kFakeCpuinfoMaxFreqPath), 2001000); + // Test the 101% and 100% cases. + CreateUint64ValueFile(base::FilePath(kFakeScalingMaxFreqPath), 2001000); + EXPECT_TRUE(daemon_.testing_); + EXPECT_CALL(metrics_lib_, SendEnumToUMA(_, 101, 101)); + daemon_.SendCpuThrottleMetrics(); + CreateUint64ValueFile(base::FilePath(kFakeScalingMaxFreqPath), 2000000); + EXPECT_CALL(metrics_lib_, SendEnumToUMA(_, 100, 101)); + daemon_.SendCpuThrottleMetrics(); +} + +TEST_F(MetricsDaemonTest, SendZramMetrics) { + EXPECT_TRUE(daemon_.testing_); + + // |compr_data_size| is the size in bytes of compressed data. + const uint64_t compr_data_size = 50 * 1000 * 1000; + // The constant '3' is a realistic but random choice. + // |orig_data_size| does not include zero pages. + const uint64_t orig_data_size = compr_data_size * 3; + const uint64_t page_size = 4096; + const uint64_t zero_pages = 10 * 1000 * 1000 / page_size; + + CreateUint64ValueFile(base::FilePath(MetricsDaemon::kComprDataSizeName), + compr_data_size); + CreateUint64ValueFile(base::FilePath(MetricsDaemon::kOrigDataSizeName), + orig_data_size); + CreateUint64ValueFile(base::FilePath(MetricsDaemon::kZeroPagesName), + zero_pages); + + const uint64_t real_orig_size = orig_data_size + zero_pages * page_size; + const uint64_t zero_ratio_percent = + zero_pages * page_size * 100 / real_orig_size; + // Ratio samples are in percents. + const uint64_t actual_ratio_sample = real_orig_size * 100 / compr_data_size; + + EXPECT_CALL(metrics_lib_, SendToUMA(_, compr_data_size >> 20, _, _, _)); + EXPECT_CALL(metrics_lib_, + SendToUMA(_, (real_orig_size - compr_data_size) >> 20, _, _, _)); + EXPECT_CALL(metrics_lib_, SendToUMA(_, actual_ratio_sample, _, _, _)); + EXPECT_CALL(metrics_lib_, SendToUMA(_, zero_pages, _, _, _)); + EXPECT_CALL(metrics_lib_, SendToUMA(_, zero_ratio_percent, _, _, _)); + + EXPECT_TRUE(daemon_.ReportZram(base::FilePath("."))); +} + +int main(int argc, char** argv) { + testing::InitGoogleTest(&argc, argv); + + return RUN_ALL_TESTS(); +} diff --git a/metrics/metrics_library.cc b/metrics/metrics_library.cc new file mode 100644 index 000000000..70c8eac2b --- /dev/null +++ b/metrics/metrics_library.cc @@ -0,0 +1,214 @@ +// Copyright (c) 2010 The Chromium OS 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 "metrics/metrics_library.h" + +#include <base/logging.h> +#include <base/strings/stringprintf.h> +#include <errno.h> +#include <sys/file.h> +#include <sys/stat.h> + +#include <cstdio> +#include <cstring> + +#include "metrics/serialization/metric_sample.h" +#include "metrics/serialization/serialization_utils.h" + +#include "policy/device_policy.h" + +static const char kAutotestPath[] = "/var/log/metrics/autotest-events"; +static const char kUMAEventsPath[] = "/var/lib/metrics/uma-events"; +static const char kConsentFile[] = "/home/chronos/Consent To Send Stats"; +static const char kCrosEventHistogramName[] = "Platform.CrOSEvent"; +static const int kCrosEventHistogramMax = 100; + +/* Add new cros events here. + * + * The index of the event is sent in the message, so please do not + * reorder the names. + */ +static const char *kCrosEventNames[] = { + "ModemManagerCommandSendFailure", // 0 + "HwWatchdogReboot", // 1 + "Cras.NoCodecsFoundAtBoot", // 2 + "Chaps.DatabaseCorrupted", // 3 + "Chaps.DatabaseRepairFailure", // 4 + "Chaps.DatabaseCreateFailure", // 5 + "Attestation.OriginSpecificExhausted", // 6 + "SpringPowerSupply.Original.High", // 7 + "SpringPowerSupply.Other.High", // 8 + "SpringPowerSupply.Original.Low", // 9 + "SpringPowerSupply.ChargerIdle", // 10 + "TPM.NonZeroDictionaryAttackCounter", // 11 + "TPM.EarlyResetDuringCommand", // 12 +}; + +time_t MetricsLibrary::cached_enabled_time_ = 0; +bool MetricsLibrary::cached_enabled_ = false; + +MetricsLibrary::MetricsLibrary() : consent_file_(kConsentFile) {} +MetricsLibrary::~MetricsLibrary() {} + +// We take buffer and buffer_size as parameters in order to simplify testing +// of various alignments of the |device_name| with |buffer_size|. +bool MetricsLibrary::IsDeviceMounted(const char* device_name, + const char* mounts_file, + char* buffer, + int buffer_size, + bool* result) { + if (buffer == nullptr || buffer_size < 1) + return false; + int mounts_fd = open(mounts_file, O_RDONLY); + if (mounts_fd < 0) + return false; + // match_offset describes: + // -1 -- not beginning of line + // 0..strlen(device_name)-1 -- this offset in device_name is next to match + // strlen(device_name) -- matched full name, just need a space. + int match_offset = 0; + bool match = false; + while (!match) { + int read_size = read(mounts_fd, buffer, buffer_size); + if (read_size <= 0) { + if (errno == -EINTR) + continue; + break; + } + for (int i = 0; i < read_size; ++i) { + if (buffer[i] == '\n') { + match_offset = 0; + continue; + } + if (match_offset < 0) { + continue; + } + if (device_name[match_offset] == '\0') { + if (buffer[i] == ' ') { + match = true; + break; + } + match_offset = -1; + continue; + } + + if (buffer[i] == device_name[match_offset]) { + ++match_offset; + } else { + match_offset = -1; + } + } + } + close(mounts_fd); + *result = match; + return true; +} + +bool MetricsLibrary::IsGuestMode() { + char buffer[256]; + bool result = false; + if (!IsDeviceMounted("guestfs", + "/proc/mounts", + buffer, + sizeof(buffer), + &result)) { + return false; + } + return result && (access("/var/run/state/logged-in", F_OK) == 0); +} + +bool MetricsLibrary::AreMetricsEnabled() { + static struct stat stat_buffer; + time_t this_check_time = time(nullptr); + if (this_check_time != cached_enabled_time_) { + cached_enabled_time_ = this_check_time; + + if (!policy_provider_.get()) + policy_provider_.reset(new policy::PolicyProvider()); + policy_provider_->Reload(); + // We initialize with the default value which is false and will be preserved + // if the policy is not set. + bool enabled = false; + bool has_policy = false; + if (policy_provider_->device_policy_is_loaded()) { + has_policy = + policy_provider_->GetDevicePolicy().GetMetricsEnabled(&enabled); + } + // If policy couldn't be loaded or the metrics policy is not set we should + // still respect the consent file if it is present for migration purposes. + // TODO(pastarmovj) + if (!has_policy) { + enabled = stat(consent_file_.c_str(), &stat_buffer) >= 0; + } + + if (enabled && !IsGuestMode()) + cached_enabled_ = true; + else + cached_enabled_ = false; + } + return cached_enabled_; +} + +void MetricsLibrary::Init() { + uma_events_file_ = kUMAEventsPath; +} + +bool MetricsLibrary::SendToAutotest(const std::string& name, int value) { + FILE* autotest_file = fopen(kAutotestPath, "a+"); + if (autotest_file == nullptr) { + PLOG(ERROR) << kAutotestPath << ": fopen"; + return false; + } + + fprintf(autotest_file, "%s=%d\n", name.c_str(), value); + fclose(autotest_file); + return true; +} + +bool MetricsLibrary::SendToUMA(const std::string& name, + int sample, + int min, + int max, + int nbuckets) { + return metrics::SerializationUtils::WriteMetricToFile( + *metrics::MetricSample::HistogramSample(name, sample, min, max, nbuckets) + .get(), + kUMAEventsPath); +} + +bool MetricsLibrary::SendEnumToUMA(const std::string& name, int sample, + int max) { + return metrics::SerializationUtils::WriteMetricToFile( + *metrics::MetricSample::LinearHistogramSample(name, sample, max).get(), + kUMAEventsPath); +} + +bool MetricsLibrary::SendSparseToUMA(const std::string& name, int sample) { + return metrics::SerializationUtils::WriteMetricToFile( + *metrics::MetricSample::SparseHistogramSample(name, sample).get(), + kUMAEventsPath); +} + +bool MetricsLibrary::SendUserActionToUMA(const std::string& action) { + return metrics::SerializationUtils::WriteMetricToFile( + *metrics::MetricSample::UserActionSample(action).get(), kUMAEventsPath); +} + +bool MetricsLibrary::SendCrashToUMA(const char *crash_kind) { + return metrics::SerializationUtils::WriteMetricToFile( + *metrics::MetricSample::CrashSample(crash_kind).get(), kUMAEventsPath); +} + +void MetricsLibrary::SetPolicyProvider(policy::PolicyProvider* provider) { + policy_provider_.reset(provider); +} + +bool MetricsLibrary::SendCrosEventToUMA(const std::string& event) { + for (size_t i = 0; i < arraysize(kCrosEventNames); i++) { + if (strcmp(event.c_str(), kCrosEventNames[i]) == 0) { + return SendEnumToUMA(kCrosEventHistogramName, i, kCrosEventHistogramMax); + } + } + return false; +} diff --git a/metrics/metrics_library.h b/metrics/metrics_library.h new file mode 100644 index 000000000..a90f3e61a --- /dev/null +++ b/metrics/metrics_library.h @@ -0,0 +1,150 @@ +// Copyright (c) 2010 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef METRICS_METRICS_LIBRARY_H_ +#define METRICS_METRICS_LIBRARY_H_ + +#include <sys/types.h> +#include <string> +#include <unistd.h> + +#include <base/compiler_specific.h> +#include <base/macros.h> +#include <base/memory/scoped_ptr.h> +#include <gtest/gtest_prod.h> // for FRIEND_TEST + +#include "policy/libpolicy.h" + +class MetricsLibraryInterface { + public: + virtual void Init() = 0; + virtual bool AreMetricsEnabled() = 0; + virtual bool SendToUMA(const std::string& name, int sample, + int min, int max, int nbuckets) = 0; + virtual bool SendEnumToUMA(const std::string& name, int sample, int max) = 0; + virtual bool SendSparseToUMA(const std::string& name, int sample) = 0; + virtual bool SendUserActionToUMA(const std::string& action) = 0; + virtual ~MetricsLibraryInterface() {} +}; + +// Library used to send metrics to both Autotest and Chrome/UMA. +class MetricsLibrary : public MetricsLibraryInterface { + public: + MetricsLibrary(); + virtual ~MetricsLibrary(); + + // Initializes the library. + void Init() override; + + // Returns whether or not the machine is running in guest mode. + bool IsGuestMode(); + + // Returns whether or not metrics collection is enabled. + bool AreMetricsEnabled() override; + + // Sends histogram data to Chrome for transport to UMA and returns + // true on success. This method results in the equivalent of an + // asynchronous non-blocking RPC to UMA_HISTOGRAM_CUSTOM_COUNTS + // inside Chrome (see base/histogram.h). + // + // |sample| is the sample value to be recorded (|min| <= |sample| < |max|). + // |min| is the minimum value of the histogram samples (|min| > 0). + // |max| is the maximum value of the histogram samples. + // |nbuckets| is the number of histogram buckets. + // [0,min) is the implicit underflow bucket. + // [|max|,infinity) is the implicit overflow bucket. + // + // Note that the memory allocated in Chrome for each histogram is + // proportional to the number of buckets. Therefore, it is strongly + // recommended to keep this number low (e.g., 50 is normal, while + // 100 is high). + bool SendToUMA(const std::string& name, int sample, + int min, int max, int nbuckets) override; + + // Sends linear histogram data to Chrome for transport to UMA and + // returns true on success. This method results in the equivalent of + // an asynchronous non-blocking RPC to UMA_HISTOGRAM_ENUMERATION + // inside Chrome (see base/histogram.h). + // + // |sample| is the sample value to be recorded (1 <= |sample| < |max|). + // |max| is the maximum value of the histogram samples. + // 0 is the implicit underflow bucket. + // [|max|,infinity) is the implicit overflow bucket. + // + // An enumeration histogram requires |max| + 1 number of + // buckets. Note that the memory allocated in Chrome for each + // histogram is proportional to the number of buckets. Therefore, it + // is strongly recommended to keep this number low (e.g., 50 is + // normal, while 100 is high). + bool SendEnumToUMA(const std::string& name, int sample, int max) override; + + // Sends sparse histogram sample to Chrome for transport to UMA. Returns + // true on success. + // + // |sample| is the 32-bit integer value to be recorded. + bool SendSparseToUMA(const std::string& name, int sample) override; + + // Sends a user action to Chrome for transport to UMA and returns true on + // success. This method results in the equivalent of an asynchronous + // non-blocking RPC to UserMetrics::RecordAction. The new metric must be + // added to chrome/tools/extract_actions.py in the Chromium repository, which + // should then be run to generate a hash for the new action. + // + // Until http://crosbug.com/11125 is fixed, the metric must also be added to + // chrome/browser/chromeos/external_metrics.cc. + // + // |action| is the user-generated event (e.g., "MuteKeyPressed"). + bool SendUserActionToUMA(const std::string& action) override; + + // Sends a signal to UMA that a crash of the given |crash_kind| + // has occurred. Used by UMA to generate stability statistics. + bool SendCrashToUMA(const char *crash_kind); + + // Sends a "generic Chrome OS event" to UMA. This is an event name + // that is translated into an enumerated histogram entry. Event names + // are added to metrics_library.cc. Optionally, they can be added + // to histograms.xml---but part of the reason for this is to simplify + // the addition of events (at the cost of having to look them up by + // number in the histograms dashboard). + bool SendCrosEventToUMA(const std::string& event); + + // Sends to Autotest and returns true on success. + static bool SendToAutotest(const std::string& name, int value); + + private: + friend class CMetricsLibraryTest; + friend class MetricsLibraryTest; + FRIEND_TEST(MetricsLibraryTest, AreMetricsEnabled); + FRIEND_TEST(MetricsLibraryTest, FormatChromeMessage); + FRIEND_TEST(MetricsLibraryTest, FormatChromeMessageTooLong); + FRIEND_TEST(MetricsLibraryTest, IsDeviceMounted); + FRIEND_TEST(MetricsLibraryTest, SendMessageToChrome); + FRIEND_TEST(MetricsLibraryTest, SendMessageToChromeUMAEventsBadFileLocation); + + // Sets |*result| to whether or not the |mounts_file| indicates that + // the |device_name| is currently mounted. Uses |buffer| of + // |buffer_size| to read the file. Returns false if any error. + bool IsDeviceMounted(const char* device_name, + const char* mounts_file, + char* buffer, int buffer_size, + bool* result); + + // This function is used by tests only to mock the device policies. + void SetPolicyProvider(policy::PolicyProvider* provider); + + // Time at which we last checked if metrics were enabled. + static time_t cached_enabled_time_; + + // Cached state of whether or not metrics were enabled. + static bool cached_enabled_; + + std::string uma_events_file_; + std::string consent_file_; + + scoped_ptr<policy::PolicyProvider> policy_provider_; + + DISALLOW_COPY_AND_ASSIGN(MetricsLibrary); +}; + +#endif // METRICS_METRICS_LIBRARY_H_ diff --git a/metrics/metrics_library_mock.h b/metrics/metrics_library_mock.h new file mode 100644 index 000000000..99892bfa0 --- /dev/null +++ b/metrics/metrics_library_mock.h @@ -0,0 +1,29 @@ +// Copyright (c) 2010 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef METRICS_METRICS_LIBRARY_MOCK_H_ +#define METRICS_METRICS_LIBRARY_MOCK_H_ + +#include <string> + +#include "metrics/metrics_library.h" + +#include <gmock/gmock.h> + +class MetricsLibraryMock : public MetricsLibraryInterface { + public: + bool metrics_enabled_ = true; + + MOCK_METHOD0(Init, void()); + MOCK_METHOD5(SendToUMA, bool(const std::string& name, int sample, + int min, int max, int nbuckets)); + MOCK_METHOD3(SendEnumToUMA, bool(const std::string& name, int sample, + int max)); + MOCK_METHOD2(SendSparseToUMA, bool(const std::string& name, int sample)); + MOCK_METHOD1(SendUserActionToUMA, bool(const std::string& action)); + + bool AreMetricsEnabled() override {return metrics_enabled_;}; +}; + +#endif // METRICS_METRICS_LIBRARY_MOCK_H_ diff --git a/metrics/metrics_library_test.cc b/metrics/metrics_library_test.cc new file mode 100644 index 000000000..7ede303c8 --- /dev/null +++ b/metrics/metrics_library_test.cc @@ -0,0 +1,214 @@ +// Copyright (c) 2010 The Chromium OS 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 <cstring> + +#include <base/files/file_util.h> +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include <policy/mock_device_policy.h> +#include <policy/libpolicy.h> + +#include "metrics/c_metrics_library.h" +#include "metrics/metrics_library.h" + +using base::FilePath; +using ::testing::_; +using ::testing::Return; +using ::testing::AnyNumber; + +static const FilePath kTestUMAEventsFile("test-uma-events"); +static const char kTestMounts[] = "test-mounts"; + +ACTION_P(SetMetricsPolicy, enabled) { + *arg0 = enabled; + return true; +} + +class MetricsLibraryTest : public testing::Test { + protected: + virtual void SetUp() { + EXPECT_TRUE(lib_.uma_events_file_.empty()); + lib_.Init(); + EXPECT_FALSE(lib_.uma_events_file_.empty()); + lib_.uma_events_file_ = kTestUMAEventsFile.value(); + EXPECT_EQ(0, WriteFile(kTestUMAEventsFile, "", 0)); + device_policy_ = new policy::MockDevicePolicy(); + EXPECT_CALL(*device_policy_, LoadPolicy()) + .Times(AnyNumber()) + .WillRepeatedly(Return(true)); + EXPECT_CALL(*device_policy_, GetMetricsEnabled(_)) + .Times(AnyNumber()) + .WillRepeatedly(SetMetricsPolicy(true)); + provider_ = new policy::PolicyProvider(device_policy_); + lib_.SetPolicyProvider(provider_); + // Defeat metrics enabled caching between tests. + lib_.cached_enabled_time_ = 0; + } + + virtual void TearDown() { + base::DeleteFile(FilePath(kTestMounts), false); + base::DeleteFile(kTestUMAEventsFile, false); + } + + void VerifyEnabledCacheHit(bool to_value); + void VerifyEnabledCacheEviction(bool to_value); + + MetricsLibrary lib_; + policy::MockDevicePolicy* device_policy_; + policy::PolicyProvider* provider_; +}; + +TEST_F(MetricsLibraryTest, IsDeviceMounted) { + static const char kTestContents[] = + "0123456789abcde 0123456789abcde\nguestfs foo bar\n"; + char buffer[1024]; + int block_sizes[] = { 1, 2, 3, 4, 5, 6, 8, 12, 14, 16, 32, 1024 }; + bool result; + EXPECT_FALSE(lib_.IsDeviceMounted("guestfs", + "nonexistent", + buffer, + 1, + &result)); + ASSERT_TRUE(base::WriteFile(base::FilePath(kTestMounts), + kTestContents, + strlen(kTestContents))); + EXPECT_FALSE(lib_.IsDeviceMounted("guestfs", + kTestMounts, + buffer, + 0, + &result)); + for (size_t i = 0; i < arraysize(block_sizes); ++i) { + EXPECT_TRUE(lib_.IsDeviceMounted("0123456789abcde", + kTestMounts, + buffer, + block_sizes[i], + &result)); + EXPECT_TRUE(result); + EXPECT_TRUE(lib_.IsDeviceMounted("guestfs", + kTestMounts, + buffer, + block_sizes[i], + &result)); + EXPECT_TRUE(result); + EXPECT_TRUE(lib_.IsDeviceMounted("0123456", + kTestMounts, + buffer, + block_sizes[i], + &result)); + EXPECT_FALSE(result); + EXPECT_TRUE(lib_.IsDeviceMounted("9abcde", + kTestMounts, + buffer, + block_sizes[i], + &result)); + EXPECT_FALSE(result); + EXPECT_TRUE(lib_.IsDeviceMounted("foo", + kTestMounts, + buffer, + block_sizes[i], + &result)); + EXPECT_FALSE(result); + EXPECT_TRUE(lib_.IsDeviceMounted("bar", + kTestMounts, + buffer, + block_sizes[i], + &result)); + EXPECT_FALSE(result); + } +} + +TEST_F(MetricsLibraryTest, AreMetricsEnabledFalse) { + EXPECT_CALL(*device_policy_, GetMetricsEnabled(_)) + .WillOnce(SetMetricsPolicy(false)); + EXPECT_FALSE(lib_.AreMetricsEnabled()); +} + +TEST_F(MetricsLibraryTest, AreMetricsEnabledTrue) { + EXPECT_TRUE(lib_.AreMetricsEnabled()); +} + +void MetricsLibraryTest::VerifyEnabledCacheHit(bool to_value) { + // We might step from one second to the next one time, but not 100 + // times in a row. + for (int i = 0; i < 100; ++i) { + lib_.cached_enabled_time_ = 0; + EXPECT_CALL(*device_policy_, GetMetricsEnabled(_)) + .WillOnce(SetMetricsPolicy(!to_value)); + ASSERT_EQ(!to_value, lib_.AreMetricsEnabled()); + ON_CALL(*device_policy_, GetMetricsEnabled(_)) + .WillByDefault(SetMetricsPolicy(to_value)); + if (lib_.AreMetricsEnabled() == !to_value) + return; + } + ADD_FAILURE() << "Did not see evidence of caching"; +} + +void MetricsLibraryTest::VerifyEnabledCacheEviction(bool to_value) { + lib_.cached_enabled_time_ = 0; + EXPECT_CALL(*device_policy_, GetMetricsEnabled(_)) + .WillOnce(SetMetricsPolicy(!to_value)); + ASSERT_EQ(!to_value, lib_.AreMetricsEnabled()); + EXPECT_CALL(*device_policy_, GetMetricsEnabled(_)) + .WillOnce(SetMetricsPolicy(to_value)); + ASSERT_LT(abs(static_cast<int>(time(nullptr) - lib_.cached_enabled_time_)), + 5); + // Sleep one second (or cheat to be faster). + --lib_.cached_enabled_time_; + ASSERT_EQ(to_value, lib_.AreMetricsEnabled()); +} + +TEST_F(MetricsLibraryTest, AreMetricsEnabledCaching) { + VerifyEnabledCacheHit(false); + VerifyEnabledCacheHit(true); + VerifyEnabledCacheEviction(false); + VerifyEnabledCacheEviction(true); +} + +class CMetricsLibraryTest : public testing::Test { + protected: + virtual void SetUp() { + lib_ = CMetricsLibraryNew(); + MetricsLibrary& ml = *reinterpret_cast<MetricsLibrary*>(lib_); + EXPECT_TRUE(ml.uma_events_file_.empty()); + CMetricsLibraryInit(lib_); + EXPECT_FALSE(ml.uma_events_file_.empty()); + ml.uma_events_file_ = kTestUMAEventsFile.value(); + EXPECT_EQ(0, WriteFile(kTestUMAEventsFile, "", 0)); + device_policy_ = new policy::MockDevicePolicy(); + EXPECT_CALL(*device_policy_, LoadPolicy()) + .Times(AnyNumber()) + .WillRepeatedly(Return(true)); + EXPECT_CALL(*device_policy_, GetMetricsEnabled(_)) + .Times(AnyNumber()) + .WillRepeatedly(SetMetricsPolicy(true)); + provider_ = new policy::PolicyProvider(device_policy_); + ml.SetPolicyProvider(provider_); + reinterpret_cast<MetricsLibrary*>(lib_)->cached_enabled_time_ = 0; + } + + virtual void TearDown() { + CMetricsLibraryDelete(lib_); + base::DeleteFile(kTestUMAEventsFile, false); + } + + CMetricsLibrary lib_; + policy::MockDevicePolicy* device_policy_; + policy::PolicyProvider* provider_; +}; + +TEST_F(CMetricsLibraryTest, AreMetricsEnabledFalse) { + EXPECT_CALL(*device_policy_, GetMetricsEnabled(_)) + .WillOnce(SetMetricsPolicy(false)); + EXPECT_FALSE(CMetricsLibraryAreMetricsEnabled(lib_)); +} + +TEST_F(CMetricsLibraryTest, AreMetricsEnabledTrue) { + EXPECT_TRUE(CMetricsLibraryAreMetricsEnabled(lib_)); +} + +int main(int argc, char** argv) { + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/metrics/persistent_integer.cc b/metrics/persistent_integer.cc new file mode 100644 index 000000000..dd38f1e6f --- /dev/null +++ b/metrics/persistent_integer.cc @@ -0,0 +1,101 @@ +// Copyright (c) 2014 The Chromium OS 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 "metrics/persistent_integer.h" + +#include <fcntl.h> + +#include <base/logging.h> +#include <base/posix/eintr_wrapper.h> + +#include "metrics/metrics_library.h" + +namespace { + +// The directory for the persistent storage. +const char kBackingFilesDirectory[] = "/var/lib/metrics/"; + +} + +namespace chromeos_metrics { + +// Static class member instantiation. +bool PersistentInteger::testing_ = false; + +PersistentInteger::PersistentInteger(const std::string& name) : + value_(0), + version_(kVersion), + name_(name), + synced_(false) { + if (testing_) { + backing_file_name_ = name_; + } else { + backing_file_name_ = kBackingFilesDirectory + name_; + } +} + +PersistentInteger::~PersistentInteger() {} + +void PersistentInteger::Set(int64_t value) { + value_ = value; + Write(); +} + +int64_t PersistentInteger::Get() { + // If not synced, then read. If the read fails, it's a good idea to write. + if (!synced_ && !Read()) + Write(); + return value_; +} + +int64_t PersistentInteger::GetAndClear() { + int64_t v = Get(); + Set(0); + return v; +} + +void PersistentInteger::Add(int64_t x) { + Set(Get() + x); +} + +void PersistentInteger::Write() { + int fd = HANDLE_EINTR(open(backing_file_name_.c_str(), + O_WRONLY | O_CREAT | O_TRUNC, + S_IWUSR | S_IRUSR | S_IRGRP | S_IROTH)); + PCHECK(fd >= 0) << "cannot open " << backing_file_name_ << " for writing"; + PCHECK((HANDLE_EINTR(write(fd, &version_, sizeof(version_))) == + sizeof(version_)) && + (HANDLE_EINTR(write(fd, &value_, sizeof(value_))) == + sizeof(value_))) + << "cannot write to " << backing_file_name_; + close(fd); + synced_ = true; +} + +bool PersistentInteger::Read() { + int fd = HANDLE_EINTR(open(backing_file_name_.c_str(), O_RDONLY)); + if (fd < 0) { + PLOG(WARNING) << "cannot open " << backing_file_name_ << " for reading"; + return false; + } + int32_t version; + int64_t value; + bool read_succeeded = false; + if (HANDLE_EINTR(read(fd, &version, sizeof(version))) == sizeof(version) && + version == version_ && + HANDLE_EINTR(read(fd, &value, sizeof(value))) == sizeof(value)) { + value_ = value; + read_succeeded = true; + synced_ = true; + } + close(fd); + return read_succeeded; +} + +void PersistentInteger::SetTestingMode(bool testing) { + testing_ = testing; +} + + +} // namespace chromeos_metrics diff --git a/metrics/persistent_integer.h b/metrics/persistent_integer.h new file mode 100644 index 000000000..b1cfcf4ef --- /dev/null +++ b/metrics/persistent_integer.h @@ -0,0 +1,67 @@ +// Copyright (c) 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef METRICS_PERSISTENT_INTEGER_H_ +#define METRICS_PERSISTENT_INTEGER_H_ + +#include <stdint.h> + +#include <string> + +namespace chromeos_metrics { + +// PersistentIntegers is a named 64-bit integer value backed by a file. +// The in-memory value acts as a write-through cache of the file value. +// If the backing file doesn't exist or has bad content, the value is 0. + +class PersistentInteger { + public: + explicit PersistentInteger(const std::string& name); + + // Virtual only because of mock. + virtual ~PersistentInteger(); + + // Sets the value. This writes through to the backing file. + void Set(int64_t v); + + // Gets the value. May sync from backing file first. + int64_t Get(); + + // Returns the name of the object. + std::string Name() { return name_; } + + // Convenience function for Get() followed by Set(0). + int64_t GetAndClear(); + + // Convenience function for v = Get, Set(v + x). + // Virtual only because of mock. + virtual void Add(int64_t x); + + // After calling with |testing| = true, changes some behavior for the purpose + // of testing. For instance: instances created while testing use the current + // directory for the backing files. + static void SetTestingMode(bool testing); + + private: + static const int kVersion = 1001; + + // Writes |value_| to the backing file, creating it if necessary. + void Write(); + + // Reads the value from the backing file, stores it in |value_|, and returns + // true if the backing file is valid. Returns false otherwise, and creates + // a valid backing file as a side effect. + bool Read(); + + int64_t value_; + int32_t version_; + std::string name_; + std::string backing_file_name_; + bool synced_; + static bool testing_; +}; + +} // namespace chromeos_metrics + +#endif // METRICS_PERSISTENT_INTEGER_H_ diff --git a/metrics/persistent_integer_mock.h b/metrics/persistent_integer_mock.h new file mode 100644 index 000000000..2061e55c0 --- /dev/null +++ b/metrics/persistent_integer_mock.h @@ -0,0 +1,25 @@ +// Copyright (c) 2010 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef METRICS_PERSISTENT_INTEGER_MOCK_H_ +#define METRICS_PERSISTENT_INTEGER_MOCK_H_ + +#include <string> + +#include <gmock/gmock.h> + +#include "metrics/persistent_integer.h" + +namespace chromeos_metrics { + +class PersistentIntegerMock : public PersistentInteger { + public: + explicit PersistentIntegerMock(const std::string& name) + : PersistentInteger(name) {} + MOCK_METHOD1(Add, void(int64_t count)); +}; + +} // namespace chromeos_metrics + +#endif // METRICS_PERSISTENT_INTEGER_MOCK_H_ diff --git a/metrics/persistent_integer_test.cc b/metrics/persistent_integer_test.cc new file mode 100644 index 000000000..a56aedec9 --- /dev/null +++ b/metrics/persistent_integer_test.cc @@ -0,0 +1,66 @@ +// Copyright (c) 2014 The Chromium OS 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 <gtest/gtest.h> + +#include <base/compiler_specific.h> +#include <base/files/file_enumerator.h> +#include <base/files/file_util.h> + +#include "metrics/persistent_integer.h" + +const char kBackingFileName[] = "1.pibakf"; +const char kBackingFilePattern[] = "*.pibakf"; + +using chromeos_metrics::PersistentInteger; + +class PersistentIntegerTest : public testing::Test { + void SetUp() override { + // Set testing mode. + chromeos_metrics::PersistentInteger::SetTestingMode(true); + } + + void TearDown() override { + // Remove backing files. The convention is that they all end in ".pibakf". + base::FileEnumerator f_enum(base::FilePath("."), + false, + base::FileEnumerator::FILES, + FILE_PATH_LITERAL(kBackingFilePattern)); + for (base::FilePath name = f_enum.Next(); + !name.empty(); + name = f_enum.Next()) { + base::DeleteFile(name, false); + } + } +}; + +TEST_F(PersistentIntegerTest, BasicChecks) { + scoped_ptr<PersistentInteger> pi(new PersistentInteger(kBackingFileName)); + + // Test initialization. + EXPECT_EQ(0, pi->Get()); + EXPECT_EQ(kBackingFileName, pi->Name()); // boring + + // Test set and add. + pi->Set(2); + pi->Add(3); + EXPECT_EQ(5, pi->Get()); + + // Test persistence. + pi.reset(new PersistentInteger(kBackingFileName)); + EXPECT_EQ(5, pi->Get()); + + // Test GetAndClear. + EXPECT_EQ(5, pi->GetAndClear()); + EXPECT_EQ(pi->Get(), 0); + + // Another persistence test. + pi.reset(new PersistentInteger(kBackingFileName)); + EXPECT_EQ(0, pi->Get()); +} + +int main(int argc, char** argv) { + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/metrics/platform2_preinstall.sh b/metrics/platform2_preinstall.sh new file mode 100755 index 000000000..ccf353ff4 --- /dev/null +++ b/metrics/platform2_preinstall.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +# Copyright (c) 2014 The Chromium OS Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +set -e + +OUT=$1 +shift +for v; do + sed -e "s/@BSLOT@/${v}/g" libmetrics.pc.in > "${OUT}/lib/libmetrics-${v}.pc" +done diff --git a/metrics/serialization/metric_sample.cc b/metrics/serialization/metric_sample.cc new file mode 100644 index 000000000..5447497ce --- /dev/null +++ b/metrics/serialization/metric_sample.cc @@ -0,0 +1,197 @@ +// Copyright 2014 The Chromium OS 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 "metrics/serialization/metric_sample.h" + +#include <string> +#include <vector> + +#include "base/logging.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_split.h" +#include "base/strings/stringprintf.h" + +namespace metrics { + +MetricSample::MetricSample(MetricSample::SampleType sample_type, + const std::string& metric_name, + int sample, + int min, + int max, + int bucket_count) + : type_(sample_type), + name_(metric_name), + sample_(sample), + min_(min), + max_(max), + bucket_count_(bucket_count) { +} + +MetricSample::~MetricSample() { +} + +bool MetricSample::IsValid() const { + return name().find(' ') == std::string::npos && + name().find('\0') == std::string::npos && !name().empty(); +} + +std::string MetricSample::ToString() const { + if (type_ == CRASH) { + return base::StringPrintf("crash%c%s%c", + '\0', + name().c_str(), + '\0'); + } else if (type_ == SPARSE_HISTOGRAM) { + return base::StringPrintf("sparsehistogram%c%s %d%c", + '\0', + name().c_str(), + sample_, + '\0'); + } else if (type_ == LINEAR_HISTOGRAM) { + return base::StringPrintf("linearhistogram%c%s %d %d%c", + '\0', + name().c_str(), + sample_, + max_, + '\0'); + } else if (type_ == HISTOGRAM) { + return base::StringPrintf("histogram%c%s %d %d %d %d%c", + '\0', + name().c_str(), + sample_, + min_, + max_, + bucket_count_, + '\0'); + } else { + // The type can only be USER_ACTION. + CHECK_EQ(type_, USER_ACTION); + return base::StringPrintf("useraction%c%s%c", + '\0', + name().c_str(), + '\0'); + } +} + +int MetricSample::sample() const { + CHECK_NE(type_, USER_ACTION); + CHECK_NE(type_, CRASH); + return sample_; +} + +int MetricSample::min() const { + CHECK_EQ(type_, HISTOGRAM); + return min_; +} + +int MetricSample::max() const { + CHECK_NE(type_, CRASH); + CHECK_NE(type_, USER_ACTION); + CHECK_NE(type_, SPARSE_HISTOGRAM); + return max_; +} + +int MetricSample::bucket_count() const { + CHECK_EQ(type_, HISTOGRAM); + return bucket_count_; +} + +// static +scoped_ptr<MetricSample> MetricSample::CrashSample( + const std::string& crash_name) { + return scoped_ptr<MetricSample>( + new MetricSample(CRASH, crash_name, 0, 0, 0, 0)); +} + +// static +scoped_ptr<MetricSample> MetricSample::HistogramSample( + const std::string& histogram_name, + int sample, + int min, + int max, + int bucket_count) { + return scoped_ptr<MetricSample>(new MetricSample( + HISTOGRAM, histogram_name, sample, min, max, bucket_count)); +} + +// static +scoped_ptr<MetricSample> MetricSample::ParseHistogram( + const std::string& serialized_histogram) { + std::vector<std::string> parts; + base::SplitString(serialized_histogram, ' ', &parts); + + if (parts.size() != 5) + return scoped_ptr<MetricSample>(); + int sample, min, max, bucket_count; + if (parts[0].empty() || !base::StringToInt(parts[1], &sample) || + !base::StringToInt(parts[2], &min) || + !base::StringToInt(parts[3], &max) || + !base::StringToInt(parts[4], &bucket_count)) { + return scoped_ptr<MetricSample>(); + } + + return HistogramSample(parts[0], sample, min, max, bucket_count); +} + +// static +scoped_ptr<MetricSample> MetricSample::SparseHistogramSample( + const std::string& histogram_name, + int sample) { + return scoped_ptr<MetricSample>( + new MetricSample(SPARSE_HISTOGRAM, histogram_name, sample, 0, 0, 0)); +} + +// static +scoped_ptr<MetricSample> MetricSample::ParseSparseHistogram( + const std::string& serialized_histogram) { + std::vector<std::string> parts; + base::SplitString(serialized_histogram, ' ', &parts); + if (parts.size() != 2) + return scoped_ptr<MetricSample>(); + int sample; + if (parts[0].empty() || !base::StringToInt(parts[1], &sample)) + return scoped_ptr<MetricSample>(); + + return SparseHistogramSample(parts[0], sample); +} + +// static +scoped_ptr<MetricSample> MetricSample::LinearHistogramSample( + const std::string& histogram_name, + int sample, + int max) { + return scoped_ptr<MetricSample>( + new MetricSample(LINEAR_HISTOGRAM, histogram_name, sample, 0, max, 0)); +} + +// static +scoped_ptr<MetricSample> MetricSample::ParseLinearHistogram( + const std::string& serialized_histogram) { + std::vector<std::string> parts; + int sample, max; + base::SplitString(serialized_histogram, ' ', &parts); + if (parts.size() != 3) + return scoped_ptr<MetricSample>(); + if (parts[0].empty() || !base::StringToInt(parts[1], &sample) || + !base::StringToInt(parts[2], &max)) { + return scoped_ptr<MetricSample>(); + } + + return LinearHistogramSample(parts[0], sample, max); +} + +// static +scoped_ptr<MetricSample> MetricSample::UserActionSample( + const std::string& action_name) { + return scoped_ptr<MetricSample>( + new MetricSample(USER_ACTION, action_name, 0, 0, 0, 0)); +} + +bool MetricSample::IsEqual(const MetricSample& metric) { + return type_ == metric.type_ && name_ == metric.name_ && + sample_ == metric.sample_ && min_ == metric.min_ && + max_ == metric.max_ && bucket_count_ == metric.bucket_count_; +} + +} // namespace metrics diff --git a/metrics/serialization/metric_sample.h b/metrics/serialization/metric_sample.h new file mode 100644 index 000000000..877114d0a --- /dev/null +++ b/metrics/serialization/metric_sample.h @@ -0,0 +1,119 @@ +// Copyright 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef METRICS_SERIALIZATION_METRIC_SAMPLE_H_ +#define METRICS_SERIALIZATION_METRIC_SAMPLE_H_ + +#include <string> + +#include "base/gtest_prod_util.h" +#include "base/macros.h" +#include "base/memory/scoped_ptr.h" + +namespace metrics { + +// This class is used by libmetrics (ChromeOS) to serialize +// and deserialize measurements to send them to a metrics sending service. +// It is meant to be a simple container with serialization functions. +class MetricSample { + public: + // Types of metric sample used. + enum SampleType { + CRASH, + HISTOGRAM, + LINEAR_HISTOGRAM, + SPARSE_HISTOGRAM, + USER_ACTION + }; + + ~MetricSample(); + + // Returns true if the sample is valid (can be serialized without ambiguity). + // + // This function should be used to filter bad samples before serializing them. + bool IsValid() const; + + // Getters for type and name. All types of metrics have these so we do not + // need to check the type. + SampleType type() const { return type_; } + const std::string& name() const { return name_; } + + // Getters for sample, min, max, bucket_count. + // Check the metric type to make sure the request make sense. (ex: a crash + // sample does not have a bucket_count so we crash if we call bucket_count() + // on it.) + int sample() const; + int min() const; + int max() const; + int bucket_count() const; + + // Returns a serialized version of the sample. + // + // The serialized message for each type is: + // crash: crash\0|name_|\0 + // user action: useraction\0|name_|\0 + // histogram: histogram\0|name_| |sample_| |min_| |max_| |bucket_count_|\0 + // sparsehistogram: sparsehistogram\0|name_| |sample_|\0 + // linearhistogram: linearhistogram\0|name_| |sample_| |max_|\0 + std::string ToString() const; + + // Builds a crash sample. + static scoped_ptr<MetricSample> CrashSample(const std::string& crash_name); + + // Builds a histogram sample. + static scoped_ptr<MetricSample> HistogramSample( + const std::string& histogram_name, + int sample, + int min, + int max, + int bucket_count); + // Deserializes a histogram sample. + static scoped_ptr<MetricSample> ParseHistogram(const std::string& serialized); + + // Builds a sparse histogram sample. + static scoped_ptr<MetricSample> SparseHistogramSample( + const std::string& histogram_name, + int sample); + // Deserializes a sparse histogram sample. + static scoped_ptr<MetricSample> ParseSparseHistogram( + const std::string& serialized); + + // Builds a linear histogram sample. + static scoped_ptr<MetricSample> LinearHistogramSample( + const std::string& histogram_name, + int sample, + int max); + // Deserializes a linear histogram sample. + static scoped_ptr<MetricSample> ParseLinearHistogram( + const std::string& serialized); + + // Builds a user action sample. + static scoped_ptr<MetricSample> UserActionSample( + const std::string& action_name); + + // Returns true if sample and this object represent the same sample (type, + // name, sample, min, max, bucket_count match). + bool IsEqual(const MetricSample& sample); + + private: + MetricSample(SampleType sample_type, + const std::string& metric_name, + const int sample, + const int min, + const int max, + const int bucket_count); + + const SampleType type_; + const std::string name_; + const int sample_; + const int min_; + const int max_; + const int bucket_count_; + + DISALLOW_COPY_AND_ASSIGN(MetricSample); +}; + +} // namespace metrics + +#endif // METRICS_SERIALIZATION_METRIC_SAMPLE_H_ diff --git a/metrics/serialization/serialization_utils.cc b/metrics/serialization/serialization_utils.cc new file mode 100644 index 000000000..9aa076a4b --- /dev/null +++ b/metrics/serialization/serialization_utils.cc @@ -0,0 +1,221 @@ +// Copyright 2014 The Chromium OS 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 "metrics/serialization/serialization_utils.h" + +#include <sys/file.h> + +#include <string> +#include <vector> + +#include "base/files/file_path.h" +#include "base/files/file_util.h" +#include "base/files/scoped_file.h" +#include "base/logging.h" +#include "base/memory/scoped_ptr.h" +#include "base/memory/scoped_vector.h" +#include "base/strings/string_split.h" +#include "base/strings/string_util.h" +#include "metrics/serialization/metric_sample.h" + +#define READ_WRITE_ALL_FILE_FLAGS \ + (S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH) + +namespace metrics { +namespace { + +// Reads the next message from |file_descriptor| into |message|. +// +// |message| will be set to the empty string if no message could be read (EOF) +// or the message was badly constructed. +// +// Returns false if no message can be read from this file anymore (EOF or +// unrecoverable error). +bool ReadMessage(int fd, std::string* message) { + CHECK(message); + + int result; + int32_t message_size; + const int32_t message_hdr_size = sizeof(message_size); + // The file containing the metrics do not leave the device so the writer and + // the reader will always have the same endianness. + result = HANDLE_EINTR(read(fd, &message_size, sizeof(message_size))); + if (result < 0) { + DPLOG(ERROR) << "reading metrics message header"; + return false; + } + if (result == 0) { + // This indicates a normal EOF. + return false; + } + if (result < message_hdr_size) { + DLOG(ERROR) << "bad read size " << result << ", expecting " + << sizeof(message_size); + return false; + } + + // kMessageMaxLength applies to the entire message: the 4-byte + // length field and the content. + if (message_size > SerializationUtils::kMessageMaxLength) { + DLOG(ERROR) << "message too long : " << message_size; + if (HANDLE_EINTR(lseek(fd, message_size - 4, SEEK_CUR)) == -1) { + DLOG(ERROR) << "error while skipping message. abort"; + return false; + } + // Badly formatted message was skipped. Treat the badly formatted sample as + // an empty sample. + message->clear(); + return true; + } + + if (message_size < message_hdr_size) { + DLOG(ERROR) << "message too short : " << message_size; + return false; + } + + message_size -= message_hdr_size; // The message size includes itself. + char buffer[SerializationUtils::kMessageMaxLength]; + if (!base::ReadFromFD(fd, buffer, message_size)) { + DPLOG(ERROR) << "reading metrics message body"; + return false; + } + *message = std::string(buffer, message_size); + return true; +} + +} // namespace + +scoped_ptr<MetricSample> SerializationUtils::ParseSample( + const std::string& sample) { + if (sample.empty()) + return scoped_ptr<MetricSample>(); + + std::vector<std::string> parts; + base::SplitString(sample, '\0', &parts); + // We should have two null terminated strings so split should produce + // three chunks. + if (parts.size() != 3) { + DLOG(ERROR) << "splitting message on \\0 produced " << parts.size() + << " parts (expected 3)"; + return scoped_ptr<MetricSample>(); + } + const std::string& name = parts[0]; + const std::string& value = parts[1]; + + if (base::LowerCaseEqualsASCII(name, "crash")) { + return MetricSample::CrashSample(value); + } else if (base::LowerCaseEqualsASCII(name, "histogram")) { + return MetricSample::ParseHistogram(value); + } else if (base::LowerCaseEqualsASCII(name, "linearhistogram")) { + return MetricSample::ParseLinearHistogram(value); + } else if (base::LowerCaseEqualsASCII(name, "sparsehistogram")) { + return MetricSample::ParseSparseHistogram(value); + } else if (base::LowerCaseEqualsASCII(name, "useraction")) { + return MetricSample::UserActionSample(value); + } else { + DLOG(ERROR) << "invalid event type: " << name << ", value: " << value; + } + return scoped_ptr<MetricSample>(); +} + +void SerializationUtils::ReadAndTruncateMetricsFromFile( + const std::string& filename, + ScopedVector<MetricSample>* metrics) { + struct stat stat_buf; + int result; + + result = stat(filename.c_str(), &stat_buf); + if (result < 0) { + if (errno != ENOENT) + DPLOG(ERROR) << filename << ": bad metrics file stat"; + + // Nothing to collect---try later. + return; + } + if (stat_buf.st_size == 0) { + // Also nothing to collect. + return; + } + base::ScopedFD fd(open(filename.c_str(), O_RDWR)); + if (fd.get() < 0) { + DPLOG(ERROR) << filename << ": cannot open"; + return; + } + result = flock(fd.get(), LOCK_EX); + if (result < 0) { + DPLOG(ERROR) << filename << ": cannot lock"; + return; + } + + // This processes all messages in the log. When all messages are + // read and processed, or an error occurs, truncate the file to zero size. + for (;;) { + std::string message; + + if (!ReadMessage(fd.get(), &message)) + break; + + scoped_ptr<MetricSample> sample = ParseSample(message); + if (sample) + metrics->push_back(sample.release()); + } + + result = ftruncate(fd.get(), 0); + if (result < 0) + DPLOG(ERROR) << "truncate metrics log"; + + result = flock(fd.get(), LOCK_UN); + if (result < 0) + DPLOG(ERROR) << "unlock metrics log"; +} + +bool SerializationUtils::WriteMetricToFile(const MetricSample& sample, + const std::string& filename) { + if (!sample.IsValid()) + return false; + + base::ScopedFD file_descriptor(open(filename.c_str(), + O_WRONLY | O_APPEND | O_CREAT, + READ_WRITE_ALL_FILE_FLAGS)); + + if (file_descriptor.get() < 0) { + DPLOG(ERROR) << filename << ": cannot open"; + return false; + } + + fchmod(file_descriptor.get(), READ_WRITE_ALL_FILE_FLAGS); + // Grab a lock to avoid chrome truncating the file + // underneath us. Keep the file locked as briefly as possible. + // Freeing file_descriptor will close the file and and remove the lock. + if (HANDLE_EINTR(flock(file_descriptor.get(), LOCK_EX)) < 0) { + DPLOG(ERROR) << filename << ": cannot lock"; + return false; + } + + std::string msg = sample.ToString(); + int32 size = msg.length() + sizeof(int32); + if (size > kMessageMaxLength) { + DLOG(ERROR) << "cannot write message: too long"; + return false; + } + + // The file containing the metrics samples will only be read by programs on + // the same device so we do not check endianness. + if (!base::WriteFileDescriptor(file_descriptor.get(), + reinterpret_cast<char*>(&size), + sizeof(size))) { + DPLOG(ERROR) << "error writing message length"; + return false; + } + + if (!base::WriteFileDescriptor( + file_descriptor.get(), msg.c_str(), msg.size())) { + DPLOG(ERROR) << "error writing message"; + return false; + } + + return true; +} + +} // namespace metrics diff --git a/metrics/serialization/serialization_utils.h b/metrics/serialization/serialization_utils.h new file mode 100644 index 000000000..5af61660f --- /dev/null +++ b/metrics/serialization/serialization_utils.h @@ -0,0 +1,48 @@ +// Copyright 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef METRICS_SERIALIZATION_SERIALIZATION_UTILS_H_ +#define METRICS_SERIALIZATION_SERIALIZATION_UTILS_H_ + +#include <string> + +#include "base/memory/scoped_ptr.h" +#include "base/memory/scoped_vector.h" + +namespace metrics { + +class MetricSample; + +// Metrics helpers to serialize and deserialize metrics collected by +// ChromeOS. +namespace SerializationUtils { + +// Deserializes a sample passed as a string and return a sample. +// The return value will either be a scoped_ptr to a Metric sample (if the +// deserialization was successful) or a NULL scoped_ptr. +scoped_ptr<MetricSample> ParseSample(const std::string& sample); + +// Reads all samples from a file and truncate the file when done. +void ReadAndTruncateMetricsFromFile(const std::string& filename, + ScopedVector<MetricSample>* metrics); + +// Serializes a sample and write it to filename. +// The format for the message is: +// message_size, serialized_message +// where +// * message_size is the total length of the message (message_size + +// serialized_message) on 4 bytes +// * serialized_message is the serialized version of sample (using ToString) +// +// NB: the file will never leave the device so message_size will be written +// with the architecture's endianness. +bool WriteMetricToFile(const MetricSample& sample, const std::string& filename); + +// Maximum length of a serialized message +static const int kMessageMaxLength = 1024; + +} // namespace SerializationUtils +} // namespace metrics + +#endif // METRICS_SERIALIZATION_SERIALIZATION_UTILS_H_ diff --git a/metrics/serialization/serialization_utils_unittest.cc b/metrics/serialization/serialization_utils_unittest.cc new file mode 100644 index 000000000..34d76cf8b --- /dev/null +++ b/metrics/serialization/serialization_utils_unittest.cc @@ -0,0 +1,169 @@ +// Copyright 2014 The Chromium OS 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 "metrics/serialization/serialization_utils.h" + +#include <base/files/file_util.h> +#include <base/files/scoped_temp_dir.h> +#include <base/logging.h> +#include <base/strings/stringprintf.h> +#include <gtest/gtest.h> + +#include "metrics/serialization/metric_sample.h" + +namespace metrics { +namespace { + +class SerializationUtilsTest : public testing::Test { + protected: + SerializationUtilsTest() { + bool success = temporary_dir.CreateUniqueTempDir(); + if (success) { + base::FilePath dir_path = temporary_dir.path(); + filename = dir_path.value() + "chromeossampletest"; + filepath = base::FilePath(filename); + } + } + + void SetUp() override { base::DeleteFile(filepath, false); } + + void TestSerialization(MetricSample* sample) { + std::string serialized(sample->ToString()); + ASSERT_EQ('\0', serialized[serialized.length() - 1]); + scoped_ptr<MetricSample> deserialized = + SerializationUtils::ParseSample(serialized); + ASSERT_TRUE(deserialized); + EXPECT_TRUE(sample->IsEqual(*deserialized.get())); + } + + std::string filename; + base::ScopedTempDir temporary_dir; + base::FilePath filepath; +}; + +TEST_F(SerializationUtilsTest, CrashSerializeTest) { + TestSerialization(MetricSample::CrashSample("test").get()); +} + +TEST_F(SerializationUtilsTest, HistogramSerializeTest) { + TestSerialization( + MetricSample::HistogramSample("myhist", 13, 1, 100, 10).get()); +} + +TEST_F(SerializationUtilsTest, LinearSerializeTest) { + TestSerialization( + MetricSample::LinearHistogramSample("linearhist", 12, 30).get()); +} + +TEST_F(SerializationUtilsTest, SparseSerializeTest) { + TestSerialization(MetricSample::SparseHistogramSample("mysparse", 30).get()); +} + +TEST_F(SerializationUtilsTest, UserActionSerializeTest) { + TestSerialization(MetricSample::UserActionSample("myaction").get()); +} + +TEST_F(SerializationUtilsTest, IllegalNameAreFilteredTest) { + scoped_ptr<MetricSample> sample1 = + MetricSample::SparseHistogramSample("no space", 10); + scoped_ptr<MetricSample> sample2 = MetricSample::LinearHistogramSample( + base::StringPrintf("here%cbhe", '\0'), 1, 3); + + EXPECT_FALSE(SerializationUtils::WriteMetricToFile(*sample1.get(), filename)); + EXPECT_FALSE(SerializationUtils::WriteMetricToFile(*sample2.get(), filename)); + int64 size = 0; + + ASSERT_TRUE(!PathExists(filepath) || base::GetFileSize(filepath, &size)); + + EXPECT_EQ(0, size); +} + +TEST_F(SerializationUtilsTest, BadInputIsCaughtTest) { + std::string input( + base::StringPrintf("sparsehistogram%cname foo%c", '\0', '\0')); + EXPECT_EQ(NULL, MetricSample::ParseSparseHistogram(input).get()); +} + +TEST_F(SerializationUtilsTest, MessageSeparatedByZero) { + scoped_ptr<MetricSample> crash = MetricSample::CrashSample("mycrash"); + + SerializationUtils::WriteMetricToFile(*crash.get(), filename); + int64 size = 0; + ASSERT_TRUE(base::GetFileSize(filepath, &size)); + // 4 bytes for the size + // 5 bytes for crash + // 7 bytes for mycrash + // 2 bytes for the \0 + // -> total of 18 + EXPECT_EQ(size, 18); +} + +TEST_F(SerializationUtilsTest, MessagesTooLongAreDiscardedTest) { + // Creates a message that is bigger than the maximum allowed size. + // As we are adding extra character (crash, \0s, etc), if the name is + // kMessageMaxLength long, it will be too long. + std::string name(SerializationUtils::kMessageMaxLength, 'c'); + + scoped_ptr<MetricSample> crash = MetricSample::CrashSample(name); + EXPECT_FALSE(SerializationUtils::WriteMetricToFile(*crash.get(), filename)); + int64 size = 0; + ASSERT_TRUE(base::GetFileSize(filepath, &size)); + EXPECT_EQ(0, size); +} + +TEST_F(SerializationUtilsTest, ReadLongMessageTest) { + base::File test_file(filepath, + base::File::FLAG_OPEN_ALWAYS | base::File::FLAG_APPEND); + std::string message(SerializationUtils::kMessageMaxLength + 1, 'c'); + + int32 message_size = message.length() + sizeof(int32); + test_file.WriteAtCurrentPos(reinterpret_cast<const char*>(&message_size), + sizeof(message_size)); + test_file.WriteAtCurrentPos(message.c_str(), message.length()); + test_file.Close(); + + scoped_ptr<MetricSample> crash = MetricSample::CrashSample("test"); + SerializationUtils::WriteMetricToFile(*crash.get(), filename); + + ScopedVector<MetricSample> samples; + SerializationUtils::ReadAndTruncateMetricsFromFile(filename, &samples); + ASSERT_EQ(size_t(1), samples.size()); + ASSERT_TRUE(samples[0] != NULL); + EXPECT_TRUE(crash->IsEqual(*samples[0])); +} + +TEST_F(SerializationUtilsTest, WriteReadTest) { + scoped_ptr<MetricSample> hist = + MetricSample::HistogramSample("myhist", 1, 2, 3, 4); + scoped_ptr<MetricSample> crash = MetricSample::CrashSample("mycrash"); + scoped_ptr<MetricSample> lhist = + MetricSample::LinearHistogramSample("linear", 1, 10); + scoped_ptr<MetricSample> shist = + MetricSample::SparseHistogramSample("mysparse", 30); + scoped_ptr<MetricSample> action = MetricSample::UserActionSample("myaction"); + + SerializationUtils::WriteMetricToFile(*hist.get(), filename); + SerializationUtils::WriteMetricToFile(*crash.get(), filename); + SerializationUtils::WriteMetricToFile(*lhist.get(), filename); + SerializationUtils::WriteMetricToFile(*shist.get(), filename); + SerializationUtils::WriteMetricToFile(*action.get(), filename); + ScopedVector<MetricSample> vect; + SerializationUtils::ReadAndTruncateMetricsFromFile(filename, &vect); + ASSERT_EQ(vect.size(), size_t(5)); + for (int i = 0; i < 5; i++) { + ASSERT_TRUE(vect[0] != NULL); + } + EXPECT_TRUE(hist->IsEqual(*vect[0])); + EXPECT_TRUE(crash->IsEqual(*vect[1])); + EXPECT_TRUE(lhist->IsEqual(*vect[2])); + EXPECT_TRUE(shist->IsEqual(*vect[3])); + EXPECT_TRUE(action->IsEqual(*vect[4])); + + int64 size = 0; + ASSERT_TRUE(base::GetFileSize(filepath, &size)); + ASSERT_EQ(0, size); +} + +} // namespace +} // namespace metrics diff --git a/metrics/syslog_parser.sh b/metrics/syslog_parser.sh new file mode 100755 index 000000000..7d064be96 --- /dev/null +++ b/metrics/syslog_parser.sh @@ -0,0 +1,69 @@ +#! /bin/sh + +# This script parses /var/log/syslog for messages from programs that log +# uptime and disk stats (number of sectors read). It then outputs +# these stats in a format usable by the metrics collector, which forwards +# them to autotest and UMA. + +# To add a new metric add a line below, as PROGRAM_NAME METRIC_NAME. +# PROGRAM_NAME is the name of the job whose start time we +# are interested in. METRIC_NAME is the prefix we want to use for +# reporting to UMA and autotest. The script prepends "Time" and +# "Sectors" to METRIC_NAME for the two available measurements, uptime +# and number of sectors read thus far. + +# You will need to emit messages similar to the following in order to add a +# a metric using this process. You will need to emit both a start and stop +# time and the metric reported will be the difference in values + +# Nov 15 08:05 localhost PROGRAM_NAME[822]: start METRIC_NAME time 12 sectors 56 +# Nov 15 08:05 localhost PROGRAM_NAME[822]: stop METRIC_NAME time 24 sectors 68 + +# If you add metrics without a start, it is assumed you are requesting the +# time differece from system start + +# Metrics we are interested in measuring +METRICS=" +upstart start_x +" + +first=1 +program="" + +# Get the metrics for all things +for m in $METRICS +do + if [ $first -eq 1 ] + then + first=0 + program_name=$m + else + first=1 + metrics_name=$m + + # Example of line from /var/log/messages: + # Nov 15 08:05:42 localhost connmand[822]: start metric time 12 sectors 56 + # "upstart:" is $5, 1234 is $9, etc. + program="${program}/$program_name([[0-9]+]:|:) start $metrics_name/\ + { + metrics_start[\"${metrics_name}Time\"] = \$9; + metrics_start[\"${metrics_name}Sectors\"] = \$11; + }" + program="${program}/$program_name([[0-9]+]:|:) stop $metrics_name/\ + { + metrics_stop[\"${metrics_name}Time\"] = \$9; + metrics_stop[\"${metrics_name}Sectors\"] = \$11; + }" + fi +done + +# Do all the differencing here +program="${program}\ +END{ + for (i in metrics_stop) { + value_time = metrics_stop[i] - metrics_start[i]; + print i \"=\" value_time; + } +}" + +exec awk "$program" /var/log/syslog diff --git a/metrics/timer.cc b/metrics/timer.cc new file mode 100644 index 000000000..99f68fefd --- /dev/null +++ b/metrics/timer.cc @@ -0,0 +1,108 @@ +// Copyright (c) 2011 The Chromium OS 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 "metrics/timer.h" + +#include <string> + +#include <base/memory/scoped_ptr.h> + +#include "metrics/metrics_library.h" + +namespace chromeos_metrics { + +base::TimeTicks ClockWrapper::GetCurrentTime() const { + return base::TimeTicks::Now(); +} + +Timer::Timer() + : timer_state_(kTimerStopped), + clock_wrapper_(new ClockWrapper()) {} + +bool Timer::Start() { + elapsed_time_ = base::TimeDelta(); // Sets elapsed_time_ to zero. + start_time_ = clock_wrapper_->GetCurrentTime(); + timer_state_ = kTimerRunning; + return true; +} + +bool Timer::Stop() { + if (timer_state_ == kTimerStopped) + return false; + if (timer_state_ == kTimerRunning) + elapsed_time_ += clock_wrapper_->GetCurrentTime() - start_time_; + timer_state_ = kTimerStopped; + return true; +} + +bool Timer::Pause() { + switch (timer_state_) { + case kTimerStopped: + if (!Start()) + return false; + timer_state_ = kTimerPaused; + return true; + case kTimerRunning: + timer_state_ = kTimerPaused; + elapsed_time_ += clock_wrapper_->GetCurrentTime() - start_time_; + return true; + default: + return false; + } +} + +bool Timer::Resume() { + switch (timer_state_) { + case kTimerStopped: + return Start(); + case kTimerPaused: + start_time_ = clock_wrapper_->GetCurrentTime(); + timer_state_ = kTimerRunning; + return true; + default: + return false; + } +} + +bool Timer::Reset() { + elapsed_time_ = base::TimeDelta(); // Sets elapsed_time_ to zero. + timer_state_ = kTimerStopped; + return true; +} + +bool Timer::HasStarted() const { + return timer_state_ != kTimerStopped; +} + +bool Timer::GetElapsedTime(base::TimeDelta* elapsed_time) const { + if (start_time_.is_null() || !elapsed_time) + return false; + *elapsed_time = elapsed_time_; + if (timer_state_ == kTimerRunning) { + *elapsed_time += clock_wrapper_->GetCurrentTime() - start_time_; + } + return true; +} + +// static +MetricsLibraryInterface* TimerReporter::metrics_lib_ = nullptr; + +TimerReporter::TimerReporter(const std::string& histogram_name, int min, + int max, int num_buckets) + : histogram_name_(histogram_name), + min_(min), + max_(max), + num_buckets_(num_buckets) {} + +bool TimerReporter::ReportMilliseconds() const { + base::TimeDelta elapsed_time; + if (!metrics_lib_ || !GetElapsedTime(&elapsed_time)) return false; + return metrics_lib_->SendToUMA(histogram_name_, + elapsed_time.InMilliseconds(), + min_, + max_, + num_buckets_); +} + +} // namespace chromeos_metrics diff --git a/metrics/timer.h b/metrics/timer.h new file mode 100644 index 000000000..52cc57892 --- /dev/null +++ b/metrics/timer.h @@ -0,0 +1,158 @@ +// Copyright (c) 2011 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Timer - class that provides timer tracking. + +#ifndef METRICS_TIMER_H_ +#define METRICS_TIMER_H_ + +#include <string> + +#include <base/macros.h> +#include <base/memory/scoped_ptr.h> +#include <base/time/time.h> +#include <gtest/gtest_prod.h> // for FRIEND_TEST + +class MetricsLibraryInterface; + +namespace chromeos_metrics { + +class TimerInterface { + public: + virtual ~TimerInterface() {} + + virtual bool Start() = 0; + virtual bool Stop() = 0; + virtual bool Reset() = 0; + virtual bool HasStarted() const = 0; +}; + +// Wrapper for calls to the system clock. +class ClockWrapper { + public: + ClockWrapper() {} + virtual ~ClockWrapper() {} + + // Returns the current time from the system. + virtual base::TimeTicks GetCurrentTime() const; + + private: + DISALLOW_COPY_AND_ASSIGN(ClockWrapper); +}; + +// Implements a Timer. +class Timer : public TimerInterface { + public: + Timer(); + virtual ~Timer() {} + + // Starts the timer. If a timer is already running, also resets current + // timer. Always returns true. + virtual bool Start(); + + // Stops the timer and calculates the total time elapsed between now and when + // Start() was called. Note that this method needs a prior call to Start(). + // Otherwise, it fails (returns false). + virtual bool Stop(); + + // Pauses a timer. If the timer is stopped, this call starts the timer in + // the paused state. Fails (returns false) if the timer is already paused. + virtual bool Pause(); + + // Restarts a paused timer (or starts a stopped timer). This method fails + // (returns false) if the timer is already running; otherwise, returns true. + virtual bool Resume(); + + // Resets the timer, erasing the current duration being tracked. Always + // returns true. + virtual bool Reset(); + + // Returns whether the timer has started or not. + virtual bool HasStarted() const; + + // Stores the current elapsed time in |elapsed_time|. If timer is stopped, + // stores the elapsed time from when Stop() was last called. Otherwise, + // calculates and stores the elapsed time since the last Start(). + // Returns false if the timer was never Start()'ed or if called with a null + // pointer argument. + virtual bool GetElapsedTime(base::TimeDelta* elapsed_time) const; + + private: + enum TimerState { kTimerStopped, kTimerRunning, kTimerPaused }; + friend class TimerTest; + friend class TimerReporterTest; + FRIEND_TEST(TimerReporterTest, StartStopReport); + FRIEND_TEST(TimerTest, InvalidElapsedTime); + FRIEND_TEST(TimerTest, InvalidStop); + FRIEND_TEST(TimerTest, PauseResumeStop); + FRIEND_TEST(TimerTest, PauseStartStopResume); + FRIEND_TEST(TimerTest, PauseStop); + FRIEND_TEST(TimerTest, Reset); + FRIEND_TEST(TimerTest, ReStart); + FRIEND_TEST(TimerTest, ResumeStartStopPause); + FRIEND_TEST(TimerTest, SeparatedTimers); + FRIEND_TEST(TimerTest, StartPauseResumePauseResumeStop); + FRIEND_TEST(TimerTest, StartPauseResumePauseStop); + FRIEND_TEST(TimerTest, StartPauseResumeStop); + FRIEND_TEST(TimerTest, StartPauseStop); + FRIEND_TEST(TimerTest, StartResumeStop); + FRIEND_TEST(TimerTest, StartStop); + + // Elapsed time of the last use of the timer. + base::TimeDelta elapsed_time_; + + // Starting time value. + base::TimeTicks start_time_; + + // Whether the timer is running, stopped, or paused. + TimerState timer_state_; + + // Wrapper for the calls to the system clock. + scoped_ptr<ClockWrapper> clock_wrapper_; + + DISALLOW_COPY_AND_ASSIGN(Timer); +}; + +// Extends the Timer class to report the elapsed time in milliseconds through +// the UMA metrics library. +class TimerReporter : public Timer { + public: + // Initializes the timer by providing a |histogram_name| to report to with + // |min|, |max| and |num_buckets| attributes for the histogram. + TimerReporter(const std::string& histogram_name, int min, int max, + int num_buckets); + virtual ~TimerReporter() {} + + // Sets the metrics library used by all instances of this class. + static void set_metrics_lib(MetricsLibraryInterface* metrics_lib) { + metrics_lib_ = metrics_lib; + } + + // Reports the current duration to UMA, in milliseconds. Returns false if + // there is nothing to report, e.g. a metrics library is not set. + virtual bool ReportMilliseconds() const; + + // Accessor methods. + const std::string& histogram_name() const { return histogram_name_; } + int min() const { return min_; } + int max() const { return max_; } + int num_buckets() const { return num_buckets_; } + + private: + friend class TimerReporterTest; + FRIEND_TEST(TimerReporterTest, StartStopReport); + FRIEND_TEST(TimerReporterTest, InvalidReport); + + static MetricsLibraryInterface* metrics_lib_; + std::string histogram_name_; + int min_; + int max_; + int num_buckets_; + + DISALLOW_COPY_AND_ASSIGN(TimerReporter); +}; + +} // namespace chromeos_metrics + +#endif // METRICS_TIMER_H_ diff --git a/metrics/timer_mock.h b/metrics/timer_mock.h new file mode 100644 index 000000000..2f2d0f4b6 --- /dev/null +++ b/metrics/timer_mock.h @@ -0,0 +1,47 @@ +// Copyright (c) 2011 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef METRICS_TIMER_MOCK_H_ +#define METRICS_TIMER_MOCK_H_ + +#include <string> + +#include <gmock/gmock.h> + +#include "metrics/timer.h" + +namespace chromeos_metrics { + +class TimerMock : public Timer { + public: + MOCK_METHOD0(Start, bool()); + MOCK_METHOD0(Stop, bool()); + MOCK_METHOD0(Reset, bool()); + MOCK_CONST_METHOD0(HasStarted, bool()); + MOCK_CONST_METHOD1(GetElapsedTime, bool(base::TimeDelta* elapsed_time)); +}; + +class TimerReporterMock : public TimerReporter { + public: + TimerReporterMock() : TimerReporter("", 0, 0, 0) {} + MOCK_METHOD0(Start, bool()); + MOCK_METHOD0(Stop, bool()); + MOCK_METHOD0(Reset, bool()); + MOCK_CONST_METHOD0(HasStarted, bool()); + MOCK_CONST_METHOD1(GetElapsedTime, bool(base::TimeDelta* elapsed_time)); + MOCK_CONST_METHOD0(ReportMilliseconds, bool()); + MOCK_CONST_METHOD0(histogram_name, std::string&()); + MOCK_CONST_METHOD0(min, int()); + MOCK_CONST_METHOD0(max, int()); + MOCK_CONST_METHOD0(num_buckets, int()); +}; + +class ClockWrapperMock : public ClockWrapper { + public: + MOCK_CONST_METHOD0(GetCurrentTime, base::TimeTicks()); +}; + +} // namespace chromeos_metrics + +#endif // METRICS_TIMER_MOCK_H_ diff --git a/metrics/timer_test.cc b/metrics/timer_test.cc new file mode 100644 index 000000000..ec6c6bdba --- /dev/null +++ b/metrics/timer_test.cc @@ -0,0 +1,452 @@ +// Copyright (c) 2011 The Chromium OS 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 <stdint.h> + +#include <base/memory/scoped_ptr.h> +#include <gmock/gmock.h> +#include <gtest/gtest.h> + +#include "metrics/metrics_library_mock.h" +#include "metrics/timer.h" +#include "metrics/timer_mock.h" + +using ::testing::_; +using ::testing::Return; + +namespace chromeos_metrics { + +namespace { +const int64_t kStime1MSec = 1400; +const int64_t kEtime1MSec = 3000; +const int64_t kDelta1MSec = 1600; + +const int64_t kStime2MSec = 4200; +const int64_t kEtime2MSec = 5000; +const int64_t kDelta2MSec = 800; + +const int64_t kStime3MSec = 6600; +const int64_t kEtime3MSec = 6800; +const int64_t kDelta3MSec = 200; +} // namespace + +class TimerTest : public testing::Test { + public: + TimerTest() : clock_wrapper_mock_(new ClockWrapperMock()) {} + + protected: + virtual void SetUp() { + EXPECT_EQ(Timer::kTimerStopped, timer_.timer_state_); + stime += base::TimeDelta::FromMilliseconds(kStime1MSec); + etime += base::TimeDelta::FromMilliseconds(kEtime1MSec); + stime2 += base::TimeDelta::FromMilliseconds(kStime2MSec); + etime2 += base::TimeDelta::FromMilliseconds(kEtime2MSec); + stime3 += base::TimeDelta::FromMilliseconds(kStime3MSec); + etime3 += base::TimeDelta::FromMilliseconds(kEtime3MSec); + } + + virtual void TearDown() {} + + Timer timer_; + scoped_ptr<ClockWrapperMock> clock_wrapper_mock_; + base::TimeTicks stime, etime, stime2, etime2, stime3, etime3; +}; + +TEST_F(TimerTest, StartStop) { + EXPECT_CALL(*clock_wrapper_mock_, GetCurrentTime()) + .WillOnce(Return(stime)) + .WillOnce(Return(etime)); + timer_.clock_wrapper_.reset(clock_wrapper_mock_.release()); + ASSERT_TRUE(timer_.Start()); + ASSERT_TRUE(timer_.start_time_ == stime); + ASSERT_TRUE(timer_.HasStarted()); + ASSERT_TRUE(timer_.Stop()); + ASSERT_EQ(timer_.elapsed_time_.InMilliseconds(), kDelta1MSec); + + base::TimeDelta elapsed_time; + ASSERT_TRUE(timer_.GetElapsedTime(&elapsed_time)); + ASSERT_EQ(timer_.elapsed_time_.InMilliseconds(), + elapsed_time.InMilliseconds()); + + ASSERT_FALSE(timer_.HasStarted()); +} + +TEST_F(TimerTest, ReStart) { + EXPECT_CALL(*clock_wrapper_mock_, GetCurrentTime()) + .WillOnce(Return(stime)) + .WillOnce(Return(etime)); + timer_.clock_wrapper_.reset(clock_wrapper_mock_.release()); + timer_.Start(); + base::TimeTicks buffer = timer_.start_time_; + timer_.Start(); + ASSERT_FALSE(timer_.start_time_ == buffer); +} + +TEST_F(TimerTest, Reset) { + EXPECT_CALL(*clock_wrapper_mock_, GetCurrentTime()) + .WillOnce(Return(stime)); + timer_.clock_wrapper_.reset(clock_wrapper_mock_.release()); + timer_.Start(); + ASSERT_TRUE(timer_.Reset()); + ASSERT_FALSE(timer_.HasStarted()); +} + +TEST_F(TimerTest, SeparatedTimers) { + EXPECT_CALL(*clock_wrapper_mock_, GetCurrentTime()) + .WillOnce(Return(stime)) + .WillOnce(Return(etime)) + .WillOnce(Return(stime2)) + .WillOnce(Return(etime2)); + timer_.clock_wrapper_.reset(clock_wrapper_mock_.release()); + ASSERT_TRUE(timer_.Start()); + ASSERT_TRUE(timer_.Stop()); + ASSERT_EQ(timer_.elapsed_time_.InMilliseconds(), kDelta1MSec); + ASSERT_TRUE(timer_.Start()); + ASSERT_TRUE(timer_.start_time_ == stime2); + ASSERT_TRUE(timer_.Stop()); + ASSERT_EQ(timer_.elapsed_time_.InMilliseconds(), kDelta2MSec); + ASSERT_FALSE(timer_.HasStarted()); + + base::TimeDelta elapsed_time; + ASSERT_TRUE(timer_.GetElapsedTime(&elapsed_time)); + ASSERT_EQ(timer_.elapsed_time_.InMilliseconds(), + elapsed_time.InMilliseconds()); +} + +TEST_F(TimerTest, InvalidStop) { + EXPECT_CALL(*clock_wrapper_mock_, GetCurrentTime()) + .WillOnce(Return(stime)) + .WillOnce(Return(etime)); + timer_.clock_wrapper_.reset(clock_wrapper_mock_.release()); + ASSERT_FALSE(timer_.Stop()); + // Now we try it again, but after a valid start/stop. + timer_.Start(); + timer_.Stop(); + base::TimeDelta elapsed_time = timer_.elapsed_time_; + ASSERT_FALSE(timer_.Stop()); + ASSERT_TRUE(elapsed_time == timer_.elapsed_time_); +} + +TEST_F(TimerTest, InvalidElapsedTime) { + base::TimeDelta elapsed_time; + ASSERT_FALSE(timer_.GetElapsedTime(&elapsed_time)); +} + +TEST_F(TimerTest, PauseStartStopResume) { + EXPECT_CALL(*clock_wrapper_mock_, GetCurrentTime()) + .WillOnce(Return(stime)) + .WillOnce(Return(stime2)) + .WillOnce(Return(etime2)) + .WillOnce(Return(stime3)) + .WillOnce(Return(etime3)); + timer_.clock_wrapper_.reset(clock_wrapper_mock_.release()); + ASSERT_TRUE(timer_.Pause()); // Starts timer paused. + ASSERT_TRUE(timer_.start_time_ == stime); + ASSERT_TRUE(timer_.HasStarted()); + + ASSERT_TRUE(timer_.Start()); // Restarts timer. + ASSERT_TRUE(timer_.start_time_ == stime2); + ASSERT_TRUE(timer_.HasStarted()); + + ASSERT_TRUE(timer_.Stop()); + ASSERT_EQ(timer_.elapsed_time_.InMilliseconds(), kDelta2MSec); + ASSERT_FALSE(timer_.HasStarted()); + base::TimeDelta elapsed_time; + ASSERT_TRUE(timer_.GetElapsedTime(&elapsed_time)); + ASSERT_EQ(timer_.elapsed_time_.InMilliseconds(), + elapsed_time.InMilliseconds()); + + ASSERT_TRUE(timer_.Resume()); + ASSERT_TRUE(timer_.HasStarted()); + ASSERT_TRUE(timer_.GetElapsedTime(&elapsed_time)); + ASSERT_EQ(kDelta3MSec, elapsed_time.InMilliseconds()); +} + +TEST_F(TimerTest, ResumeStartStopPause) { + EXPECT_CALL(*clock_wrapper_mock_, GetCurrentTime()) + .WillOnce(Return(stime)) + .WillOnce(Return(stime2)) + .WillOnce(Return(etime2)) + .WillOnce(Return(stime3)); + timer_.clock_wrapper_.reset(clock_wrapper_mock_.release()); + ASSERT_TRUE(timer_.Resume()); + ASSERT_TRUE(timer_.start_time_ == stime); + ASSERT_TRUE(timer_.HasStarted()); + + ASSERT_TRUE(timer_.Start()); + ASSERT_TRUE(timer_.start_time_ == stime2); + ASSERT_TRUE(timer_.HasStarted()); + + ASSERT_TRUE(timer_.Stop()); + ASSERT_EQ(timer_.elapsed_time_.InMilliseconds(), kDelta2MSec); + ASSERT_FALSE(timer_.HasStarted()); + base::TimeDelta elapsed_time; + ASSERT_TRUE(timer_.GetElapsedTime(&elapsed_time)); + ASSERT_EQ(timer_.elapsed_time_.InMilliseconds(), + elapsed_time.InMilliseconds()); + + ASSERT_TRUE(timer_.Pause()); + ASSERT_TRUE(timer_.HasStarted()); + ASSERT_TRUE(timer_.GetElapsedTime(&elapsed_time)); + ASSERT_EQ(0, elapsed_time.InMilliseconds()); +} + +TEST_F(TimerTest, StartResumeStop) { + EXPECT_CALL(*clock_wrapper_mock_, GetCurrentTime()) + .WillOnce(Return(stime)) + .WillOnce(Return(etime)); + timer_.clock_wrapper_.reset(clock_wrapper_mock_.release()); + ASSERT_TRUE(timer_.Start()); + ASSERT_TRUE(timer_.start_time_ == stime); + ASSERT_TRUE(timer_.HasStarted()); + + ASSERT_FALSE(timer_.Resume()); + ASSERT_TRUE(timer_.start_time_ == stime); + ASSERT_TRUE(timer_.HasStarted()); + + ASSERT_TRUE(timer_.Stop()); + ASSERT_EQ(timer_.elapsed_time_.InMilliseconds(), kDelta1MSec); + ASSERT_FALSE(timer_.HasStarted()); + base::TimeDelta elapsed_time; + ASSERT_TRUE(timer_.GetElapsedTime(&elapsed_time)); + ASSERT_EQ(timer_.elapsed_time_.InMilliseconds(), + elapsed_time.InMilliseconds()); +} + +TEST_F(TimerTest, StartPauseStop) { + EXPECT_CALL(*clock_wrapper_mock_, GetCurrentTime()) + .WillOnce(Return(stime)) + .WillOnce(Return(etime)); + timer_.clock_wrapper_.reset(clock_wrapper_mock_.release()); + ASSERT_TRUE(timer_.Start()); + ASSERT_TRUE(timer_.start_time_ == stime); + ASSERT_TRUE(timer_.HasStarted()); + + ASSERT_TRUE(timer_.Pause()); + ASSERT_TRUE(timer_.HasStarted()); + ASSERT_EQ(timer_.elapsed_time_.InMilliseconds(), kDelta1MSec); + base::TimeDelta elapsed_time; + ASSERT_TRUE(timer_.GetElapsedTime(&elapsed_time)); + ASSERT_EQ(timer_.elapsed_time_.InMilliseconds(), + elapsed_time.InMilliseconds()); + + ASSERT_TRUE(timer_.Stop()); + ASSERT_EQ(timer_.elapsed_time_.InMilliseconds(), kDelta1MSec); + ASSERT_FALSE(timer_.HasStarted()); + ASSERT_TRUE(timer_.GetElapsedTime(&elapsed_time)); + ASSERT_EQ(timer_.elapsed_time_.InMilliseconds(), + elapsed_time.InMilliseconds()); +} + +TEST_F(TimerTest, StartPauseResumeStop) { + EXPECT_CALL(*clock_wrapper_mock_, GetCurrentTime()) + .WillOnce(Return(stime)) + .WillOnce(Return(etime)) + .WillOnce(Return(stime2)) + .WillOnce(Return(etime2)); + timer_.clock_wrapper_.reset(clock_wrapper_mock_.release()); + ASSERT_TRUE(timer_.Start()); + ASSERT_TRUE(timer_.start_time_ == stime); + ASSERT_TRUE(timer_.HasStarted()); + + ASSERT_TRUE(timer_.Pause()); + ASSERT_TRUE(timer_.HasStarted()); + ASSERT_EQ(timer_.elapsed_time_.InMilliseconds(), kDelta1MSec); + base::TimeDelta elapsed_time; + ASSERT_TRUE(timer_.GetElapsedTime(&elapsed_time)); + ASSERT_EQ(timer_.elapsed_time_.InMilliseconds(), + elapsed_time.InMilliseconds()); + + ASSERT_TRUE(timer_.Resume()); + ASSERT_TRUE(timer_.HasStarted()); + + ASSERT_TRUE(timer_.Stop()); + ASSERT_EQ(timer_.elapsed_time_.InMilliseconds(), kDelta1MSec + kDelta2MSec); + ASSERT_FALSE(timer_.HasStarted()); + ASSERT_TRUE(timer_.GetElapsedTime(&elapsed_time)); + ASSERT_EQ(timer_.elapsed_time_.InMilliseconds(), + elapsed_time.InMilliseconds()); +} + +TEST_F(TimerTest, PauseStop) { + EXPECT_CALL(*clock_wrapper_mock_, GetCurrentTime()) + .WillOnce(Return(stime)); + timer_.clock_wrapper_.reset(clock_wrapper_mock_.release()); + ASSERT_TRUE(timer_.Pause()); + ASSERT_TRUE(timer_.start_time_ == stime); + ASSERT_TRUE(timer_.HasStarted()); + ASSERT_EQ(timer_.elapsed_time_.InMilliseconds(), 0); + + ASSERT_TRUE(timer_.Stop()); + ASSERT_EQ(timer_.elapsed_time_.InMilliseconds(), 0); + ASSERT_FALSE(timer_.HasStarted()); + base::TimeDelta elapsed_time; + ASSERT_TRUE(timer_.GetElapsedTime(&elapsed_time)); + ASSERT_EQ(timer_.elapsed_time_.InMilliseconds(), + elapsed_time.InMilliseconds()); +} + +TEST_F(TimerTest, PauseResumeStop) { + EXPECT_CALL(*clock_wrapper_mock_, GetCurrentTime()) + .WillOnce(Return(stime)) + .WillOnce(Return(stime2)) + .WillOnce(Return(etime2)); + timer_.clock_wrapper_.reset(clock_wrapper_mock_.release()); + ASSERT_TRUE(timer_.Pause()); + ASSERT_TRUE(timer_.start_time_ == stime); + ASSERT_TRUE(timer_.HasStarted()); + + ASSERT_TRUE(timer_.Resume()); + ASSERT_TRUE(timer_.HasStarted()); + + ASSERT_TRUE(timer_.Stop()); + ASSERT_EQ(timer_.elapsed_time_.InMilliseconds(), kDelta2MSec); + ASSERT_FALSE(timer_.HasStarted()); + base::TimeDelta elapsed_time; + ASSERT_TRUE(timer_.GetElapsedTime(&elapsed_time)); + ASSERT_EQ(timer_.elapsed_time_.InMilliseconds(), + elapsed_time.InMilliseconds()); +} + +TEST_F(TimerTest, StartPauseResumePauseStop) { + EXPECT_CALL(*clock_wrapper_mock_, GetCurrentTime()) + .WillOnce(Return(stime)) + .WillOnce(Return(etime)) + .WillOnce(Return(stime2)) + .WillOnce(Return(stime3)) + .WillOnce(Return(etime3)); + timer_.clock_wrapper_.reset(clock_wrapper_mock_.release()); + ASSERT_TRUE(timer_.Start()); + ASSERT_TRUE(timer_.start_time_ == stime); + ASSERT_TRUE(timer_.HasStarted()); + + ASSERT_TRUE(timer_.Pause()); + ASSERT_TRUE(timer_.HasStarted()); + ASSERT_EQ(timer_.elapsed_time_.InMilliseconds(), kDelta1MSec); + base::TimeDelta elapsed_time; + ASSERT_TRUE(timer_.GetElapsedTime(&elapsed_time)); + ASSERT_EQ(timer_.elapsed_time_.InMilliseconds(), + elapsed_time.InMilliseconds()); + + ASSERT_TRUE(timer_.Resume()); + ASSERT_TRUE(timer_.HasStarted()); + // Make sure GetElapsedTime works while we're running. + ASSERT_TRUE(timer_.GetElapsedTime(&elapsed_time)); + ASSERT_EQ(kDelta1MSec + kStime3MSec - kStime2MSec, + elapsed_time.InMilliseconds()); + + ASSERT_TRUE(timer_.Pause()); + ASSERT_TRUE(timer_.HasStarted()); + ASSERT_EQ(timer_.elapsed_time_.InMilliseconds(), + kDelta1MSec + kEtime3MSec - kStime2MSec); + ASSERT_TRUE(timer_.GetElapsedTime(&elapsed_time)); + ASSERT_EQ(timer_.elapsed_time_.InMilliseconds(), + elapsed_time.InMilliseconds()); + + ASSERT_TRUE(timer_.Stop()); + ASSERT_EQ(timer_.elapsed_time_.InMilliseconds(), + kDelta1MSec + kEtime3MSec - kStime2MSec); + ASSERT_FALSE(timer_.HasStarted()); + ASSERT_TRUE(timer_.GetElapsedTime(&elapsed_time)); + ASSERT_EQ(timer_.elapsed_time_.InMilliseconds(), + elapsed_time.InMilliseconds()); +} + +TEST_F(TimerTest, StartPauseResumePauseResumeStop) { + EXPECT_CALL(*clock_wrapper_mock_, GetCurrentTime()) + .WillOnce(Return(stime)) + .WillOnce(Return(etime)) + .WillOnce(Return(stime2)) + .WillOnce(Return(etime2)) + .WillOnce(Return(stime3)) + .WillOnce(Return(etime3)); + timer_.clock_wrapper_.reset(clock_wrapper_mock_.release()); + ASSERT_TRUE(timer_.Start()); + ASSERT_TRUE(timer_.start_time_ == stime); + ASSERT_TRUE(timer_.HasStarted()); + + ASSERT_TRUE(timer_.Pause()); + ASSERT_TRUE(timer_.HasStarted()); + ASSERT_EQ(timer_.elapsed_time_.InMilliseconds(), kDelta1MSec); + base::TimeDelta elapsed_time; + ASSERT_TRUE(timer_.GetElapsedTime(&elapsed_time)); + ASSERT_EQ(timer_.elapsed_time_.InMilliseconds(), + elapsed_time.InMilliseconds()); + + ASSERT_TRUE(timer_.Resume()); + ASSERT_TRUE(timer_.HasStarted()); + + ASSERT_TRUE(timer_.Pause()); + ASSERT_TRUE(timer_.HasStarted()); + ASSERT_EQ(timer_.elapsed_time_.InMilliseconds(), kDelta1MSec + kDelta2MSec); + ASSERT_TRUE(timer_.GetElapsedTime(&elapsed_time)); + ASSERT_EQ(timer_.elapsed_time_.InMilliseconds(), + elapsed_time.InMilliseconds()); + + ASSERT_TRUE(timer_.Resume()); + ASSERT_TRUE(timer_.HasStarted()); + + ASSERT_TRUE(timer_.Stop()); + ASSERT_EQ(timer_.elapsed_time_.InMilliseconds(), + kDelta1MSec + kDelta2MSec + kDelta3MSec); + ASSERT_FALSE(timer_.HasStarted()); + ASSERT_TRUE(timer_.GetElapsedTime(&elapsed_time)); + ASSERT_EQ(timer_.elapsed_time_.InMilliseconds(), + elapsed_time.InMilliseconds()); +} + +static const char kMetricName[] = "test-timer"; +static const int kMinSample = 0; +static const int kMaxSample = 120 * 1E6; +static const int kNumBuckets = 50; + +class TimerReporterTest : public testing::Test { + public: + TimerReporterTest() : timer_reporter_(kMetricName, kMinSample, kMaxSample, + kNumBuckets), + clock_wrapper_mock_(new ClockWrapperMock()) {} + + protected: + virtual void SetUp() { + timer_reporter_.set_metrics_lib(&lib_); + EXPECT_EQ(timer_reporter_.histogram_name_, kMetricName); + EXPECT_EQ(timer_reporter_.min_, kMinSample); + EXPECT_EQ(timer_reporter_.max_, kMaxSample); + EXPECT_EQ(timer_reporter_.num_buckets_, kNumBuckets); + stime += base::TimeDelta::FromMilliseconds(kStime1MSec); + etime += base::TimeDelta::FromMilliseconds(kEtime1MSec); + } + + virtual void TearDown() { + timer_reporter_.set_metrics_lib(nullptr); + } + + TimerReporter timer_reporter_; + MetricsLibraryMock lib_; + scoped_ptr<ClockWrapperMock> clock_wrapper_mock_; + base::TimeTicks stime, etime; +}; + +TEST_F(TimerReporterTest, StartStopReport) { + EXPECT_CALL(*clock_wrapper_mock_, GetCurrentTime()) + .WillOnce(Return(stime)) + .WillOnce(Return(etime)); + timer_reporter_.clock_wrapper_.reset(clock_wrapper_mock_.release()); + EXPECT_CALL(lib_, SendToUMA(kMetricName, kDelta1MSec, kMinSample, kMaxSample, + kNumBuckets)).WillOnce(Return(true)); + ASSERT_TRUE(timer_reporter_.Start()); + ASSERT_TRUE(timer_reporter_.Stop()); + ASSERT_TRUE(timer_reporter_.ReportMilliseconds()); +} + +TEST_F(TimerReporterTest, InvalidReport) { + ASSERT_FALSE(timer_reporter_.ReportMilliseconds()); +} + +} // namespace chromeos_metrics + +int main(int argc, char **argv) { + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/metrics/uploader/metrics_hashes.cc b/metrics/uploader/metrics_hashes.cc new file mode 100644 index 000000000..87405a377 --- /dev/null +++ b/metrics/uploader/metrics_hashes.cc @@ -0,0 +1,39 @@ +// Copyright 2014 The Chromium OS 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 "metrics/uploader/metrics_hashes.h" + +#include "base/logging.h" +#include "base/md5.h" +#include "base/sys_byteorder.h" + +namespace metrics { + +namespace { + +// Converts the 8-byte prefix of an MD5 hash into a uint64 value. +inline uint64_t HashToUInt64(const std::string& hash) { + uint64_t value; + DCHECK_GE(hash.size(), sizeof(value)); + memcpy(&value, hash.data(), sizeof(value)); + return base::HostToNet64(value); +} + +} // namespace + +uint64_t HashMetricName(const std::string& name) { + // Create an MD5 hash of the given |name|, represented as a byte buffer + // encoded as an std::string. + base::MD5Context context; + base::MD5Init(&context); + base::MD5Update(&context, name); + + base::MD5Digest digest; + base::MD5Final(&digest, &context); + + std::string hash_str(reinterpret_cast<char*>(digest.a), arraysize(digest.a)); + return HashToUInt64(hash_str); +} + +} // namespace metrics diff --git a/metrics/uploader/metrics_hashes.h b/metrics/uploader/metrics_hashes.h new file mode 100644 index 000000000..8679077e1 --- /dev/null +++ b/metrics/uploader/metrics_hashes.h @@ -0,0 +1,18 @@ +// Copyright 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef METRICS_UPLOADER_METRICS_HASHES_H_ +#define METRICS_UPLOADER_METRICS_HASHES_H_ + +#include <string> + +namespace metrics { + +// Computes a uint64 hash of a given string based on its MD5 hash. Suitable for +// metric names. +uint64_t HashMetricName(const std::string& name); + +} // namespace metrics + +#endif // METRICS_UPLOADER_METRICS_HASHES_H_ diff --git a/metrics/uploader/metrics_hashes_unittest.cc b/metrics/uploader/metrics_hashes_unittest.cc new file mode 100644 index 000000000..f7e390f64 --- /dev/null +++ b/metrics/uploader/metrics_hashes_unittest.cc @@ -0,0 +1,32 @@ +// Copyright 2014 The Chromium OS 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 "metrics/uploader/metrics_hashes.h" + +#include <base/format_macros.h> +#include <base/macros.h> +#include <base/strings/stringprintf.h> +#include <gtest/gtest.h> + +namespace metrics { + +// Make sure our ID hashes are the same as what we see on the server side. +TEST(MetricsUtilTest, HashMetricName) { + static const struct { + std::string input; + std::string output; + } cases[] = { + {"Back", "0x0557fa923dcee4d0"}, + {"Forward", "0x67d2f6740a8eaebf"}, + {"NewTab", "0x290eb683f96572f1"}, + }; + + for (size_t i = 0; i < arraysize(cases); ++i) { + uint64_t hash = HashMetricName(cases[i].input); + std::string hash_hex = base::StringPrintf("0x%016" PRIx64, hash); + EXPECT_EQ(cases[i].output, hash_hex); + } +} + +} // namespace metrics diff --git a/metrics/uploader/metrics_log.cc b/metrics/uploader/metrics_log.cc new file mode 100644 index 000000000..4d493b8aa --- /dev/null +++ b/metrics/uploader/metrics_log.cc @@ -0,0 +1,41 @@ +// Copyright 2014 The Chromium OS 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 "uploader/metrics_log.h" + +#include <string> + +#include "metrics/uploader/proto/system_profile.pb.h" +#include "uploader/system_profile_setter.h" + +// We use default values for the MetricsLogBase constructor as the setter will +// override them. +MetricsLog::MetricsLog() + : MetricsLogBase("", 0, metrics::MetricsLogBase::ONGOING_LOG, "") { +} + +void MetricsLog::IncrementUserCrashCount() { + metrics::SystemProfileProto::Stability* stability( + uma_proto()->mutable_system_profile()->mutable_stability()); + int current = stability->other_user_crash_count(); + stability->set_other_user_crash_count(current + 1); +} + +void MetricsLog::IncrementKernelCrashCount() { + metrics::SystemProfileProto::Stability* stability( + uma_proto()->mutable_system_profile()->mutable_stability()); + int current = stability->kernel_crash_count(); + stability->set_kernel_crash_count(current + 1); +} + +void MetricsLog::IncrementUncleanShutdownCount() { + metrics::SystemProfileProto::Stability* stability( + uma_proto()->mutable_system_profile()->mutable_stability()); + int current = stability->unclean_system_shutdown_count(); + stability->set_unclean_system_shutdown_count(current + 1); +} + +void MetricsLog::PopulateSystemProfile(SystemProfileSetter* profile_setter) { + profile_setter->Populate(uma_proto()); +} diff --git a/metrics/uploader/metrics_log.h b/metrics/uploader/metrics_log.h new file mode 100644 index 000000000..579632578 --- /dev/null +++ b/metrics/uploader/metrics_log.h @@ -0,0 +1,42 @@ +// Copyright 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef METRICS_UPLOADER_METRICS_LOG_H_ +#define METRICS_UPLOADER_METRICS_LOG_H_ + +#include <string> + +#include <base/macros.h> + +#include "metrics/uploader/metrics_log_base.h" + +// This file defines a set of user experience metrics data recorded by +// the MetricsService. This is the unit of data that is sent to the server. +class SystemProfileSetter; + +// This class provides base functionality for logging metrics data. +class MetricsLog : public metrics::MetricsLogBase { + public: + // The constructor doesn't set any metadata. The metadata is only set by a + // SystemProfileSetter. + MetricsLog(); + + void IncrementUserCrashCount(); + void IncrementKernelCrashCount(); + void IncrementUncleanShutdownCount(); + + // Populate the system profile with system information using setter. + void PopulateSystemProfile(SystemProfileSetter* setter); + + private: + FRIEND_TEST(UploadServiceTest, LogContainsAggregatedValues); + FRIEND_TEST(UploadServiceTest, LogKernelCrash); + FRIEND_TEST(UploadServiceTest, LogUncleanShutdown); + FRIEND_TEST(UploadServiceTest, LogUserCrash); + FRIEND_TEST(UploadServiceTest, UnknownCrashIgnored); + + DISALLOW_COPY_AND_ASSIGN(MetricsLog); +}; + +#endif // METRICS_UPLOADER_METRICS_LOG_H_ diff --git a/metrics/uploader/metrics_log_base.cc b/metrics/uploader/metrics_log_base.cc new file mode 100644 index 000000000..7fe1ae1a6 --- /dev/null +++ b/metrics/uploader/metrics_log_base.cc @@ -0,0 +1,142 @@ +// Copyright 2014 The Chromium OS 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 "metrics/uploader/metrics_log_base.h" + +#include "base/metrics/histogram_base.h" +#include "base/metrics/histogram_samples.h" +#include "metrics/uploader/metrics_hashes.h" +#include "metrics/uploader/proto/histogram_event.pb.h" +#include "metrics/uploader/proto/system_profile.pb.h" +#include "metrics/uploader/proto/user_action_event.pb.h" + +using base::Histogram; +using base::HistogramBase; +using base::HistogramSamples; +using base::SampleCountIterator; +using base::Time; +using base::TimeDelta; +using metrics::HistogramEventProto; +using metrics::SystemProfileProto; +using metrics::UserActionEventProto; + +namespace metrics { +namespace { + +// Any id less than 16 bytes is considered to be a testing id. +bool IsTestingID(const std::string& id) { + return id.size() < 16; +} + +} // namespace + +MetricsLogBase::MetricsLogBase(const std::string& client_id, + int session_id, + LogType log_type, + const std::string& version_string) + : num_events_(0), + locked_(false), + log_type_(log_type) { + DCHECK_NE(NO_LOG, log_type); + if (IsTestingID(client_id)) + uma_proto_.set_client_id(0); + else + uma_proto_.set_client_id(Hash(client_id)); + + uma_proto_.set_session_id(session_id); + uma_proto_.mutable_system_profile()->set_build_timestamp(GetBuildTime()); + uma_proto_.mutable_system_profile()->set_app_version(version_string); +} + +MetricsLogBase::~MetricsLogBase() {} + +// static +uint64_t MetricsLogBase::Hash(const std::string& value) { + uint64_t hash = metrics::HashMetricName(value); + + // The following log is VERY helpful when folks add some named histogram into + // the code, but forgot to update the descriptive list of histograms. When + // that happens, all we get to see (server side) is a hash of the histogram + // name. We can then use this logging to find out what histogram name was + // being hashed to a given MD5 value by just running the version of Chromium + // in question with --enable-logging. + DVLOG(1) << "Metrics: Hash numeric [" << value << "]=[" << hash << "]"; + + return hash; +} + +// static +int64_t MetricsLogBase::GetBuildTime() { + static int64_t integral_build_time = 0; + if (!integral_build_time) { + Time time; + const char* kDateTime = __DATE__ " " __TIME__ " GMT"; + bool result = Time::FromString(kDateTime, &time); + DCHECK(result); + integral_build_time = static_cast<int64_t>(time.ToTimeT()); + } + return integral_build_time; +} + +// static +int64_t MetricsLogBase::GetCurrentTime() { + return (base::TimeTicks::Now() - base::TimeTicks()).InSeconds(); +} + +void MetricsLogBase::CloseLog() { + DCHECK(!locked_); + locked_ = true; +} + +void MetricsLogBase::GetEncodedLog(std::string* encoded_log) { + DCHECK(locked_); + uma_proto_.SerializeToString(encoded_log); +} + +void MetricsLogBase::RecordUserAction(const std::string& key) { + DCHECK(!locked_); + + UserActionEventProto* user_action = uma_proto_.add_user_action_event(); + user_action->set_name_hash(Hash(key)); + user_action->set_time(GetCurrentTime()); + + ++num_events_; +} + +void MetricsLogBase::RecordHistogramDelta(const std::string& histogram_name, + const HistogramSamples& snapshot) { + DCHECK(!locked_); + DCHECK_NE(0, snapshot.TotalCount()); + + // We will ignore the MAX_INT/infinite value in the last element of range[]. + + HistogramEventProto* histogram_proto = uma_proto_.add_histogram_event(); + histogram_proto->set_name_hash(Hash(histogram_name)); + histogram_proto->set_sum(snapshot.sum()); + + for (scoped_ptr<SampleCountIterator> it = snapshot.Iterator(); !it->Done(); + it->Next()) { + HistogramBase::Sample min; + HistogramBase::Sample max; + HistogramBase::Count count; + it->Get(&min, &max, &count); + HistogramEventProto::Bucket* bucket = histogram_proto->add_bucket(); + bucket->set_min(min); + bucket->set_max(max); + bucket->set_count(count); + } + + // Omit fields to save space (see rules in histogram_event.proto comments). + for (int i = 0; i < histogram_proto->bucket_size(); ++i) { + HistogramEventProto::Bucket* bucket = histogram_proto->mutable_bucket(i); + if (i + 1 < histogram_proto->bucket_size() && + bucket->max() == histogram_proto->bucket(i + 1).min()) { + bucket->clear_max(); + } else if (bucket->max() == bucket->min() + 1) { + bucket->clear_min(); + } + } +} + +} // namespace metrics diff --git a/metrics/uploader/metrics_log_base.h b/metrics/uploader/metrics_log_base.h new file mode 100644 index 000000000..e871c0fac --- /dev/null +++ b/metrics/uploader/metrics_log_base.h @@ -0,0 +1,110 @@ +// Copyright 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// This file defines a set of user experience metrics data recorded by +// the MetricsService. This is the unit of data that is sent to the server. + +#ifndef METRICS_UPLOADER_METRICS_LOG_BASE_H_ +#define METRICS_UPLOADER_METRICS_LOG_BASE_H_ + +#include <string> + +#include "base/macros.h" +#include "base/metrics/histogram.h" +#include "base/time/time.h" +#include "metrics/uploader/proto/chrome_user_metrics_extension.pb.h" + +namespace base { +class HistogramSamples; +} // namespace base + +namespace metrics { + +// This class provides base functionality for logging metrics data. +class MetricsLogBase { + public: + // TODO(asvitkine): Remove the NO_LOG value. + enum LogType { + INITIAL_STABILITY_LOG, // The initial log containing stability stats. + ONGOING_LOG, // Subsequent logs in a session. + NO_LOG, // Placeholder value for when there is no log. + }; + + // Creates a new metrics log of the specified type. + // client_id is the identifier for this profile on this installation + // session_id is an integer that's incremented on each application launch + MetricsLogBase(const std::string& client_id, + int session_id, + LogType log_type, + const std::string& version_string); + virtual ~MetricsLogBase(); + + // Computes the MD5 hash of the given string, and returns the first 8 bytes of + // the hash. + static uint64_t Hash(const std::string& value); + + // Get the GMT buildtime for the current binary, expressed in seconds since + // January 1, 1970 GMT. + // The value is used to identify when a new build is run, so that previous + // reliability stats, from other builds, can be abandoned. + static int64_t GetBuildTime(); + + // Convenience function to return the current time at a resolution in seconds. + // This wraps base::TimeTicks, and hence provides an abstract time that is + // always incrementing for use in measuring time durations. + static int64_t GetCurrentTime(); + + // Records a user-initiated action. + void RecordUserAction(const std::string& key); + + // Record any changes in a given histogram for transmission. + void RecordHistogramDelta(const std::string& histogram_name, + const base::HistogramSamples& snapshot); + + // Stop writing to this record and generate the encoded representation. + // None of the Record* methods can be called after this is called. + void CloseLog(); + + // Fills |encoded_log| with the serialized protobuf representation of the + // record. Must only be called after CloseLog() has been called. + void GetEncodedLog(std::string* encoded_log); + + int num_events() { return num_events_; } + + void set_hardware_class(const std::string& hardware_class) { + uma_proto_.mutable_system_profile()->mutable_hardware()->set_hardware_class( + hardware_class); + } + + LogType log_type() const { return log_type_; } + + protected: + bool locked() const { return locked_; } + + metrics::ChromeUserMetricsExtension* uma_proto() { return &uma_proto_; } + const metrics::ChromeUserMetricsExtension* uma_proto() const { + return &uma_proto_; + } + + // TODO(isherman): Remove this once the XML pipeline is outta here. + int num_events_; // the number of events recorded in this log + + private: + // locked_ is true when record has been packed up for sending, and should + // no longer be written to. It is only used for sanity checking and is + // not a real lock. + bool locked_; + + // The type of the log, i.e. initial or ongoing. + const LogType log_type_; + + // Stores the protocol buffer representation for this log. + metrics::ChromeUserMetricsExtension uma_proto_; + + DISALLOW_COPY_AND_ASSIGN(MetricsLogBase); +}; + +} // namespace metrics + +#endif // METRICS_UPLOADER_METRICS_LOG_BASE_H_ diff --git a/metrics/uploader/metrics_log_base_unittest.cc b/metrics/uploader/metrics_log_base_unittest.cc new file mode 100644 index 000000000..5da428adc --- /dev/null +++ b/metrics/uploader/metrics_log_base_unittest.cc @@ -0,0 +1,125 @@ +// Copyright 2014 The Chromium OS 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 "metrics/uploader/metrics_log_base.h" + +#include <string> + +#include <base/metrics/bucket_ranges.h> +#include <base/metrics/sample_vector.h> +#include <gtest/gtest.h> + +#include "metrics/uploader/proto/chrome_user_metrics_extension.pb.h" + +namespace metrics { + +namespace { + +class TestMetricsLogBase : public MetricsLogBase { + public: + TestMetricsLogBase() + : MetricsLogBase("client_id", 1, MetricsLogBase::ONGOING_LOG, "1.2.3.4") { + } + virtual ~TestMetricsLogBase() {} + + using MetricsLogBase::uma_proto; + + private: + DISALLOW_COPY_AND_ASSIGN(TestMetricsLogBase); +}; + +} // namespace + +TEST(MetricsLogBaseTest, LogType) { + MetricsLogBase log1("id", 0, MetricsLogBase::ONGOING_LOG, "1.2.3"); + EXPECT_EQ(MetricsLogBase::ONGOING_LOG, log1.log_type()); + + MetricsLogBase log2("id", 0, MetricsLogBase::INITIAL_STABILITY_LOG, "1.2.3"); + EXPECT_EQ(MetricsLogBase::INITIAL_STABILITY_LOG, log2.log_type()); +} + +TEST(MetricsLogBaseTest, EmptyRecord) { + MetricsLogBase log("totally bogus client ID", 137, + MetricsLogBase::ONGOING_LOG, "bogus version"); + log.set_hardware_class("sample-class"); + log.CloseLog(); + + std::string encoded; + log.GetEncodedLog(&encoded); + + // A couple of fields are hard to mock, so these will be copied over directly + // for the expected output. + metrics::ChromeUserMetricsExtension parsed; + ASSERT_TRUE(parsed.ParseFromString(encoded)); + + metrics::ChromeUserMetricsExtension expected; + expected.set_client_id(5217101509553811875); // Hashed bogus client ID + expected.set_session_id(137); + expected.mutable_system_profile()->set_build_timestamp( + parsed.system_profile().build_timestamp()); + expected.mutable_system_profile()->set_app_version("bogus version"); + expected.mutable_system_profile()->mutable_hardware()->set_hardware_class( + "sample-class"); + + EXPECT_EQ(expected.SerializeAsString(), encoded); +} + +TEST(MetricsLogBaseTest, HistogramBucketFields) { + // Create buckets: 1-5, 5-7, 7-8, 8-9, 9-10, 10-11, 11-12. + base::BucketRanges ranges(8); + ranges.set_range(0, 1); + ranges.set_range(1, 5); + ranges.set_range(2, 7); + ranges.set_range(3, 8); + ranges.set_range(4, 9); + ranges.set_range(5, 10); + ranges.set_range(6, 11); + ranges.set_range(7, 12); + + base::SampleVector samples(&ranges); + samples.Accumulate(3, 1); // Bucket 1-5. + samples.Accumulate(6, 1); // Bucket 5-7. + samples.Accumulate(8, 1); // Bucket 8-9. (7-8 skipped) + samples.Accumulate(10, 1); // Bucket 10-11. (9-10 skipped) + samples.Accumulate(11, 1); // Bucket 11-12. + + TestMetricsLogBase log; + log.RecordHistogramDelta("Test", samples); + + const metrics::ChromeUserMetricsExtension* uma_proto = log.uma_proto(); + const metrics::HistogramEventProto& histogram_proto = + uma_proto->histogram_event(uma_proto->histogram_event_size() - 1); + + // Buckets with samples: 1-5, 5-7, 8-9, 10-11, 11-12. + // Should become: 1-/, 5-7, /-9, 10-/, /-12. + ASSERT_EQ(5, histogram_proto.bucket_size()); + + // 1-5 becomes 1-/ (max is same as next min). + EXPECT_TRUE(histogram_proto.bucket(0).has_min()); + EXPECT_FALSE(histogram_proto.bucket(0).has_max()); + EXPECT_EQ(1, histogram_proto.bucket(0).min()); + + // 5-7 stays 5-7 (no optimization possible). + EXPECT_TRUE(histogram_proto.bucket(1).has_min()); + EXPECT_TRUE(histogram_proto.bucket(1).has_max()); + EXPECT_EQ(5, histogram_proto.bucket(1).min()); + EXPECT_EQ(7, histogram_proto.bucket(1).max()); + + // 8-9 becomes /-9 (min is same as max - 1). + EXPECT_FALSE(histogram_proto.bucket(2).has_min()); + EXPECT_TRUE(histogram_proto.bucket(2).has_max()); + EXPECT_EQ(9, histogram_proto.bucket(2).max()); + + // 10-11 becomes 10-/ (both optimizations apply, omit max is prioritized). + EXPECT_TRUE(histogram_proto.bucket(3).has_min()); + EXPECT_FALSE(histogram_proto.bucket(3).has_max()); + EXPECT_EQ(10, histogram_proto.bucket(3).min()); + + // 11-12 becomes /-12 (last record must keep max, min is same as max - 1). + EXPECT_FALSE(histogram_proto.bucket(4).has_min()); + EXPECT_TRUE(histogram_proto.bucket(4).has_max()); + EXPECT_EQ(12, histogram_proto.bucket(4).max()); +} + +} // namespace metrics diff --git a/metrics/uploader/mock/mock_system_profile_setter.h b/metrics/uploader/mock/mock_system_profile_setter.h new file mode 100644 index 000000000..c6e8f0d1e --- /dev/null +++ b/metrics/uploader/mock/mock_system_profile_setter.h @@ -0,0 +1,20 @@ +// Copyright 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef METRICS_UPLOADER_MOCK_MOCK_SYSTEM_PROFILE_SETTER_H_ +#define METRICS_UPLOADER_MOCK_MOCK_SYSTEM_PROFILE_SETTER_H_ + +#include "uploader/system_profile_setter.h" + +namespace metrics { +class ChromeUserMetricsExtension; +} + +// Mock profile setter used for testing. +class MockSystemProfileSetter : public SystemProfileSetter { + public: + void Populate(metrics::ChromeUserMetricsExtension* profile_proto) override {} +}; + +#endif // METRICS_UPLOADER_MOCK_MOCK_SYSTEM_PROFILE_SETTER_H_ diff --git a/metrics/uploader/mock/sender_mock.cc b/metrics/uploader/mock/sender_mock.cc new file mode 100644 index 000000000..064ec6d01 --- /dev/null +++ b/metrics/uploader/mock/sender_mock.cc @@ -0,0 +1,24 @@ +// Copyright 2014 The Chromium OS 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 "uploader/mock/sender_mock.h" + +SenderMock::SenderMock() { + Reset(); +} + +bool SenderMock::Send(const std::string& content, const std::string& hash) { + send_call_count_ += 1; + last_message_ = content; + is_good_proto_ = last_message_proto_.ParseFromString(content); + return should_succeed_; +} + +void SenderMock::Reset() { + send_call_count_ = 0; + last_message_ = ""; + should_succeed_ = true; + last_message_proto_.Clear(); + is_good_proto_ = false; +} diff --git a/metrics/uploader/mock/sender_mock.h b/metrics/uploader/mock/sender_mock.h new file mode 100644 index 000000000..159b64527 --- /dev/null +++ b/metrics/uploader/mock/sender_mock.h @@ -0,0 +1,48 @@ +// Copyright 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef METRICS_UPLOADER_MOCK_SENDER_MOCK_H_ +#define METRICS_UPLOADER_MOCK_SENDER_MOCK_H_ + +#include <string> + +#include "base/compiler_specific.h" +#include "metrics/uploader/proto/chrome_user_metrics_extension.pb.h" +#include "uploader/sender.h" + +class SenderMock : public Sender { + public: + SenderMock(); + + bool Send(const std::string& content, const std::string& hash) override; + void Reset(); + + bool is_good_proto() { return is_good_proto_; } + int send_call_count() { return send_call_count_; } + const std::string last_message() { return last_message_; } + metrics::ChromeUserMetricsExtension last_message_proto() { + return last_message_proto_; + } + void set_should_succeed(bool succeed) { should_succeed_ = succeed; } + + private: + // Is set to true if the proto was parsed successfully. + bool is_good_proto_; + + // If set to true, the Send method will return true to simulate a successful + // send. + bool should_succeed_; + + // Count of how many times Send was called since the last reset. + int send_call_count_; + + // Last message received by Send. + std::string last_message_; + + // If is_good_proto is true, last_message_proto is the deserialized + // representation of last_message. + metrics::ChromeUserMetricsExtension last_message_proto_; +}; + +#endif // METRICS_UPLOADER_MOCK_SENDER_MOCK_H_ diff --git a/metrics/uploader/proto/README b/metrics/uploader/proto/README new file mode 100644 index 000000000..9bd3249b0 --- /dev/null +++ b/metrics/uploader/proto/README @@ -0,0 +1,25 @@ +Copyright 2015 The Chromium OS Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. + + +This directory contains the protocol buffers used by the standalone metrics +uploader. Those protobuffers are copied from the chromium protobuffers from +https://chromium.googlesource.com/chromium/src/+/master/components/metrics/proto/ +at 3bfe5f2b4c03d2cac718d137ed14cd2c6354bfed. + +Any change to this protobuf must first be made to the backend's protobuf and be +compatible with the chromium protobuffers. + + +Q: Why fork the chromium protobuffers ? +A: The standalone metrics uploader needs chromium os fields that are not defined +by the chromium protobufs. Instead of pushing chromium os specific changes to +chromium, we can add them only to chromium os (and to the backend of course). + + +Q: What's the difference between those protobuffers and chromium's protobuffers? +A: When the protobuffers were copied, some chromium specific protobuffers were +not imported: +* omnibox related protobuffers. +* performance profiling protobuffers (not used in chromium os). diff --git a/metrics/uploader/proto/chrome_user_metrics_extension.proto b/metrics/uploader/proto/chrome_user_metrics_extension.proto new file mode 100644 index 000000000..f712fc9f7 --- /dev/null +++ b/metrics/uploader/proto/chrome_user_metrics_extension.proto @@ -0,0 +1,60 @@ +// Copyright 2014 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. +// +// Protocol buffer for Chrome UMA (User Metrics Analysis). +// +// Note: this protobuf must be compatible with the one in chromium. + +syntax = "proto2"; + +option optimize_for = LITE_RUNTIME; +option java_outer_classname = "ChromeUserMetricsExtensionProtos"; +option java_package = "org.chromium.components.metrics"; + +package metrics; + +import "histogram_event.proto"; +import "system_profile.proto"; +import "user_action_event.proto"; + +// Next tag: 13 +message ChromeUserMetricsExtension { + // The product (i.e. end user application) for a given UMA log. + enum Product { + // Google Chrome product family. + CHROME = 0; + } + // The product corresponding to this log. The field type is int32 instead of + // Product so that downstream users of the Chromium metrics component can + // introduce products without needing to make changes to the Chromium code + // (though they still need to add the new product to the server-side enum). + // Note: The default value is Chrome, so Chrome products will not transmit + // this field. + optional int32 product = 10 [default = 0]; + + // The id of the client install that generated these events. + // + // For Chrome clients, this id is unique to a top-level (one level above the + // "Default" directory) Chrome user data directory [1], and so is shared among + // all Chrome user profiles contained in this user data directory. + // An id of 0 is reserved for test data (monitoring and internal testing) and + // should normally be ignored in analysis of the data. + // [1] http://www.chromium.org/user-experience/user-data-directory + optional fixed64 client_id = 1; + + // The session id for this user. + // Values such as tab ids are only meaningful within a particular session. + // The client keeps track of the session id and sends it with each event. + // The session id is simply an integer that is incremented each time the user + // relaunches Chrome. + optional int32 session_id = 2; + + // Information about the user's browser and system configuration. + optional SystemProfileProto system_profile = 3; + + // This message will log one or more of the following event types: + repeated UserActionEventProto user_action_event = 4; + repeated HistogramEventProto histogram_event = 6; + +} diff --git a/metrics/uploader/proto/histogram_event.proto b/metrics/uploader/proto/histogram_event.proto new file mode 100644 index 000000000..4b68094ff --- /dev/null +++ b/metrics/uploader/proto/histogram_event.proto @@ -0,0 +1,47 @@ +// Copyright 2014 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. +// +// Histogram-collected metrics. + +syntax = "proto2"; + +option optimize_for = LITE_RUNTIME; +option java_outer_classname = "HistogramEventProtos"; +option java_package = "org.chromium.components.metrics"; + +package metrics; + +// Next tag: 4 +message HistogramEventProto { + // The name of the histogram, hashed. + optional fixed64 name_hash = 1; + + // The sum of all the sample values. + // Together with the total count of the sample values, this allows us to + // compute the average value. The count of all sample values is just the sum + // of the counts of all the buckets. + optional int64 sum = 2; + + // The per-bucket data. + message Bucket { + // Each bucket's range is bounded by min <= x < max. + // It is valid to omit one of these two fields in a bucket, but not both. + // If the min field is omitted, its value is assumed to be equal to max - 1. + // If the max field is omitted, its value is assumed to be equal to the next + // bucket's min value (possibly computed per above). The last bucket in a + // histogram should always include the max field. + optional int64 min = 1; + optional int64 max = 2; + + // The bucket's index in the list of buckets, sorted in ascending order. + // This field was intended to provide extra redundancy to detect corrupted + // records, but was never used. As of M31, it is no longer sent by Chrome + // clients to reduce the UMA upload size. + optional int32 bucket_index = 3 [deprecated = true]; + + // The number of entries in this bucket. + optional int64 count = 4; + } + repeated Bucket bucket = 3; +} diff --git a/metrics/uploader/proto/system_profile.proto b/metrics/uploader/proto/system_profile.proto new file mode 100644 index 000000000..d33ff6097 --- /dev/null +++ b/metrics/uploader/proto/system_profile.proto @@ -0,0 +1,747 @@ +// Copyright 2014 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. +// +// Stores information about the user's brower and system configuration. +// The system configuration fields are recorded once per client session. + +syntax = "proto2"; + +option optimize_for = LITE_RUNTIME; +option java_outer_classname = "SystemProfileProtos"; +option java_package = "org.chromium.components.metrics"; + +package metrics; + +// Next tag: 21 +message SystemProfileProto { + // The time when the client was compiled/linked, in seconds since the epoch. + optional int64 build_timestamp = 1; + + // A version number string for the application. + // Most commonly this is the browser version number found in a user agent + // string, and is typically a 4-tuple of numbers separated by periods. In + // cases where the user agent version might be ambiguous (example: Linux 64- + // bit build, rather than 32-bit build, or a Windows version used in some + // special context, such as ChromeFrame running in IE), then this may include + // some additional postfix to provide clarification not available in the UA + // string. + // + // An example of a browser version 4-tuple is "5.0.322.0". Currently used + // postfixes are: + // + // "-64": a 64-bit build + // "-F": Chrome is running under control of ChromeFrame + // "-devel": this is not an official build of Chrome + // + // A full version number string could look similar to: + // "5.0.322.0-F-devel". + // + // This value, when available, is more trustworthy than the UA string + // associated with the request; and including the postfix, may be more + // specific. + optional string app_version = 2; + + // The brand code or distribution tag assigned to a partner, if available. + // Brand codes are only available on Windows. Not every Windows install + // though will have a brand code. + optional string brand_code = 12; + + // The possible channels for an installation, from least to most stable. + enum Channel { + CHANNEL_UNKNOWN = 0; // Unknown channel -- perhaps an unofficial build? + CHANNEL_CANARY = 1; + CHANNEL_DEV = 2; + CHANNEL_BETA = 3; + CHANNEL_STABLE = 4; + } + optional Channel channel = 10; + + // True if Chrome build is ASan-instrumented. + optional bool is_asan_build = 20 [default = false]; + + // The date the user enabled UMA, in seconds since the epoch. + // If the user has toggled the UMA enabled state multiple times, this will + // be the most recent date on which UMA was enabled. + // For privacy, this is rounded to the nearest hour. + optional int64 uma_enabled_date = 3; + + // The time when the client was installed, in seconds since the epoch. + // For privacy, this is rounded to the nearest hour. + optional int64 install_date = 16; + + // The user's selected application locale, i.e. the user interface language. + // The locale includes a language code and, possibly, also a country code, + // e.g. "en-US". + optional string application_locale = 4; + + message BrilloDeviceData { + optional string build_target_id = 1; + } + optional BrilloDeviceData brillo = 21; + + // Information on the user's operating system. + message OS { + // The user's operating system. This should be one of: + // - Android + // - Windows NT + // - Linux (includes ChromeOS) + // - iPhone OS + // - Mac OS X + optional string name = 1; + + // The version of the OS. The meaning of this field is OS-dependent. + optional string version = 2; + + // The fingerprint of the build. This field is used only on Android. + optional string fingerprint = 3; + + // Whether the version of iOS appears to be "jailbroken". This field is + // used only on iOS. Chrome for iOS detects whether device contains a + // DynamicLibraries/ directory. It's a necessary but insufficient indicator + // of whether the operating system has been jailbroken. + optional bool is_jailbroken = 4; + } + optional OS os = 5; + + // Next tag for Hardware: 18 + // Information on the user's hardware. + message Hardware { + // The CPU architecture (x86, PowerPC, x86_64, ...) + optional string cpu_architecture = 1; + + // The amount of RAM present on the system, in megabytes. + optional int64 system_ram_mb = 2; + + // The base memory address that chrome.dll was loaded at. + // (Logged only on Windows.) + optional int64 dll_base = 3; + + // The Chrome OS device hardware class ID is a unique string associated with + // each Chrome OS device product revision generally assigned at hardware + // qualification time. The hardware class effectively identifies the + // configured system components such as CPU, WiFi adapter, etc. + // + // An example of such a hardware class is "IEC MARIO PONY 6101". An + // internal database associates this hardware class with the qualified + // device specifications including OEM information, schematics, hardware + // qualification reports, test device tags, etc. + optional string hardware_class = 4; + + // The number of physical screens. + optional int32 screen_count = 5; + + // The screen dimensions of the primary screen, in pixels. + optional int32 primary_screen_width = 6; + optional int32 primary_screen_height = 7; + + // The device scale factor of the primary screen. + optional float primary_screen_scale_factor = 12; + + // Max DPI for any attached screen. (Windows only) + optional float max_dpi_x = 9; + optional float max_dpi_y = 10; + + // Information on the CPU obtained by CPUID. + message CPU { + // A 12 character string naming the vendor, e.g. "GeniuneIntel". + optional string vendor_name = 1; + + // The signature reported by CPUID (from EAX). + optional uint32 signature = 2; + + // Number of logical processors/cores on the current machine. + optional uint32 num_cores = 3; + } + optional CPU cpu = 13; + + // Information on the GPU + message Graphics { + // The GPU manufacturer's vendor id. + optional uint32 vendor_id = 1; + + // The GPU manufacturer's device id for the chip set. + optional uint32 device_id = 2; + + // The driver version on the GPU. + optional string driver_version = 3; + + // The driver date on the GPU. + optional string driver_date = 4; + + // The GL_VENDOR string. An example of a gl_vendor string is + // "Imagination Technologies". "" if we are not using OpenGL. + optional string gl_vendor = 6; + + // The GL_RENDERER string. An example of a gl_renderer string is + // "PowerVR SGX 540". "" if we are not using OpenGL. + optional string gl_renderer = 7; + } + optional Graphics gpu = 8; + + // Information about Bluetooth devices paired with the system. + message Bluetooth { + // Whether Bluetooth is present on this system. + optional bool is_present = 1; + + // Whether Bluetooth is enabled on this system. + optional bool is_enabled = 2; + + // Describes a paired device. + message PairedDevice { + // Assigned class of the device. This is a bitfield according to the + // Bluetooth specification available at the following URL: + // https://www.bluetooth.org/en-us/specification/assigned-numbers-overview/baseband + optional uint32 bluetooth_class = 1; + + // Decoded device type. + enum Type { + DEVICE_UNKNOWN = 0; + DEVICE_COMPUTER = 1; + DEVICE_PHONE = 2; + DEVICE_MODEM = 3; + DEVICE_AUDIO = 4; + DEVICE_CAR_AUDIO = 5; + DEVICE_VIDEO = 6; + DEVICE_PERIPHERAL = 7; + DEVICE_JOYSTICK = 8; + DEVICE_GAMEPAD = 9; + DEVICE_KEYBOARD = 10; + DEVICE_MOUSE = 11; + DEVICE_TABLET = 12; + DEVICE_KEYBOARD_MOUSE_COMBO = 13; + } + optional Type type = 2; + + // Vendor prefix of the Bluetooth address, these are OUI registered by + // the IEEE and are encoded with the first byte in bits 16-23, the + // second byte in bits 8-15 and the third byte in bits 0-7. + // + // ie. Google's OUI (00:1A:11) is encoded as 0x00001A11 + optional uint32 vendor_prefix = 4; + + // The Vendor ID of a device, returned in vendor_id below, can be + // either allocated by the Bluetooth SIG or USB IF, providing two + // completely overlapping namespaces for identifiers. + // + // This field should be read along with vendor_id to correctly + // identify the vendor. For example Google is identified by either + // vendor_id_source = VENDOR_ID_BLUETOOTH, vendor_id = 0x00E0 or + // vendor_id_source = VENDOR_ID_USB, vendor_id = 0x18D1. + // + // If the device does not support the Device ID specification the + // unknown value will be set. + enum VendorIDSource { + VENDOR_ID_UNKNOWN = 0; + VENDOR_ID_BLUETOOTH = 1; + VENDOR_ID_USB = 2; + } + optional VendorIDSource vendor_id_source = 8; + + // Vendor ID of the device, where available. + optional uint32 vendor_id = 5; + + // Product ID of the device, where available. + optional uint32 product_id = 6; + + // Device ID of the device, generally the release or version number in + // BCD format, where available. + optional uint32 device_id = 7; + } + repeated PairedDevice paired_device = 3; + } + optional Bluetooth bluetooth = 11; + + // Whether the internal display produces touch events. Omitted if unknown. + // Logged on ChromeOS only. + optional bool internal_display_supports_touch = 14; + + // Vendor ids and product ids of external touchscreens. + message TouchScreen { + // Touch screen vendor id. + optional uint32 vendor_id = 1; + // Touch screen product id. + optional uint32 product_id = 2; + } + // Lists vendor and product ids of external touchscreens. + // Logged on ChromeOS only. + repeated TouchScreen external_touchscreen = 15; + + // Drive messages are currently logged on Windows 7+, iOS, and Android. + message Drive { + // Whether this drive incurs a time penalty when randomly accessed. This + // should be true for spinning disks but false for SSDs or other + // flash-based drives. + optional bool has_seek_penalty = 1; + } + // The drive that the application executable was loaded from. + optional Drive app_drive = 16; + // The drive that the current user data directory was loaded from. + optional Drive user_data_drive = 17; + } + optional Hardware hardware = 6; + + // Information about the network connection. + message Network { + // Set to true if connection_type changed during the lifetime of the log. + optional bool connection_type_is_ambiguous = 1; + + // See net::NetworkChangeNotifier::ConnectionType. + enum ConnectionType { + CONNECTION_UNKNOWN = 0; + CONNECTION_ETHERNET = 1; + CONNECTION_WIFI = 2; + CONNECTION_2G = 3; + CONNECTION_3G = 4; + CONNECTION_4G = 5; + CONNECTION_BLUETOOTH = 6; + } + // The connection type according to NetworkChangeNotifier. + optional ConnectionType connection_type = 2; + + // Set to true if wifi_phy_layer_protocol changed during the lifetime of the log. + optional bool wifi_phy_layer_protocol_is_ambiguous = 3; + + // See net::WifiPHYLayerProtocol. + enum WifiPHYLayerProtocol { + WIFI_PHY_LAYER_PROTOCOL_NONE = 0; + WIFI_PHY_LAYER_PROTOCOL_ANCIENT = 1; + WIFI_PHY_LAYER_PROTOCOL_A = 2; + WIFI_PHY_LAYER_PROTOCOL_B = 3; + WIFI_PHY_LAYER_PROTOCOL_G = 4; + WIFI_PHY_LAYER_PROTOCOL_N = 5; + WIFI_PHY_LAYER_PROTOCOL_UNKNOWN = 6; + } + // The physical layer mode of the associated wifi access point, if any. + optional WifiPHYLayerProtocol wifi_phy_layer_protocol = 4; + + // Describe wifi access point information. + message WifiAccessPoint { + // Vendor prefix of the access point's BSSID, these are OUIs + // (Organizationally Unique Identifiers) registered by + // the IEEE and are encoded with the first byte in bits 16-23, the + // second byte in bits 8-15 and the third byte in bits 0-7. + optional uint32 vendor_prefix = 1; + + // Access point seurity mode definitions. + enum SecurityMode { + SECURITY_UNKNOWN = 0; + SECURITY_WPA = 1; + SECURITY_WEP = 2; + SECURITY_RSN = 3; + SECURITY_802_1X = 4; + SECURITY_PSK = 5; + SECURITY_NONE = 6; + } + // The security mode of the access point. + optional SecurityMode security_mode = 2; + + // Vendor specific information. + message VendorInformation { + // The model number, for example "0". + optional string model_number = 1; + + // The model name (sometimes the same as the model_number), + // for example "WZR-HP-AG300H". + optional string model_name = 2; + + // The device name (sometimes the same as the model_number), + // for example "Dummynet" + optional string device_name = 3; + + // The list of vendor-specific OUIs (Organziationally Unqiue + // Identifiers). These are provided by the vendor through WPS + // (Wireless Provisioning Service) information elements, which + // identifies the content of the element. + repeated uint32 element_identifier = 4; + } + // The wireless access point vendor information. + optional VendorInformation vendor_info = 3; + } + // Information of the wireless AP that device is connected to. + optional WifiAccessPoint access_point_info = 5; + } + optional Network network = 13; + + // Information on the Google Update install that is managing this client. + message GoogleUpdate { + // Whether the Google Update install is system-level or user-level. + optional bool is_system_install = 1; + + // The date at which Google Update last started performing an automatic + // update check, in seconds since the Unix epoch. + optional int64 last_automatic_start_timestamp = 2; + + // The date at which Google Update last successfully sent an update check + // and recieved an intact response from the server, in seconds since the + // Unix epoch. (The updates don't need to be successfully installed.) + optional int64 last_update_check_timestamp = 3; + + // Describes a product being managed by Google Update. (This can also + // describe Google Update itself.) + message ProductInfo { + // The current version of the product that is installed. + optional string version = 1; + + // The date at which Google Update successfully updated this product, + // stored in seconds since the Unix epoch. This is updated when an update + // is successfully applied, or if the server reports that no update + // is available. + optional int64 last_update_success_timestamp = 2; + + // The result reported by the product updater on its last run. + enum InstallResult { + INSTALL_RESULT_SUCCESS = 0; + INSTALL_RESULT_FAILED_CUSTOM_ERROR = 1; + INSTALL_RESULT_FAILED_MSI_ERROR = 2; + INSTALL_RESULT_FAILED_SYSTEM_ERROR = 3; + INSTALL_RESULT_EXIT_CODE = 4; + } + optional InstallResult last_result = 3; + + // The error code reported by the product updater on its last run. This + // will typically be a error code specific to the product installer. + optional int32 last_error = 4; + + // The extra error code reported by the product updater on its last run. + // This will typically be a Win32 error code. + optional int32 last_extra_error = 5; + } + optional ProductInfo google_update_status = 4; + optional ProductInfo client_status = 5; + } + optional GoogleUpdate google_update = 11; + + // Information on all installed plugins. + message Plugin { + // The plugin's self-reported name and filename (without path). + optional string name = 1; + optional string filename = 2; + + // The plugin's version. + optional string version = 3; + + // True if the plugin is disabled. + // If a client has multiple local Chrome user accounts, this is logged based + // on the first user account launched during the current session. + optional bool is_disabled = 4; + + // True if the plugin is PPAPI. + optional bool is_pepper = 5; + } + repeated Plugin plugin = 7; + + // Figures that can be used to generate application stability metrics. + // All values are counts of events since the last time that these + // values were reported. + // Next tag: 24 + message Stability { + // Total amount of time that the program was running, in seconds, + // since the last time a log was recorded, as measured using a client-side + // clock implemented via TimeTicks, which guarantees that it is monotonic + // and does not jump if the user changes his/her clock. The TimeTicks + // implementation also makes the clock not count time the computer is + // suspended. + optional int64 incremental_uptime_sec = 1; + + // Total amount of time that the program was running, in seconds, + // since startup, as measured using a client-side clock implemented + // via TimeTicks, which guarantees that it is monotonic and does not + // jump if the user changes his/her clock. The TimeTicks implementation + // also makes the clock not count time the computer is suspended. + // This field was added for M-35. + optional int64 uptime_sec = 23; + + // Page loads along with renderer crashes and hangs, since page load count + // roughly corresponds to usage. + optional int32 page_load_count = 2; + optional int32 renderer_crash_count = 3; + optional int32 renderer_hang_count = 4; + + // Number of renderer crashes that were for extensions. These crashes are + // not counted in renderer_crash_count. + optional int32 extension_renderer_crash_count = 5; + + // Number of non-renderer child process crashes. + optional int32 child_process_crash_count = 6; + + // Number of times the browser has crashed while logged in as the "other + // user" (guest) account. + // Logged on ChromeOS only. + optional int32 other_user_crash_count = 7; + + // Number of times the kernel has crashed. + // Logged on ChromeOS only. + optional int32 kernel_crash_count = 8; + + // Number of times the system has shut down uncleanly. + // Logged on ChromeOS only. + optional int32 unclean_system_shutdown_count = 9; + + // + // All the remaining fields in the Stability are recorded at most once per + // client session. + // + + // The number of times the program was launched. + // This will typically be equal to 1. However, it is possible that Chrome + // was unable to upload stability metrics for previous launches (e.g. due to + // crashing early during startup), and hence this value might be greater + // than 1. + optional int32 launch_count = 15; + // The number of times that it didn't exit cleanly (which we assume to be + // mostly crashes). + optional int32 crash_count = 16; + + // The number of times the program began, but did not complete, the shutdown + // process. (For example, this may occur when Windows is shutting down, and + // it only gives the process a few seconds to clean up.) + optional int32 incomplete_shutdown_count = 17; + + // The number of times the program was able register with breakpad crash + // services. + optional int32 breakpad_registration_success_count = 18; + + // The number of times the program failed to register with breakpad crash + // services. If crash registration fails then when the program crashes no + // crash report will be generated. + optional int32 breakpad_registration_failure_count = 19; + + // The number of times the program has run under a debugger. This should + // be an exceptional condition. Running under a debugger prevents crash + // dumps from being generated. + optional int32 debugger_present_count = 20; + + // The number of times the program has run without a debugger attached. + // This should be most common scenario and should be very close to + // |launch_count|. + optional int32 debugger_not_present_count = 21; + + // Stability information for all installed plugins. + message PluginStability { + // The relevant plugin's information (name, etc.) + optional Plugin plugin = 1; + + // The number of times this plugin's process was launched. + optional int32 launch_count = 2; + + // The number of times this plugin was instantiated on a web page. + // This will be >= |launch_count|. + // (A page load with multiple sections drawn by this plugin will + // increase this count multiple times.) + optional int32 instance_count = 3; + + // The number of times this plugin process crashed. + // This value will be <= |launch_count|. + optional int32 crash_count = 4; + + // The number of times this plugin could not be loaded. + optional int32 loading_error_count = 5; + } + repeated PluginStability plugin_stability = 22; + } + optional Stability stability = 8; + + // Description of a field trial or experiment that the user is currently + // enrolled in. + // All metrics reported in this upload can potentially be influenced by the + // field trial. + message FieldTrial { + // The name of the field trial, as a 32-bit identifier. + // Currently, the identifier is a hash of the field trial's name. + optional fixed32 name_id = 1; + + // The user's group within the field trial, as a 32-bit identifier. + // Currently, the identifier is a hash of the group's name. + optional fixed32 group_id = 2; + } + repeated FieldTrial field_trial = 9; + + // Information about the A/V output device(s) (typically just a TV). + // However, a configuration may have one or more intermediate A/V devices + // between the source device and the TV (e.g. an A/V receiver, video + // processor, etc.). + message ExternalAudioVideoDevice { + // The manufacturer name (possibly encoded as a 3-letter code, e.g. "YMH" + // for Yamaha). + optional string manufacturer_name = 1; + + // The model name (e.g. "RX-V1900"). Some devices may report generic names + // like "receiver" or use the full manufacturer name (e.g "PHILIPS"). + optional string model_name = 2; + + // The product code (e.g. "0218"). + optional string product_code = 3; + + // The device types. A single device can have multiple types (e.g. a set-top + // box could be both a tuner and a player). The same type may even be + // repeated (e.g a device that reports two tuners). + enum AVDeviceType { + AV_DEVICE_TYPE_UNKNOWN = 0; + AV_DEVICE_TYPE_TV = 1; + AV_DEVICE_TYPE_RECORDER = 2; + AV_DEVICE_TYPE_TUNER = 3; + AV_DEVICE_TYPE_PLAYER = 4; + AV_DEVICE_TYPE_AUDIO_SYSTEM = 5; + } + repeated AVDeviceType av_device_type = 4; + + // The year of manufacture. + optional int32 manufacture_year = 5; + + // The week of manufacture. + // Note: per the Wikipedia EDID article, numbering for this field may not + // be consistent between manufacturers. + optional int32 manufacture_week = 6; + + // Max horizontal resolution in pixels. + optional int32 horizontal_resolution = 7; + + // Max vertical resolution in pixels. + optional int32 vertical_resolution = 8; + + // Audio capabilities of the device. + // Ref: http://en.wikipedia.org/wiki/Extended_display_identification_data + message AudioDescription { + // Audio format + enum AudioFormat { + AUDIO_FORMAT_UNKNOWN = 0; + AUDIO_FORMAT_LPCM = 1; + AUDIO_FORMAT_AC_3 = 2; + AUDIO_FORMAT_MPEG1 = 3; + AUDIO_FORMAT_MP3 = 4; + AUDIO_FORMAT_MPEG2 = 5; + AUDIO_FORMAT_AAC = 6; + AUDIO_FORMAT_DTS = 7; + AUDIO_FORMAT_ATRAC = 8; + AUDIO_FORMAT_ONE_BIT = 9; + AUDIO_FORMAT_DD_PLUS = 10; + AUDIO_FORMAT_DTS_HD = 11; + AUDIO_FORMAT_MLP_DOLBY_TRUEHD = 12; + AUDIO_FORMAT_DST_AUDIO = 13; + AUDIO_FORMAT_MICROSOFT_WMA_PRO = 14; + } + optional AudioFormat audio_format = 1; + + // Number of channels (e.g. 1, 2, 8, etc.). + optional int32 num_channels = 2; + + // Supported sample frequencies in Hz (e.g. 32000, 44100, etc.). + // Multiple frequencies may be specified. + repeated int32 sample_frequency_hz = 3; + + // Maximum bit rate in bits/s. + optional int32 max_bit_rate_per_second = 4; + + // Bit depth (e.g. 16, 20, 24, etc.). + optional int32 bit_depth = 5; + } + repeated AudioDescription audio_description = 9; + + // The position in AV setup. + // A value of 0 means this device is the TV. + // A value of 1 means this device is directly connected to one of + // the TV's inputs. + // Values > 1 indicate there are 1 or more devices between this device + // and the TV. + optional int32 position_in_setup = 10; + + // Whether this device is in the path to the TV. + optional bool is_in_path_to_tv = 11; + + // The CEC version the device supports. + // CEC stands for Consumer Electronics Control, a part of the HDMI + // specification. Not all HDMI devices support CEC. + // Only devices that support CEC will report a value here. + optional int32 cec_version = 12; + + // This message reports CEC commands seen by a device. + // After each log is sent, this information is cleared and gathered again. + // By collecting CEC status information by opcode we can determine + // which CEC features can be supported. + message CECCommand { + // The CEC command opcode. CEC supports up to 256 opcodes. + // We add only one CECCommand message per unique opcode. Only opcodes + // seen by the device will be reported. The remainder of the message + // accumulates status for this opcode (and device). + optional int32 opcode = 1; + + // The total number of commands received from the external device. + optional int32 num_received_direct = 2; + + // The number of commands received from the external device as part of a + // broadcast message. + optional int32 num_received_broadcast = 3; + + // The total number of commands sent to the external device. + optional int32 num_sent_direct = 4; + + // The number of commands sent to the external device as part of a + // broadcast message. + optional int32 num_sent_broadcast = 5; + + // The number of aborted commands for unknown reasons. + optional int32 num_aborted_unknown_reason = 6; + + // The number of aborted commands because of an unrecognized opcode. + optional int32 num_aborted_unrecognized = 7; + } + repeated CECCommand cec_command = 13; + } + repeated ExternalAudioVideoDevice external_audio_video_device = 14; + + // Information about the current wireless access point. Collected directly + // from the wireless access point via standard apis if the device is + // connected to the Internet wirelessly. Introduced for Chrome on TV devices + // but also can be collected by ChromeOS, Android or other clients. + message ExternalAccessPoint { + // The manufacturer name, for example "ASUSTeK Computer Inc.". + optional string manufacturer = 1; + + // The model name, for example "Wi-Fi Protected Setup Router". + optional string model_name = 2; + + // The model number, for example "RT-N16". + optional string model_number = 3; + + // The device name (sometime same as model_number), for example "RT-N16". + optional string device_name = 4; + } + optional ExternalAccessPoint external_access_point = 15; + + // Number of users currently signed into a multiprofile session. + // A zero value indicates that the user count changed while the log is open. + // Logged only on ChromeOS. + optional uint32 multi_profile_user_count = 17; + + // Information about extensions that are installed, masked to provide better + // privacy. Only extensions from a single profile are reported; this will + // generally be the profile used when the browser is started. The profile + // reported on will remain consistent at least until the browser is + // relaunched (or the profile is deleted by the user). + // + // Each client first picks a value for client_key derived from its UMA + // client_id: + // client_key = client_id % 4096 + // Then, each installed extension is mapped into a hash bucket according to + // bucket = CityHash64(StringPrintf("%d:%s", + // client_key, extension_id)) % 1024 + // The client reports the set of hash buckets occupied by all installed + // extensions. If multiple extensions map to the same bucket, that bucket is + // still only reported once. + repeated int32 occupied_extension_bucket = 18; + + // The state of loaded extensions for this system. The system can have either + // no applicable extensions, extensions only from the webstore and verified by + // the webstore, extensions only from the webstore but not verified, or + // extensions not from the store. If there is a single off-store extension, + // then HAS_OFFSTORE is reported. This should be kept in sync with the + // corresponding enum in chrome/browser/metrics/extensions_metrics_provider.cc + enum ExtensionsState { + NO_EXTENSIONS = 0; + NO_OFFSTORE_VERIFIED = 1; + NO_OFFSTORE_UNVERIFIED = 2; + HAS_OFFSTORE = 3; + } + optional ExtensionsState offstore_extensions_state = 19; +} diff --git a/metrics/uploader/proto/user_action_event.proto b/metrics/uploader/proto/user_action_event.proto new file mode 100644 index 000000000..30a93180d --- /dev/null +++ b/metrics/uploader/proto/user_action_event.proto @@ -0,0 +1,23 @@ +// Copyright 2014 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. +// +// Stores information about an event that occurs in response to a user action, +// e.g. an interaction with a browser UI element. + +syntax = "proto2"; + +option optimize_for = LITE_RUNTIME; +option java_outer_classname = "UserActionEventProtos"; +option java_package = "org.chromium.components.metrics"; + +package metrics; + +// Next tag: 3 +message UserActionEventProto { + // The name of the action, hashed. + optional fixed64 name_hash = 1; + + // The timestamp for the event, in seconds since the epoch. + optional int64 time = 2; +} diff --git a/metrics/uploader/sender.h b/metrics/uploader/sender.h new file mode 100644 index 000000000..521183407 --- /dev/null +++ b/metrics/uploader/sender.h @@ -0,0 +1,18 @@ +// Copyright 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef METRICS_UPLOADER_SENDER_H_ +#define METRICS_UPLOADER_SENDER_H_ + +#include <string> + +// Abstract class for a Sender that uploads a metrics message. +class Sender { + public: + virtual ~Sender() {} + // Sends a message |content| with its sha1 hash |hash| + virtual bool Send(const std::string& content, const std::string& hash) = 0; +}; + +#endif // METRICS_UPLOADER_SENDER_H_ diff --git a/metrics/uploader/sender_http.cc b/metrics/uploader/sender_http.cc new file mode 100644 index 000000000..8488b6666 --- /dev/null +++ b/metrics/uploader/sender_http.cc @@ -0,0 +1,38 @@ +// Copyright 2014 The Chromium OS 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 "metrics/uploader/sender_http.h" + +#include <string> + +#include <base/logging.h> +#include <base/strings/string_number_conversions.h> +#include <chromeos/http/http_utils.h> +#include <chromeos/mime_utils.h> + +HttpSender::HttpSender(const std::string server_url) + : server_url_(server_url) {} + +bool HttpSender::Send(const std::string& content, + const std::string& content_hash) { + const std::string hash = + base::HexEncode(content_hash.data(), content_hash.size()); + + chromeos::http::HeaderList headers = {{"X-Chrome-UMA-Log-SHA1", hash}}; + chromeos::ErrorPtr error; + auto response = chromeos::http::PostTextAndBlock( + server_url_, + content, + chromeos::mime::application::kWwwFormUrlEncoded, + headers, + chromeos::http::Transport::CreateDefault(), + &error); + if (!response || response->ExtractDataAsString() != "OK") { + if (error) { + DLOG(ERROR) << "Failed to send data: " << error->GetMessage(); + } + return false; + } + return true; +} diff --git a/metrics/uploader/sender_http.h b/metrics/uploader/sender_http.h new file mode 100644 index 000000000..4880b2880 --- /dev/null +++ b/metrics/uploader/sender_http.h @@ -0,0 +1,29 @@ +// Copyright 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef METRICS_UPLOADER_SENDER_HTTP_H_ +#define METRICS_UPLOADER_SENDER_HTTP_H_ + +#include <string> + +#include <base/macros.h> + +#include "metrics/uploader/sender.h" + +// Sender implemented using http_utils from libchromeos +class HttpSender : public Sender { + public: + explicit HttpSender(std::string server_url); + ~HttpSender() override = default; + // Sends |content| whose SHA1 hash is |hash| to server_url with a synchronous + // POST request to server_url. + bool Send(const std::string& content, const std::string& hash) override; + + private: + const std::string server_url_; + + DISALLOW_COPY_AND_ASSIGN(HttpSender); +}; + +#endif // METRICS_UPLOADER_SENDER_HTTP_H_ diff --git a/metrics/uploader/system_profile_cache.cc b/metrics/uploader/system_profile_cache.cc new file mode 100644 index 000000000..ea4a38c2c --- /dev/null +++ b/metrics/uploader/system_profile_cache.cc @@ -0,0 +1,238 @@ +// Copyright 2014 The Chromium OS 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 "metrics/uploader/system_profile_cache.h" + +#include <string> +#include <vector> + +#include "base/files/file_util.h" +#include "base/guid.h" +#include "base/logging.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_util.h" +#include "base/sys_info.h" +#include "metrics/persistent_integer.h" +#include "metrics/uploader/metrics_log_base.h" +#include "metrics/uploader/proto/chrome_user_metrics_extension.pb.h" +#include "vboot/crossystem.h" + +namespace { + +const char kPersistentGUIDFile[] = "/var/lib/metrics/Sysinfo.GUID"; +const char kPersistentSessionIdFilename[] = "Sysinfo.SessionId"; +const char kProductIdFieldName[] = "GOOGLE_METRICS_PRODUCT_ID"; + +} // namespace + +std::string ChannelToString( + const metrics::SystemProfileProto_Channel& channel) { + switch (channel) { + case metrics::SystemProfileProto::CHANNEL_STABLE: + return "STABLE"; + case metrics::SystemProfileProto::CHANNEL_DEV: + return "DEV"; + case metrics::SystemProfileProto::CHANNEL_BETA: + return "BETA"; + case metrics::SystemProfileProto::CHANNEL_CANARY: + return "CANARY"; + default: + return "UNKNOWN"; + } +} + +SystemProfileCache::SystemProfileCache() + : initialized_(false), + testing_(false), + config_root_("/"), + session_id_(new chromeos_metrics::PersistentInteger( + kPersistentSessionIdFilename)) { +} + +SystemProfileCache::SystemProfileCache(bool testing, + const std::string& config_root) + : initialized_(false), + testing_(testing), + config_root_(config_root), + session_id_(new chromeos_metrics::PersistentInteger( + kPersistentSessionIdFilename)) { +} + +bool SystemProfileCache::Initialize() { + CHECK(!initialized_) + << "this should be called only once in the metrics_daemon lifetime."; + + std::string chromeos_version; + std::string board; + std::string build_type; + if (!base::SysInfo::GetLsbReleaseValue("CHROMEOS_RELEASE_NAME", + &profile_.os_name) || + !base::SysInfo::GetLsbReleaseValue("CHROMEOS_RELEASE_VERSION", + &profile_.os_version) || + !base::SysInfo::GetLsbReleaseValue("CHROMEOS_RELEASE_BOARD", &board) || + !base::SysInfo::GetLsbReleaseValue("CHROMEOS_RELEASE_BUILD_TYPE", + &build_type) || + !GetChromeOSVersion(&chromeos_version) || + !GetHardwareId(&profile_.hardware_class)) { + DLOG(ERROR) << "failing to initialize profile cache"; + return false; + } + + std::string channel_string; + base::SysInfo::GetLsbReleaseValue("CHROMEOS_RELEASE_TRACK", &channel_string); + profile_.channel = ProtoChannelFromString(channel_string); + + profile_.app_version = chromeos_version + " (" + build_type + ")" + + ChannelToString(profile_.channel) + " " + board; + + // If the product id is not defined, use the default one from the protobuf. + profile_.product_id = metrics::ChromeUserMetricsExtension::CHROME; + if (GetProductId(&profile_.product_id)) { + DLOG(INFO) << "Set the product id to " << profile_.product_id; + } + + profile_.client_id = + testing_ ? "client_id_test" : GetPersistentGUID(kPersistentGUIDFile); + + // Increment the session_id everytime we initialize this. If metrics_daemon + // does not crash, this should correspond to the number of reboots of the + // system. + // TODO(bsimonnet): Change this to map to the number of time system-services + // is started. + session_id_->Add(1); + profile_.session_id = static_cast<int32_t>(session_id_->Get()); + + initialized_ = true; + return initialized_; +} + +bool SystemProfileCache::InitializeOrCheck() { + return initialized_ || Initialize(); +} + +void SystemProfileCache::Populate( + metrics::ChromeUserMetricsExtension* metrics_proto) { + CHECK(metrics_proto); + CHECK(InitializeOrCheck()) + << "failed to initialize system information."; + + // The client id is hashed before being sent. + metrics_proto->set_client_id( + metrics::MetricsLogBase::Hash(profile_.client_id)); + metrics_proto->set_session_id(profile_.session_id); + + // Sets the product id. + metrics_proto->set_product(profile_.product_id); + + metrics::SystemProfileProto* profile_proto = + metrics_proto->mutable_system_profile(); + profile_proto->mutable_hardware()->set_hardware_class( + profile_.hardware_class); + profile_proto->set_app_version(profile_.app_version); + profile_proto->set_channel(profile_.channel); + + metrics::SystemProfileProto_OS* os = profile_proto->mutable_os(); + os->set_name(profile_.os_name); + os->set_version(profile_.os_version); +} + +std::string SystemProfileCache::GetPersistentGUID(const std::string& filename) { + std::string guid; + base::FilePath filepath(filename); + if (!base::ReadFileToString(filepath, &guid)) { + guid = base::GenerateGUID(); + // If we can't read or write the file, the guid will not be preserved during + // the next reboot. Crash. + CHECK(base::WriteFile(filepath, guid.c_str(), guid.size())); + } + return guid; +} + +bool SystemProfileCache::GetChromeOSVersion(std::string* version) { + if (testing_) { + *version = "0.0.0.0"; + return true; + } + + std::string milestone, build, branch, patch; + unsigned tmp; + if (base::SysInfo::GetLsbReleaseValue("CHROMEOS_RELEASE_CHROME_MILESTONE", + &milestone) && + base::SysInfo::GetLsbReleaseValue("CHROMEOS_RELEASE_BUILD_NUMBER", + &build) && + base::SysInfo::GetLsbReleaseValue("CHROMEOS_RELEASE_BRANCH_NUMBER", + &branch) && + base::SysInfo::GetLsbReleaseValue("CHROMEOS_RELEASE_PATCH_NUMBER", + &patch)) { + // Convert to uint to ensure those fields are positive numbers. + if (base::StringToUint(milestone, &tmp) && + base::StringToUint(build, &tmp) && + base::StringToUint(branch, &tmp) && + base::StringToUint(patch, &tmp)) { + std::vector<std::string> parts = {milestone, build, branch, patch}; + *version = JoinString(parts, '.'); + return true; + } + DLOG(INFO) << "The milestone, build, branch or patch is not a positive " + << "number."; + return false; + } + DLOG(INFO) << "Field missing from /etc/lsb-release"; + return false; +} + +bool SystemProfileCache::GetHardwareId(std::string* hwid) { + CHECK(hwid); + + if (testing_) { + // if we are in test mode, we do not call crossystem directly. + DLOG(INFO) << "skipping hardware id"; + *hwid = ""; + return true; + } + + char buffer[128]; + if (buffer != VbGetSystemPropertyString("hwid", buffer, sizeof(buffer))) { + LOG(ERROR) << "error getting hwid"; + return false; + } + + *hwid = std::string(buffer); + return true; +} + +bool SystemProfileCache::GetProductId(int* product_id) const { + chromeos::OsReleaseReader reader; + if (testing_) { + base::FilePath root(config_root_); + reader.LoadTestingOnly(root); + } else { + reader.Load(); + } + + std::string id; + if (reader.GetString(kProductIdFieldName, &id)) { + CHECK(base::StringToInt(id, product_id)) << "Failed to convert product_id " + << id << " to int."; + return true; + } + return false; +} + +metrics::SystemProfileProto_Channel SystemProfileCache::ProtoChannelFromString( + const std::string& channel) { + + if (channel == "stable-channel") { + return metrics::SystemProfileProto::CHANNEL_STABLE; + } else if (channel == "dev-channel") { + return metrics::SystemProfileProto::CHANNEL_DEV; + } else if (channel == "beta-channel") { + return metrics::SystemProfileProto::CHANNEL_BETA; + } else if (channel == "canary-channel") { + return metrics::SystemProfileProto::CHANNEL_CANARY; + } + + DLOG(INFO) << "unknown channel: " << channel; + return metrics::SystemProfileProto::CHANNEL_UNKNOWN; +} diff --git a/metrics/uploader/system_profile_cache.h b/metrics/uploader/system_profile_cache.h new file mode 100644 index 000000000..e7a73370a --- /dev/null +++ b/metrics/uploader/system_profile_cache.h @@ -0,0 +1,91 @@ +// Copyright 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef METRICS_UPLOADER_SYSTEM_PROFILE_CACHE_H_ +#define METRICS_UPLOADER_SYSTEM_PROFILE_CACHE_H_ + +#include <stdint.h> + +#include <string> + +#include "base/compiler_specific.h" +#include "base/gtest_prod_util.h" +#include "base/memory/scoped_ptr.h" +#include "chromeos/osrelease_reader.h" +#include "metrics/persistent_integer.h" +#include "metrics/uploader/proto/system_profile.pb.h" +#include "metrics/uploader/system_profile_setter.h" + +namespace metrics { +class ChromeUserMetricsExtension; +} + +struct SystemProfile { + std::string os_name; + std::string os_version; + metrics::SystemProfileProto::Channel channel; + std::string app_version; + std::string hardware_class; + std::string client_id; + int32_t session_id; + int32_t product_id; +}; + +// Retrieves general system informations needed by the protobuf for context and +// remembers them to avoid expensive calls. +// +// The cache is populated lazily. The only method needed is Populate. +class SystemProfileCache : public SystemProfileSetter { + public: + SystemProfileCache(); + + SystemProfileCache(bool testing, const std::string& config_root); + + // Populates the ProfileSystem protobuf with system information. + void Populate(metrics::ChromeUserMetricsExtension* profile_proto) override; + + // Converts a string representation of the channel (|channel|-channel) to a + // SystemProfileProto_Channel + static metrics::SystemProfileProto_Channel ProtoChannelFromString( + const std::string& channel); + + // Gets the persistent GUID and create it if it has not been created yet. + static std::string GetPersistentGUID(const std::string& filename); + + private: + friend class UploadServiceTest; + FRIEND_TEST(UploadServiceTest, ExtractChannelFromDescription); + FRIEND_TEST(UploadServiceTest, ReadKeyValueFromFile); + FRIEND_TEST(UploadServiceTest, SessionIdIncrementedAtInitialization); + FRIEND_TEST(UploadServiceTest, ValuesInConfigFileAreSent); + + // Fetches all informations and populates |profile_| + bool Initialize(); + + // Initializes |profile_| only if it has not been yet initialized. + bool InitializeOrCheck(); + + // Gets the hardware ID using crossystem + bool GetHardwareId(std::string* hwid); + + // Gets the product ID from the GOOGLE_METRICS_PRODUCT_ID field. + bool GetProductId(int* product_id) const; + + // Generate the formatted chromeos version from the fields in + // /etc/lsb-release. The format is A.B.C.D where A, B, C and D are positive + // integer representing: + // * the chrome milestone + // * the build number + // * the branch number + // * the patch number + bool GetChromeOSVersion(std::string* version); + + bool initialized_; + bool testing_; + std::string config_root_; + scoped_ptr<chromeos_metrics::PersistentInteger> session_id_; + SystemProfile profile_; +}; + +#endif // METRICS_UPLOADER_SYSTEM_PROFILE_CACHE_H_ diff --git a/metrics/uploader/system_profile_setter.h b/metrics/uploader/system_profile_setter.h new file mode 100644 index 000000000..c535664a0 --- /dev/null +++ b/metrics/uploader/system_profile_setter.h @@ -0,0 +1,21 @@ +// Copyright 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef METRICS_UPLOADER_SYSTEM_PROFILE_SETTER_H_ +#define METRICS_UPLOADER_SYSTEM_PROFILE_SETTER_H_ + +namespace metrics { +class ChromeUserMetricsExtension; +} + +// Abstract class used to delegate populating SystemProfileProto with system +// information to simplify testing. +class SystemProfileSetter { + public: + virtual ~SystemProfileSetter() {} + // Populates the protobuf with system informations. + virtual void Populate(metrics::ChromeUserMetricsExtension* profile_proto) = 0; +}; + +#endif // METRICS_UPLOADER_SYSTEM_PROFILE_SETTER_H_ diff --git a/metrics/uploader/upload_service.cc b/metrics/uploader/upload_service.cc new file mode 100644 index 000000000..92c9e1061 --- /dev/null +++ b/metrics/uploader/upload_service.cc @@ -0,0 +1,224 @@ +// Copyright 2014 The Chromium OS 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 "metrics/uploader/upload_service.h" + +#include <string> + +#include <base/bind.h> +#include <base/logging.h> +#include <base/memory/scoped_vector.h> +#include <base/message_loop/message_loop.h> +#include <base/metrics/histogram.h> +#include <base/metrics/histogram_base.h> +#include <base/metrics/histogram_snapshot_manager.h> +#include <base/metrics/sparse_histogram.h> +#include <base/metrics/statistics_recorder.h> +#include <base/sha1.h> + +#include "metrics/serialization/metric_sample.h" +#include "metrics/serialization/serialization_utils.h" +#include "metrics/uploader/metrics_log.h" +#include "metrics/uploader/sender_http.h" +#include "metrics/uploader/system_profile_cache.h" + +const int UploadService::kMaxFailedUpload = 10; + +UploadService::UploadService(SystemProfileSetter* setter, + MetricsLibraryInterface* metrics_lib, + const std::string& server) + : system_profile_setter_(setter), + metrics_lib_(metrics_lib), + histogram_snapshot_manager_(this), + sender_(new HttpSender(server)), + testing_(false) { +} + +UploadService::UploadService(SystemProfileSetter* setter, + MetricsLibraryInterface* metrics_lib, + const std::string& server, + bool testing) + : UploadService(setter, metrics_lib, server) { + testing_ = testing; +} + +void UploadService::Init(const base::TimeDelta& upload_interval, + const std::string& metrics_file) { + base::StatisticsRecorder::Initialize(); + metrics_file_ = metrics_file; + + if (!testing_) { + base::MessageLoop::current()->PostDelayedTask(FROM_HERE, + base::Bind(&UploadService::UploadEventCallback, + base::Unretained(this), + upload_interval), + upload_interval); + } +} + +void UploadService::StartNewLog() { + CHECK(!staged_log_) << "the staged log should be discarded before starting " + "a new metrics log"; + MetricsLog* log = new MetricsLog(); + log->PopulateSystemProfile(system_profile_setter_.get()); + current_log_.reset(log); +} + +void UploadService::UploadEventCallback(const base::TimeDelta& interval) { + UploadEvent(); + + base::MessageLoop::current()->PostDelayedTask(FROM_HERE, + base::Bind(&UploadService::UploadEventCallback, + base::Unretained(this), + interval), + interval); +} + +void UploadService::UploadEvent() { + if (staged_log_) { + // Previous upload failed, retry sending the logs. + SendStagedLog(); + return; + } + + // Previous upload successful, reading metrics sample from the file. + ReadMetrics(); + GatherHistograms(); + + // No samples found. Exit to avoid sending an empty log. + if (!current_log_) + return; + + StageCurrentLog(); + SendStagedLog(); +} + +void UploadService::SendStagedLog() { + CHECK(staged_log_) << "staged_log_ must exist to be sent"; + + // If metrics are not enabled, discard the log and exit. + if (!metrics_lib_->AreMetricsEnabled()) { + LOG(INFO) << "Metrics disabled. Don't upload metrics samples."; + staged_log_.reset(); + return; + } + + std::string log_text; + staged_log_->GetEncodedLog(&log_text); + if (!sender_->Send(log_text, base::SHA1HashString(log_text))) { + ++failed_upload_count_; + if (failed_upload_count_ <= kMaxFailedUpload) { + LOG(WARNING) << "log upload failed " << failed_upload_count_ + << " times. It will be retried later."; + return; + } + LOG(WARNING) << "log failed more than " << kMaxFailedUpload << " times."; + } else { + LOG(INFO) << "uploaded " << log_text.length() << " bytes"; + } + // Discard staged log. + staged_log_.reset(); +} + +void UploadService::Reset() { + staged_log_.reset(); + current_log_.reset(); + failed_upload_count_ = 0; +} + +void UploadService::ReadMetrics() { + CHECK(!staged_log_) + << "cannot read metrics until the old logs have been discarded"; + + ScopedVector<metrics::MetricSample> vector; + metrics::SerializationUtils::ReadAndTruncateMetricsFromFile( + metrics_file_, &vector); + + int i = 0; + for (ScopedVector<metrics::MetricSample>::iterator it = vector.begin(); + it != vector.end(); it++) { + metrics::MetricSample* sample = *it; + AddSample(*sample); + i++; + } + DLOG(INFO) << i << " samples read"; +} + +void UploadService::AddSample(const metrics::MetricSample& sample) { + base::HistogramBase* counter; + switch (sample.type()) { + case metrics::MetricSample::CRASH: + AddCrash(sample.name()); + break; + case metrics::MetricSample::HISTOGRAM: + counter = base::Histogram::FactoryGet( + sample.name(), sample.min(), sample.max(), sample.bucket_count(), + base::Histogram::kUmaTargetedHistogramFlag); + counter->Add(sample.sample()); + break; + case metrics::MetricSample::SPARSE_HISTOGRAM: + counter = base::SparseHistogram::FactoryGet( + sample.name(), base::HistogramBase::kUmaTargetedHistogramFlag); + counter->Add(sample.sample()); + break; + case metrics::MetricSample::LINEAR_HISTOGRAM: + counter = base::LinearHistogram::FactoryGet( + sample.name(), + 1, + sample.max(), + sample.max() + 1, + base::Histogram::kUmaTargetedHistogramFlag); + counter->Add(sample.sample()); + break; + case metrics::MetricSample::USER_ACTION: + GetOrCreateCurrentLog()->RecordUserAction(sample.name()); + break; + default: + break; + } +} + +void UploadService::AddCrash(const std::string& crash_name) { + if (crash_name == "user") { + GetOrCreateCurrentLog()->IncrementUserCrashCount(); + } else if (crash_name == "kernel") { + GetOrCreateCurrentLog()->IncrementKernelCrashCount(); + } else if (crash_name == "uncleanshutdown") { + GetOrCreateCurrentLog()->IncrementUncleanShutdownCount(); + } else { + DLOG(ERROR) << "crash name unknown" << crash_name; + } +} + +void UploadService::GatherHistograms() { + base::StatisticsRecorder::Histograms histograms; + base::StatisticsRecorder::GetHistograms(&histograms); + + histogram_snapshot_manager_.PrepareDeltas( + base::Histogram::kNoFlags, base::Histogram::kUmaTargetedHistogramFlag); +} + +void UploadService::RecordDelta(const base::HistogramBase& histogram, + const base::HistogramSamples& snapshot) { + GetOrCreateCurrentLog()->RecordHistogramDelta(histogram.histogram_name(), + snapshot); +} + +void UploadService::StageCurrentLog() { + CHECK(!staged_log_) + << "staged logs must be discarded before another log can be staged"; + + if (!current_log_) return; + + staged_log_.swap(current_log_); + staged_log_->CloseLog(); + failed_upload_count_ = 0; +} + +MetricsLog* UploadService::GetOrCreateCurrentLog() { + if (!current_log_) { + StartNewLog(); + } + return current_log_.get(); +} diff --git a/metrics/uploader/upload_service.h b/metrics/uploader/upload_service.h new file mode 100644 index 000000000..ebbb54f2a --- /dev/null +++ b/metrics/uploader/upload_service.h @@ -0,0 +1,153 @@ +// Copyright 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef METRICS_UPLOADER_UPLOAD_SERVICE_H_ +#define METRICS_UPLOADER_UPLOAD_SERVICE_H_ + +#include <string> + +#include "base/metrics/histogram_base.h" +#include "base/metrics/histogram_flattener.h" +#include "base/metrics/histogram_snapshot_manager.h" + +#include "metrics/metrics_library.h" +#include "metrics/uploader/metrics_log.h" +#include "metrics/uploader/sender.h" +#include "metrics/uploader/system_profile_cache.h" + +namespace metrics { +class ChromeUserMetricsExtension; +class CrashSample; +class HistogramSample; +class LinearHistogramSample; +class MetricSample; +class SparseHistogramSample; +class UserActionSample; +} + +class SystemProfileSetter; + +// Service responsible for uploading the metrics periodically to the server. +// This service works as a simple 2-state state-machine. +// +// The two states are the presence or not of a staged log. +// A staged log is a compressed protobuffer containing both the aggregated +// metrics and event and information about the client. (product, hardware id, +// etc...). +// +// At regular intervals, the upload event will be triggered and the following +// will happen: +// * if a staged log is present: +// The previous upload may have failed for various reason. We then retry to +// upload the same log. +// - if the upload is successful, we discard the log (therefore +// transitioning back to no staged log) +// - if the upload fails, we keep the log to try again later. +// We do not try to read the metrics that are stored on +// the disk as we want to avoid storing the metrics in memory. +// +// * if no staged logs are present: +// Read all metrics from the disk, aggregate them and try to send them. +// - if the upload succeeds, we discard the staged log (transitioning back +// to the no staged log state) +// - if the upload fails, we keep the staged log in memory to retry +// uploading later. +// +class UploadService : public base::HistogramFlattener { + public: + explicit UploadService(SystemProfileSetter* setter, + MetricsLibraryInterface* metrics_lib, + const std::string& server); + + void Init(const base::TimeDelta& upload_interval, + const std::string& metrics_file); + + // Starts a new log. The log needs to be regenerated after each successful + // launch as it is destroyed when staging the log. + void StartNewLog(); + + // Event callback for handling MessageLoop events. + void UploadEventCallback(const base::TimeDelta& interval); + + // Triggers an upload event. + void UploadEvent(); + + // Sends the staged log. + void SendStagedLog(); + + // Implements inconsistency detection to match HistogramFlattener's + // interface. + void InconsistencyDetected( + base::HistogramBase::Inconsistency problem) override {} + void UniqueInconsistencyDetected( + base::HistogramBase::Inconsistency problem) override {} + void InconsistencyDetectedInLoggedCount(int amount) override {} + + private: + friend class UploadServiceTest; + + FRIEND_TEST(UploadServiceTest, CanSendMultipleTimes); + FRIEND_TEST(UploadServiceTest, DiscardLogsAfterTooManyFailedUpload); + FRIEND_TEST(UploadServiceTest, EmptyLogsAreNotSent); + FRIEND_TEST(UploadServiceTest, FailedSendAreRetried); + FRIEND_TEST(UploadServiceTest, LogContainsAggregatedValues); + FRIEND_TEST(UploadServiceTest, LogEmptyAfterUpload); + FRIEND_TEST(UploadServiceTest, LogEmptyByDefault); + FRIEND_TEST(UploadServiceTest, LogKernelCrash); + FRIEND_TEST(UploadServiceTest, LogUncleanShutdown); + FRIEND_TEST(UploadServiceTest, LogUserCrash); + FRIEND_TEST(UploadServiceTest, UnknownCrashIgnored); + FRIEND_TEST(UploadServiceTest, ValuesInConfigFileAreSent); + + // Private constructor for use in unit testing. + UploadService(SystemProfileSetter* setter, + MetricsLibraryInterface* metrics_lib, + const std::string& server, + bool testing); + + // If a staged log fails to upload more than kMaxFailedUpload times, it + // will be discarded. + static const int kMaxFailedUpload; + + // Resets the internal state. + void Reset(); + + // Reads all the metrics from the disk. + void ReadMetrics(); + + // Adds a generic sample to the current log. + void AddSample(const metrics::MetricSample& sample); + + // Adds a crash to the current log. + void AddCrash(const std::string& crash_name); + + // Aggregates all histogram available in memory and store them in the current + // log. + void GatherHistograms(); + + // Callback for HistogramSnapshotManager to store the histograms. + void RecordDelta(const base::HistogramBase& histogram, + const base::HistogramSamples& snapshot) override; + + // Compiles all the samples received into a single protobuf and adds all + // system information. + void StageCurrentLog(); + + // Returns the current log. If there is no current log, creates it first. + MetricsLog* GetOrCreateCurrentLog(); + + scoped_ptr<SystemProfileSetter> system_profile_setter_; + MetricsLibraryInterface* metrics_lib_; + base::HistogramSnapshotManager histogram_snapshot_manager_; + scoped_ptr<Sender> sender_; + int failed_upload_count_; + scoped_ptr<MetricsLog> current_log_; + scoped_ptr<MetricsLog> staged_log_; + + std::string metrics_file_; + + bool testing_; +}; + +#endif // METRICS_UPLOADER_UPLOAD_SERVICE_H_ diff --git a/metrics/uploader/upload_service_test.cc b/metrics/uploader/upload_service_test.cc new file mode 100644 index 000000000..ee17e1533 --- /dev/null +++ b/metrics/uploader/upload_service_test.cc @@ -0,0 +1,257 @@ +// Copyright 2014 The Chromium OS 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 <gtest/gtest.h> + +#include "base/at_exit.h" +#include "base/files/file_util.h" +#include "base/files/scoped_temp_dir.h" +#include "base/logging.h" +#include "base/sys_info.h" +#include "metrics/metrics_library_mock.h" +#include "metrics/serialization/metric_sample.h" +#include "metrics/uploader/metrics_log.h" +#include "metrics/uploader/mock/mock_system_profile_setter.h" +#include "metrics/uploader/mock/sender_mock.h" +#include "metrics/uploader/proto/chrome_user_metrics_extension.pb.h" +#include "metrics/uploader/proto/histogram_event.pb.h" +#include "metrics/uploader/proto/system_profile.pb.h" +#include "metrics/uploader/system_profile_cache.h" +#include "metrics/uploader/upload_service.h" + +static const char kMetricsServer[] = "https://clients4.google.com/uma/v2"; +static const char kMetricsFilePath[] = "/var/run/metrics/uma-events"; + +class UploadServiceTest : public testing::Test { + protected: + UploadServiceTest() + : cache_(true, "/"), + upload_service_(new MockSystemProfileSetter(), &metrics_lib_, + kMetricsServer, true), + exit_manager_(new base::AtExitManager()) { + sender_ = new SenderMock; + upload_service_.sender_.reset(sender_); + upload_service_.Init(base::TimeDelta::FromMinutes(30), kMetricsFilePath); + } + + virtual void SetUp() { + CHECK(dir_.CreateUniqueTempDir()); + upload_service_.GatherHistograms(); + upload_service_.Reset(); + sender_->Reset(); + + chromeos_metrics::PersistentInteger::SetTestingMode(true); + cache_.session_id_.reset(new chromeos_metrics::PersistentInteger( + dir_.path().Append("session_id").value())); + } + + scoped_ptr<metrics::MetricSample> Crash(const std::string& name) { + return metrics::MetricSample::CrashSample(name); + } + + base::ScopedTempDir dir_; + SenderMock* sender_; + SystemProfileCache cache_; + UploadService upload_service_; + MetricsLibraryMock metrics_lib_; + + scoped_ptr<base::AtExitManager> exit_manager_; +}; + +// Tests that the right crash increments a values. +TEST_F(UploadServiceTest, LogUserCrash) { + upload_service_.AddSample(*Crash("user").get()); + + MetricsLog* log = upload_service_.current_log_.get(); + metrics::ChromeUserMetricsExtension* proto = log->uma_proto(); + + EXPECT_EQ(1, proto->system_profile().stability().other_user_crash_count()); +} + +TEST_F(UploadServiceTest, LogUncleanShutdown) { + upload_service_.AddSample(*Crash("uncleanshutdown")); + + EXPECT_EQ(1, upload_service_.current_log_ + ->uma_proto() + ->system_profile() + .stability() + .unclean_system_shutdown_count()); +} + +TEST_F(UploadServiceTest, LogKernelCrash) { + upload_service_.AddSample(*Crash("kernel")); + + EXPECT_EQ(1, upload_service_.current_log_ + ->uma_proto() + ->system_profile() + .stability() + .kernel_crash_count()); +} + +TEST_F(UploadServiceTest, UnknownCrashIgnored) { + upload_service_.AddSample(*Crash("foo")); + + // The log should be empty. + EXPECT_FALSE(upload_service_.current_log_); +} + +TEST_F(UploadServiceTest, FailedSendAreRetried) { + sender_->set_should_succeed(false); + + upload_service_.AddSample(*Crash("user")); + upload_service_.UploadEvent(); + EXPECT_EQ(1, sender_->send_call_count()); + std::string sent_string = sender_->last_message(); + + upload_service_.UploadEvent(); + EXPECT_EQ(2, sender_->send_call_count()); + EXPECT_EQ(sent_string, sender_->last_message()); +} + +TEST_F(UploadServiceTest, DiscardLogsAfterTooManyFailedUpload) { + sender_->set_should_succeed(false); + upload_service_.AddSample(*Crash("user")); + + for (int i = 0; i < UploadService::kMaxFailedUpload; i++) { + upload_service_.UploadEvent(); + } + + EXPECT_TRUE(upload_service_.staged_log_); + upload_service_.UploadEvent(); + EXPECT_FALSE(upload_service_.staged_log_); +} + +TEST_F(UploadServiceTest, EmptyLogsAreNotSent) { + upload_service_.UploadEvent(); + EXPECT_FALSE(upload_service_.current_log_); + EXPECT_EQ(0, sender_->send_call_count()); +} + +TEST_F(UploadServiceTest, LogEmptyByDefault) { + UploadService upload_service(new MockSystemProfileSetter(), &metrics_lib_, + kMetricsServer); + + // current_log_ should be initialized later as it needs AtExitManager to exit + // in order to gather system information from SysInfo. + EXPECT_FALSE(upload_service.current_log_); +} + +TEST_F(UploadServiceTest, CanSendMultipleTimes) { + upload_service_.AddSample(*Crash("user")); + upload_service_.UploadEvent(); + + std::string first_message = sender_->last_message(); + + upload_service_.AddSample(*Crash("kernel")); + upload_service_.UploadEvent(); + + EXPECT_NE(first_message, sender_->last_message()); +} + +TEST_F(UploadServiceTest, LogEmptyAfterUpload) { + upload_service_.AddSample(*Crash("user")); + + EXPECT_TRUE(upload_service_.current_log_); + + upload_service_.UploadEvent(); + EXPECT_FALSE(upload_service_.current_log_); +} + +TEST_F(UploadServiceTest, LogContainsAggregatedValues) { + scoped_ptr<metrics::MetricSample> histogram = + metrics::MetricSample::HistogramSample("foo", 10, 0, 42, 10); + upload_service_.AddSample(*histogram.get()); + + + scoped_ptr<metrics::MetricSample> histogram2 = + metrics::MetricSample::HistogramSample("foo", 11, 0, 42, 10); + upload_service_.AddSample(*histogram2.get()); + + upload_service_.GatherHistograms(); + metrics::ChromeUserMetricsExtension* proto = + upload_service_.current_log_->uma_proto(); + EXPECT_EQ(1, proto->histogram_event().size()); +} + +TEST_F(UploadServiceTest, ExtractChannelFromString) { + EXPECT_EQ( + SystemProfileCache::ProtoChannelFromString( + "developer-build"), + metrics::SystemProfileProto::CHANNEL_UNKNOWN); + + EXPECT_EQ(metrics::SystemProfileProto::CHANNEL_DEV, + SystemProfileCache::ProtoChannelFromString("dev-channel")); + + EXPECT_EQ(metrics::SystemProfileProto::CHANNEL_UNKNOWN, + SystemProfileCache::ProtoChannelFromString("dev-channel test")); +} + +TEST_F(UploadServiceTest, ValuesInConfigFileAreSent) { + std::string name("os name"); + std::string content( + "CHROMEOS_RELEASE_NAME=" + name + + "\nCHROMEOS_RELEASE_VERSION=version\n" + "CHROMEOS_RELEASE_DESCRIPTION=description beta-channel test\n" + "CHROMEOS_RELEASE_TRACK=beta-channel\n" + "CHROMEOS_RELEASE_BUILD_TYPE=developer build\n" + "CHROMEOS_RELEASE_BOARD=myboard"); + + base::SysInfo::SetChromeOSVersionInfoForTest(content, base::Time()); + scoped_ptr<metrics::MetricSample> histogram = + metrics::MetricSample::SparseHistogramSample("myhistogram", 1); + SystemProfileCache* local_cache_ = new SystemProfileCache(true, "/"); + local_cache_->session_id_.reset(new chromeos_metrics::PersistentInteger( + dir_.path().Append("session_id").value())); + + upload_service_.system_profile_setter_.reset(local_cache_); + // Reset to create the new log with the profile setter. + upload_service_.Reset(); + upload_service_.AddSample(*histogram.get()); + upload_service_.UploadEvent(); + + EXPECT_EQ(1, sender_->send_call_count()); + EXPECT_TRUE(sender_->is_good_proto()); + EXPECT_EQ(1, sender_->last_message_proto().histogram_event().size()); + + EXPECT_EQ(name, sender_->last_message_proto().system_profile().os().name()); + EXPECT_EQ(metrics::SystemProfileProto::CHANNEL_BETA, + sender_->last_message_proto().system_profile().channel()); + EXPECT_NE(0, sender_->last_message_proto().client_id()); + EXPECT_NE(0, + sender_->last_message_proto().system_profile().build_timestamp()); + EXPECT_NE(0, sender_->last_message_proto().session_id()); +} + +TEST_F(UploadServiceTest, PersistentGUID) { + std::string tmp_file = dir_.path().Append("tmpfile").value(); + + std::string first_guid = SystemProfileCache::GetPersistentGUID(tmp_file); + std::string second_guid = SystemProfileCache::GetPersistentGUID(tmp_file); + + // The GUID are cached. + EXPECT_EQ(first_guid, second_guid); + + base::DeleteFile(base::FilePath(tmp_file), false); + + first_guid = SystemProfileCache::GetPersistentGUID(tmp_file); + base::DeleteFile(base::FilePath(tmp_file), false); + second_guid = SystemProfileCache::GetPersistentGUID(tmp_file); + + // Random GUIDs are generated (not all the same). + EXPECT_NE(first_guid, second_guid); +} + +TEST_F(UploadServiceTest, SessionIdIncrementedAtInitialization) { + cache_.Initialize(); + int session_id = cache_.profile_.session_id; + cache_.initialized_ = false; + cache_.Initialize(); + EXPECT_EQ(cache_.profile_.session_id, session_id + 1); +} + +int main(int argc, char** argv) { + testing::InitGoogleTest(&argc, argv); + + return RUN_ALL_TESTS(); +} |