// 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 #include #include #include #include "base/android/jni_android.h" #include "base/android/jni_array.h" #include "base/android/jni_string.h" #include "base/metrics/histogram.h" #include "base/test/metrics/histogram_tester.h" #include "jank_metric_uma_recorder.h" #include "testing/gmock/include/gmock/gmock.h" #include "testing/gtest/include/gtest/gtest.h" using ::testing::ElementsAre; using ::testing::IsEmpty; namespace base::android { namespace { jlongArray GenerateJavaLongArray(JNIEnv* env, const int64_t long_array[], const size_t array_length) { ScopedJavaLocalRef java_long_array = ToJavaLongArray(env, long_array, array_length); return java_long_array.Release(); } // Durations are received in nanoseconds, but are recorded to UMA in // milliseconds. const int64_t kDurations[] = { 1'000'000, // 1ms 2'000'000, // 2ms 30'000'000, // 30ms 10'000'000, // 10ms 60'000'000, // 60ms 1'000'000, // 1ms 1'000'000, // 1ms 20'000'000, // 20ms }; const size_t kDurationsLen = std::size(kDurations); jintArray GenerateJavaIntArray(JNIEnv* env, const int int_array[], const size_t array_length) { ScopedJavaLocalRef java_int_array = ToJavaIntArray(env, int_array, array_length); return java_int_array.Release(); } const int kMissedVsyncs[] = { 0, 0, 2, 0, 1, 0, 0, 0, }; const size_t kMissedVsyncsLen = kDurationsLen; static_assert(kDurationsLen == kMissedVsyncsLen); const size_t kNumFrames = kDurationsLen; struct ScrollTestCase { JankScenario scenario; std::string test_name; int num_frames; std::string suffix; }; } // namespace TEST(JankMetricUMARecorder, TestUMARecording) { JNIEnv* env = AttachCurrentThread(); jlongArray java_durations = GenerateJavaLongArray(env, kDurations, kDurationsLen); jintArray java_missed_vsyncs = GenerateJavaIntArray(env, kMissedVsyncs, kMissedVsyncsLen); const int kMinScenario = static_cast(JankScenario::PERIODIC_REPORTING); const int kMaxScenario = static_cast(JankScenario::MAX_VALUE); // keep one histogram tester outside to ensure that each histogram is a // different one rather than just the same string over and over. HistogramTester complete_histogram_tester; size_t total_histograms = 0; for (int i = kMinScenario; i < kMaxScenario; ++i) { if ((i == static_cast(JankScenario::WEBVIEW_SCROLLING)) || (i == static_cast(JankScenario::FEED_SCROLLING))) { continue; } // HistogramTester takes a snapshot of currently incremented counters so // everything is scoped to just this iteration of the for loop. HistogramTester histogram_tester; RecordJankMetrics( env, /* java_durations_ns= */ base::android::JavaParamRef(env, java_durations), /* java_missed_vsyncs = */ base::android::JavaParamRef(env, java_missed_vsyncs), /* java_reporting_interval_start_time = */ 0, /* java_reporting_interval_duration = */ 1000, /* java_scenario_enum = */ i); const std::string kDurationName = GetAndroidFrameTimelineDurationHistogramName( static_cast(i)); const std::string kJankyName = GetAndroidFrameTimelineJankHistogramName(static_cast(i)); // Only one Duration and one Jank scenario should be incremented. base::HistogramTester::CountsMap count_map = histogram_tester.GetTotalCountsForPrefix("Android.FrameTimelineJank."); EXPECT_EQ(count_map.size(), 2ul); EXPECT_EQ(count_map[kDurationName], 8) << kDurationName; EXPECT_EQ(count_map[kJankyName], 8) << kJankyName; // And we should be two more then last iteration, but don't do any other // verification because each iteration will do their own. base::HistogramTester::CountsMap total_count_map = complete_histogram_tester.GetTotalCountsForPrefix( "Android.FrameTimelineJank."); EXPECT_EQ(total_count_map.size(), total_histograms + 2); total_histograms += 2; EXPECT_THAT(histogram_tester.GetAllSamples(kDurationName), ElementsAre(Bucket(1, 3), Bucket(2, 1), Bucket(10, 1), Bucket(20, 1), Bucket(29, 1), Bucket(57, 1))) << kDurationName; EXPECT_THAT(histogram_tester.GetAllSamples(kJankyName), ElementsAre(Bucket(FrameJankStatus::kJanky, 2), Bucket(FrameJankStatus::kNonJanky, 6))) << kJankyName; } } TEST(JankMetricUMARecorder, TestWebviewScrollingScenario) { JNIEnv* env = AttachCurrentThread(); jlongArray java_durations = GenerateJavaLongArray(env, kDurations, kDurationsLen); jintArray java_missed_vsyncs = GenerateJavaIntArray(env, kMissedVsyncs, kMissedVsyncsLen); const int scenario = static_cast(JankScenario::WEBVIEW_SCROLLING); HistogramTester histogram_tester; RecordJankMetrics( env, /* java_durations_ns= */ base::android::JavaParamRef(env, java_durations), /* java_missed_vsyncs = */ base::android::JavaParamRef(env, java_missed_vsyncs), /* java_reporting_interval_start_time = */ 0, /* java_reporting_interval_duration = */ 1000, scenario); const std::string kDurationName = "Android.FrameTimelineJank.Duration.WebviewScrolling"; const std::string kJankyName = "Android.FrameTimelineJank.FrameJankStatus.WebviewScrolling"; histogram_tester.ExpectTotalCount(kDurationName, 0); histogram_tester.ExpectTotalCount(kJankyName, 0); } TEST(JankMetricUMARecorder, TestCombinedWebviewScrollingScenario) { JNIEnv* env = AttachCurrentThread(); jlongArray java_durations = GenerateJavaLongArray(env, kDurations, kDurationsLen); jintArray java_missed_vsyncs = GenerateJavaIntArray(env, kMissedVsyncs, kMissedVsyncsLen); const int scenario = static_cast(JankScenario::COMBINED_WEBVIEW_SCROLLING); HistogramTester histogram_tester; RecordJankMetrics( env, /* java_durations_ns= */ base::android::JavaParamRef(env, java_durations), /* java_missed_vsyncs = */ base::android::JavaParamRef(env, java_missed_vsyncs), /* java_reporting_interval_start_time = */ 0, /* java_reporting_interval_duration = */ 1000, scenario); // |COMBINED_WEBVIEW_SCROLLING| scenario uses 'WebviewScrolling' suffix for // emitting the per frame metrics. const std::string kDurationName = "Android.FrameTimelineJank.Duration.WebviewScrolling"; const std::string kJankyName = "Android.FrameTimelineJank.FrameJankStatus.WebviewScrolling"; histogram_tester.ExpectTotalCount(kDurationName, kNumFrames); histogram_tester.ExpectTotalCount(kJankyName, kNumFrames); } class JankMetricUMARecorderPerScrollTests : public testing::Test, public testing::WithParamInterface {}; INSTANTIATE_TEST_SUITE_P( JankMetricUMARecorderPerScrollTests, JankMetricUMARecorderPerScrollTests, testing::ValuesIn({ {JankScenario::WEBVIEW_SCROLLING, "EmitsSmallScrollHistogramInWebview", 10, ".Small"}, {JankScenario::WEBVIEW_SCROLLING, "EmitsMediumScrollHistogramInWebview", 50, ".Medium"}, {JankScenario::WEBVIEW_SCROLLING, "EmitsLargeScrollHistogramInWebview", 65, ".Large"}, {JankScenario::FEED_SCROLLING, "EmitsSmallScrollHistogramInFeed", 10, ".Small"}, {JankScenario::FEED_SCROLLING, "EmitsMediumScrollHistogramInFeed", 50, ".Medium"}, {JankScenario::FEED_SCROLLING, "EmitsLargeScrollHistogramInFeed", 65, ".Large"}, }), [](const testing::TestParamInfo< JankMetricUMARecorderPerScrollTests::ParamType>& info) { return info.param.test_name; }); TEST_P(JankMetricUMARecorderPerScrollTests, EmitsPerScrollHistograms) { const ScrollTestCase& params = GetParam(); JNIEnv* env = AttachCurrentThread(); HistogramTester histogram_tester; std::vector durations = {1000000L, 1000000L, 1000000L}; std::vector missed_vsyncs = {0, 3, 1}; const int expected_janky_frames = 2; const int expected_vsyncs_max = 3; const int expected_vsyncs_sum = 4; for (int i = durations.size(); i < params.num_frames; i++) { durations.push_back(1000000L); missed_vsyncs.push_back(0); } jlongArray java_durations = GenerateJavaLongArray(env, durations.data(), durations.size()); jintArray java_missed_vsyncs = GenerateJavaIntArray(env, missed_vsyncs.data(), missed_vsyncs.size()); RecordJankMetrics( env, base::android::JavaParamRef(env, java_durations), base::android::JavaParamRef(env, java_missed_vsyncs), /* java_reporting_interval_start_time = */ 0, /* java_reporting_interval_duration = */ 1000, static_cast(params.scenario)); int expected_delayed_frames_percentage = (100 * expected_janky_frames) / params.num_frames; std::string scenario_name = ""; if (params.scenario == JankScenario::WEBVIEW_SCROLLING) { scenario_name = "WebviewScrolling"; } else { DCHECK_EQ(params.scenario, JankScenario::FEED_SCROLLING); scenario_name = "FeedScrolling"; } std::string delayed_frames_histogram = "Android.FrameTimelineJank." + scenario_name + ".DelayedFramesPercentage." "PerScroll"; std::string missed_vsyncs_max_histogram = "Android.FrameTimelineJank." + scenario_name + ".MissedVsyncsMax." "PerScroll"; std::string missed_vsyncs_sum_histogram = "Android.FrameTimelineJank." + scenario_name + ".MissedVsyncsSum." "PerScroll"; // Should emit non-bucketed scroll histograms. histogram_tester.ExpectUniqueSample(delayed_frames_histogram, expected_delayed_frames_percentage, 1); histogram_tester.ExpectUniqueSample(missed_vsyncs_max_histogram, expected_vsyncs_max, 1); histogram_tester.ExpectUniqueSample(missed_vsyncs_sum_histogram, expected_vsyncs_sum, 1); // Should emit bucketed scroll histograms, suffixed with scroll size like // Small, Medium, Large. histogram_tester.ExpectUniqueSample(delayed_frames_histogram + params.suffix, expected_delayed_frames_percentage, 1); histogram_tester.ExpectUniqueSample( missed_vsyncs_max_histogram + params.suffix, expected_vsyncs_max, 1); histogram_tester.ExpectUniqueSample( missed_vsyncs_sum_histogram + params.suffix, expected_vsyncs_sum, 1); } } // namespace base::android