blob: fcb83c487a9d86c635a62302f2fa3f7c52258422 [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/flatland/flatland_manager.h"
#include <lib/syslog/cpp/macros.h>
#include <gtest/gtest.h>
#include "fuchsia/ui/scenic/internal/cpp/fidl.h"
#include "lib/gtest/real_loop_fixture.h"
#include "src/ui/scenic/lib/flatland/renderer/mocks/mock_buffer_collection_importer.h"
#include "src/ui/scenic/lib/flatland/tests/mock_flatland_presenter.h"
#include "src/ui/scenic/lib/scheduling/frame_scheduler.h"
#include "src/ui/scenic/lib/scheduling/id.h"
using ::testing::_;
using ::testing::Return;
using flatland::FlatlandManager;
using flatland::FlatlandPresenter;
using flatland::LinkSystem;
using flatland::MockFlatlandPresenter;
using flatland::UberStructSystem;
using fuchsia::ui::scenic::internal::Flatland;
using fuchsia::ui::scenic::internal::Flatland_Present_Result;
// These macros works like functions that check a variety of conditions, but if those conditions
// fail, the line number for the failure will appear in-line rather than in a function.
// This macro calls Present() on a Flatland object and immediately triggers the session update
// for all sessions so that changes from that Present() are visible in global systems. This is
// primarily useful for testing the user-facing Flatland API.
//
// This macro must be used within a test using the FlatlandManagerTest harness.
//
// |flatland| is a Flatland object constructed with the MockFlatlandPresenter owned by the
// FlatlandManagerTest harness. |session_id| is the SessionId for |flatland|. |expect_success|
// should be false if the call to Present() is expected to trigger an error.
#define PRESENT(flatland, session_id, expect_success) \
{ \
const auto num_pending_sessions = GetNumPendingSessionUpdates(session_id); \
if (expect_success) { \
EXPECT_CALL(*mock_flatland_presenter_, RegisterPresent(session_id, _)); \
EXPECT_CALL(*mock_flatland_presenter_, ScheduleUpdateForSession(_, _)); \
} \
bool processed_callback = false; \
flatland->Present(/*requested_presentation_time=*/0, /*acquire_fences=*/{}, \
/*release_fences=*/{}, [&](Flatland_Present_Result result) { \
EXPECT_EQ(!expect_success, result.is_err()); \
if (!expect_success) { \
EXPECT_EQ(fuchsia::ui::scenic::internal::Error::BAD_OPERATION, \
result.err()); \
} \
processed_callback = true; \
}); \
/* Wait for the worker thread to process the request. */ \
RunLoopUntil([this, session_id, num_pending_sessions] { \
return GetNumPendingSessionUpdates(session_id) > num_pending_sessions; \
}); \
/* Wait for the callback to run as well. */ \
RunLoopUntil([&processed_callback] { return processed_callback; }); \
}
namespace {
class FlatlandManagerTest : public gtest::RealLoopFixture {
public:
FlatlandManagerTest()
: uber_struct_system_(std::make_shared<UberStructSystem>()),
link_system_(std::make_shared<LinkSystem>(uber_struct_system_->GetNextInstanceId())) {}
void SetUp() override {
gtest::RealLoopFixture::SetUp();
mock_flatland_presenter_ = new ::testing::StrictMock<MockFlatlandPresenter>();
ON_CALL(*mock_flatland_presenter_, RegisterPresent(_, _))
.WillByDefault(::testing::Invoke(
[&](scheduling::SessionId session_id, std::vector<zx::event> release_fences) {
EXPECT_TRUE(release_fences.empty());
const auto next_present_id = scheduling::GetNextPresentId();
pending_presents_.insert({session_id, next_present_id});
return next_present_id;
}));
ON_CALL(*mock_flatland_presenter_, ScheduleUpdateForSession(_, _))
.WillByDefault(::testing::Invoke(
[&](zx::time requested_presentation_time, scheduling::SchedulingIdPair id_pair) {
// The ID pair must be already registered.
EXPECT_TRUE(pending_presents_.count(id_pair));
// Ensure present IDs are strictly increasing.
auto& queue = pending_session_updates_[id_pair.session_id];
EXPECT_TRUE(queue.empty() || queue.back() < id_pair.present_id);
// Save the pending present ID.
queue.push(id_pair.present_id);
}));
flatland_presenter_ = std::shared_ptr<FlatlandPresenter>(mock_flatland_presenter_);
manager_ = std::make_unique<FlatlandManager>(
flatland_presenter_, uber_struct_system_, link_system_,
std::vector<std::shared_ptr<flatland::BufferCollectionImporter>>());
}
void TearDown() override {
// Triggers cleanup of manager resources for Flatland instances that have exited.
RunLoopUntilIdle();
// |manager_| may have been reset during the test.
if (manager_) {
EXPECT_EQ(manager_->GetSessionCount(), 0ul);
}
auto snapshot = uber_struct_system_->Snapshot();
EXPECT_TRUE(snapshot.empty());
manager_.reset();
flatland_presenter_.reset();
gtest::RealLoopFixture::TearDown();
}
// Returns the number of currently pending session updates for |session_id|.
size_t GetNumPendingSessionUpdates(scheduling::SessionId session_id) {
const auto& queue = pending_session_updates_[session_id];
return queue.size();
}
// Returns the next pending PresentId for |session_id| and removes it from the list of pending
// session updates. Fails if |session_id| has no pending presents.
scheduling::PresentId PopPendingPresent(scheduling::SessionId session_id) {
auto& queue = pending_session_updates_[session_id];
EXPECT_FALSE(queue.empty());
auto next_present_id = queue.front();
queue.pop();
return next_present_id;
}
protected:
::testing::StrictMock<MockFlatlandPresenter>* mock_flatland_presenter_;
const std::shared_ptr<UberStructSystem> uber_struct_system_;
std::unique_ptr<FlatlandManager> manager_;
// Storage for |mock_flatland_presenter_|.
std::set<scheduling::SchedulingIdPair> pending_presents_;
std::unordered_map<scheduling::SessionId, std::queue<scheduling::PresentId>>
pending_session_updates_;
private:
std::shared_ptr<FlatlandPresenter> flatland_presenter_;
const std::shared_ptr<LinkSystem> link_system_;
};
} // namespace
namespace flatland {
namespace test {
TEST_F(FlatlandManagerTest, CreateFlatlands) {
fidl::InterfacePtr<fuchsia::ui::scenic::internal::Flatland> flatland1;
manager_->CreateFlatland(flatland1.NewRequest());
fidl::InterfacePtr<fuchsia::ui::scenic::internal::Flatland> flatland2;
manager_->CreateFlatland(flatland2.NewRequest());
RunLoopUntilIdle();
EXPECT_TRUE(flatland1.is_bound());
EXPECT_TRUE(flatland2.is_bound());
EXPECT_EQ(manager_->GetSessionCount(), 2ul);
}
TEST_F(FlatlandManagerTest, ManagerDiesBeforeClients) {
fidl::InterfacePtr<fuchsia::ui::scenic::internal::Flatland> flatland;
manager_->CreateFlatland(flatland.NewRequest());
RunLoopUntilIdle();
EXPECT_TRUE(flatland.is_bound());
EXPECT_EQ(manager_->GetSessionCount(), 1ul);
// Explicitly kill the server.
manager_.reset();
RunLoopUntilIdle();
EXPECT_FALSE(flatland.is_bound());
}
TEST_F(FlatlandManagerTest, ManagerImmediatelySendsPresentTokens) {
// Setup a Flatland instance with an OnPresentTokensReturned() callback.
fidl::InterfacePtr<fuchsia::ui::scenic::internal::Flatland> flatland;
manager_->CreateFlatland(flatland.NewRequest());
const scheduling::SessionId id = uber_struct_system_->GetLatestInstanceId();
uint32_t returned_tokens = 0;
flatland.events().OnPresentTokensReturned = [&returned_tokens](uint32_t present_tokens) {
returned_tokens = present_tokens;
};
// Run until the instance receives the initial allotment of tokens.
RunLoopUntil([&returned_tokens]() { return returned_tokens != 0; });
EXPECT_EQ(returned_tokens, scheduling::FrameScheduler::kMaxPresentsInFlight - 1u);
}
TEST_F(FlatlandManagerTest, UpdateSessionsReturnsPresentTokens) {
// Setup two Flatland instances with OnPresentTokensReturned() callbacks.
fidl::InterfacePtr<fuchsia::ui::scenic::internal::Flatland> flatland1;
manager_->CreateFlatland(flatland1.NewRequest());
const scheduling::SessionId id1 = uber_struct_system_->GetLatestInstanceId();
uint32_t returned_tokens1 = 0;
flatland1.events().OnPresentTokensReturned = [&returned_tokens1](uint32_t present_tokens) {
returned_tokens1 = present_tokens;
};
fidl::InterfacePtr<fuchsia::ui::scenic::internal::Flatland> flatland2;
manager_->CreateFlatland(flatland2.NewRequest());
const scheduling::SessionId id2 = uber_struct_system_->GetLatestInstanceId();
uint32_t returned_tokens2 = 0;
flatland2.events().OnPresentTokensReturned = [&returned_tokens2](uint32_t present_tokens) {
returned_tokens2 = present_tokens;
};
// Run both instances receive their initial allotment of tokens, then forget those tokens.
RunLoopUntil([&returned_tokens1]() { return returned_tokens1 != 0; });
returned_tokens1 = 0;
RunLoopUntil([&returned_tokens2]() { return returned_tokens2 != 0; });
returned_tokens2 = 0;
// Present both instances twice, but don't update sessions.
PRESENT(flatland1, id1, true);
PRESENT(flatland1, id1, true);
PRESENT(flatland2, id2, true);
PRESENT(flatland2, id2, true);
auto snapshot = uber_struct_system_->Snapshot();
EXPECT_TRUE(snapshot.empty());
EXPECT_EQ(GetNumPendingSessionUpdates(id1), 2ul);
EXPECT_EQ(GetNumPendingSessionUpdates(id2), 2ul);
// Update the first session, but only with the first PresentId, which should push an UberStruct
// and return one token to the first instance.
auto next_present_id1 = PopPendingPresent(id1);
manager_->UpdateSessions({{id1, next_present_id1}}, /*trace_id=*/0);
snapshot = uber_struct_system_->Snapshot();
EXPECT_EQ(snapshot.size(), 1u);
EXPECT_TRUE(snapshot.count(id1));
EXPECT_FALSE(snapshot.count(id2));
RunLoopUntil([&returned_tokens1]() { return returned_tokens1 != 0; });
EXPECT_EQ(returned_tokens1, 1u);
EXPECT_EQ(returned_tokens2, 0u);
EXPECT_EQ(GetNumPendingSessionUpdates(id1), 1ul);
EXPECT_EQ(GetNumPendingSessionUpdates(id2), 2ul);
returned_tokens1 = 0;
// Update only the second session and consume both PresentIds, which should push an UberStruct
// and return two tokens to the second instance.
auto next_present_id2 = PopPendingPresent(id2);
next_present_id2 = PopPendingPresent(id2);
manager_->UpdateSessions({{id2, next_present_id2}}, /*trace_id=*/0);
snapshot = uber_struct_system_->Snapshot();
EXPECT_EQ(snapshot.size(), 2u);
EXPECT_TRUE(snapshot.count(id1));
EXPECT_TRUE(snapshot.count(id2));
RunLoopUntil([&returned_tokens2]() { return returned_tokens2 != 0; });
EXPECT_EQ(returned_tokens1, 0u);
EXPECT_EQ(returned_tokens2, 2u);
EXPECT_EQ(GetNumPendingSessionUpdates(id1), 1ul);
EXPECT_EQ(GetNumPendingSessionUpdates(id2), 0ul);
}
TEST_F(FlatlandManagerTest, OnFramePresentedEvent) {
// Setup two Flatland instances with OnFramePresented() callbacks.
fidl::InterfacePtr<fuchsia::ui::scenic::internal::Flatland> flatland1;
manager_->CreateFlatland(flatland1.NewRequest());
const scheduling::SessionId id1 = uber_struct_system_->GetLatestInstanceId();
std::optional<fuchsia::scenic::scheduling::FramePresentedInfo> info1;
flatland1.events().OnFramePresented =
[&info1](fuchsia::scenic::scheduling::FramePresentedInfo info) { info1 = std::move(info); };
fidl::InterfacePtr<fuchsia::ui::scenic::internal::Flatland> flatland2;
manager_->CreateFlatland(flatland2.NewRequest());
const scheduling::SessionId id2 = uber_struct_system_->GetLatestInstanceId();
std::optional<fuchsia::scenic::scheduling::FramePresentedInfo> info2;
flatland2.events().OnFramePresented =
[&info2](fuchsia::scenic::scheduling::FramePresentedInfo info) { info2 = std::move(info); };
// Present both instances twice, but don't update sessions.
PRESENT(flatland1, id1, true);
PRESENT(flatland1, id1, true);
PRESENT(flatland2, id2, true);
PRESENT(flatland2, id2, true);
// Call OnFramePresented() with a PresentId for the first session and ensure the event fires.
scheduling::PresentTimestamps timestamps{
.presented_time = zx::time(111),
.vsync_interval = zx::duration(11),
};
zx::time latch_time1 = zx::time(123);
auto next_present_id1 = PopPendingPresent(id1);
std::unordered_map<scheduling::SessionId,
std::map<scheduling::PresentId, /*latched_time*/ zx::time>>
latch_times;
latch_times[id1] = {{next_present_id1, latch_time1}};
manager_->OnFramePresented(latch_times, timestamps);
// Wait until the event has fired.
RunLoopUntil([&info1]() { return info1.has_value(); });
// Verify that info1 contains the expected data.
EXPECT_EQ(zx::time(info1->actual_presentation_time), timestamps.presented_time);
EXPECT_EQ(info1->num_presents_allowed, 0ul);
EXPECT_EQ(info1->presentation_infos.size(), 1ul);
EXPECT_EQ(zx::time(info1->presentation_infos[0].latched_time()), latch_time1);
// Run the loop again to show that info2 hasn't been populated.
RunLoopUntilIdle();
EXPECT_FALSE(info2.has_value());
// Call OnFramePresented with all the remaining PresentIds and ensure an event fires for both.
info1.reset();
latch_times.clear();
timestamps = scheduling::PresentTimestamps({
.presented_time = zx::time(222),
.vsync_interval = zx::duration(22),
});
latch_time1 = zx::time(234);
auto latch_time2_1 = zx::time(345);
auto latch_time2_2 = zx::time(456);
next_present_id1 = PopPendingPresent(id1);
auto next_present_id2_1 = PopPendingPresent(id2);
auto next_present_id2_2 = PopPendingPresent(id2);
latch_times[id1] = {{next_present_id1, latch_time1}};
latch_times[id2] = {{next_present_id2_1, latch_time2_1}, {next_present_id2_2, latch_time2_2}};
manager_->OnFramePresented(latch_times, timestamps);
// Wait until both events have fired.
RunLoopUntil([&info1]() { return info1.has_value(); });
RunLoopUntil([&info2]() { return info2.has_value(); });
// Verify that both infos contain the expected data.
EXPECT_EQ(zx::time(info1->actual_presentation_time), timestamps.presented_time);
EXPECT_EQ(info1->num_presents_allowed, 0ul);
EXPECT_EQ(info1->presentation_infos.size(), 1ul);
EXPECT_EQ(zx::time(info1->presentation_infos[0].latched_time()), latch_time1);
EXPECT_EQ(zx::time(info2->actual_presentation_time), timestamps.presented_time);
EXPECT_EQ(info2->num_presents_allowed, 0ul);
EXPECT_EQ(info2->presentation_infos.size(), 2ul);
EXPECT_EQ(zx::time(info2->presentation_infos[0].latched_time()), latch_time2_1);
EXPECT_EQ(zx::time(info2->presentation_infos[1].latched_time()), latch_time2_2);
}
#undef PRESENT
} // namespace test
} // namespace flatland