blob: 4aa5009b42768495e696d162faa3a9b683343242 [file] [log] [blame]
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/public/platform/media/power_status_helper.h"
#include <utility>
#include "base/check.h"
#include "base/check_op.h"
#include "base/metrics/histogram_macros.h"
#include "media/base/pipeline_metadata.h"
#include "services/device/public/mojom/battery_status.mojom.h"
namespace blink {
namespace {
using ::device::mojom::BatteryStatusPtr;
static constexpr const char* kBatteryDeltaHistogram =
"Media.PlaybackPower.BatteryDelta";
static constexpr const char* kElapsedTimeHistogram =
"Media.PlaybackPower.ElapsedTime";
// Minimum enum value that we'll generate, inclusive.
static constexpr int kMinEnumValue = 0;
// Maximum enum value that we'll generate, inclusive.
static constexpr int kMaxEnumValue =
PowerStatusHelper::Bits::kCodecBitsH264 |
PowerStatusHelper::Bits::kCodecBitsVP9Profile0 |
PowerStatusHelper::Bits::kCodecBitsVP9Profile2 |
PowerStatusHelper::Bits::kResolution360p |
PowerStatusHelper::Bits::kResolution720p |
PowerStatusHelper::Bits::kResolution1080p |
PowerStatusHelper::Bits::kFrameRate30 |
PowerStatusHelper::Bits::kFrameRate60 |
PowerStatusHelper::Bits::kFullScreenNo |
PowerStatusHelper::Bits::kFullScreenYes;
// UMA buckets are always [uma_min, uma_max). The first bucket is an implicit
// underflow bucket [0, uma_min), and the last is the overflow bucket
// [uma_max, infinity). The underflow bucket isn't counted, but the overflow
// bucket is.
// Minimum bucket number. Since we always get an underflow bucket, we choose
// the minimum bucket to be one higher than the minimum enum value. That way.
// the minimum enum goes to the underflow bucket.
// //tools/metrics/histograms/README.md#count-histograms_choosing-min-and-max
// for details.
static constexpr int kMinUmaValue = kMinEnumValue + 1;
// We want to avoid using the overflow bucket. See
// //tools/metrics/histograms/README.md#count-histograms_choosing-min-and-max
// and UMA_HISTOGRAM_EXACT_LINEAR for details.
// Max value as reported to UMA, which is the lower bound of the overflow
// bucket. Add one, since we want the overflow bucket to be unused.
static constexpr int kMaxUmaValue = kMaxEnumValue + 1;
// Number of buckets we want, which includes the overflow bucket but not the
// implicit underflow bucket.
// NOTE: We add two here, else we don't quite get enough buckets. This was
// emperically determined by checking Histogram::bucket_ranges(). It might
// be the case that we should subtract one here and from |kMaxUmaValue|, but
// either way, i think it works out. We just have one unused bucket at worst,
// which won't be renumbered even if we start using it later.
static constexpr int kNumUmaBuckets = kMaxUmaValue - kMinUmaValue + 2;
// For example, if kMinEnum == 0 and kMaxEnum == 5 (inclusive), then:
// kMinUma = 1 (0 is implicit), kMaxUma = 6, and we'll want the implicit
// underflow bucket plus kNumUmaBuckets = 6 more.
// [0, 1) << implicit underflow bucket, kMinEnum goes here
// [1, 2)
// [2, 3)
// [3, 4)
// [4, 5)
// [5, 6) << kMaxEnum goes here
// [6, infinity) << unused overflow bucket.
// We can have more, but shouldn't without talking to metrics folks.
static_assert(kNumUmaBuckets < 100, "Too many buckets");
} // namespace
PowerStatusHelper::PowerStatusHelper(
CreateBatteryMonitorCB create_battery_monitor_cb)
: create_battery_monitor_cb_(std::move(create_battery_monitor_cb)) {}
PowerStatusHelper::~PowerStatusHelper() = default;
// static
absl::optional<int> PowerStatusHelper::BucketFor(
bool is_playing,
bool has_video,
media::VideoCodec codec,
media::VideoCodecProfile profile,
gfx::Size natural_size,
bool is_fullscreen,
absl::optional<int> average_fps) {
if (!is_playing)
return {};
if (!has_video)
return {};
int bucket = 0;
if (codec == media::VideoCodec::kCodecH264)
bucket |= Bits::kCodecBitsH264;
else if (profile == media::VP9PROFILE_PROFILE0)
bucket |= Bits::kCodecBitsVP9Profile0;
else if (profile == media::VP9PROFILE_PROFILE2)
bucket |= Bits::kCodecBitsVP9Profile2;
else
return {};
// We could take into account rotation, but ignore it for now.
if (natural_size == gfx::Size(640, 360))
bucket |= kResolution360p;
else if (natural_size == gfx::Size(1280, 720))
bucket |= kResolution720p;
else if (natural_size == gfx::Size(1920, 1080))
bucket |= kResolution1080p;
else
return {};
// Estimate the frame rate. Since 24 is popular, allow a wide range around
// 30fps, since it's likely the same for power.
if (!average_fps)
return {};
else if (*average_fps == 60)
bucket |= kFrameRate60;
else if (*average_fps >= 24 && *average_fps <= 30)
bucket |= kFrameRate30;
else
return {};
bucket |= is_fullscreen ? kFullScreenYes : kFullScreenNo;
return bucket;
}
// static
const char* PowerStatusHelper::BatteryDeltaHistogram() {
return kBatteryDeltaHistogram;
}
// static
const char* PowerStatusHelper::ElapsedTimeHistogram() {
return kElapsedTimeHistogram;
}
void PowerStatusHelper::SetIsPlaying(bool is_playing) {
is_playing_ = is_playing;
OnAnyStateChange();
}
void PowerStatusHelper::SetMetadata(const media::PipelineMetadata& metadata) {
has_video_ = metadata.has_video;
codec_ = metadata.video_decoder_config.codec();
profile_ = metadata.video_decoder_config.profile();
natural_size_ = metadata.video_decoder_config.natural_size();
OnAnyStateChange();
}
void PowerStatusHelper::SetIsFullscreen(bool is_fullscreen) {
is_fullscreen_ = is_fullscreen;
OnAnyStateChange();
}
void PowerStatusHelper::SetAverageFrameRate(absl::optional<int> average_fps) {
average_fps_ = average_fps;
OnAnyStateChange();
}
void PowerStatusHelper::UpdatePowerExperimentState(bool state) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
experiment_state_ = state;
OnAnyStateChange();
}
void PowerStatusHelper::OnAnyStateChange() {
absl::optional<int> old_bucket = current_bucket_;
current_bucket_.reset();
// If we're the power experiment, then we might have a bucket. Else, we
// definitely don't.
if (experiment_state_) {
current_bucket_ = BucketFor(is_playing_, has_video_, codec_, profile_,
natural_size_, is_fullscreen_, average_fps_);
}
// If we're changing buckets, then request power updates with a new generation
// id. This lets us separate readings from the old bucket.
if (current_bucket_ && (!old_bucket || *current_bucket_ != *old_bucket)) {
// Also reset the baseline, in case we're changing buckets. We don't want
// to include any battery drain that should have been in the first bucket.
StartMonitoring();
} else if (old_bucket && !current_bucket_) {
// We don't need power updates, but we had them before.
StopMonitoring();
}
}
void PowerStatusHelper::OnBatteryStatus(
device::mojom::BatteryStatusPtr battery_status) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
QueryNextStatus();
if (battery_status->charging) {
// If we're charging, then wait until we stop. Take a new baseline then.
battery_level_baseline_.reset();
return;
}
// Compute the amount of time since our last update. Note that, if this is
// the first status update since we (re)started monitoring, then the baseline
// should be unset, so |elapsed| will be ignored. That's good, since it could
// be quite far in the past since we've had an update.
const base::TimeTicks now = base::TimeTicks::Now();
// Convert to floating point 0-100 from 0-1.
const float current_level = battery_status->level * 100;
// If we don't have a baseline, then use |current_level| and |now|. In the
// future, we might want to wait until the battery drain is reported twice,
// since we don't know how much of a fractional percent remains in this
// initial baseline. For now, just ignore that.
if (!battery_level_baseline_) {
battery_level_baseline_ = current_level;
last_update_ = now;
return;
}
// Second or later update since we started monitoring / stopped charging.
// Compute the battery used. Note that positive numbers indicate that the
// battery has gone down.
const float delta = *battery_level_baseline_ - current_level;
DCHECK(current_bucket_);
DCHECK_GE(delta, 0.);
// See if we can record some nonzero battery drain and elapsed time, when
// converted to int. We can only record ints in UMA.
const int delta_int = static_cast<int>(delta);
const base::TimeDelta elapsed = now - last_update_;
const int elapsed_msec = elapsed.InMilliseconds();
if (delta_int > 0 && elapsed_msec > 0) {
// Record that we consumed |delta_int| battery percent in |elapsed_msec|.
base::LinearHistogram::FactoryGet(
BatteryDeltaHistogram(), kMinUmaValue, kMaxUmaValue, kNumUmaBuckets,
base::HistogramBase::kUmaTargetedHistogramFlag)
->AddCount(*current_bucket_, delta_int);
base::LinearHistogram::FactoryGet(
ElapsedTimeHistogram(), kMinUmaValue, kMaxUmaValue, kNumUmaBuckets,
base::HistogramBase::kUmaTargetedHistogramFlag)
->AddCount(*current_bucket_, elapsed_msec);
// Update the baseline to |current_level|, but include any fractional
// unrecorded amount so that we can record it later.
battery_level_baseline_ = current_level + (delta - delta_int);
// Don't bother remembering any fractional msec.
last_update_ = now;
}
}
void PowerStatusHelper::StartMonitoring() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!battery_monitor_.is_bound()) {
auto pending = create_battery_monitor_cb_.Run();
if (!pending.is_valid())
return;
battery_monitor_.Bind(std::move(pending));
// In case it's not available for some reason, do nothing.
if (!battery_monitor_.is_bound())
return;
// Start querying for status as long as we're connected.
QueryNextStatus();
}
// Any baseline that we had should be reset, since we're called to start or
// restart monitoring when our bucket changes.
battery_level_baseline_.reset();
}
void PowerStatusHelper::StopMonitoring() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
battery_monitor_.reset();
}
void PowerStatusHelper::QueryNextStatus() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(battery_monitor_.is_bound());
// Remember that overlapping calls are not allowed by BatteryMonitor, and are
// treated as a connection error. Unretained since we own |battery_monitor_|.
battery_monitor_->QueryNextStatus(base::BindOnce(
&PowerStatusHelper::OnBatteryStatus, base::Unretained(this)));
}
} // namespace blink