chromium/chrome/browser/metrics/perf/metric_collector.cc

// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/metrics/perf/metric_collector.h"

#include <utility>

#include "base/functional/bind.h"
#include "base/metrics/histogram_functions.h"
#include "base/rand_util.h"
#include "base/system/sys_info.h"
#include "third_party/metrics_proto/sampled_profile.pb.h"

namespace metrics {

namespace internal {

namespace {

// Name prefix of the histogram that represents the success and various failure
// modes for a collector.
const char kCollectionOutcomeHistogramPrefix[] = "ChromeOS.CWP.Collect";

// This is used to space out session restore collections in the face of several
// notifications in a short period of time. There should be no less than this
// much time between collections.
const int kMinIntervalBetweenSessionRestoreCollectionsInSec = 30;

// Returns a random TimeDelta uniformly selected between zero and |max|.
base::TimeDelta RandTimeDelta(base::TimeDelta max) {
  return max.is_positive() ? base::RandTimeDeltaUpTo(max) : max;
}

// PerfDataProto is defined elsewhere with more fields than the definition in
// Chromium's copy of perf_data.proto. During deserialization, the protobuf
// data could contain fields that are defined elsewhere but not in
// perf_data.proto, resulting in some data in |unknown_fields| for the message
// types within PerfDataProto.
//
// This function deletes those dangling unknown fields if they are in messages
// containing strings. See comments in perf_data.proto describing the fields
// that have been intentionally left out. Note that all unknown fields will be
// removed from those messages, not just unknown string fields.
void RemoveUnknownFieldsFromMessagesWithStrings(PerfDataProto* proto) {
  // Clean up PerfEvent::MMapEvent and PerfEvent::CommEvent.
  for (PerfDataProto::PerfEvent& event : *proto->mutable_events()) {
    if (event.has_comm_event())
      event.mutable_comm_event()->mutable_unknown_fields()->clear();
    if (event.has_mmap_event())
      event.mutable_mmap_event()->mutable_unknown_fields()->clear();
  }
  // Clean up PerfBuildID.
  for (PerfDataProto::PerfBuildID& build_id : *proto->mutable_build_ids()) {
    build_id.mutable_unknown_fields()->clear();
  }
  // Clean up StringMetadata and StringMetadata::StringAndMd5sumPrefix.
  if (proto->has_string_metadata()) {
    proto->mutable_string_metadata()->mutable_unknown_fields()->clear();
    if (proto->string_metadata().has_perf_command_line_whole()) {
      proto->mutable_string_metadata()
          ->mutable_perf_command_line_whole()
          ->mutable_unknown_fields()
          ->clear();
    }
  }
  for (PerfDataProto::PerfEventType& event_type :
       *proto->mutable_event_types()) {
    event_type.mutable_unknown_fields()->clear();
  }
  for (PerfDataProto::PerfPMUMappingsMetadata& mapping :
       *proto->mutable_pmu_mappings()) {
    mapping.mutable_unknown_fields()->clear();
  }
  proto->mutable_unknown_fields()->clear();
}

}  // namespace

MetricCollector::MetricCollector(const std::string& name,
                                 const CollectionParams& collection_params)
    : collection_params_(collection_params),
      collect_uma_histogram_(std::string(kCollectionOutcomeHistogramPrefix) +
                             name) {
  // Allow rebinding |sequence_checker_| to the sequence the collector runs on.
  DETACH_FROM_SEQUENCE(sequence_checker_);
}

MetricCollector::~MetricCollector() = default;

void MetricCollector::Init() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  SetUp();
}

void MetricCollector::AddCachedDataDelta(size_t delta) {
  cached_data_size_ += delta;
}

void MetricCollector::ResetCachedDataSize() {
  cached_data_size_ = 0;
}

void MetricCollector::RecordUserLogin(base::TimeTicks login_time) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  login_time_ = login_time;
  next_profiling_interval_start_ = login_time;
  ScheduleIntervalCollection();
}
void MetricCollector::StopTimer() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  timer_.AbandonAndStop();
}

