Chromium Code Reviews| Index: third_party/WebKit/Source/core/html/shadow/MediaControlTimelineMetrics.cpp |
| diff --git a/third_party/WebKit/Source/core/html/shadow/MediaControlTimelineMetrics.cpp b/third_party/WebKit/Source/core/html/shadow/MediaControlTimelineMetrics.cpp |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..913e204863ddad349fffe4d7e1e6b0961d360463 |
| --- /dev/null |
| +++ b/third_party/WebKit/Source/core/html/shadow/MediaControlTimelineMetrics.cpp |
| @@ -0,0 +1,378 @@ |
| +// 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 "core/html/shadow/MediaControlTimelineMetrics.h" |
| + |
| +#include <stdint.h> |
| +#include <cmath> |
| +#include <limits> |
| +#include "platform/KeyboardCodes.h" |
| +#include "platform/wtf/StdLibExtras.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 |
| +}; |
| + |
| +// Inclusive 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, 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}; |
| +// Must match length of UMA MediaTimelinePercent enum. |
| +constexpr int32_t kPercentBucketCount = 51; |
| +static_assert(arraysize(kPercentIntervals) * 2 - 1 == kPercentBucketCount, |
| + "Intervals must match UMA MediaTimelinePercent enum"); |
| + |
| +// Corresponds to two UMA enums of different sizes! Values are the inclusive |
| +// 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[] = { |
| + 0, // 0 |
| + 16, // 16ms |
| + 32, // 32ms |
| + 64, // 64ms |
| + 128, // 128ms |
| + 256, // 256ms |
| + 512, // 512ms |
| + 1000, // 1s |
| + 2000, // 2s |
| + 4000, // 4s |
| + 8000, // 8s |
| + 15000, // 15s |
|
Ilya Sherman
2017/04/01 00:42:41
Optional nit: Maybe write this as 15*1000, and sim
johnme
2017/04/03 16:16:48
Done.
|
| + 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(arraysize(kTimeDeltaMSIntervals) == kAbsTimeDeltaBucketCount, |
| + "Intervals must match UMA MediaTimelineAbsTimeDelta enum"); |
| +static_assert(arraysize(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 nonNegativeBucketCount = arraysize(kPercentIntervals); |
| + constexpr int32_t negativeBucketCount = arraysize(kPercentIntervals) - 1; |
| + bool negative = percent < 0; |
| + double absPercent = std::abs(percent); |
| + for (int32_t i = 0; i < nonNegativeBucketCount; i++) { |
| + if (absPercent <= kPercentIntervals[i]) |
| + return negativeBucketCount + (negative ? -i : +i); |
| + } |
| + // No NOTREACHED since percent may exit -100..100 due to floating point error. |
| + return negative ? 0 : kPercentBucketCount - 1; |
| +} |
| + |
| +// Calculates index of UMA MediaTimelineAbsTimeDelta enum corresponding to |
| +// |sumAbsDeltaSeconds|. |
| +int32_t toAbsTimeDeltaSample(double sumAbsDeltaSeconds) { |
| + double sumAbsDeltaMS = 1000 * sumAbsDeltaSeconds; |
| + for (int32_t i = 0; i < kAbsTimeDeltaBucketCount; i++) { |
| + if (sumAbsDeltaMS <= kTimeDeltaMSIntervals[i]) |
| + return i; |
| + } |
| + NOTREACHED(); |
| + return kAbsTimeDeltaBucketCount - 1; |
| +} |
| + |
| +// Calculates index of UMA MediaTimelineTimeDelta enum corresponding to |
| +// |deltaSeconds|. Negative values use kTimeDeltaMSIntervals in reverse. |
| +int32_t toTimeDeltaSample(double deltaSeconds) { |
| + constexpr int32_t nonNegativeBucketCount = arraysize(kTimeDeltaMSIntervals); |
| + constexpr int32_t negativeBucketCount = arraysize(kTimeDeltaMSIntervals) - 1; |
| + bool negative = deltaSeconds < 0; |
| + double absDeltaMS = 1000 * std::abs(deltaSeconds); |
| + for (int32_t i = 0; i < nonNegativeBucketCount; i++) { |
| + if (absDeltaMS <= kTimeDeltaMSIntervals[i]) |
| + return negativeBucketCount + (negative ? -i : +i); |
| + } |
| + NOTREACHED(); |
| + 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 timelineWidth, TimeDelta duration) { |
| + int32_t sample = static_cast<int32_t>(duration.InMilliseconds()); |
| + RECORD_TIMELINE_UMA_BY_WIDTH(timelineWidth, DragGestureDuration, sample, |
| + CustomCountHistogram, 1 /* 1 ms */, |
| + 60000 /* 1 minute */, 50); |
| +} |
| +void recordDragPercentByWidth(int timelineWidth, double percent) { |
| + int32_t sample = toPercentSample(percent); |
| + RECORD_TIMELINE_UMA_BY_WIDTH(timelineWidth, DragPercent, sample, |
| + EnumerationHistogram, kPercentBucketCount); |
| +} |
| +void recordDragSumAbsTimeDeltaByWidth(int timelineWidth, |
| + double sumAbsDeltaSeconds) { |
| + int32_t sample = toAbsTimeDeltaSample(sumAbsDeltaSeconds); |
| + RECORD_TIMELINE_UMA_BY_WIDTH(timelineWidth, DragSumAbsTimeDelta, sample, |
| + EnumerationHistogram, kAbsTimeDeltaBucketCount); |
| +} |
| +void recordDragTimeDeltaByWidth(int timelineWidth, double deltaSeconds) { |
| + int32_t sample = toTimeDeltaSample(deltaSeconds); |
| + RECORD_TIMELINE_UMA_BY_WIDTH(timelineWidth, DragTimeDelta, sample, |
| + EnumerationHistogram, kTimeDeltaBucketCount); |
| +} |
| +void recordSeekTypeByWidth(int timelineWidth, SeekType type) { |
| + int32_t sample = static_cast<int32_t>(type); |
| + constexpr int32_t bucketCount = static_cast<int32_t>(SeekType::kLast) + 1; |
| + RECORD_TIMELINE_UMA_BY_WIDTH(timelineWidth, SeekType, sample, |
| + EnumerationHistogram, bucketCount); |
| +} |
| + |
| +#undef RECORD_TIMELINE_UMA_BY_WIDTH |
| +#undef ELSEIF_WIDTH_RECORD_TIMELINE_UMA |
| + |
| +} // namespace |
| + |
| +void MediaControlTimelineMetrics::startGesture(bool fromThumb) { |
| + // Initialize gesture tracking. |
| + m_state = fromThumb ? State::kGestureFromThumb : State::kGestureFromElsewhere; |
| + m_dragStartTimeTicks = TimeTicks::Now(); |
| + m_dragDeltaMediaSeconds = 0; |
| + m_dragSumAbsDeltaMediaSeconds = 0; |
| +} |
| + |
| +void MediaControlTimelineMetrics::onInput(double fromSeconds, |
| + double toSeconds) { |
| + switch (m_state) { |
| + case State::kInactive: |
| + // Unexpected input. |
| + m_state = State::kInactive; |
| + break; |
| + case State::kGestureFromThumb: |
| + // Drag confirmed now input has been received. |
| + m_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. |
| + m_state = State::kClick; |
| + break; |
| + case State::kClick: |
| + // Drag confirmed now further input has been received. |
| + m_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 (m_state != State::kDragFromThumb && m_state != State::kDragFromElsewhere) |
| + return; |
| + |
| + float deltaMediaSeconds = static_cast<float>(toSeconds - fromSeconds); |
| + m_dragDeltaMediaSeconds += deltaMediaSeconds; |
| + m_dragSumAbsDeltaMediaSeconds += std::abs(deltaMediaSeconds); |
| +} |
| + |
| +void MediaControlTimelineMetrics::recordEndGesture( |
| + int timelineWidth, |
| + double mediaDurationSeconds) { |
| + State endState = m_state; |
| + m_state = State::kInactive; // Reset tracking. |
| + |
| + SeekType seekType; |
| + switch (endState) { |
| + 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: |
| + seekType = SeekType::kDragFromCurrentPosition; |
| + break; |
| + case State::kClick: |
| + seekType = SeekType::kClick; |
| + break; |
| + case State::kDragFromElsewhere: |
| + seekType = SeekType::kDragFromElsewhere; |
| + break; |
| + } |
| + |
| + recordSeekTypeByWidth(timelineWidth, seekType); |
| + |
| + if (seekType == SeekType::kClick) |
| + return; // Metrics below are only for drags. |
| + |
| + recordDragGestureDurationByWidth(timelineWidth, |
| + TimeTicks::Now() - m_dragStartTimeTicks); |
| + if (std::isfinite(mediaDurationSeconds)) { |
| + recordDragPercentByWidth( |
| + timelineWidth, 100.0 * m_dragDeltaMediaSeconds / mediaDurationSeconds); |
| + } |
| + recordDragSumAbsTimeDeltaByWidth(timelineWidth, |
| + m_dragSumAbsDeltaMediaSeconds); |
| + recordDragTimeDeltaByWidth(timelineWidth, m_dragDeltaMediaSeconds); |
| +} |
| + |
| +void MediaControlTimelineMetrics::startKey() { |
| + m_state = State::kKeyDown; |
| +} |
| + |
| +void MediaControlTimelineMetrics::recordEndKey(int timelineWidth, int keyCode) { |
| + State endState = m_state; |
| + m_state = State::kInactive; // Reset tracking. |
| + if (endState != State::kKeyDown) |
| + return; // Pointer and keys were interleaved. Skip UMA in this edge case. |
| + |
| + SeekType type; |
| + switch (keyCode) { |
| + 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(timelineWidth, type); |
| +} |
| + |
| +void MediaControlTimelineMetrics::recordPlaying( |
| + WebScreenOrientationType orientation, |
| + bool isFullscreen, |
| + int timelineWidth) { |
| + bool isPortrait; |
| + switch (orientation) { |
| + case WebScreenOrientationPortraitPrimary: |
| + case WebScreenOrientationPortraitSecondary: |
| + isPortrait = true; |
| + break; |
| + case WebScreenOrientationLandscapePrimary: |
| + case WebScreenOrientationLandscapeSecondary: |
| + isPortrait = false; |
| + break; |
| + case WebScreenOrientationUndefined: |
| + 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 (!m_hasNeverBeenPlaying) |
| + return; |
| + m_hasNeverBeenPlaying = false; |
| + |
| + constexpr int32_t min = 1; |
| + constexpr int32_t max = 7680; // Equivalent to an 80inch wide 8K monitor. |
| + constexpr int32_t bucketCount = 50; |
| + // Record merged histogram for all configurations. |
| + DEFINE_STATIC_LOCAL(CustomCountHistogram, allConfigurationsWidthHistogram, |
| + ("Media.Timeline.Width", min, max, bucketCount)); |
| + allConfigurationsWidthHistogram.count(timelineWidth); |
| + // Record configuration-specific histogram. |
| + if (!isFullscreen) { |
| + if (isPortrait) { |
| + DEFINE_STATIC_LOCAL( |
| + CustomCountHistogram, widthHistogram, |
| + ("Media.Timeline.Width.InlinePortrait", min, max, bucketCount)); |
| + widthHistogram.count(timelineWidth); |
| + } else { |
| + DEFINE_STATIC_LOCAL( |
| + CustomCountHistogram, widthHistogram, |
| + ("Media.Timeline.Width.InlineLandscape", min, max, bucketCount)); |
| + widthHistogram.count(timelineWidth); |
| + } |
| + } else { |
| + if (isPortrait) { |
| + DEFINE_STATIC_LOCAL( |
| + CustomCountHistogram, widthHistogram, |
| + ("Media.Timeline.Width.FullscreenPortrait", min, max, bucketCount)); |
| + widthHistogram.count(timelineWidth); |
| + } else { |
| + DEFINE_STATIC_LOCAL( |
| + CustomCountHistogram, widthHistogram, |
| + ("Media.Timeline.Width.FullscreenLandscape", min, max, bucketCount)); |
| + widthHistogram.count(timelineWidth); |
| + } |
| + } |
| +} |
| + |
| +} // namespace blink |