blob: 5851ff4ce61c6d42bf0a7a696172d9d3744c2fcc [file] [log] [blame]
// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <memory>
#include "base/functional/bind.h"
#include "base/run_loop.h"
#include "base/strings/string_number_conversions.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_trace_processor.h"
#include "build/build_config.h"
#include "cc/base/switches.h"
#include "content/browser/renderer_host/render_widget_host_impl.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/common/input/synthetic_gesture.h"
#include "content/common/input/synthetic_gesture_controller.h"
#include "content/common/input/synthetic_gesture_params.h"
#include "content/common/input/synthetic_gesture_target.h"
#include "content/common/input/synthetic_smooth_scroll_gesture.h"
#include "content/common/input/synthetic_smooth_scroll_gesture_params.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/render_widget_host_view.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/hit_test_region_observer.h"
#include "content/shell/browser/shell.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/common/input/synthetic_web_input_event_builders.h"
#include "third_party/blink/public/common/switches.h"
#include "ui/base/ui_base_features.h"
#include "ui/native_theme/features/native_theme_features.h"
#if BUILDFLAG(IS_MAC)
#include "ui/base/test/scoped_preferred_scroller_style_mac.h"
#endif
namespace {
const char kDataURL[] =
"data:text/html;charset=utf-8,"
"<!DOCTYPE html>"
"<html>"
"<head>"
"<title>Scroll latency histograms browsertests.</title>"
"<style>"
"body {"
" height:9000px;"
" overscroll-behavior:none;"
"}"
"</style>"
"</head>"
"<body>"
"<div id='spinner'>Spinning</div>"
"</body>"
"<script>"
"var degree = 0;"
"function spin() {"
"degree = degree + 3;"
"if (degree >= 360)"
"degree -= 360;"
"document.getElementById('spinner').style['transform'] = "
"'rotate(' + degree + 'deg)';"
"requestAnimationFrame(spin);"
"}"
"spin();"
"</script>"
"</html>";
} // namespace
namespace content {
class ScrollLatencyBrowserTest : public ContentBrowserTest {
public:
ScrollLatencyBrowserTest() {}
ScrollLatencyBrowserTest(const ScrollLatencyBrowserTest&) = delete;
ScrollLatencyBrowserTest& operator=(const ScrollLatencyBrowserTest&) = delete;
~ScrollLatencyBrowserTest() override {}
RenderWidgetHostImpl* GetWidgetHost() {
return RenderWidgetHostImpl::From(shell()
->web_contents()
->GetPrimaryMainFrame()
->GetRenderViewHost()
->GetWidget());
}
// TODO(tdresser): Find a way to avoid sleeping like this. See
// crbug.com/405282 for details.
void GiveItSomeTime(int delay_ms = 10) {
base::RunLoop run_loop;
base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE, run_loop.QuitClosure(), base::Milliseconds(delay_ms));
run_loop.Run();
}
void OnSyntheticGestureCompleted(SyntheticGesture::Result result) {
EXPECT_EQ(SyntheticGesture::GESTURE_FINISHED, result);
run_loop_->Quit();
}
protected:
void SetUpCommandLine(base::CommandLine* command_line) override {
// Set the scroll animation duration to a large number so that
// we ensure secondary GestureScrollUpdates update the animation
// instead of starting a new one.
command_line->AppendSwitchASCII(
switches::kCCScrollAnimationDurationForTesting, "10000000");
}
void LoadURL() {
const GURL data_url(kDataURL);
EXPECT_TRUE(NavigateToURL(shell(), data_url));
RenderWidgetHostImpl* host = GetWidgetHost();
HitTestRegionObserver observer(host->GetFrameSinkId());
// Wait for the hit test data to be ready
observer.WaitForHitTestData();
}
// Generate the gestures associated with kNumWheelScrolls ticks,
// scrolling by |distance|. This will perform a smooth scroll on platforms
// which support it.
void DoSmoothWheelScroll(const gfx::Vector2d& distance) {
std::vector<gfx::Vector2d> scroll_events = {
gfx::Vector2d(distance.x(), -distance.y()),
gfx::Vector2d(distance.x(), -distance.y())};
DoScroll(distance, scroll_events);
}
void DoScroll(gfx::Vector2d begin_position,
std::vector<gfx::Vector2d> scroll_events,
int delay_ms = 10) {
blink::WebGestureEvent event =
blink::SyntheticWebGestureEventBuilder::BuildScrollBegin(
begin_position.x(), -begin_position.y(),
blink::WebGestureDevice::kTouchpad, 1);
event.data.scroll_begin.delta_hint_units =
ui::ScrollGranularity::kScrollByPixel;
GetWidgetHost()->ForwardGestureEvent(event);
for (uint32_t i = 0; i < scroll_events.size(); i++) {
// Install a VisualStateCallback and wait for the callback in response
// to each GestureScrollUpdate before sending the next GSU. This will
// ensure the events are not coalesced (resulting in fewer end-to-end
// latency histograms being logged).
// We must install a callback for each gesture since they are one-shot
// callbacks.
shell()->web_contents()->GetPrimaryMainFrame()->InsertVisualStateCallback(
base::BindOnce(&ScrollLatencyBrowserTest::InvokeVisualStateCallback,
base::Unretained(this)));
auto distance_to_scroll = scroll_events[i];
blink::WebGestureEvent event2 =
blink::SyntheticWebGestureEventBuilder::BuildScrollUpdate(
distance_to_scroll.x(), distance_to_scroll.y(), 0,
blink::WebGestureDevice::kTouchpad);
event2.data.scroll_update.delta_units =
ui::ScrollGranularity::kScrollByPixel;
GetWidgetHost()->ForwardGestureEvent(event2);
while (visual_state_callback_count_ <= i) {
// TODO: There's currently no way to block until a GPU swap
// completes. Until then we need to spin and wait. See
// crbug.com/897520 for more details.
GiveItSomeTime(delay_ms);
}
}
}
void InvokeVisualStateCallback(bool result) {
EXPECT_TRUE(result);
visual_state_callback_count_++;
}
// Returns true if the given histogram has recorded the expected number of
// samples.
[[nodiscard]] bool VerifyRecordedSamplesForHistogram(
const size_t num_samples,
const std::string& histogram_name) const {
return num_samples == GetSampleCountForHistogram(histogram_name);
}
size_t GetSampleCountForHistogram(const std::string& histogram_name) const {
return histogram_tester_.GetAllSamples(histogram_name).size();
}
std::string GetScaledDeltaTraceValue(int offset) {
return base::NumberToString(
static_cast<double>(GetWidgetHost()->GetDeviceScaleFactor() * offset));
}
std::unique_ptr<base::RunLoop> run_loop_;
protected:
base::HistogramTester histogram_tester_;
uint32_t visual_state_callback_count_ = 0;
};
// Do an upward touch scroll, and verify that no scroll metrics is recorded when
// the scroll event is ignored.
IN_PROC_BROWSER_TEST_F(ScrollLatencyBrowserTest,
ScrollLatencyNotRecordedIfGSUIgnored) {
LoadURL();
auto scroll_update_watcher = std::make_unique<InputMsgWatcher>(
GetWidgetHost(), blink::WebInputEvent::Type::kGestureScrollUpdate);
// Try to scroll upward, the GSU(s) will get ignored since the scroller is at
// its extent.
SyntheticSmoothScrollGestureParams params;
params.gesture_source_type = content::mojom::GestureSourceType::kTouchInput;
params.anchor = gfx::PointF(10, 10);
params.distances.push_back(gfx::Vector2d(0, 60));
run_loop_ = std::make_unique<base::RunLoop>();
std::unique_ptr<SyntheticSmoothScrollGesture> gesture(
new SyntheticSmoothScrollGesture(params));
GetWidgetHost()->QueueSyntheticGesture(
std::move(gesture),
base::BindOnce(&ScrollLatencyBrowserTest::OnSyntheticGestureCompleted,
base::Unretained(this)));
// Runs until we get the OnSyntheticGestureCompleted callback and verify that
// the first GSU event is ignored.
run_loop_->Run();
EXPECT_EQ(blink::mojom::InputEventResultState::kNoConsumerExists,
scroll_update_watcher->GetAckStateWaitIfNecessary());
// Wait for one frame and then verify that the scroll metrics are not
// recorded.
std::unique_ptr<RenderFrameSubmissionObserver> frame_observer =
std::make_unique<RenderFrameSubmissionObserver>(
GetWidgetHost()->render_frame_metadata_provider());
frame_observer->WaitForAnyFrameSubmission();
EXPECT_TRUE(VerifyRecordedSamplesForHistogram(
0, "EventLatency.GestureScrollUpdate.TotalLatency2"));
}
// TODO(crbug.com/370658912) heap-use-after-free on Win ASAN.
#if BUILDFLAG(IS_WIN) && defined(ADDRESS_SANITIZER)
#define MAYBE_ScrollingEventLatencyTrace DISABLED_ScrollingEventLatencyTrace
#else
#define MAYBE_ScrollingEventLatencyTrace ScrollingEventLatencyTrace
#endif // BUILDFLAG(IS_WIN) && defined(ADDRESS_SANITIZER)
// A basic smoke test verifying that key scroll-related events are recorded
// during scrolling. This test performs a simple scroll and expects to see three
// EventLatency events with the correct types.
IN_PROC_BROWSER_TEST_F(ScrollLatencyBrowserTest,
MAYBE_ScrollingEventLatencyTrace) {
LoadURL();
base::test::TestTraceProcessor ttp;
ttp.StartTrace("input.scrolling");
DoSmoothWheelScroll(gfx::Vector2d(0, 100));
while (!VerifyRecordedSamplesForHistogram(
1, "EventLatency.GestureScrollUpdate.TotalLatency2")) {
GiveItSomeTime();
FetchHistogramsFromChildProcesses();
}
absl::Status status = ttp.StopAndParseTrace();
ASSERT_TRUE(status.ok()) << status.message();
std::string query =
R"(
SELECT EXTRACT_ARG(arg_set_id, 'event_latency.event_type') AS type
FROM slice
WHERE name = 'EventLatency'
)";
auto result = ttp.RunQuery(query);
ASSERT_TRUE(result.has_value()) << result.error();
EXPECT_THAT(result.value(),
::testing::ElementsAre(
std::vector<std::string>{"type"},
std::vector<std::string>{"GESTURE_SCROLL_BEGIN"},
std::vector<std::string>{"FIRST_GESTURE_SCROLL_UPDATE"},
std::vector<std::string>{"GESTURE_SCROLL_UPDATE"}));
}
// Basic smoke test for predictor jank metrics.
#if BUILDFLAG(IS_ANDROID)
// TODO(b/345225978): the android-x86-rel bot occasionally flakes due to
// missing events after Swap.
#if !defined(ARCH_CPU_X86_FAMILY)
IN_PROC_BROWSER_TEST_F(ScrollLatencyBrowserTest, ScrollingPredictor) {
LoadURL();
base::test::TestTraceProcessor ttp;
ttp.StartTrace("input.scrolling");
const gfx::Vector2d scroll_1(0, -5);
const gfx::Vector2d scroll_2(0, 1); // Previous update and subsequent update
// will produce jank where
// min(prev, next)/curr > threshold.
const gfx::Vector2d scroll_3(0, -5); // Previous update and subsequent update
// will not produce jank.
const gfx::Vector2d scroll_4(0, -10); // Previous update and subsequent
// update will produce jank where
// curr/max(prev, next) > threshold.
const gfx::Vector2d scroll_5(0, 5);
DoScroll(gfx::Vector2d(0, 25),
{scroll_1, scroll_2, scroll_3, scroll_4, scroll_5},
16 /*delay_ms - ensure there is enough time for vsync*/
);
// Allow enough time for the inputs to be processed and the trace events
// to be recorded.
GiveItSomeTime(100);
absl::Status status = ttp.StopAndParseTrace();
ASSERT_TRUE(status.ok()) << status.message();
std::string query =
R"(
INCLUDE PERFETTO MODULE chrome.scroll_jank.predictor_error;
SELECT
prev_delta,
delta_y,
next_delta,
predictor_jank,
delta_threshold
FROM chrome_predictor_error
ORDER BY present_ts
;
)";
auto result = ttp.RunQuery(query);
ASSERT_TRUE(result.has_value()) << result.error();
EXPECT_THAT(
result.value(),
::testing::ElementsAre(
std::vector<std::string>{"prev_delta", "delta_y", "next_delta",
"predictor_jank", "delta_threshold"},
std::vector<std::string>{GetScaledDeltaTraceValue(scroll_1.y()),
GetScaledDeltaTraceValue(scroll_2.y()),
GetScaledDeltaTraceValue(scroll_3.y()),
"3.8", "1.2"},
std::vector<std::string>{GetScaledDeltaTraceValue(scroll_2.y()),
GetScaledDeltaTraceValue(scroll_3.y()),
GetScaledDeltaTraceValue(scroll_4.y()), "0",
"1.2"},
std::vector<std::string>{GetScaledDeltaTraceValue(scroll_3.y()),
GetScaledDeltaTraceValue(scroll_4.y()),
GetScaledDeltaTraceValue(scroll_5.y()),
"0.8", "1.2"}));
}
#endif // !defined(ARCH_CPU_X86_FAMILY)
#endif // BUILDFLAG(IS_ANDROID)
} // namespace content