blob: a6c879e937ce7333de828cc6c08fd875c69b547e [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 "content/browser/renderer_host/render_widget_targeter.h"
#include <memory>
#include "base/bind.h"
#include "base/callback.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/rand_util.h"
#include "content/browser/renderer_host/render_widget_host_impl.h"
#include "content/browser/renderer_host/render_widget_host_view_base.h"
#include "third_party/blink/public/common/input/web_input_event.h"
#include "ui/events/blink/blink_event_util.h"
namespace content {
namespace {
gfx::PointF ComputeEventLocation(const blink::WebInputEvent& event) {
if (blink::WebInputEvent::IsMouseEventType(event.GetType()) ||
event.GetType() == blink::WebInputEvent::Type::kMouseWheel) {
return static_cast<const blink::WebMouseEvent&>(event).PositionInWidget();
}
if (blink::WebInputEvent::IsTouchEventType(event.GetType())) {
return static_cast<const blink::WebTouchEvent&>(event)
.touches[0]
.PositionInWidget();
}
if (blink::WebInputEvent::IsGestureEventType(event.GetType()))
return static_cast<const blink::WebGestureEvent&>(event).PositionInWidget();
return gfx::PointF();
}
bool IsMouseMiddleClick(const blink::WebInputEvent& event) {
return (event.GetType() == blink::WebInputEvent::Type::kMouseDown &&
static_cast<const blink::WebMouseEvent&>(event).button ==
blink::WebPointerProperties::Button::kMiddle);
}
constexpr const char kTracingCategory[] = "input,latency";
constexpr base::TimeDelta kAsyncHitTestTimeout = base::Seconds(5);
} // namespace
class TracingUmaTracker {
public:
explicit TracingUmaTracker(const char* metric_name)
: id_(next_id_++),
start_time_(base::TimeTicks::Now()),
metric_name_(metric_name) {
TRACE_EVENT_NESTABLE_ASYNC_BEGIN0(
kTracingCategory, metric_name_,
TRACE_ID_WITH_SCOPE("UmaTracker", TRACE_ID_LOCAL(id_)));
}
TracingUmaTracker(const TracingUmaTracker&) = delete;
TracingUmaTracker& operator=(const TracingUmaTracker&) = delete;
~TracingUmaTracker() = default;
TracingUmaTracker(TracingUmaTracker&& tracker) = default;
void StopAndRecord() {
TRACE_EVENT_NESTABLE_ASYNC_END0(
kTracingCategory, metric_name_,
TRACE_ID_WITH_SCOPE("UmaTracker", TRACE_ID_LOCAL(id_)));
UmaHistogramTimes(metric_name_, base::TimeTicks::Now() - start_time_);
}
private:
const int id_;
const base::TimeTicks start_time_;
// These variables must be string literals and live for the duration
// of the program since tracing stores pointers.
const char* metric_name_;
static int next_id_;
};
int TracingUmaTracker::next_id_ = 1;
RenderWidgetTargetResult::RenderWidgetTargetResult() = default;
RenderWidgetTargetResult::RenderWidgetTargetResult(
const RenderWidgetTargetResult&) = default;
RenderWidgetTargetResult::RenderWidgetTargetResult(
RenderWidgetHostViewBase* in_view,
bool in_should_query_view,
absl::optional<gfx::PointF> in_location,
bool in_latched_target)
: view(in_view),
should_query_view(in_should_query_view),
target_location(in_location),
latched_target(in_latched_target) {}
RenderWidgetTargetResult::~RenderWidgetTargetResult() = default;
RenderWidgetTargeter::TargetingRequest::TargetingRequest(
base::WeakPtr<RenderWidgetHostViewBase> root_view,
const blink::WebInputEvent& event,
const ui::LatencyInfo& latency) {
this->root_view = std::move(root_view);
this->location = ComputeEventLocation(event);
this->event = event.Clone();
this->latency = latency;
}
RenderWidgetTargeter::TargetingRequest::TargetingRequest(
base::WeakPtr<RenderWidgetHostViewBase> root_view,
const gfx::PointF& location,
RenderWidgetHostAtPointCallback callback) {
this->root_view = std::move(root_view);
this->location = location;
this->callback = std::move(callback);
}
RenderWidgetTargeter::TargetingRequest::TargetingRequest(
TargetingRequest&& request) = default;
RenderWidgetTargeter::TargetingRequest& RenderWidgetTargeter::TargetingRequest::
operator=(TargetingRequest&&) = default;
RenderWidgetTargeter::TargetingRequest::~TargetingRequest() = default;
void RenderWidgetTargeter::TargetingRequest::RunCallback(
RenderWidgetHostViewBase* target,
absl::optional<gfx::PointF> point) {
if (!callback.is_null()) {
std::move(callback).Run(target ? target->GetWeakPtr() : nullptr, point);
}
}
bool RenderWidgetTargeter::TargetingRequest::MergeEventIfPossible(
const blink::WebInputEvent& new_event) {
if (event && !blink::WebInputEvent::IsTouchEventType(new_event.GetType()) &&
!blink::WebInputEvent::IsGestureEventType(new_event.GetType()) &&
event->CanCoalesce(new_event)) {
event->Coalesce(new_event);
return true;
}
return false;
}
bool RenderWidgetTargeter::TargetingRequest::IsWebInputEventRequest() const {
return !!event;
}
blink::WebInputEvent* RenderWidgetTargeter::TargetingRequest::GetEvent() {
return event.get();
}
RenderWidgetHostViewBase* RenderWidgetTargeter::TargetingRequest::GetRootView()
const {
return root_view.get();
}
gfx::PointF RenderWidgetTargeter::TargetingRequest::GetLocation() const {
return location;
}
const ui::LatencyInfo& RenderWidgetTargeter::TargetingRequest::GetLatency()
const {
return latency;
}
RenderWidgetTargeter::RenderWidgetTargeter(Delegate* delegate)
: async_hit_test_timeout_delay_(kAsyncHitTestTimeout),
trace_id_(base::RandUint64()),
delegate_(delegate) {
DCHECK(delegate_);
}
RenderWidgetTargeter::~RenderWidgetTargeter() = default;
void RenderWidgetTargeter::FindTargetAndDispatch(
RenderWidgetHostViewBase* root_view,
const blink::WebInputEvent& event,
const ui::LatencyInfo& latency) {
DCHECK(blink::WebInputEvent::IsMouseEventType(event.GetType()) ||
event.GetType() == blink::WebInputEvent::Type::kMouseWheel ||
blink::WebInputEvent::IsTouchEventType(event.GetType()) ||
(blink::WebInputEvent::IsGestureEventType(event.GetType()) &&
(static_cast<const blink::WebGestureEvent&>(event).SourceDevice() ==
blink::WebGestureDevice::kTouchscreen ||
static_cast<const blink::WebGestureEvent&>(event).SourceDevice() ==
blink::WebGestureDevice::kTouchpad)));
if (!requests_.empty()) {
auto& request = requests_.back();
if (request.MergeEventIfPossible(event))
return;
}
TargetingRequest request(root_view->GetWeakPtr(), event, latency);
ResolveTargetingRequest(std::move(request));
}
void RenderWidgetTargeter::FindTargetAndCallback(
RenderWidgetHostViewBase* root_view,
const gfx::PointF& point,
RenderWidgetHostAtPointCallback callback) {
TargetingRequest request(root_view->GetWeakPtr(), point, std::move(callback));
ResolveTargetingRequest(std::move(request));
}
void RenderWidgetTargeter::ResolveTargetingRequest(TargetingRequest request) {
if (request_in_flight_) {
requests_.push(std::move(request));
return;
}
RenderWidgetTargetResult result;
auto* request_target = request.GetRootView();
auto request_target_location = request.GetLocation();
if (request.IsWebInputEventRequest()) {
result = is_autoscroll_in_progress_
? middle_click_result_
: delegate_->FindTargetSynchronously(request_target,
*request.GetEvent());
// |result.target_location| is utilized to update the position in widget for
// an event. If we are in autoscroll mode, we used cached data. So we need
// to update the target location of the |result|.
if (is_autoscroll_in_progress_) {
result.target_location = request_target_location;
}
if (!is_autoscroll_in_progress_ &&
IsMouseMiddleClick(*request.GetEvent())) {
if (!result.should_query_view)
middle_click_result_ = result;
}
} else {
result = delegate_->FindTargetSynchronouslyAtPoint(request_target,
request_target_location);
}
RenderWidgetHostViewBase* target = result.view;
if (!is_autoscroll_in_progress_ && result.should_query_view) {
TRACE_EVENT_WITH_FLOW2(
"viz,benchmark", "Event.Pipeline", TRACE_ID_GLOBAL(trace_id_),
TRACE_EVENT_FLAG_FLOW_OUT, "step", "QueryClient(Start)",
"event_location", request.GetLocation().ToString());
// TODO(kenrb, sadrul): When all event types support asynchronous hit
// testing, we should be able to have FindTargetSynchronously return the
// view and location to use for the renderer hit test query.
// Currently it has to return the surface hit test target, for event types
// that ignore |result.should_query_view|, and therefore we have to use
// root_view and the original event location for the initial query.
// Do not compare hit test results if we are forced to do async hit testing
// by HitTestQuery.
QueryClient(request_target, request_target_location, nullptr, gfx::PointF(),
std::move(request));
} else {
FoundTarget(target, result.target_location, &request);
}
}
void RenderWidgetTargeter::ViewWillBeDestroyed(RenderWidgetHostViewBase* view) {
unresponsive_views_.erase(view);
if (is_autoscroll_in_progress_ && middle_click_result_.view == view) {
SetIsAutoScrollInProgress(false);
}
}
bool RenderWidgetTargeter::HasEventsPendingDispatch() const {
return request_in_flight_ || !requests_.empty();
}
void RenderWidgetTargeter::SetIsAutoScrollInProgress(
bool autoscroll_in_progress) {
is_autoscroll_in_progress_ = autoscroll_in_progress;
// If middle click autoscroll ends, reset |middle_click_result_|.
if (!autoscroll_in_progress)
middle_click_result_ = RenderWidgetTargetResult();
}
void RenderWidgetTargeter::QueryClient(
RenderWidgetHostViewBase* target,
const gfx::PointF& target_location,
RenderWidgetHostViewBase* last_request_target,
const gfx::PointF& last_target_location,
TargetingRequest request) {
auto& target_client = target->host()->input_target_client();
// |target_client| may not be set yet for this |target| on Mac, need to
// understand why this happens. https://crbug.com/859492.
// We do not verify hit testing result under this circumstance.
if (!target_client.is_bound() || !target_client.is_connected()) {
FoundTarget(target, target_location, &request);
return;
}
const gfx::PointF location = request.GetLocation();
request_in_flight_ = std::move(request);
TracingUmaTracker tracker("Event.AsyncTargeting.ResponseTime");
async_hit_test_timeout_.Start(
FROM_HERE, async_hit_test_timeout_delay_,
base::BindOnce(
&RenderWidgetTargeter::AsyncHitTestTimedOut,
weak_ptr_factory_.GetWeakPtr(), target->GetWeakPtr(), target_location,
last_request_target ? last_request_target->GetWeakPtr() : nullptr,
last_target_location));
target_client.set_disconnect_handler(base::BindOnce(
&RenderWidgetTargeter::OnInputTargetDisconnect,
weak_ptr_factory_.GetWeakPtr(), target->GetWeakPtr(), target_location));
TRACE_EVENT_WITH_FLOW2(
"viz,benchmark", "Event.Pipeline", TRACE_ID_GLOBAL(trace_id_),
TRACE_EVENT_FLAG_FLOW_IN | TRACE_EVENT_FLAG_FLOW_OUT, "step",
"QueryClient", "event_location", location.ToString());
target_client->FrameSinkIdAt(
target_location, trace_id_,
base::BindOnce(&RenderWidgetTargeter::FoundFrameSinkId,
weak_ptr_factory_.GetWeakPtr(), target->GetWeakPtr(),
++last_request_id_, target_location, std::move(tracker)));
}
void RenderWidgetTargeter::FlushEventQueue() {
bool events_being_flushed = false;
while (!request_in_flight_ && !requests_.empty()) {
auto request = std::move(requests_.front());
requests_.pop();
// The root-view has gone away. Ignore this event, and try to process the
// next event.
if (!request.GetRootView())
continue;
// Only notify the delegate once that the current event queue is being
// flushed. Once all the events are flushed, notify the delegate again.
if (!events_being_flushed) {
delegate_->SetEventsBeingFlushed(true);
events_being_flushed = true;
}
ResolveTargetingRequest(std::move(request));
}
delegate_->SetEventsBeingFlushed(false);
}
void RenderWidgetTargeter::FoundFrameSinkId(
base::WeakPtr<RenderWidgetHostViewBase> target,
uint32_t request_id,
const gfx::PointF& target_location,
TracingUmaTracker tracker,
const viz::FrameSinkId& frame_sink_id,
const gfx::PointF& transformed_location) {
if (!target)
return;
tracker.StopAndRecord();
uint32_t last_id = last_request_id_;
bool in_flight = request_in_flight_.has_value();
if (request_id != last_id || !in_flight) {
// This is a response to a request that already timed out, so the event
// should have already been dispatched. Mark the renderer as responsive
// and otherwise ignore this response.
unresponsive_views_.erase(target.get());
return;
}
TargetingRequest request = std::move(request_in_flight_.value());
request_in_flight_.reset();
async_hit_test_timeout_.Stop();
target->host()->input_target_client().set_disconnect_handler(
base::OnceClosure());
auto* view = delegate_->FindViewFromFrameSinkId(frame_sink_id);
if (!view)
view = target.get();
// If a client returned an embedded target, then it might be necessary to
// continue asking the clients until a client claims an event for itself.
if (view == target.get() ||
unresponsive_views_.find(view) != unresponsive_views_.end() ||
!delegate_->ShouldContinueHitTesting(view)) {
// Reduced scope is required since FoundTarget can trigger another query
// which would end up linked to the current query.
{
TRACE_EVENT_WITH_FLOW1("viz,benchmark", "Event.Pipeline",
TRACE_ID_GLOBAL(trace_id_),
TRACE_EVENT_FLAG_FLOW_IN, "step", "FoundTarget");
}
if (request.IsWebInputEventRequest() &&
IsMouseMiddleClick(*request.GetEvent())) {
middle_click_result_ = {view, false, transformed_location, false};
}
FoundTarget(view, transformed_location, &request);
} else {
QueryClient(view, transformed_location, target.get(), target_location,
std::move(request));
}
}
void RenderWidgetTargeter::FoundTarget(
RenderWidgetHostViewBase* target,
const absl::optional<gfx::PointF>& target_location,
TargetingRequest* request) {
DCHECK(request);
// RenderWidgetHostViewMac can be deleted asynchronously, in which case the
// View will be valid but there will no longer be a RenderWidgetHostImpl.
if (!request->GetRootView() || !request->GetRootView()->GetRenderWidgetHost())
return;
if (request->IsWebInputEventRequest()) {
delegate_->DispatchEventToTarget(request->GetRootView(), target,
request->GetEvent(), request->GetLatency(),
target_location);
} else {
request->RunCallback(target, target_location);
}
FlushEventQueue();
}
void RenderWidgetTargeter::AsyncHitTestTimedOut(
base::WeakPtr<RenderWidgetHostViewBase> current_request_target,
const gfx::PointF& current_target_location,
base::WeakPtr<RenderWidgetHostViewBase> last_request_target,
const gfx::PointF& last_target_location) {
DCHECK(request_in_flight_);
TargetingRequest request = std::move(request_in_flight_.value());
request_in_flight_.reset();
if (!request.GetRootView())
return;
if (current_request_target) {
// Mark view as unresponsive so further events will not be sent to it.
unresponsive_views_.insert(current_request_target.get());
// Reset disconnect handler for view.
current_request_target->host()
->input_target_client()
.set_disconnect_handler(base::OnceClosure());
}
if (request.GetRootView() == current_request_target.get()) {
// When a request to the top-level frame times out then the event gets
// sent there anyway. It will trigger the hung renderer dialog if the
// renderer fails to process it.
FoundTarget(current_request_target.get(), current_target_location,
&request);
} else {
FoundTarget(last_request_target.get(), last_target_location, &request);
}
}
void RenderWidgetTargeter::OnInputTargetDisconnect(
base::WeakPtr<RenderWidgetHostViewBase> target,
const gfx::PointF& location) {
if (!async_hit_test_timeout_.IsRunning())
return;
async_hit_test_timeout_.Stop();
TargetingRequest request = std::move(request_in_flight_.value());
request_in_flight_.reset();
// Since we couldn't find the target frame among the child-frames
// we process the event in the current frame.
FoundTarget(target.get(), location, &request);
}
} // namespace content