chromium/components/exo/frame_timing_history.cc

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

#include "components/exo/frame_timing_history.h"

#include "base/logging.h"
#include "base/metrics/histogram_macros.h"
#include "base/not_fatal_until.h"

namespace exo {
namespace {

constexpr size_t kRollingHistorySize = 60u;
constexpr double kFrameTransferDurationEstimationPercentile = 90.0;

// Reports metrics when the number of data points reaches this threshold.
constexpr int32_t kReportMetricsThreshold = 100;

}  // namespace

FrameTimingHistory::FrameTimingHistory()
    : frame_transfer_duration_history_(kRollingHistorySize) {}

FrameTimingHistory::~FrameTimingHistory() = default;

base::TimeDelta FrameTimingHistory::GetFrameTransferDurationEstimate() const {
  return frame_transfer_duration_history_.Percentile(
      kFrameTransferDurationEstimationPercentile);
}

void FrameTimingHistory::BeginFrameArrived(const viz::BeginFrameId& id) {
  begin_frame_arrival_time_[id] = base::TimeTicks::Now();
}

void FrameTimingHistory::FrameArrived() {
  last_frame_arrival_time_ = base::TimeTicks::Now();
}

void FrameTimingHistory::FrameSubmitted(const viz::BeginFrameId& begin_frame_id,
                                        uint32_t frame_token) {
  DCHECK(pending_submitted_time_.find(frame_token) ==
         pending_submitted_time_.end());

  base::TimeTicks submitted_time = base::TimeTicks::Now();
  pending_submitted_time_[frame_token] = submitted_time;

  // At destruction time, LayerTreeFrameSinkHolder submits an empty frame which
  // is not received from the client, skip reporting value for that.
  if (!last_frame_arrival_time_.is_null()) {
    UMA_HISTOGRAM_CUSTOM_MICROSECONDS_TIMES(
        "Graphics.Exo.Smoothness.FrameArrivalToSubmission",
        submitted_time - last_frame_arrival_time_, base::Microseconds(1),
        base::Milliseconds(50), 50);
  }

  auto iter = begin_frame_arrival_time_.find(begin_frame_id);
  // This could be an unsolicited frame submission. In that case
  // `begin_frame_id` won't be found in the map.
  if (iter != begin_frame_arrival_time_.end()) {
    UMA_HISTOGRAM_CUSTOM_MICROSECONDS_TIMES(
        "Graphics.Exo.Smoothness.BeginFrameArrivalToSubmission",
        submitted_time - iter->second, base::Microseconds(1),
        base::Milliseconds(50), 50);
  }

  RecordFrameResponseToRemote(begin_frame_id, /*did_not_produce=*/false,
                              submitted_time);
  RecordFrameHandled(/*discarded=*/false);

  consecutive_did_not_produce_count_ = 0;
}

void FrameTimingHistory::FrameDidNotProduce(const viz::BeginFrameId& id) {
  RecordFrameResponseToRemote(id, /*did_not_produce=*/true,
                              base::TimeTicks::Now());

  consecutive_did_not_produce_count_++;
}

void FrameTimingHistory::FrameReceivedAtRemoteSide(
    uint32_t frame_token,
    base::TimeTicks received_time) {
  auto iter = pending_submitted_time_.find(frame_token);

  CHECK(iter != pending_submitted_time_.end(), base::NotFatalUntil::M130)
      << "Frame submitted time information is missing. Frame Token: "
      << frame_token;

  DCHECK_GE(received_time, iter->second);
  frame_transfer_duration_history_.InsertSample(received_time - iter->second);
  pending_submitted_time_.erase(iter);

  // FrameSubmitted() / FrameReceivedAtRemoteSide() are supposed to match, so
  // that the map won't grow indefinitely.
  DCHECK_LE(pending_submitted_time_.size(), 60u);
}

void FrameTimingHistory::FrameDiscarded() {
  RecordFrameHandled(/*discarded=*/true);
}

void FrameTimingHistory::MayRecordDidNotProduceToFrameArrvial(bool valid) {
  if (last_did_not_produce_time_.is_null()) {
    return;
  }

  base::TimeDelta duration =
      valid ? (base::TimeTicks::Now() - last_did_not_produce_time_)
            : base::TimeDelta();

  // Measures the time duration between Exo sending a DidNotProduceFrame
  // response and the next frame arrival, if the next frame arrives before a new
  // BeginFrame. Reported for clients with high-resolution clocks.

  // This metric is used to measure whether the deadline Exo uses to wait for
  // frames is reasonable. Please note that a value is reported for each
  // DidNotProduceFrame. If (1) DidNotProduceFrame is issued when there are
  // already queued BeginFrames; or (2) a new BeginFrame arrives before the next
  // frame; or (3) BeginFrames are paused, then the value reported is 0.
  LOCAL_HISTOGRAM_CUSTOM_MICROSECONDS_TIMES(
      "Graphics.Exo.Smoothness.DidNotProduceToFrameArrival", duration,
      base::Microseconds(1), base::Milliseconds(50), 50);

  last_did_not_produce_time_ = {};
}

void FrameTimingHistory::RecordFrameResponseToRemote(
    const viz::BeginFrameId& begin_frame_id,
    bool did_not_produce,
    base::TimeTicks response_time) {
  begin_frame_arrival_time_.erase(begin_frame_id);
  // All BeginFrames are supposed to be matched with either a frame
  // submission or a DidNotProduce response, except at destruction time. So the
  // map shouldn't grow indefinitely.
  DCHECK_LE(begin_frame_arrival_time_.size(), 60u);

  last_did_not_produce_time_ =
      did_not_produce ? response_time : base::TimeTicks();

  frame_response_count_++;
  if (did_not_produce) {
    frame_response_did_not_produce_++;
  }

  if (frame_response_count_ >= kReportMetricsThreshold) {
    // Tracks the percent of BeginFrames that Exo receives and responds with
    // DidNotProduceFrame.
    // A new value is reported for every 100 BeginFrames that a shell surface
    // receives. Note that this metric is reported only when there are
    // sufficient number of BeginFrames (>=100).
    LOCAL_HISTOGRAM_PERCENTAGE(
        "Graphics.Exo.Smoothness.PercentDidNotProduceFrame",
        frame_response_did_not_produce_ * 100 / frame_response_count_);
    frame_response_count_ = 0;
    frame_response_did_not_produce_ = 0;
  }
}

void FrameTimingHistory::RecordFrameHandled(bool discarded) {
  last_frame_arrival_time_ = base::TimeTicks();

  frame_handling_count_++;
  if (discarded) {
    frame_handling_discarded_++;
  }

  if (frame_handling_count_ >= kReportMetricsThreshold) {
    // Tracks the percent of compositor frames that are submitted from Exo
    // clients and directly discarded without being sent to the GPU process. A
    // new value is reported for every 100 frames that a shell surface submits.
    // Note that this metric is reported only when there are sufficient number
    // of frames (>=100).
    LOCAL_HISTOGRAM_PERCENTAGE(
        "Graphics.Exo.Smoothness.PercentFrameDiscarded",
        frame_handling_discarded_ * 100 / frame_handling_count_);
    frame_handling_count_ = 0;
    frame_handling_discarded_ = 0;
  }
}

}  // namespace exo