chromium/base/android/jank_metric_uma_recorder.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 "base/android/jank_metric_uma_recorder.h"

#include <cstdint>

#include "base/android/jni_android.h"
#include "base/android/jni_array.h"
#include "base/metrics/histogram_functions.h"
#include "base/notreached.h"
#include "base/time/time.h"
#include "base/trace_event/base_tracing.h"
#include "jank_metric_uma_recorder.h"

// Must come after all headers that specialize FromJniType() / ToJniType().
#include "base/jank_tracker_jni/JankMetricUMARecorder_jni.h"

namespace base::android {

namespace {

// Histogram min, max and no. of buckets.
constexpr int kVsyncCountsMin = 1;
constexpr int kVsyncCountsMax = 50;
constexpr int kVsyncCountsBuckets = 25;

enum class PerScrollHistogramType {
  kPercentage = 0,
  kMax = 1,
  kSum = 2,
};

const char* GetPerScrollHistogramName(JankScenario scenario,
                                      int num_frames,
                                      PerScrollHistogramType type,
                                      bool with_scroll_size_suffix) {
#define HISTOGRAM_NAME(hist_scenario, hist_type, length)     \
  "Android.FrameTimelineJank." #hist_scenario "." #hist_type \
  "."                                                        \
  "PerScroll" #length
  if (scenario == JankScenario::WEBVIEW_SCROLLING) {
    if (type == PerScrollHistogramType::kPercentage) {
      if (!with_scroll_size_suffix) {
        return HISTOGRAM_NAME(WebviewScrolling, DelayedFramesPercentage,
                              /*no suffix*/);
      }
      if (num_frames <= 16) {
        return HISTOGRAM_NAME(WebviewScrolling, DelayedFramesPercentage,
                              .Small);
      } else if (num_frames <= 64) {
        return HISTOGRAM_NAME(WebviewScrolling, DelayedFramesPercentage,
                              .Medium);
      } else {
        return HISTOGRAM_NAME(WebviewScrolling, DelayedFramesPercentage,
                              .Large);
      }
    } else if (type == PerScrollHistogramType::kMax) {
      if (!with_scroll_size_suffix) {
        return HISTOGRAM_NAME(WebviewScrolling, MissedVsyncsMax, /*no suffix*/);
      }
      if (num_frames <= 16) {
        return HISTOGRAM_NAME(WebviewScrolling, MissedVsyncsMax, .Small);
      } else if (num_frames <= 64) {
        return HISTOGRAM_NAME(WebviewScrolling, MissedVsyncsMax, .Medium);
      } else {
        return HISTOGRAM_NAME(WebviewScrolling, MissedVsyncsMax, .Large);
      }
    } else {
      DCHECK_EQ(type, PerScrollHistogramType::kSum);
      if (!with_scroll_size_suffix) {
        return HISTOGRAM_NAME(WebviewScrolling, MissedVsyncsSum, /*no suffix*/);
      }
      if (num_frames <= 16) {
        return HISTOGRAM_NAME(WebviewScrolling, MissedVsyncsSum, .Small);
      } else if (num_frames <= 64) {
        return HISTOGRAM_NAME(WebviewScrolling, MissedVsyncsSum, .Medium);
      } else {
        return HISTOGRAM_NAME(WebviewScrolling, MissedVsyncsSum, .Large);
      }
    }
  } else {
    DCHECK_EQ(scenario, JankScenario::FEED_SCROLLING);
    if (type == PerScrollHistogramType::kPercentage) {
      if (!with_scroll_size_suffix) {
        return HISTOGRAM_NAME(FeedScrolling, DelayedFramesPercentage,
                              /*no suffix*/);
      }
      if (num_frames <= 16) {
        return HISTOGRAM_NAME(FeedScrolling, DelayedFramesPercentage, .Small);
      } else if (num_frames <= 64) {
        return HISTOGRAM_NAME(FeedScrolling, DelayedFramesPercentage, .Medium);
      } else {
        return HISTOGRAM_NAME(FeedScrolling, DelayedFramesPercentage, .Large);
      }
    } else if (type == PerScrollHistogramType::kMax) {
      if (!with_scroll_size_suffix) {
        return HISTOGRAM_NAME(FeedScrolling, MissedVsyncsMax, /*no suffix*/);
      }
      if (num_frames <= 16) {
        return HISTOGRAM_NAME(FeedScrolling, MissedVsyncsMax, .Small);
      } else if (num_frames <= 64) {
        return HISTOGRAM_NAME(FeedScrolling, MissedVsyncsMax, .Medium);
      } else {
        return HISTOGRAM_NAME(FeedScrolling, MissedVsyncsMax, .Large);
      }
    } else {
      DCHECK_EQ(type, PerScrollHistogramType::kSum);
      if (!with_scroll_size_suffix) {
        return HISTOGRAM_NAME(FeedScrolling, MissedVsyncsSum, /*no suffix*/);
      }
      if (num_frames <= 16) {
        return HISTOGRAM_NAME(FeedScrolling, MissedVsyncsSum, .Small);
      } else if (num_frames <= 64) {
        return HISTOGRAM_NAME(FeedScrolling, MissedVsyncsSum, .Medium);
      } else {
        return HISTOGRAM_NAME(FeedScrolling, MissedVsyncsSum, .Large);
      }
    }
  }
#undef HISTOGRAM_NAME
}

// Emits trace event for all scenarios and per scroll histograms for webview and
// feed scrolling scenarios.
void EmitMetrics(JankScenario scenario,
                 int janky_frame_count,
                 int missed_vsyncs_max,
                 int missed_vsyncs_sum,
                 int num_presented_frames,
                 int64_t reporting_interval_start_time,
                 int64_t reporting_interval_duration) {
  DCHECK_GT(num_presented_frames, 0);
  int delayed_frames_percentage =
      (100 * janky_frame_count) / num_presented_frames;
  if (reporting_interval_start_time > 0) {
    // The following code does nothing if base tracing is disabled.
    [[maybe_unused]] int non_janky_frame_count =
        num_presented_frames - janky_frame_count;
    [[maybe_unused]] auto t = perfetto::Track(static_cast<uint64_t>(
        reporting_interval_start_time + static_cast<int>(scenario)));
    TRACE_EVENT_BEGIN(
        "android_webview.timeline,android.ui.jank",
        "JankMetricsReportingInterval", t,
        base::TimeTicks::FromUptimeMillis(reporting_interval_start_time),
        "janky_frames", janky_frame_count, "non_janky_frames",
        non_janky_frame_count, "scenario", static_cast<int>(scenario),
        "delayed_frames_percentage", delayed_frames_percentage,
        "missed_vsyns_max", missed_vsyncs_max, "missed_vsyncs_sum",
        missed_vsyncs_sum);
    TRACE_EVENT_END(
        "android_webview.timeline,android.ui.jank", t,
        base::TimeTicks::FromUptimeMillis(
            (reporting_interval_start_time + reporting_interval_duration)));
  }

  if (scenario != JankScenario::WEBVIEW_SCROLLING &&
      scenario != JankScenario::FEED_SCROLLING) {
    return;
  }
  // Emit non-bucketed per scroll metrics.
  base::UmaHistogramPercentage(
      GetPerScrollHistogramName(scenario, num_presented_frames,
                                PerScrollHistogramType::kPercentage,
                                /*with_scroll_size_suffix=*/false),
      delayed_frames_percentage);
  base::UmaHistogramCustomCounts(
      GetPerScrollHistogramName(scenario, num_presented_frames,
                                PerScrollHistogramType::kMax,
                                /*with_scroll_size_suffix=*/false),
      missed_vsyncs_max, kVsyncCountsMin, kVsyncCountsMax, kVsyncCountsBuckets);
  base::UmaHistogramCustomCounts(
      GetPerScrollHistogramName(scenario, num_presented_frames,
                                PerScrollHistogramType::kSum,
                                /*with_scroll_size_suffix=*/false),
      missed_vsyncs_sum, kVsyncCountsMin, kVsyncCountsMax, kVsyncCountsBuckets);

  // Emit bucketed per scroll metrics where scrolls are divided into three
  // buckets Small, Medium, Large.
  base::UmaHistogramPercentage(
      GetPerScrollHistogramName(scenario, num_presented_frames,
                                PerScrollHistogramType::kPercentage,
                                /*with_scroll_size_suffix=*/true),
      delayed_frames_percentage);
  base::UmaHistogramCustomCounts(
      GetPerScrollHistogramName(scenario, num_presented_frames,
                                PerScrollHistogramType::kMax,
                                /*with_scroll_size_suffix=*/true),
      missed_vsyncs_max, kVsyncCountsMin, kVsyncCountsMax, kVsyncCountsBuckets);
  base::UmaHistogramCustomCounts(
      GetPerScrollHistogramName(scenario, num_presented_frames,
                                PerScrollHistogramType::kSum,
                                /*with_scroll_size_suffix=*/true),
      missed_vsyncs_sum, kVsyncCountsMin, kVsyncCountsMax, kVsyncCountsBuckets);
}

}  // namespace

const char* GetAndroidFrameTimelineJankHistogramName(JankScenario scenario) {
#define HISTOGRAM_NAME(x) "Android.FrameTimelineJank.FrameJankStatus." #x
  switch (scenario) {
    case JankScenario::PERIODIC_REPORTING:
      return HISTOGRAM_NAME(Total);
    case JankScenario::OMNIBOX_FOCUS:
      return HISTOGRAM_NAME(OmniboxFocus);
    case JankScenario::NEW_TAB_PAGE:
      return HISTOGRAM_NAME(NewTabPage);
    case JankScenario::STARTUP:
      return HISTOGRAM_NAME(Startup);
    case JankScenario::TAB_SWITCHER:
      return HISTOGRAM_NAME(TabSwitcher);
    case JankScenario::OPEN_LINK_IN_NEW_TAB:
      return HISTOGRAM_NAME(OpenLinkInNewTab);
    case JankScenario::START_SURFACE_HOMEPAGE:
      return HISTOGRAM_NAME(StartSurfaceHomepage);
    case JankScenario::START_SURFACE_TAB_SWITCHER:
      return HISTOGRAM_NAME(StartSurfaceTabSwitcher);
    case JankScenario::FEED_SCROLLING:
      return HISTOGRAM_NAME(FeedScrolling);
    case JankScenario::WEBVIEW_SCROLLING:
      return HISTOGRAM_NAME(WebviewScrolling);
    case JankScenario::COMBINED_WEBVIEW_SCROLLING:
      // Emit per frame metrics for combined scrolling scenario with same
      // histogram name as webview scrolling. This is fine since we don't emit
      // per frame metrics for |WEBVIEW_SCROLLING| scenario.
      return HISTOGRAM_NAME(WebviewScrolling);
    default:
      NOTREACHED();
  }
#undef HISTOGRAM_NAME
}

const char* GetAndroidFrameTimelineDurationHistogramName(
    JankScenario scenario) {
#define HISTOGRAM_NAME(x) "Android.FrameTimelineJank.Duration." #x
  switch (scenario) {
    case JankScenario::PERIODIC_REPORTING:
      return HISTOGRAM_NAME(Total);
    case JankScenario::OMNIBOX_FOCUS:
      return HISTOGRAM_NAME(OmniboxFocus);
    case JankScenario::NEW_TAB_PAGE:
      return HISTOGRAM_NAME(NewTabPage);
    case JankScenario::STARTUP:
      return HISTOGRAM_NAME(Startup);
    case JankScenario::TAB_SWITCHER:
      return HISTOGRAM_NAME(TabSwitcher);
    case JankScenario::OPEN_LINK_IN_NEW_TAB:
      return HISTOGRAM_NAME(OpenLinkInNewTab);
    case JankScenario::START_SURFACE_HOMEPAGE:
      return HISTOGRAM_NAME(StartSurfaceHomepage);
    case JankScenario::START_SURFACE_TAB_SWITCHER:
      return HISTOGRAM_NAME(StartSurfaceTabSwitcher);
    case JankScenario::FEED_SCROLLING:
      return HISTOGRAM_NAME(FeedScrolling);
    case JankScenario::WEBVIEW_SCROLLING:
      return HISTOGRAM_NAME(WebviewScrolling);
    case JankScenario::COMBINED_WEBVIEW_SCROLLING:
      // Emit per frame metrics for combined scrolling scenario with same
      // histogram name as webview scrolling. This is fine since we don't emit
      // per frame metrics for |WEBVIEW_SCROLLING| scenario.
      return HISTOGRAM_NAME(WebviewScrolling);
    default:
      NOTREACHED();
  }
#undef HISTOGRAM_NAME
}

// This function is called from Java with JNI, it's declared in
// base/base_jni/JankMetricUMARecorder_jni.h which is an autogenerated
// header. The actual implementation is in RecordJankMetrics for simpler
// testing.
void JNI_JankMetricUMARecorder_RecordJankMetrics(
    JNIEnv* env,
    const base::android::JavaParamRef<jlongArray>& java_durations_ns,
    const base::android::JavaParamRef<jintArray>& java_missed_vsyncs,
    jlong java_reporting_interval_start_time,
    jlong java_reporting_interval_duration,
    jint java_scenario_enum) {
  RecordJankMetrics(env, java_durations_ns, java_missed_vsyncs,
                    java_reporting_interval_start_time,
                    java_reporting_interval_duration, java_scenario_enum);
}

void RecordJankMetrics(
    JNIEnv* env,
    const base::android::JavaParamRef<jlongArray>& java_durations_ns,
    const base::android::JavaParamRef<jintArray>& java_missed_vsyncs,
    jlong java_reporting_interval_start_time,
    jlong java_reporting_interval_duration,
    jint java_scenario_enum) {
  std::vector<int64_t> durations_ns;
  JavaLongArrayToInt64Vector(env, java_durations_ns, &durations_ns);

  std::vector<int> missed_vsyncs;
  JavaIntArrayToIntVector(env, java_missed_vsyncs, &missed_vsyncs);

  JankScenario scenario = static_cast<JankScenario>(java_scenario_enum);

  const char* frame_duration_histogram_name =
      GetAndroidFrameTimelineDurationHistogramName(scenario);
  const char* janky_frames_per_scenario_histogram_name =
      GetAndroidFrameTimelineJankHistogramName(scenario);

  // We don't want to emit per frame metircs for WEBVIEW SCROLLING scenario
  // which tracks individual scrolls differentiated by gesture_scroll_id.
  // Scroll related per frame metrics are emitted from
  // COMBINED_WEBVIEW_SCROLLING scenario to avoid emitting duplicate metrics for
  // overlapping scrolls.
  const bool emit_per_frame_metrics =
      scenario != JankScenario::WEBVIEW_SCROLLING;

  if (emit_per_frame_metrics) {
    for (const int64_t frame_duration_ns : durations_ns) {
      base::UmaHistogramTimes(frame_duration_histogram_name,
                              base::Nanoseconds(frame_duration_ns));
    }
  }

  int janky_frame_count = 0;
  int missed_vsyncs_max = 0;
  int missed_vsyncs_sum = 0;
  const int num_presented_frames = static_cast<int>(missed_vsyncs.size());

  for (int curr_frame_missed_vsyncs : missed_vsyncs) {
    bool is_janky = curr_frame_missed_vsyncs > 0;
    if (curr_frame_missed_vsyncs > missed_vsyncs_max) {
      missed_vsyncs_max = curr_frame_missed_vsyncs;
    }
    missed_vsyncs_sum += curr_frame_missed_vsyncs;

    if (emit_per_frame_metrics) {
      base::UmaHistogramEnumeration(
          janky_frames_per_scenario_histogram_name,
          is_janky ? FrameJankStatus::kJanky : FrameJankStatus::kNonJanky);
    }
    if (is_janky) {
      ++janky_frame_count;
    }
  }

  if (num_presented_frames > 0) {
    EmitMetrics(scenario, janky_frame_count, missed_vsyncs_max,
                missed_vsyncs_sum, num_presented_frames,
                java_reporting_interval_start_time,
                java_reporting_interval_duration);
  }
}

}  // namespace base::android