blob: 36ea65a2dc8f8f3ebea74149d37b10f239d5bf80 [file] [log] [blame]
// Copyright 2020 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "src/ui/scenic/lib/input/injector.h"
#include <lib/syslog/cpp/macros.h>
#include <lib/trace/event.h>
#include <src/lib/fostr/fidl/fuchsia/ui/pointerinjector/formatting.h>
#include "lib/async/cpp/time.h"
#include "lib/async/default.h"
#include "src/ui/scenic/lib/input/constants.h"
#include "src/ui/scenic/lib/utils/math.h"
#include <glm/glm.hpp>
namespace scenic_impl::input {
using fuchsia::ui::pointerinjector::EventPhase;
namespace {
// A histogram that ranges from 1ms to ~8s.
constexpr zx::duration kLatencyHistogramFloor = zx::msec(1);
constexpr zx::duration kLatencyHistogramInitialStep = zx::msec(1);
constexpr uint64_t kLatencyHistogramStepMultiplier = 2;
constexpr size_t kLatencyHistogramBuckets = 14;
// Retain this many touch event buckets. This ensures we see at most this
// many of them even if there isn't 10 minutes of consistent touch activity.
constexpr size_t kNumRetainedTouchEventBuckets = 10;
uint64_t GetCurrentMinute(const zx::time timestamp) { return timestamp.get() / zx::min(1).get(); }
} // namespace
InjectorInspector::InjectorInspector(inspect::Node node)
: node_(std::move(node)),
history_stats_node_(node_.CreateLazyValues("Injection history",
[this] {
inspect::Inspector insp;
ReportStats(insp);
return fpromise::make_ok_promise(
std::move(insp));
})),
viewport_event_latency_(node_.CreateExponentialUintHistogram(
"viewport_event_latency_usecs", kLatencyHistogramFloor.to_usecs(),
kLatencyHistogramInitialStep.to_usecs(), kLatencyHistogramStepMultiplier,
kLatencyHistogramBuckets)),
pointer_event_latency_(node_.CreateExponentialUintHistogram(
"pointer_event_latency_usecs", kLatencyHistogramFloor.to_usecs(),
kLatencyHistogramInitialStep.to_usecs(), kLatencyHistogramStepMultiplier,
kLatencyHistogramBuckets)) {}
void InjectorInspector::OnPointerInjectorEvent(const fuchsia::ui::pointerinjector::Event& event) {
FX_DCHECK(event.has_data() && event.has_timestamp());
FX_DCHECK(async_get_default_dispatcher());
const zx::time now = async::Now(async_get_default_dispatcher());
const zx::duration latency = now - zx::time(event.timestamp());
if (event.data().is_viewport()) {
viewport_event_latency_.Insert(latency.to_usecs());
} else if (event.data().is_pointer_sample()) {
UpdateHistory(now);
pointer_event_latency_.Insert(latency.to_usecs());
} else {
FX_LOGS(ERROR) << "pointerinjector::Event dropped from inspect metrics. Unexpected data type.";
}
}
void InjectorInspector::UpdateHistory(const zx::time now) {
const uint64_t current_minute = GetCurrentMinute(now);
// Add elements to the front and pop from the back so that the newest element will be read out
// first when we later iterate over the deque.
if (history_.empty() || history_.front().minute_key != current_minute) {
history_.push_front({
.minute_key = current_minute,
});
}
history_.front().num_injected_events++;
// Pop off everything older than |kNumMinutesOfHistory|.
while (history_.size() > kNumRetainedTouchEventBuckets &&
(current_minute - history_.back().minute_key) >= kNumMinutesOfHistory) {
history_.pop_back();
}
}
void InjectorInspector::ReportStats(inspect::Inspector& inspector) const {
inspect::Node node = inspector.GetRoot().CreateChild(
"Last " + std::to_string(kNumMinutesOfHistory) + " minutes of injected events");
uint64_t total = 0;
const uint64_t current_minute = GetCurrentMinute(async::Now(async_get_default_dispatcher()));
for (const auto& [minute, num_injected_events] : history_) {
if (minute + kNumMinutesOfHistory <= current_minute) {
break;
}
node.CreateUint("Events at minute " + std::to_string(minute), num_injected_events, &inspector);
total += num_injected_events;
}
node.CreateUint("Total", total, &inspector);
inspector.emplace(std::move(node));
}
namespace {
bool HasRequiredFields(const fuchsia::ui::pointerinjector::PointerSample& pointer) {
return pointer.has_pointer_id() && pointer.has_phase() && pointer.has_position_in_viewport();
}
bool AreValidExtents(const std::array<std::array<float, 2>, 2>& extents) {
for (auto& point : extents) {
for (float f : point) {
if (!std::isfinite(f)) {
return false;
}
}
}
const float min_x = extents[0][0];
const float min_y = extents[0][1];
const float max_x = extents[1][0];
const float max_y = extents[1][1];
return std::isless(min_x, max_x) && std::isless(min_y, max_y);
}
} // namespace
Injector::Injector(inspect::Node inspect_node, InjectorSettings settings, Viewport viewport,
fidl::InterfaceRequest<fuchsia::ui::pointerinjector::Device> device,
fit::function<bool(/*descendant*/ zx_koid_t, /*ancestor*/ zx_koid_t)>
is_descendant_and_connected,
fit::function<void()> on_channel_closed)
: settings_(std::move(settings)),
viewport_(std::move(viewport)),
binding_(this, std::move(device)),
is_descendant_and_connected_(std::move(is_descendant_and_connected)),
on_channel_closed_(std::move(on_channel_closed)),
inspector_(std::move(inspect_node)) {
FX_DCHECK(is_descendant_and_connected_);
FX_LOGS(INFO) << "Injector : Registered new injector with "
<< " Device Id: " << settings_.device_id
<< " Device Type: " << static_cast<uint32_t>(settings_.device_type)
<< " Dispatch Policy: " << static_cast<uint32_t>(settings_.dispatch_policy)
<< " Context koid: " << settings_.context_koid
<< " and Target koid: " << settings_.target_koid;
binding_.set_error_handler([this](zx_status_t) {
// Clean up ongoing streams before calling the supplied error handler.
CancelOngoingStreams();
// NOTE: Triggers destruction of this object.
on_channel_closed_();
});
}
void Injector::Inject(std::vector<fuchsia::ui::pointerinjector::Event> events,
InjectCallback callback) {
TRACE_DURATION("input", "Injector::Inject");
if (!is_descendant_and_connected_(settings_.target_koid, settings_.context_koid)) {
FX_LOGS(ERROR) << "Inject() called with Context (koid: " << settings_.context_koid
<< ") and Target (koid: " << settings_.target_koid
<< ") making an invalid hierarchy.";
CloseChannel(ZX_ERR_BAD_STATE);
return;
}
if (events.empty()) {
FX_LOGS(ERROR) << "Inject() called without any events";
CloseChannel(ZX_ERR_INVALID_ARGS);
return;
}
for (const auto& event : events) {
if (!event.has_timestamp() || !event.has_data()) {
FX_LOGS(ERROR) << "Inject() called with an incomplete event";
CloseChannel(ZX_ERR_INVALID_ARGS);
return;
}
inspector_.OnPointerInjectorEvent(event);
if (event.data().is_viewport()) {
const auto& new_viewport = event.data().viewport();
{
const zx_status_t result = IsValidViewport(new_viewport);
if (result != ZX_OK) {
// Errors printed inside IsValidViewport. Just close channel here.
CloseChannel(result);
return;
}
}
viewport_ = {.extents = {new_viewport.extents()},
.context_from_viewport_transform = utils::ColumnMajorMat3ArrayToMat4(
new_viewport.viewport_to_context_transform())};
continue;
} else if (event.data().is_pointer_sample()) {
const auto& pointer_sample = event.data().pointer_sample();
const auto [result, stream_id] = ValidatePointerSample(pointer_sample);
if (result != ZX_OK) {
CloseChannel(result);
return;
}
if (event.has_trace_flow_id()) {
TRACE_FLOW_END("input", "dispatch_event_to_scenic", event.trace_flow_id());
}
ForwardEvent(event, stream_id);
continue;
} else {
// Should be unreachable.
FX_LOGS(WARNING) << "Unknown fuchsia::ui::pointerinjector::Data received";
}
}
callback();
}
std::pair<zx_status_t, StreamId> Injector::ValidatePointerSample(
const fuchsia::ui::pointerinjector::PointerSample& pointer_sample) {
if (!HasRequiredFields(pointer_sample)) {
FX_LOGS(ERROR)
<< "Injected fuchsia::ui::pointerinjector::PointerSample was missing required fields";
return {ZX_ERR_INVALID_ARGS, kInvalidStreamId};
}
const auto [x, y] = pointer_sample.position_in_viewport();
if (!std::isfinite(x) || !std::isfinite(y)) {
FX_LOGS(ERROR) << "fuchsia::ui::pointerinjector::PointerSample contained a NaN or inf value";
return {ZX_ERR_INVALID_ARGS, kInvalidStreamId};
}
// Enforce event stream ordering rules. It keeps the event stream clean for downstream clients.
const auto stream_id = ValidateEventStream(pointer_sample.pointer_id(), pointer_sample.phase());
if (stream_id == kInvalidStreamId) {
return {ZX_ERR_BAD_STATE, kInvalidStreamId};
}
return {ZX_OK, stream_id};
}
StreamId Injector::ValidateEventStream(uint32_t pointer_id, EventPhase phase) {
const bool stream_is_ongoing = ongoing_streams_.count(pointer_id) > 0;
const bool double_add = stream_is_ongoing && phase == EventPhase::ADD;
const bool invalid_start = !stream_is_ongoing && phase != EventPhase::ADD;
if (double_add) {
FX_LOGS(ERROR) << "Inject() called with invalid event stream: double-add, ptr-id: "
<< pointer_id << ", stream-event-count: " << ongoing_streams_.count(pointer_id)
<< ", phase: " << (int)phase;
return kInvalidStreamId;
}
if (invalid_start) {
FX_LOGS(ERROR) << "Inject() called with invalid event stream: invalid-start, ptr-id: "
<< pointer_id << ", stream-event-count: " << ongoing_streams_.count(pointer_id)
<< ", phase: " << (int)phase;
return kInvalidStreamId;
}
// Update stream state.
StreamId stream_id = kInvalidStreamId;
if (phase == EventPhase::ADD) {
ongoing_streams_.emplace(pointer_id, NewStreamId());
stream_id = ongoing_streams_.at(pointer_id);
} else if (phase == EventPhase::REMOVE || phase == EventPhase::CANCEL) {
stream_id = ongoing_streams_.at(pointer_id);
ongoing_streams_.erase(pointer_id);
} else {
stream_id = ongoing_streams_.at(pointer_id);
}
FX_DCHECK(stream_id != kInvalidStreamId);
return stream_id;
}
void Injector::CancelOngoingStreams() {
// Inject CANCEL event for each ongoing stream.
for (const auto [pointer_id, stream_id] : ongoing_streams_) {
CancelStream(pointer_id, stream_id);
}
ongoing_streams_.clear();
}
void Injector::CloseChannel(zx_status_t epitaph) {
CancelOngoingStreams();
binding_.Close(epitaph);
// NOTE: Triggers destruction of this object.
on_channel_closed_();
}
zx_status_t Injector::IsValidViewport(const fuchsia::ui::pointerinjector::Viewport& viewport) {
if (!viewport.has_extents() || !viewport.has_viewport_to_context_transform()) {
FX_LOGS(ERROR) << "Provided fuchsia::ui::pointerinjector::Viewport had missing fields";
return ZX_ERR_INVALID_ARGS;
}
if (!AreValidExtents(viewport.extents())) {
FX_LOGS(ERROR)
<< "Provided fuchsia::ui::pointerinjector::Viewport had invalid extents. Extents min: {"
<< viewport.extents()[0][0] << ", " << viewport.extents()[0][1] << "} max: {"
<< viewport.extents()[1][0] << ", " << viewport.extents()[1][1] << "}";
return ZX_ERR_INVALID_ARGS;
}
if (std::any_of(viewport.viewport_to_context_transform().begin(),
viewport.viewport_to_context_transform().end(),
[](float f) { return !std::isfinite(f); })) {
FX_LOGS(ERROR) << "Provided fuchsia::ui::pointerinjector::Viewport "
"viewport_to_context_transform contained a NaN or infinity";
return ZX_ERR_INVALID_ARGS;
}
// Must be invertible, i.e. determinant must be non-zero.
const glm::mat4 viewport_to_context_transform =
utils::ColumnMajorMat3ArrayToMat4(viewport.viewport_to_context_transform());
if (fabs(glm::determinant(viewport_to_context_transform)) <=
std::numeric_limits<float>::epsilon()) {
FX_LOGS(ERROR) << "Provided fuchsia::ui::pointerinjector::Viewport had a non-invertible matrix";
return ZX_ERR_INVALID_ARGS;
}
return ZX_OK;
}
} // namespace scenic_impl::input