void MetricCollector::ScheduleSuspendDoneCollection(
    base::TimeDelta sleep_duration) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  // Collect a profile only 1/|sampling_factor| of the time, to avoid
  // collecting too much data. (0 means disable the trigger)
  const auto& resume_params = collection_params_.resume_from_suspend;
  if (resume_params.sampling_factor == 0 ||
      base::RandGenerator(resume_params.sampling_factor) != 0)
    return;

  // Override any existing profiling.
  if (timer_.IsRunning())
    timer_.Stop();

  // Randomly pick a delay before doing the collection.
  base::TimeDelta collection_delay =
      RandTimeDelta(resume_params.max_collection_delay);
  timer_.Start(FROM_HERE, collection_delay,
               base::BindOnce(&MetricCollector::CollectPerfDataAfterResume,
                              GetWeakPtr(), sleep_duration, collection_delay));
}

void MetricCollector::OnJankStarted() {
  // Fill out a SampledProfile protobuf that will contain the collected data.
  auto sampled_profile = std::make_unique<SampledProfile>();
  sampled_profile->set_trigger_event(SampledProfile::JANKY_TASK);

  CollectIfNecessary(std::move(sampled_profile));
}

void MetricCollector::OnJankStopped() {
  StopCollection();
}

void MetricCollector::ScheduleSessionRestoreCollection(int num_tabs_restored) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  // Collect a profile only 1/|sampling_factor| of the time, to avoid
  // collecting too much data. (0 means disable the trigger)
  const auto& restore_params = collection_params_.restore_session;
  if (restore_params.sampling_factor == 0 ||
      base::RandGenerator(restore_params.sampling_factor) != 0) {
    return;
  }

  const auto min_interval =
      base::Seconds(kMinIntervalBetweenSessionRestoreCollectionsInSec);
  const base::TimeDelta time_since_last_collection =
      (base::TimeTicks::Now() - last_session_restore_collection_time_);
  // Do not collect if there hasn't been enough elapsed time since the last
  // collection.
  if (!last_session_restore_collection_time_.is_null() &&
      time_since_last_collection < min_interval) {
    return;
  }

  // Stop any existing scheduled collection.
  if (timer_.IsRunning())
    timer_.Stop();

  // Randomly pick a delay before doing the collection.
  base::TimeDelta collection_delay =
      RandTimeDelta(restore_params.max_collection_delay);
  timer_.Start(
      FROM_HERE, collection_delay,
      base::BindOnce(&MetricCollector::CollectPerfDataAfterSessionRestore,
                     GetWeakPtr(), collection_delay, num_tabs_restored));
}

void MetricCollector::AddToUmaHistogram(CollectionAttemptStatus outcome) const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  base::UmaHistogramEnumeration(collect_uma_histogram_, outcome,
                                CollectionAttemptStatus::NUM_OUTCOMES);
}

void MetricCollector::CollectPerfDataAfterResume(
    base::TimeDelta sleep_duration,
    base::TimeDelta time_after_resume) {
  // Fill out a SampledProfile protobuf that will contain the collected data.
  auto sampled_profile = std::make_unique<SampledProfile>();
  sampled_profile->set_trigger_event(SampledProfile::RESUME_FROM_SUSPEND);
  sampled_profile->set_suspend_duration_ms(sleep_duration.InMilliseconds());
  sampled_profile->set_ms_after_resume(time_after_resume.InMilliseconds());

  CollectIfNecessary(std::move(sampled_profile));
}

void MetricCollector::CollectPerfDataAfterSessionRestore(
    base::TimeDelta time_after_restore,
    int num_tabs_restored) {
  // Fill out a SampledProfile protobuf that will contain the collected data.
  auto sampled_profile = std::make_unique<SampledProfile>();
  sampled_profile->set_trigger_event(SampledProfile::RESTORE_SESSION);
  sampled_profile->set_ms_after_restore(time_after_restore.InMilliseconds());
  sampled_profile->set_num_tabs_restored(num_tabs_restored);

  CollectIfNecessary(std::move(sampled_profile));
  last_session_restore_collection_time_ = base::TimeTicks::Now();
}

