blob: 3824dc4d92baec9f36f8ffa9760830a1939683d7 [file] [log] [blame]
// Copyright 2013 The Flutter 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 "flutter/shell/common/shell_test.h"
#include "flutter/testing/testing.h"
// CREATE_NATIVE_ENTRY is leaky by design
// NOLINTBEGIN(clang-analyzer-core.StackAddressEscape)
namespace flutter {
namespace testing {
// Throughout these tests, the choice of time unit is irrelevant as long as all
// times have the same units.
using UnitlessTime = int;
// Signature of a generator function that takes the frame index as input and
// returns the time of that frame.
using Generator = std::function<UnitlessTime(int)>;
//----------------------------------------------------------------------------
/// Simulate n input events where the i-th one is delivered at delivery_time(i).
///
/// Simulation results will be written into events_consumed_at_frame whose
/// length will be equal to the number of frames drawn. Each element in the
/// vector is the number of input events consumed up to that frame. (We can't
/// return such vector because ASSERT_TRUE requires return type of void.)
///
/// We assume (and check) that the delivery latency is some base latency plus a
/// random latency where the random latency must be within one frame:
///
/// 1. latency = delivery_time(i) - j * frame_time = base_latency +
/// random_latency
/// 2. 0 <= base_latency, 0 <= random_latency < frame_time
///
/// We also assume that there will be at least one input event per frame if
/// there were no latency. Let j = floor( (delivery_time(i) - base_latency) /
/// frame_time ) be the frame index if there were no latency. Then the set of j
/// should be all integers from 0 to continuous_frame_count - 1 for some
/// integer continuous_frame_count.
///
/// (Note that there coulds be multiple input events within one frame.)
///
/// The test here is insensitive to the choice of time unit as long as
/// delivery_time and frame_time are in the same unit.
static void TestSimulatedInputEvents(
ShellTest* fixture,
int num_events,
UnitlessTime base_latency,
Generator delivery_time,
UnitlessTime frame_time,
std::vector<UnitlessTime>& events_consumed_at_frame,
bool restart_engine = false) {
///// Begin constructing shell ///////////////////////////////////////////////
auto settings = fixture->CreateSettingsForFixture();
std::unique_ptr<Shell> shell = fixture->CreateShell({
.settings = settings,
.platform_view_create_callback = ShellTestPlatformViewBuilder({
.simulate_vsync = true,
}),
});
auto configuration = RunConfiguration::InferFromSettings(settings);
configuration.SetEntrypoint("onPointerDataPacketMain");
// The following 4 variables are only accessed in the UI thread by
// nativeOnPointerDataPacket and nativeOnBeginFrame between their
// initializations and `shell.reset()`.
events_consumed_at_frame.clear();
bool will_draw_new_frame = true;
int events_consumed = 0;
int frame_drawn = 0;
auto nativeOnPointerDataPacket = [&events_consumed_at_frame,
&will_draw_new_frame, &events_consumed,
&frame_drawn](Dart_NativeArguments args) {
events_consumed += 1;
if (will_draw_new_frame) {
frame_drawn += 1;
will_draw_new_frame = false;
events_consumed_at_frame.push_back(events_consumed);
} else {
events_consumed_at_frame.back() = events_consumed;
}
};
fixture->AddNativeCallback("NativeOnPointerDataPacket",
CREATE_NATIVE_ENTRY(nativeOnPointerDataPacket));
ASSERT_TRUE(configuration.IsValid());
fixture->RunEngine(shell.get(), std::move(configuration));
if (restart_engine) {
auto new_configuration = RunConfiguration::InferFromSettings(settings);
new_configuration.SetEntrypoint("onPointerDataPacketMain");
ASSERT_TRUE(new_configuration.IsValid());
fixture->RestartEngine(shell.get(), std::move(new_configuration));
}
///// End constructing shell /////////////////////////////////////////////////
ASSERT_GE(base_latency, 0);
// Check that delivery_time satisfies our assumptions.
int continuous_frame_count = 0;
for (int i = 0; i < num_events; i += 1) {
// j is the frame index of event i if there were no latency.
int j = static_cast<int>((delivery_time(i) - base_latency) / frame_time);
if (j == continuous_frame_count) {
continuous_frame_count += 1;
}
double random_latency = delivery_time(i) - j * frame_time - base_latency;
ASSERT_GE(random_latency, 0);
ASSERT_LT(random_latency, frame_time);
// If there were no latency, there should be at least one event per frame.
// Hence j should never skip any integer less than continuous_frame_count.
ASSERT_LT(j, continuous_frame_count);
}
// This has to be running on a different thread than Platform thread to avoid
// dead locks.
auto simulation = std::async(std::launch::async, [&]() {
// i is the input event's index.
// j is the frame's index.
for (int i = 0, j = 0; i < num_events; j += 1) {
double t = j * frame_time;
while (i < num_events && delivery_time(i) <= t) {
ShellTest::DispatchFakePointerData(shell.get());
i += 1;
}
ShellTest::VSyncFlush(shell.get(), will_draw_new_frame);
}
// Finally, issue a vsync for the pending event that may be generated duing
// the last vsync.
ShellTest::VSyncFlush(shell.get(), will_draw_new_frame);
});
simulation.wait();
TaskRunners task_runners = fixture->GetTaskRunnersForFixture();
fml::AutoResetWaitableEvent latch;
task_runners.GetPlatformTaskRunner()->PostTask([&shell, &latch]() mutable {
shell.reset();
latch.Signal();
});
latch.Wait();
// Make sure that all events have been consumed so
// https://github.com/flutter/flutter/issues/40863 won't happen again.
ASSERT_EQ(events_consumed_at_frame.back(), num_events);
}
void CreateSimulatedPointerData(PointerData& data,
PointerData::Change change,
double dx,
double dy) {
data.time_stamp = 0;
data.change = change;
data.kind = PointerData::DeviceKind::kTouch;
data.signal_kind = PointerData::SignalKind::kNone;
data.device = 0;
data.pointer_identifier = 0;
data.physical_x = dx;
data.physical_y = dy;
data.physical_delta_x = 0.0;
data.physical_delta_y = 0.0;
data.buttons = 0;
data.obscured = 0;
data.synthesized = 0;
data.pressure = 0.0;
data.pressure_min = 0.0;
data.pressure_max = 0.0;
data.distance = 0.0;
data.distance_max = 0.0;
data.size = 0.0;
data.radius_major = 0.0;
data.radius_minor = 0.0;
data.radius_min = 0.0;
data.radius_max = 0.0;
data.orientation = 0.0;
data.tilt = 0.0;
data.platformData = 0;
data.scroll_delta_x = 0.0;
data.scroll_delta_y = 0.0;
}
TEST_F(ShellTest, MissAtMostOneFrameForIrregularInputEvents) {
// We don't use `constexpr int frame_time` here because MSVC doesn't handle
// it well with lambda capture.
UnitlessTime frame_time = 10;
UnitlessTime base_latency = 0.5 * frame_time;
Generator extreme = [frame_time, base_latency](int i) {
return static_cast<UnitlessTime>(
i * frame_time + base_latency +
(i % 2 == 0 ? 0.1 * frame_time : 0.9 * frame_time));
};
constexpr int n = 40;
std::vector<int> events_consumed_at_frame;
TestSimulatedInputEvents(this, n, base_latency, extreme, frame_time,
events_consumed_at_frame);
int frame_drawn = events_consumed_at_frame.size();
ASSERT_GE(frame_drawn, n - 1);
// Make sure that it also works after an engine restart.
TestSimulatedInputEvents(this, n, base_latency, extreme, frame_time,
events_consumed_at_frame, true /* restart_engine */);
int frame_drawn_after_restart = events_consumed_at_frame.size();
ASSERT_GE(frame_drawn_after_restart, n - 1);
}
TEST_F(ShellTest, DelayAtMostOneEventForFasterThanVSyncInputEvents) {
// We don't use `constexpr int frame_time` here because MSVC doesn't handle
// it well with lambda capture.
UnitlessTime frame_time = 10;
UnitlessTime base_latency = 0.2 * frame_time;
Generator double_sampling = [frame_time, base_latency](int i) {
return static_cast<UnitlessTime>(i * 0.5 * frame_time + base_latency);
};
constexpr int n = 40;
std::vector<int> events_consumed_at_frame;
TestSimulatedInputEvents(this, n, base_latency, double_sampling, frame_time,
events_consumed_at_frame);
// Draw one extra frame due to delaying a pending packet for the next frame.
int frame_drawn = events_consumed_at_frame.size();
ASSERT_EQ(frame_drawn, n / 2 + 1);
for (int i = 0; i < n / 2; i += 1) {
ASSERT_GE(events_consumed_at_frame[i], 2 * i - 1);
}
}
TEST_F(ShellTest, HandlesActualIphoneXsInputEvents) {
// Actual delivery times measured on iPhone Xs, in the unit of frame_time
// (16.67ms for 60Hz).
static constexpr double iphone_xs_times[] = {0.15,
1.0773046874999999,
2.1738720703124996,
3.0579052734374996,
4.0890087890624995,
5.0952685546875,
6.1251708984375,
7.1253076171875,
8.125927734374999,
9.37248046875,
10.133950195312499,
11.161201171875,
12.226992187499999,
13.1443798828125,
14.440327148437499,
15.091684570312498,
16.138681640625,
17.126469726562497,
18.1592431640625,
19.371372070312496,
20.033774414062496,
21.021782226562497,
22.070053710937497,
23.325541992187496,
24.119648437499997,
25.084262695312496,
26.077866210937497,
27.036547851562496,
28.035073242187497,
29.081411132812498,
30.066064453124998,
31.089360351562497,
32.086142578125,
33.4618798828125,
34.14697265624999,
35.0513525390625,
36.136025390624994,
37.1618408203125,
38.144472656249995,
39.201123046875,
40.4339501953125,
41.1552099609375,
42.102128906249995,
43.0426318359375,
44.070131835937495,
45.08862304687499,
46.091469726562494};
constexpr int n = sizeof(iphone_xs_times) / sizeof(iphone_xs_times[0]);
// We don't use `constexpr int frame_time` here because MSVC doesn't handle
// it well with lambda capture.
UnitlessTime frame_time = 10000;
double base_latency_f = 0.0;
for (int i = 0; i < 10; i++) {
base_latency_f += 0.1;
// Everything is converted to int to avoid floating point error in
// TestSimulatedInputEvents.
UnitlessTime base_latency =
static_cast<UnitlessTime>(base_latency_f * frame_time);
Generator iphone_xs_generator = [frame_time, base_latency](int i) {
return base_latency +
static_cast<UnitlessTime>(iphone_xs_times[i] * frame_time);
};
std::vector<int> events_consumed_at_frame;
TestSimulatedInputEvents(this, n, base_latency, iphone_xs_generator,
frame_time, events_consumed_at_frame);
int frame_drawn = events_consumed_at_frame.size();
ASSERT_GE(frame_drawn, n - 1);
}
}
TEST_F(ShellTest, CanCorrectlyPipePointerPacket) {
// Sets up shell with test fixture.
auto settings = CreateSettingsForFixture();
std::unique_ptr<Shell> shell = CreateShell({
.settings = settings,
.platform_view_create_callback = ShellTestPlatformViewBuilder({
.simulate_vsync = true,
}),
});
auto configuration = RunConfiguration::InferFromSettings(settings);
configuration.SetEntrypoint("onPointerDataPacketMain");
// Sets up native handler.
fml::AutoResetWaitableEvent reportLatch;
std::vector<int64_t> result_sequence;
auto nativeOnPointerDataPacket = [&reportLatch, &result_sequence](
Dart_NativeArguments args) {
Dart_Handle exception = nullptr;
result_sequence = tonic::DartConverter<std::vector<int64_t>>::FromArguments(
args, 0, exception);
reportLatch.Signal();
};
// Starts engine.
AddNativeCallback("NativeOnPointerDataPacket",
CREATE_NATIVE_ENTRY(nativeOnPointerDataPacket));
ASSERT_TRUE(configuration.IsValid());
RunEngine(shell.get(), std::move(configuration));
// Starts test.
auto packet = std::make_unique<PointerDataPacket>(6);
PointerData data;
CreateSimulatedPointerData(data, PointerData::Change::kAdd, 0.0, 0.0);
packet->SetPointerData(0, data);
CreateSimulatedPointerData(data, PointerData::Change::kHover, 3.0, 0.0);
packet->SetPointerData(1, data);
CreateSimulatedPointerData(data, PointerData::Change::kDown, 3.0, 0.0);
packet->SetPointerData(2, data);
CreateSimulatedPointerData(data, PointerData::Change::kMove, 3.0, 4.0);
packet->SetPointerData(3, data);
CreateSimulatedPointerData(data, PointerData::Change::kUp, 3.0, 4.0);
packet->SetPointerData(4, data);
CreateSimulatedPointerData(data, PointerData::Change::kRemove, 3.0, 4.0);
packet->SetPointerData(5, data);
ShellTest::DispatchPointerData(shell.get(), std::move(packet));
bool will_draw_new_frame;
ShellTest::VSyncFlush(shell.get(), will_draw_new_frame);
reportLatch.Wait();
size_t expect_length = 6;
ASSERT_EQ(result_sequence.size(), expect_length);
ASSERT_EQ(PointerData::Change(result_sequence[0]), PointerData::Change::kAdd);
ASSERT_EQ(PointerData::Change(result_sequence[1]),
PointerData::Change::kHover);
ASSERT_EQ(PointerData::Change(result_sequence[2]),
PointerData::Change::kDown);
ASSERT_EQ(PointerData::Change(result_sequence[3]),
PointerData::Change::kMove);
ASSERT_EQ(PointerData::Change(result_sequence[4]), PointerData::Change::kUp);
ASSERT_EQ(PointerData::Change(result_sequence[5]),
PointerData::Change::kRemove);
// Cleans up shell.
ASSERT_TRUE(DartVMRef::IsInstanceRunning());
DestroyShell(std::move(shell));
ASSERT_FALSE(DartVMRef::IsInstanceRunning());
}
TEST_F(ShellTest, CanCorrectlySynthesizePointerPacket) {
// Sets up shell with test fixture.
auto settings = CreateSettingsForFixture();
std::unique_ptr<Shell> shell = CreateShell({
.settings = settings,
.platform_view_create_callback = ShellTestPlatformViewBuilder({
.simulate_vsync = true,
}),
});
auto configuration = RunConfiguration::InferFromSettings(settings);
configuration.SetEntrypoint("onPointerDataPacketMain");
// Sets up native handler.
fml::AutoResetWaitableEvent reportLatch;
std::vector<int64_t> result_sequence;
auto nativeOnPointerDataPacket = [&reportLatch, &result_sequence](
Dart_NativeArguments args) {
Dart_Handle exception = nullptr;
result_sequence = tonic::DartConverter<std::vector<int64_t>>::FromArguments(
args, 0, exception);
reportLatch.Signal();
};
// Starts engine.
AddNativeCallback("NativeOnPointerDataPacket",
CREATE_NATIVE_ENTRY(nativeOnPointerDataPacket));
ASSERT_TRUE(configuration.IsValid());
RunEngine(shell.get(), std::move(configuration));
// Starts test.
auto packet = std::make_unique<PointerDataPacket>(4);
PointerData data;
CreateSimulatedPointerData(data, PointerData::Change::kAdd, 0.0, 0.0);
packet->SetPointerData(0, data);
CreateSimulatedPointerData(data, PointerData::Change::kDown, 3.0, 0.0);
packet->SetPointerData(1, data);
CreateSimulatedPointerData(data, PointerData::Change::kUp, 3.0, 4.0);
packet->SetPointerData(2, data);
CreateSimulatedPointerData(data, PointerData::Change::kRemove, 3.0, 4.0);
packet->SetPointerData(3, data);
ShellTest::DispatchPointerData(shell.get(), std::move(packet));
bool will_draw_new_frame;
ShellTest::VSyncFlush(shell.get(), will_draw_new_frame);
reportLatch.Wait();
size_t expect_length = 6;
ASSERT_EQ(result_sequence.size(), expect_length);
ASSERT_EQ(PointerData::Change(result_sequence[0]), PointerData::Change::kAdd);
// The pointer data packet converter should synthesize a hover event.
ASSERT_EQ(PointerData::Change(result_sequence[1]),
PointerData::Change::kHover);
ASSERT_EQ(PointerData::Change(result_sequence[2]),
PointerData::Change::kDown);
// The pointer data packet converter should synthesize a move event.
ASSERT_EQ(PointerData::Change(result_sequence[3]),
PointerData::Change::kMove);
ASSERT_EQ(PointerData::Change(result_sequence[4]), PointerData::Change::kUp);
ASSERT_EQ(PointerData::Change(result_sequence[5]),
PointerData::Change::kRemove);
// Cleans up shell.
ASSERT_TRUE(DartVMRef::IsInstanceRunning());
DestroyShell(std::move(shell));
ASSERT_FALSE(DartVMRef::IsInstanceRunning());
}
} // namespace testing
} // namespace flutter
// NOLINTEND(clang-analyzer-core.StackAddressEscape)