| // 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 <jni.h> |
| |
| #include <cstddef> |
| #include <cstdint> |
| #include <vector> |
| |
| #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<jlongArray> 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<jintArray> 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; |
| |
| 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<int>(JankScenario::PERIODIC_REPORTING); |
| const int kMaxScenario = static_cast<int>(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<int>(JankScenario::WEBVIEW_SCROLLING)) || |
| (i == static_cast<int>(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<jlongArray>(env, java_durations), |
| /* java_missed_vsyncs = */ |
| base::android::JavaParamRef<jintArray>(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<JankScenario>(i)); |
| const std::string kJankyName = |
| GetAndroidFrameTimelineJankHistogramName(static_cast<JankScenario>(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; |
| } |
| } |
| |
| class JankMetricUMARecorderPerScrollTests |
| : public testing::Test, |
| public testing::WithParamInterface<ScrollTestCase> {}; |
| INSTANTIATE_TEST_SUITE_P( |
| JankMetricUMARecorderPerScrollTests, |
| JankMetricUMARecorderPerScrollTests, |
| testing::ValuesIn<ScrollTestCase>({ |
| {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<int64_t> durations = {1000000L, 1000000L, 1000000L}; |
| std::vector<int> 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<jlongArray>(env, java_durations), |
| base::android::JavaParamRef<jintArray>(env, java_missed_vsyncs), |
| /* java_reporting_interval_start_time = */ 0, |
| /* java_reporting_interval_duration = */ 1000, |
| static_cast<int>(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." + |
| params.suffix; |
| std::string missed_vsyncs_max_histogram = "Android.FrameTimelineJank." + |
| scenario_name + |
| ".MissedVsyncsMax." |
| "PerScroll." + |
| params.suffix; |
| std::string missed_vsyncs_sum_histogram = "Android.FrameTimelineJank." + |
| scenario_name + |
| ".MissedVsyncsSum." |
| "PerScroll." + |
| params.suffix; |
| 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); |
| } |
| |
| } // namespace base::android |