blob: 907358012341c9e65633271163ad8385bc80deb2 [file] [log] [blame]
// Copyright 2017 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/renderer/modules/media_controls/elements/media_control_timeline_metrics.h"
#include <stdint.h>
#include <cmath>
#include <limits>
#include "base/numerics/safe_conversions.h"
#include "base/stl_util.h"
#include "third_party/blink/renderer/platform/keyboard_codes.h"
#include "third_party/blink/renderer/platform/wtf/std_lib_extras.h"
namespace blink {
namespace {
// Correponds to UMA MediaTimelineSeekType enum. Enum values can be added, but
// must never be renumbered or deleted and reused.
enum class SeekType {
kClick = 0,
kDragFromCurrentPosition = 1,
kDragFromElsewhere = 2,
kKeyboardArrowKey = 3,
kKeyboardPageUpDownKey = 4,
kKeyboardHomeEndKey = 5,
// Update kLast when adding new values.
kLast = kKeyboardHomeEndKey
};
// Exclusive upper bounds for the positive buckets of UMA MediaTimelinePercent
// enum, which are reflected to form the negative buckets. The custom enum is
// because UMA count histograms don't support negative values. Values must not
// be added/modified/removed due to the way the negative buckets are formed.
constexpr double kPercentIntervals[] = {
0, // Dedicated zero bucket so upper bound is inclusive, unlike the others.
0.1, 0.2, 0.3, 0.5, 0.7, 1.0, 1.5, 2.0, 3.0, 5.0, 7.0, 10.0,
15.0, 20.0, 25.0, 30.0, 35.0, 40.0, 45.0, 50.0, 60.0, 70.0, 80.0, 90.0,
100.0 // 100% upper bound is inclusive, unlike the others.
};
// Must match length of UMA MediaTimelinePercent enum.
constexpr int32_t kPercentBucketCount = 51;
static_assert(base::size(kPercentIntervals) * 2 - 1 == kPercentBucketCount,
"Intervals must match UMA MediaTimelinePercent enum");
// Corresponds to two UMA enums of different sizes! Values are the exclusive
// upper bounds for buckets of UMA MediaTimelineAbsTimeDelta enum with the same
// index, and also for the positive buckets of UMA MediaTimelineTimeDelta enum,
// which are reflected to form the negative buckets. MediaTimelineTimeDelta
// needed a custom enum because UMA count histograms don't support negative
// values, and MediaTimelineAbsTimeDelta uses the same mechanism so the values
// can be compared easily. Values must not be added/modified/removed due to the
// way the negative buckets are formed.
constexpr double kTimeDeltaMSIntervals[] = {
1, // 1ms
16, // 16ms
32, // 32ms
64, // 64ms
128, // 128ms
256, // 256ms
512, // 512ms
1000, // 1s
2000, // 2s
4000, // 4s
8000, // 8s
15000, // 15s
30000, // 30s
60000, // 1m
120000, // 2m
240000, // 4m
480000, // 8m
900000, // 15m
1800000, // 30m
3600000, // 1h
7200000, // 2h
14400000, // 4h
28800000, // 8h
57600000, // 16h
std::numeric_limits<double>::infinity()};
// Must match length of UMA MediaTimelineAbsTimeDelta enum.
constexpr int32_t kAbsTimeDeltaBucketCount = 25;
// Must match length of UMA MediaTimelineTimeDelta enum.
constexpr int32_t kTimeDeltaBucketCount = 49;
static_assert(base::size(kTimeDeltaMSIntervals) == kAbsTimeDeltaBucketCount,
"Intervals must match UMA MediaTimelineAbsTimeDelta enum");
static_assert(base::size(kTimeDeltaMSIntervals) * 2 - 1 ==
kTimeDeltaBucketCount,
"Intervals must match UMA MediaTimelineTimeDelta enum");
// Calculates index of UMA MediaTimelinePercent enum corresponding to |percent|.
// Negative values use kPercentIntervals in reverse.
int32_t ToPercentSample(double percent) {
constexpr int32_t kNonNegativeBucketCount = base::size(kPercentIntervals);
constexpr int32_t kNegativeBucketCount = base::size(kPercentIntervals) - 1;
bool negative = percent < 0;
double abs_percent = std::abs(percent);
if (abs_percent == 0)
return kNegativeBucketCount; // Dedicated zero bucket.
for (int32_t i = 0; i < kNonNegativeBucketCount; i++) {
if (abs_percent < kPercentIntervals[i])
return kNegativeBucketCount + (negative ? -i : +i);
}
// No NOTREACHED since the +/-100 bounds are inclusive (even if they are
// slightly exceeded due to floating point inaccuracies).
return negative ? 0 : kPercentBucketCount - 1;
}
// Calculates index of UMA MediaTimelineAbsTimeDelta enum corresponding to
// |sumAbsDeltaSeconds|.
int32_t ToAbsTimeDeltaSample(double sum_abs_delta_seconds) {
double sum_abs_delta_ms = 1000 * sum_abs_delta_seconds;
if (sum_abs_delta_ms == 0)
return 0; // Dedicated zero bucket.
for (int32_t i = 0; i < kAbsTimeDeltaBucketCount; i++) {
if (sum_abs_delta_ms < kTimeDeltaMSIntervals[i])
return i;
}
NOTREACHED() << "sumAbsDeltaSeconds shouldn't be infinite";
return kAbsTimeDeltaBucketCount - 1;
}
// Calculates index of UMA MediaTimelineTimeDelta enum corresponding to
// |deltaSeconds|. Negative values use kTimeDeltaMSIntervals in reverse.
int32_t ToTimeDeltaSample(double delta_seconds) {
constexpr int32_t kNonNegativeBucketCount = base::size(kTimeDeltaMSIntervals);
constexpr int32_t kNegativeBucketCount =
base::size(kTimeDeltaMSIntervals) - 1;
bool negative = delta_seconds < 0;
double abs_delta_ms = 1000 * std::abs(delta_seconds);
if (abs_delta_ms == 0)
return kNegativeBucketCount; // Dedicated zero bucket.
for (int32_t i = 0; i < kNonNegativeBucketCount; i++) {
if (abs_delta_ms < kTimeDeltaMSIntervals[i])
return kNegativeBucketCount + (negative ? -i : +i);
}
NOTREACHED() << "deltaSeconds shouldn't be infinite";
return negative ? 0 : kTimeDeltaBucketCount - 1;
}
// Helper for RECORD_TIMELINE_UMA_BY_WIDTH.
#define ELSEIF_WIDTH_RECORD_TIMELINE_UMA(width, minWidth, maxWidth, metric, \
sample, HistogramType, ...) \
else if (width >= minWidth) { \
DEFINE_STATIC_LOCAL( \
HistogramType, metric##minWidth##_##maxWidth##Histogram, \
("Media.Timeline." #metric "." #minWidth "_" #maxWidth, \
##__VA_ARGS__)); \
metric##minWidth##_##maxWidth##Histogram.Count(sample); \
}
// Records UMA with a histogram suffix based on timelineWidth.
#define RECORD_TIMELINE_UMA_BY_WIDTH(timelineWidth, metric, sample, \
HistogramType, ...) \
do { \
int width = timelineWidth; /* Avoid multiple evaluation. */ \
if (false) { \
/* This if(false) allows all the conditions below to start with else. */ \
} \
ELSEIF_WIDTH_RECORD_TIMELINE_UMA(width, 512, inf, metric, sample, \
HistogramType, ##__VA_ARGS__) \
ELSEIF_WIDTH_RECORD_TIMELINE_UMA(width, 256, 511, metric, sample, \
HistogramType, ##__VA_ARGS__) \
ELSEIF_WIDTH_RECORD_TIMELINE_UMA(width, 128, 255, metric, sample, \
HistogramType, ##__VA_ARGS__) \
ELSEIF_WIDTH_RECORD_TIMELINE_UMA(width, 80, 127, metric, sample, \
HistogramType, ##__VA_ARGS__) \
ELSEIF_WIDTH_RECORD_TIMELINE_UMA(width, 48, 79, metric, sample, \
HistogramType, ##__VA_ARGS__) \
ELSEIF_WIDTH_RECORD_TIMELINE_UMA(width, 32, 47, metric, sample, \
HistogramType, ##__VA_ARGS__) \
else { \
/* Skip logging if timeline is narrower than minimum suffix bucket. */ \
/* If this happens a lot, it'll show up in Media.Timeline.Width. */ \
} \
} while (false)
void RecordDragGestureDurationByWidth(int timeline_width, TimeDelta duration) {
int32_t sample = base::saturated_cast<int32_t>(duration.InMilliseconds());
RECORD_TIMELINE_UMA_BY_WIDTH(timeline_width, DragGestureDuration, sample,
CustomCountHistogram, 1 /* 1 ms */,
60000 /* 1 minute */, 50);
}
void RecordDragPercentByWidth(int timeline_width, double percent) {
int32_t sample = ToPercentSample(percent);
RECORD_TIMELINE_UMA_BY_WIDTH(timeline_width, DragPercent, sample,
EnumerationHistogram, kPercentBucketCount);
}
void RecordDragSumAbsTimeDeltaByWidth(int timeline_width,
double sum_abs_delta_seconds) {
int32_t sample = ToAbsTimeDeltaSample(sum_abs_delta_seconds);
RECORD_TIMELINE_UMA_BY_WIDTH(timeline_width, DragSumAbsTimeDelta, sample,
EnumerationHistogram, kAbsTimeDeltaBucketCount);
}
void RecordDragTimeDeltaByWidth(int timeline_width, double delta_seconds) {
int32_t sample = ToTimeDeltaSample(delta_seconds);
RECORD_TIMELINE_UMA_BY_WIDTH(timeline_width, DragTimeDelta, sample,
EnumerationHistogram, kTimeDeltaBucketCount);
}
void RecordSeekTypeByWidth(int timeline_width, SeekType type) {
int32_t sample = static_cast<int32_t>(type);
constexpr int32_t kBucketCount = static_cast<int32_t>(SeekType::kLast) + 1;
RECORD_TIMELINE_UMA_BY_WIDTH(timeline_width, SeekType, sample,
EnumerationHistogram, kBucketCount);
}
#undef RECORD_TIMELINE_UMA_BY_WIDTH
#undef ELSEIF_WIDTH_RECORD_TIMELINE_UMA
} // namespace
void MediaControlTimelineMetrics::StartGesture(bool from_thumb) {
// Initialize gesture tracking.
state_ = from_thumb ? State::kGestureFromThumb : State::kGestureFromElsewhere;
drag_start_time_ticks_ = CurrentTimeTicks();
drag_delta_media_seconds_ = 0;
drag_sum_abs_delta_media_seconds_ = 0;
}
void MediaControlTimelineMetrics::RecordEndGesture(
int timeline_width,
double media_duration_seconds) {
State end_state = state_;
state_ = State::kInactive; // Reset tracking.
SeekType seek_type =
SeekType::kLast; // Arbitrary inital val to appease MSVC.
switch (end_state) {
case State::kInactive:
case State::kKeyDown:
return; // Pointer and keys were interleaved. Skip UMA in this edge case.
case State::kGestureFromThumb:
case State::kGestureFromElsewhere:
return; // Empty gesture with no calls to gestureInput.
case State::kDragFromThumb:
seek_type = SeekType::kDragFromCurrentPosition;
break;
case State::kClick:
seek_type = SeekType::kClick;
break;
case State::kDragFromElsewhere:
seek_type = SeekType::kDragFromElsewhere;
break;
}
RecordSeekTypeByWidth(timeline_width, seek_type);
if (seek_type == SeekType::kClick)
return; // Metrics below are only for drags.
RecordDragGestureDurationByWidth(timeline_width,
CurrentTimeTicks() - drag_start_time_ticks_);
if (std::isfinite(media_duration_seconds)) {
RecordDragPercentByWidth(timeline_width, 100.0 * drag_delta_media_seconds_ /
media_duration_seconds);
}
RecordDragSumAbsTimeDeltaByWidth(timeline_width,
drag_sum_abs_delta_media_seconds_);
RecordDragTimeDeltaByWidth(timeline_width, drag_delta_media_seconds_);
}
void MediaControlTimelineMetrics::StartKey() {
state_ = State::kKeyDown;
}
void MediaControlTimelineMetrics::RecordEndKey(int timeline_width,
int key_code) {
State end_state = state_;
state_ = State::kInactive; // Reset tracking.
if (end_state != State::kKeyDown)
return; // Pointer and keys were interleaved. Skip UMA in this edge case.
SeekType type;
switch (key_code) {
case VKEY_UP:
case VKEY_DOWN:
case VKEY_LEFT:
case VKEY_RIGHT:
type = SeekType::kKeyboardArrowKey;
break;
case VKEY_PRIOR: // PageUp
case VKEY_NEXT: // PageDown
type = SeekType::kKeyboardPageUpDownKey;
break;
case VKEY_HOME:
case VKEY_END:
type = SeekType::kKeyboardHomeEndKey;
break;
default:
return; // Other keys don't seek (at time of writing).
}
RecordSeekTypeByWidth(timeline_width, type);
}
void MediaControlTimelineMetrics::OnInput(double from_seconds,
double to_seconds) {
switch (state_) {
case State::kInactive:
// Unexpected input.
state_ = State::kInactive;
break;
case State::kGestureFromThumb:
// Drag confirmed now input has been received.
state_ = State::kDragFromThumb;
break;
case State::kGestureFromElsewhere:
// Click/drag confirmed now input has been received. Assume it's a click
// until further input is received.
state_ = State::kClick;
break;
case State::kClick:
// Drag confirmed now further input has been received.
state_ = State::kDragFromElsewhere;
break;
case State::kDragFromThumb:
case State::kDragFromElsewhere:
// Continue tracking drag.
break;
case State::kKeyDown:
// Continue tracking key.
break;
}
// The following tracking is only for drags. Note that we exclude kClick here,
// as even if it progresses to a kDragFromElsewhere, the first input event
// corresponds to the position jump from the pointer down on the track.
if (state_ != State::kDragFromThumb && state_ != State::kDragFromElsewhere)
return;
float delta_media_seconds = static_cast<float>(to_seconds - from_seconds);
drag_delta_media_seconds_ += delta_media_seconds;
drag_sum_abs_delta_media_seconds_ += std::abs(delta_media_seconds);
}
void MediaControlTimelineMetrics::RecordPlaying(
WebScreenOrientationType orientation,
bool is_fullscreen,
int timeline_width) {
bool is_portrait = false; // Arbitrary initial value to appease MSVC.
switch (orientation) {
case kWebScreenOrientationPortraitPrimary:
case kWebScreenOrientationPortraitSecondary:
is_portrait = true;
break;
case kWebScreenOrientationLandscapePrimary:
case kWebScreenOrientationLandscapeSecondary:
is_portrait = false;
break;
case kWebScreenOrientationUndefined:
return; // Skip UMA in the unlikely event we fail to detect orientation.
}
// Only record the first time each media element enters the playing state.
if (!has_never_been_playing_)
return;
has_never_been_playing_ = false;
constexpr int32_t kMin = 1;
constexpr int32_t kMax = 7680; // Equivalent to an 80inch wide 8K monitor.
constexpr int32_t kBucketCount = 50;
// Record merged histogram for all configurations.
DEFINE_STATIC_LOCAL(CustomCountHistogram, all_configurations_width_histogram,
("Media.Timeline.Width", kMin, kMax, kBucketCount));
all_configurations_width_histogram.Count(timeline_width);
// Record configuration-specific histogram.
if (!is_fullscreen) {
if (is_portrait) {
DEFINE_STATIC_LOCAL(
CustomCountHistogram, width_histogram,
("Media.Timeline.Width.InlinePortrait", kMin, kMax, kBucketCount));
width_histogram.Count(timeline_width);
} else {
DEFINE_STATIC_LOCAL(
CustomCountHistogram, width_histogram,
("Media.Timeline.Width.InlineLandscape", kMin, kMax, kBucketCount));
width_histogram.Count(timeline_width);
}
} else {
if (is_portrait) {
DEFINE_STATIC_LOCAL(CustomCountHistogram, width_histogram,
("Media.Timeline.Width.FullscreenPortrait", kMin,
kMax, kBucketCount));
width_histogram.Count(timeline_width);
} else {
DEFINE_STATIC_LOCAL(CustomCountHistogram, width_histogram,
("Media.Timeline.Width.FullscreenLandscape", kMin,
kMax, kBucketCount));
width_histogram.Count(timeline_width);
}
}
}
} // namespace blink