void MetricCollector::ScheduleIntervalCollection() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (timer_.IsRunning())
    return;
  // Schedule periodic collection only if periodic_interval is non-zero. A value
  // of zero is the escape hatch for turning periodic collection off via Finch.
  if (collection_params_.periodic_interval.is_zero())
    return;

  const base::TimeTicks now = base::TimeTicks::Now();
  base::TimeTicks interval_end =
      next_profiling_interval_start_ + collection_params_.periodic_interval;
  if (now > interval_end) {
    // We somehow missed at least one window. Start over.
    next_profiling_interval_start_ = now;
    interval_end = now + collection_params_.periodic_interval;
  }

  // Pick a random time in the current interval.
  base::TimeTicks scheduled_time =
      next_profiling_interval_start_ +
      RandTimeDelta(collection_params_.periodic_interval);
  // If the scheduled time has already passed in the time it took to make the
  // above calculations, trigger the collection event immediately.
  if (scheduled_time < now)
    scheduled_time = now;

  timer_.Start(
      FROM_HERE, scheduled_time - now,
      base::BindOnce(&MetricCollector::DoPeriodicCollection, GetWeakPtr()));

  // Update the profiling interval tracker to the start of the next interval.
  next_profiling_interval_start_ = interval_end;
}

void MetricCollector::DoPeriodicCollection() {
  auto sampled_profile = std::make_unique<SampledProfile>();
  sampled_profile->set_trigger_event(SampledProfile::PERIODIC_COLLECTION);

  CollectIfNecessary(std::move(sampled_profile));
}

void MetricCollector::CollectIfNecessary(
    std::unique_ptr<SampledProfile> sampled_profile) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (ShouldCollect()) {
    // Do the actual profile collection.
    CollectProfile(std::move(sampled_profile));
  }

  // Schedule another interval collection. This call makes sense regardless of
  // whether or not the current collection was interval-triggered. If it had
  // been another type of trigger event, the interval timer would have been
  // halted, so it makes sense to reschedule a new interval collection.
  ScheduleIntervalCollection();
}

bool MetricCollector::ShouldCollect() const {
  return true;
}

void MetricCollector::SaveSerializedPerfProto(
    std::unique_ptr<SampledProfile> sampled_profile,
    std::string serialized_proto) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (serialized_proto.empty()) {
    AddToUmaHistogram(CollectionAttemptStatus::ILLEGAL_DATA_RETURNED);
    return;
  }

  PerfDataProto perf_data_proto;
  if (!perf_data_proto.ParseFromString(serialized_proto)) {
    AddToUmaHistogram(CollectionAttemptStatus::PROTOBUF_NOT_PARSED);
    return;
  }
  // Don't save the profile if there are no samples.
  if (perf_data_proto.stats().num_sample_events() == 0) {
    AddToUmaHistogram(CollectionAttemptStatus::SESSION_HAS_ZERO_SAMPLES);
    return;
  }
  RemoveUnknownFieldsFromMessagesWithStrings(&perf_data_proto);
  sampled_profile->mutable_perf_data()->Swap(&perf_data_proto);

  sampled_profile->set_ms_after_boot(base::SysInfo::Uptime().InMilliseconds());
  DCHECK(!login_time_.is_null());
  sampled_profile->set_ms_after_login(
      (base::TimeTicks::Now() - login_time_).InMilliseconds());

  // Run |profile_done_callback_| on success.
  AddToUmaHistogram(CollectionAttemptStatus::SUCCESS);
  profile_done_callback_.Run(std::move(sampled_profile));
}

}  // namespace internal

}  // namespace metrics