// 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, span<const int64_t> longs) {
ScopedJavaLocalRef<jlongArray> java_long_array = ToJavaLongArray(env, longs);
return java_long_array.Release();
}
jintArray GenerateJavaIntArray(JNIEnv* env, span<const int> ints) {
ScopedJavaLocalRef<jintArray> java_int_array = ToJavaIntArray(env, ints);
return java_int_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 int kMissedVsyncs[] = {
0, 0, 2, 0, 1, 0, 0, 0,
};
static_assert(std::size(kDurations) == std::size(kMissedVsyncs));
const size_t kNumFrames = std::size(kDurations);
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);
jintArray java_missed_vsyncs = GenerateJavaIntArray(env, kMissedVsyncs);
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;
}
}
TEST(JankMetricUMARecorder, TestWebviewScrollingScenario) {
JNIEnv* env = AttachCurrentThread();
jlongArray java_durations = GenerateJavaLongArray(env, kDurations);
jintArray java_missed_vsyncs = GenerateJavaIntArray(env, kMissedVsyncs);
const int scenario = static_cast<int>(JankScenario::WEBVIEW_SCROLLING);
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, 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);
jintArray java_missed_vsyncs = GenerateJavaIntArray(env, kMissedVsyncs);
const int scenario =
static_cast<int>(JankScenario::COMBINED_WEBVIEW_SCROLLING);
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, 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<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);
jintArray java_missed_vsyncs = GenerateJavaIntArray(env, missed_vsyncs);
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";
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