blob: 2b729e5857df44d469333264dfdc6326376eda4f [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 <lib/async-testing/test_loop.h>
#include <lib/async/default.h>
#include <lib/async/dispatcher.h>
#include <lib/fit/thread_checker.h>
#include <lib/syslog/cpp/macros.h>
#include <atomic>
#include <chrono>
#include <cstddef>
#include <mutex>
#include <thread>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include "src/ui/scenic/lib/flatland/flatland_presenter_impl.h"
#include "src/ui/scenic/lib/flatland/tests/logging_event_loop.h"
#include "src/ui/scenic/lib/scheduling/tests/mocks/frame_scheduler_mocks.h"
#include "src/ui/scenic/lib/utils/helpers.h"
using flatland::FlatlandPresenterImpl;
namespace flatland::test {
namespace {
// This harness uses a real loop instead of a test loop since the multithreading test requires the
// tasks posted by the FlatlandPresenterImpl to run without blocking the worker threads.
class FlatlandPresenterTest : public LoggingEventLoop, public ::testing::Test {
public:
std::shared_ptr<FlatlandPresenterImpl> CreateFlatlandPresenterImpl(
scheduling::FrameScheduler& scheduler) {
return std::make_shared<FlatlandPresenterImpl>(dispatcher(), scheduler);
}
};
} // namespace
TEST_F(FlatlandPresenterTest, RegisterPresentForwardsToFrameScheduler) {
scheduling::test::MockFrameScheduler frame_scheduler;
// Capture the relevant arguments of the RegisterPresent() call.
scheduling::SessionId last_session_id = scheduling::kInvalidSessionId;
scheduling::PresentId last_present_id = scheduling::kInvalidPresentId;
frame_scheduler.set_register_present_callback(
[&last_session_id, &last_present_id](scheduling::SessionId session_id,
std::vector<zx::event> release_fences,
scheduling::PresentId present_id) {
last_session_id = session_id;
last_present_id = present_id;
});
auto presenter = CreateFlatlandPresenterImpl(frame_scheduler);
const scheduling::SessionId kSessionId = 2;
const scheduling::PresentId present_id = scheduling::GetNextPresentId();
presenter->ScheduleUpdateForSession(zx::time(0), {kSessionId, present_id}, /*unsquashable=*/false,
/*release_fences=*/{}, false);
RunLoopUntilIdle();
EXPECT_EQ(last_session_id, kSessionId);
EXPECT_EQ(last_present_id, present_id);
}
TEST_F(FlatlandPresenterTest, ScheduleUpdateForSessionForwardsToFrameScheduler) {
scheduling::test::MockFrameScheduler frame_scheduler;
// Capture the relevant arguments of the ScheduleUpdateForSession() call.
zx::time last_presentation_time = zx::time(0);
auto last_id_pair = scheduling::SchedulingIdPair({
.session_id = scheduling::kInvalidSessionId,
.present_id = scheduling::kInvalidPresentId,
});
bool last_squashable = false;
bool last_schedule_asap = false;
frame_scheduler.set_schedule_update_for_session_callback(
[&last_presentation_time, &last_id_pair, &last_squashable, &last_schedule_asap](
zx::time presentation_time, scheduling::SchedulingIdPair id_pair, bool squashable,
bool schedule_asap) {
last_presentation_time = presentation_time;
last_id_pair = id_pair;
last_squashable = squashable;
last_schedule_asap = schedule_asap;
});
auto presenter = CreateFlatlandPresenterImpl(frame_scheduler);
const auto kIdPair = scheduling::SchedulingIdPair({
.session_id = 1,
.present_id = 2,
});
const zx::time kPresentationTime = zx::time(123);
const bool kUnsquashable = false;
bool kScheduleAsap = false;
presenter->ScheduleUpdateForSession(kPresentationTime, kIdPair, kUnsquashable,
/*release_fences=*/{}, kScheduleAsap);
RunLoopUntilIdle();
EXPECT_EQ(last_presentation_time, kPresentationTime);
EXPECT_EQ(last_id_pair, kIdPair);
EXPECT_EQ(last_squashable, !kUnsquashable);
EXPECT_EQ(last_schedule_asap, kScheduleAsap);
// Send another update where schedule_asap is true.
kScheduleAsap = true;
const auto kIdPair2 = scheduling::SchedulingIdPair({
.session_id = 1,
.present_id = 3,
});
presenter->ScheduleUpdateForSession(kPresentationTime, kIdPair2, kUnsquashable,
/*release_fences=*/{}, /*schedule_asap=*/kScheduleAsap);
RunLoopUntilIdle();
// zx::time is zeroed.
EXPECT_EQ(last_presentation_time, zx::time(0));
EXPECT_EQ(last_id_pair, kIdPair2);
EXPECT_EQ(last_squashable, !kUnsquashable);
EXPECT_EQ(last_schedule_asap, kScheduleAsap);
}
TEST_F(FlatlandPresenterTest, ScheduleAsapModifiesPresentationTime) {
scheduling::test::MockFrameScheduler frame_scheduler;
// Capture the relevant arguments of the ScheduleUpdateForSession() call.
zx::time last_presentation_time = zx::time(0);
auto last_id_pair = scheduling::SchedulingIdPair({
.session_id = scheduling::kInvalidSessionId,
.present_id = scheduling::kInvalidPresentId,
});
bool last_squashable = false;
bool last_schedule_asap = false;
frame_scheduler.set_schedule_update_for_session_callback(
[&last_presentation_time, &last_id_pair, &last_squashable, &last_schedule_asap](
zx::time presentation_time, scheduling::SchedulingIdPair id_pair, bool squashable,
bool schedule_asap) {
last_presentation_time = presentation_time;
last_id_pair = id_pair;
last_squashable = squashable;
last_schedule_asap = schedule_asap;
});
auto presenter = CreateFlatlandPresenterImpl(frame_scheduler);
const auto kIdPair = scheduling::SchedulingIdPair({
.session_id = 1,
.present_id = 2,
});
const zx::time kPresentationTime = zx::time(123);
const bool kUnsquashable = false;
const bool kScheduleAsap = true;
presenter->ScheduleUpdateForSession(kPresentationTime, kIdPair, kUnsquashable,
/*release_fences=*/{}, kScheduleAsap);
RunLoopUntilIdle();
// zx::time is zeroed.
EXPECT_EQ(last_presentation_time, zx::time(0));
EXPECT_EQ(last_id_pair, kIdPair);
EXPECT_EQ(last_squashable, !kUnsquashable);
EXPECT_EQ(last_schedule_asap, kScheduleAsap);
}
TEST_F(FlatlandPresenterTest, RemoveSessionForwardsToFrameScheduler) {
scheduling::test::MockFrameScheduler frame_scheduler;
// FlatlandPresenter should first remove the session_id and *then* schedule a new frame for it.
// Capture both ordering and arguments to confirm.
std::vector<std::pair<std::string, scheduling::SessionId>> results;
frame_scheduler.set_remove_session_callback([&results](scheduling::SessionId session_id) {
results.emplace_back("RemoveSession", session_id);
});
frame_scheduler.set_schedule_update_for_session_callback(
[&results](auto time, auto id_pair, auto squashable, auto schedule_asap) {
results.emplace_back("Schedule", id_pair.session_id);
});
auto presenter = CreateFlatlandPresenterImpl(frame_scheduler);
const scheduling::SessionId kSessionId = 1;
presenter->RemoveSession(kSessionId, std::nullopt);
RunLoopUntilIdle();
// Since this function runs on the main thread, no RunLoopUntilIdle() is necessary.
EXPECT_THAT(results, testing::ElementsAre(std::make_pair("RemoveSession", kSessionId),
std::make_pair("Schedule", kSessionId)));
}
TEST_F(FlatlandPresenterTest, GetFuturePresentationInfosForwardsToFrameScheduler) {
scheduling::test::MockFrameScheduler frame_scheduler;
// Capture the relevant arguments of the GetFuturePresentationInfos() call.
zx::duration last_requested_prediction_span;
const zx::time kLatchPoint = zx::time(15122);
const zx::time kPresentationTime = zx::time(15410);
frame_scheduler.set_get_future_presentation_infos_callback(
[&last_requested_prediction_span, kLatchPoint,
kPresentationTime](zx::duration requested_prediction_span) {
last_requested_prediction_span = requested_prediction_span;
std::vector<scheduling::FuturePresentationInfo> presentation_infos(1);
presentation_infos[0].latch_point = kLatchPoint;
presentation_infos[0].presentation_time = kPresentationTime;
return presentation_infos;
});
auto presenter = CreateFlatlandPresenterImpl(frame_scheduler);
std::vector<scheduling::FuturePresentationInfo> presentation_infos =
presenter->GetFuturePresentationInfos();
RunLoopUntilIdle();
// The requested prediction span should be reasonable - greater than 1 frame's worth of data.
EXPECT_GT(last_requested_prediction_span, zx::msec(17));
EXPECT_EQ(presentation_infos.size(), 1u);
EXPECT_EQ(presentation_infos[0].latch_point, kLatchPoint);
EXPECT_EQ(presentation_infos[0].presentation_time, kPresentationTime);
}
// Helper function for TakeReleaseFences test below. Encapsulates two calls which always happen
// together in the test: UpdateSessions() and TakeReleaseFences().
static std::vector<zx::event> TakeReleaseFences(
const std::shared_ptr<FlatlandPresenterImpl>& presenter,
const std::unordered_map<scheduling::SessionId, scheduling::PresentId>& sessions_to_update) {
presenter->AccumulateReleaseFences(sessions_to_update);
return presenter->TakeReleaseFences();
}
TEST_F(FlatlandPresenterTest, TakeReleaseFences) {
// The frame scheduler isn't actually used for this test, although it *is* required for the
// presenter to properly stash the release fences (not inherently, just an implementation detail).
scheduling::test::MockFrameScheduler frame_scheduler;
auto presenter = CreateFlatlandPresenterImpl(frame_scheduler);
const scheduling::SessionId kSessionIdA = 3;
const scheduling::SessionId kSessionIdB = 7;
// Create release fences
std::vector<zx::event> release_fences_A1 = utils::CreateEventArray(2);
std::vector<zx_koid_t> release_fence_koids_A1 = utils::ExtractKoids(release_fences_A1);
std::vector<zx::event> release_fences_A2 = utils::CreateEventArray(2);
std::vector<zx_koid_t> release_fence_koids_A2 = utils::ExtractKoids(release_fences_A2);
std::vector<zx::event> release_fences_B1 = utils::CreateEventArray(2);
std::vector<zx_koid_t> release_fence_koids_B1 = utils::ExtractKoids(release_fences_B1);
std::vector<zx::event> release_fences_B2 = utils::CreateEventArray(2);
std::vector<zx_koid_t> release_fence_koids_B2 = utils::ExtractKoids(release_fences_B2);
std::vector<zx::event> release_fences_B3 = utils::CreateEventArray(2);
std::vector<zx_koid_t> release_fence_koids_B3 = utils::ExtractKoids(release_fences_B3);
const auto present_id_A1 = scheduling::GetNextPresentId();
presenter->ScheduleUpdateForSession(zx::time(0), {kSessionIdA, present_id_A1},
/*unsquashable=*/true, std::move(release_fences_A1), false);
const auto present_id_A2 = scheduling::GetNextPresentId();
presenter->ScheduleUpdateForSession(zx::time(0), {kSessionIdA, present_id_A2},
/*unsquashable=*/true, std::move(release_fences_A2), false);
const auto present_id_B1 = scheduling::GetNextPresentId();
presenter->ScheduleUpdateForSession(zx::time(0), {kSessionIdB, present_id_B1},
/*unsquashable=*/true, std::move(release_fences_B1), false);
const auto present_id_B2 = scheduling::GetNextPresentId();
presenter->ScheduleUpdateForSession(zx::time(0), {kSessionIdB, present_id_B2},
/*unsquashable=*/true, std::move(release_fences_B2), false);
// There will be no fences yet, because ScheduleUpdateForSession() stashes the fences in a task
// dispatched to the main thread, which hasn't run yet.
auto fences_empty = TakeReleaseFences(presenter, {
{kSessionIdA, present_id_A2},
{kSessionIdB, present_id_B1},
});
EXPECT_TRUE(fences_empty.empty());
// Try to take the same fences. We should see the fences for A1/A2/B1, but not B2. Note that we
// don't explicitly mention A1, but we get the fences for it too, because A2 has a higher present
// ID for the same session ID.
RunLoopUntilIdle();
auto fences_A1A2B1 = TakeReleaseFences(presenter, {
{kSessionIdA, present_id_A2},
{kSessionIdB, present_id_B1},
});
EXPECT_EQ(fences_A1A2B1.size(), release_fence_koids_A1.size() + release_fence_koids_A2.size() +
release_fence_koids_B1.size());
auto fences_A1A2B1_koids = utils::ExtractKoids(fences_A1A2B1);
for (auto koid : release_fence_koids_A1) {
EXPECT_THAT(fences_A1A2B1_koids, testing::Contains(koid));
}
for (auto koid : release_fence_koids_A2) {
EXPECT_THAT(fences_A1A2B1_koids, testing::Contains(koid));
}
for (auto koid : release_fence_koids_B1) {
EXPECT_THAT(fences_A1A2B1_koids, testing::Contains(koid));
}
// Register one more present.
const auto present_id_B3 = scheduling::GetNextPresentId();
presenter->ScheduleUpdateForSession(zx::time(0), {kSessionIdB, present_id_B3},
/*unsquashable=*/true, std::move(release_fences_B3), false);
RunLoopUntilIdle();
auto fences_B2B3 = TakeReleaseFences(presenter, {
{kSessionIdB, present_id_B3},
});
EXPECT_EQ(fences_B2B3.size(), release_fence_koids_B2.size() + release_fence_koids_B3.size());
auto fences_B2B3_koids = utils::ExtractKoids(fences_B2B3);
for (auto koid : release_fence_koids_B2) {
EXPECT_THAT(fences_B2B3_koids, testing::Contains(koid));
}
for (auto koid : release_fence_koids_B3) {
EXPECT_THAT(fences_B2B3_koids, testing::Contains(koid));
}
}
TEST_F(FlatlandPresenterTest, MultithreadedAccess) {
scheduling::test::MockFrameScheduler frame_scheduler;
// The FrameScheduler will be accessed in a thread-safe way, so the test instead collects the
// registered presents and scheduled updates and ensures each function was called the correct
// number of times with the correct set of ID pairs.
std::set<scheduling::SchedulingIdPair> registered_presents;
std::set<scheduling::SchedulingIdPair> scheduled_updates;
// Also use a generic function call counter to test mutual exclusion between function calls.
size_t function_count = 0;
frame_scheduler.set_register_present_callback(
[&registered_presents, &function_count](scheduling::SessionId session_id,
std::vector<zx::event> release_fences,
scheduling::PresentId present_id) {
registered_presents.insert({session_id, present_id});
++function_count;
});
frame_scheduler.set_schedule_update_for_session_callback(
[&scheduled_updates, &function_count](zx::time presentation_time,
scheduling::SchedulingIdPair id_pair, bool squashable,
bool schedule_asap) {
scheduled_updates.insert(id_pair);
++function_count;
});
frame_scheduler.set_get_future_presentation_infos_callback(
[&function_count](zx::duration requested_prediction_span)
-> std::vector<scheduling::FuturePresentationInfo> {
++function_count;
std::vector<scheduling::FuturePresentationInfo> infos;
return infos;
});
auto presenter = CreateFlatlandPresenterImpl(frame_scheduler);
// Start 10 "sessions", each of which registers 100 presents and schedules 100 updates.
static constexpr uint64_t kNumSessions = 10;
static constexpr uint64_t kNumPresents = 100;
std::vector<std::thread> threads;
std::mutex mutex;
std::unordered_set<scheduling::PresentId> present_ids;
const auto now = std::chrono::steady_clock::now();
const auto then = now + std::chrono::milliseconds(50);
std::atomic<uint64_t> sessions_posted_all_tasks = 0;
std::atomic<uint64_t> loop_quits = 0;
for (uint64_t session_id = 1; session_id <= kNumSessions; ++session_id) {
std::thread thread([then, session_id, &mutex, &present_ids, &presenter, &loop_quits,
&sessions_posted_all_tasks]() {
// Because each of the threads do a fixed amount of work, they may trigger in succession
// without overlap. In order to bombard the system with concurrent requests, stall thread
// execution until a specific time.
std::this_thread::sleep_until(then);
std::vector<scheduling::PresentId> presents;
uint64_t presentation_info_count = 0;
// Create a thread checker so that we can verify that the GetFuturePresentationInfos()
// response runs on the correct thread.
fit::thread_checker checker;
EXPECT_TRUE(checker.is_thread_valid());
// Set loop and dispatcher which is needed for GetFuturePresentationInfos().
async::Loop loop(&kAsyncLoopConfigAttachToCurrentThread);
for (uint64_t i = 0; i < kNumPresents; ++i) {
// ScheduleUpdateForSession() is one of the two functions being tested.
auto present_id = scheduling::GetNextPresentId();
presenter->ScheduleUpdateForSession(zx::time(0), {session_id, present_id},
/*unsquashable=*/true, /*release_fences=*/{}, false);
presents.push_back(present_id);
// Yield with some randomness so the threads get jumbled up a bit.
if (std::rand() % 4 == 0) {
std::this_thread::yield();
}
presentation_info_count++;
EXPECT_TRUE(checker.is_thread_valid());
// When all kNumPresents tasks are posted back, this thread can safely return.
if (presentation_info_count == kNumPresents) {
loop_quits++;
loop.Quit();
}
}
// Acquire the test mutex and insert all IDs for later evaluation.
{
std::scoped_lock lock(mutex);
for (const auto& present_id : presents) {
present_ids.insert(present_id);
}
}
sessions_posted_all_tasks++;
// This thread should run until it receives all replies back from the frame scheduler.
loop.Run();
});
threads.push_back(std::move(thread));
}
// Make calls directly to the FrameScheduler to mimic GFX, which runs on the "main" looper, which
// in this test is just this thread.
static constexpr scheduling::SessionId kGfxSessionId = kNumSessions + 1;
static constexpr uint64_t kNumGfxPresents = 500;
std::vector<scheduling::PresentId> gfx_presents;
std::this_thread::sleep_until(then);
for (uint64_t i = 0; i < kNumGfxPresents; ++i) {
// RegisterPresent() is one of the three functions being tested.
auto present_id = scheduling::GetNextPresentId();
frame_scheduler.RegisterPresent(kGfxSessionId, /*release_fences=*/{}, present_id);
gfx_presents.push_back(present_id);
// ScheduleUpdateForSession() is the second function being tested.
frame_scheduler.ScheduleUpdateForSession(zx::time(0), {kGfxSessionId, present_id},
/*squashable=*/true, /*schedule_asap=*/false);
}
{
std::scoped_lock lock(mutex);
for (const auto& present_id : gfx_presents) {
present_ids.insert(present_id);
}
}
// We need to be careful to account for the race where this line can be reached before all t
// sessions have posted their GetFuturePresentationTimes() messages, leading the test to deadlock.
//
// First ensure all t threads have posted all their tasks on the main dispatcher.
RunLoopUntil([&sessions_posted_all_tasks] { return sessions_posted_all_tasks == kNumSessions; });
// Then, ensure the main dispatcher can reply to all the tasks, and posts its replies.
RunLoopUntilIdle();
// Finally, join all threads that can now process the replies.
for (auto& t : threads) {
t.join();
}
// Flush all the tasks posted by the presenter.
RunLoopUntilIdle();
// Verify that all the PresentIds are unique and that the sets from both mock functions have the
// same number of ID pairs.
static constexpr uint64_t kTotalNumPresents = (kNumSessions * kNumPresents) + kNumGfxPresents;
EXPECT_EQ(present_ids.size(), kTotalNumPresents);
EXPECT_EQ(registered_presents.size(), kTotalNumPresents);
EXPECT_EQ(scheduled_updates.size(), kTotalNumPresents);
// Verify that the correct total number of function calls were made.
EXPECT_EQ(function_count, kTotalNumPresents * 2ul);
// Verify that the sets from both mock functions are identical.
EXPECT_THAT(registered_presents, ::testing::ElementsAreArray(scheduled_updates));
// Verify that every session received the total number of presentation_infos.
EXPECT_EQ(loop_quits, kNumSessions);
}
} // namespace flatland::test