| // 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/fit/thread_checker.h> |
| #include <lib/syslog/cpp/macros.h> |
| #include <lib/ui/scenic/cpp/view_identity.h> |
| |
| #include <gtest/gtest.h> |
| |
| #include "fuchsia/ui/composition/cpp/fidl.h" |
| #include "src/ui/scenic/lib/allocation/mock_buffer_collection_importer.h" |
| #include "src/ui/scenic/lib/flatland/tests/logging_event_loop.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::AtLeast; |
| using ::testing::Return; |
| |
| using flatland::FlatlandManager; |
| using flatland::FlatlandPresenter; |
| using flatland::LinkSystem; |
| using flatland::MockFlatlandPresenter; |
| using flatland::UberStructSystem; |
| using fuchsia::ui::composition::Flatland; |
| using fuchsia::ui::composition::FlatlandError; |
| using fuchsia::ui::composition::OnNextFrameBeginValues; |
| |
| // 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_, ScheduleUpdateForSession(_, _, _, _, _)); \ |
| } \ |
| fuchsia::ui::composition::PresentArgs present_args; \ |
| present_args.set_requested_presentation_time(0); \ |
| present_args.set_acquire_fences({}); \ |
| present_args.set_release_fences({}); \ |
| present_args.set_unsquashable(false); \ |
| flatland->Present(std::move(present_args)); \ |
| /* If expecting success, wait for the worker thread to process the request. */ \ |
| if (expect_success) { \ |
| RunLoopUntil([this, session_id, num_pending_sessions] { \ |
| return GetNumPendingSessionUpdates(session_id) > num_pending_sessions; \ |
| }); \ |
| } \ |
| } |
| |
| namespace { |
| |
| class FlatlandManagerTest : public LoggingEventLoop, public ::testing::Test { |
| public: |
| FlatlandManagerTest() |
| : uber_struct_system_(std::make_shared<UberStructSystem>()), |
| link_system_(std::make_shared<LinkSystem>(uber_struct_system_->GetNextInstanceId())) {} |
| |
| void SetUp() override { |
| ::testing::Test::SetUp(); |
| |
| mock_flatland_presenter_ = std::make_shared<::testing::StrictMock<MockFlatlandPresenter>>(); |
| |
| ON_CALL(*mock_flatland_presenter_, ScheduleUpdateForSession(_, _, _, _, _)) |
| .WillByDefault(::testing::Invoke( |
| [&](zx::time requested_presentation_time, scheduling::SchedulingIdPair id_pair, |
| bool unsquashable, std::vector<zx::event> release_fences, bool schedule_asap) { |
| EXPECT_TRUE(release_fences.empty()); |
| |
| // The ID pair must not be already registered. |
| EXPECT_FALSE(pending_presents_.contains(id_pair)); |
| pending_presents_.insert(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); |
| })); |
| |
| ON_CALL(*mock_flatland_presenter_, GetFuturePresentationInfos()) |
| .WillByDefault(::testing::Invoke([&]() { |
| // The requested_prediction_span should be at least one frame. |
| |
| // Give back at least one info. |
| std::vector<scheduling::FuturePresentationInfo> presentation_infos; |
| auto& info = presentation_infos.emplace_back(); |
| info.latch_point = zx::time(5); |
| info.presentation_time = zx::time(10); |
| |
| return presentation_infos; |
| })); |
| |
| ON_CALL(*mock_flatland_presenter_, RemoveSession(_, _)) |
| .WillByDefault(::testing::Invoke( |
| [&](scheduling::SessionId session_id, std::optional<zx::event> release_fence) { |
| async::PostTask(this->dispatcher(), [&, session_id]() { |
| std::lock_guard lock(removed_session_thread_checker_); |
| removed_sessions_.insert(session_id); |
| }); |
| })); |
| |
| const display::WireDisplayId kDisplayId = {.value = 1}; |
| constexpr uint32_t kDisplayWidth = 640; |
| constexpr uint32_t kDisplayHeight = 480; |
| std::vector<std::shared_ptr<allocation::BufferCollectionImporter>> importers; |
| manager_ = std::make_unique<FlatlandManager>( |
| dispatcher(), mock_flatland_presenter_, uber_struct_system_, link_system_, |
| std::make_shared<display::Display>(kDisplayId, kDisplayWidth, kDisplayHeight), importers, |
| /*register_view_focuser*/ [this](auto...) { view_focuser_registered_ = true; }, |
| /*register_view_ref_focused*/ [this](auto...) { view_ref_focused_registered_ = true; }, |
| /*register_touch_source*/ [this](auto...) { touch_source_registered_ = true; }, |
| /*register_mouse_source*/ [this](auto...) { mouse_source_registered_ = true; }); |
| } |
| |
| void TearDown() override { |
| // |manager_| may have been reset during the test. If not, run until all sessions have closed, |
| // which depends on the worker threads receiving "peer closed" for the clients created in |
| // the tests. |
| { |
| std::lock_guard lock(removed_session_thread_checker_); |
| std::for_each(removed_sessions_.begin(), removed_sessions_.end(), [](auto session_id) { |
| FX_LOGS(INFO) << "`removed_sessions_` includes " << session_id; |
| }); |
| removed_sessions_.clear(); |
| } |
| if (manager_) { |
| const size_t initial_session_count = manager_->GetSessionCount(); |
| FX_LOGS(INFO) << "initial_session_count=" << initial_session_count; |
| EXPECT_CALL(*mock_flatland_presenter_, RemoveSession(_, _)) |
| .Times(AtLeast(initial_session_count)); |
| RunLoopUntil([this, initial_session_count] { |
| std::lock_guard lock(removed_session_thread_checker_); |
| // It could be tempting to only check a single condition here. However, |
| // it won't work as expected. `FlatlandManager` posts the task |
| // destroying `Flatland` instance on the Session's loop thread, and then |
| // deletes the session from its session list immediately. |
| // |
| // The `Flatland` destructor later calls presenter API to remove the |
| // session, which is handled on the presenter handler's FIDL loop. |
| // There may be a critical section in between and we should make sure |
| // both conditions are fulfilled before proceeding. |
| auto current_session_count = manager_->GetSessionCount(); |
| std::for_each(removed_sessions_.begin(), removed_sessions_.end(), [](auto session_id) { |
| FX_LOGS(INFO) << "`removed_sessions_` includes " << session_id; |
| }); |
| FX_LOGS(INFO) << "current_session_count=" << current_session_count; |
| |
| // We can't test for equality between `removed_sessions_.size()` and `initial_session_count` |
| // due to a race condition: the about-to-be-destroyed session might already have been |
| // removed from the manager, but the presenter is not notified until the end of the Flatland |
| // destructor, which occurs on a different thread. |
| return current_session_count == 0 && removed_sessions_.size() >= initial_session_count; |
| }); |
| } |
| |
| auto snapshot = uber_struct_system_->Snapshot(); |
| EXPECT_TRUE(snapshot.empty()); |
| |
| manager_.reset(); |
| RunLoopUntilIdle(); |
| |
| EXPECT_EQ(uber_struct_system_->GetSessionCount(), 0ul); |
| |
| pending_presents_.clear(); |
| pending_session_updates_.clear(); |
| mock_flatland_presenter_.reset(); |
| { |
| std::lock_guard lock(removed_session_thread_checker_); |
| removed_sessions_.clear(); |
| } |
| |
| ::testing::Test::TearDown(); |
| } |
| |
| fidl::InterfacePtr<fuchsia::ui::composition::Flatland> CreateFlatland() { |
| fidl::InterfacePtr<fuchsia::ui::composition::Flatland> flatland; |
| const scheduling::SessionId id = manager_->CreateFlatland(flatland.NewRequest(dispatcher())); |
| FX_LOGS(INFO) << "Created flatland with ID " << id; |
| return flatland; |
| } |
| |
| // 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: |
| std::shared_ptr<::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_; |
| |
| // std::unordered_set is not thread-safe. Here we add the thread checker |
| // to make sure that it is only used on the test loop (which runs on the same |
| // thread as test main thread). |
| fit::thread_checker removed_session_thread_checker_; |
| std::unordered_set<scheduling::SessionId> removed_sessions_ |
| FIT_GUARDED(removed_session_thread_checker_); |
| |
| const std::shared_ptr<LinkSystem> link_system_; |
| |
| bool view_focuser_registered_ = false; |
| bool view_ref_focused_registered_ = false; |
| bool touch_source_registered_ = false; |
| bool mouse_source_registered_ = false; |
| }; |
| |
| } // namespace |
| |
| namespace flatland::test { |
| |
| TEST_F(FlatlandManagerTest, CreateFlatlands) { |
| fidl::InterfacePtr<fuchsia::ui::composition::Flatland> flatland1 = CreateFlatland(); |
| |
| fidl::InterfacePtr<fuchsia::ui::composition::Flatland> flatland2 = CreateFlatland(); |
| |
| RunLoopUntilIdle(); |
| |
| EXPECT_TRUE(flatland1.is_bound()); |
| EXPECT_TRUE(flatland2.is_bound()); |
| EXPECT_EQ(manager_->GetSessionCount(), 2ul); |
| } |
| |
| TEST_F(FlatlandManagerTest, CreateViewportedFlatlands) { |
| fuchsia::ui::views::ViewportCreationToken parent_token; |
| fuchsia::ui::views::ViewCreationToken child_token; |
| ASSERT_EQ(ZX_OK, zx::channel::create(0, &parent_token.value, &child_token.value)); |
| |
| fidl::InterfacePtr<fuchsia::ui::composition::Flatland> parent = CreateFlatland(); |
| const fuchsia::ui::composition::ContentId kLinkId = {1}; |
| fidl::InterfacePtr<fuchsia::ui::composition::ChildViewWatcher> child_view_watcher; |
| fuchsia::ui::composition::ViewportProperties properties; |
| properties.set_logical_size({1, 2}); |
| parent->CreateViewport(kLinkId, std::move(parent_token), std::move(properties), |
| child_view_watcher.NewRequest()); |
| { |
| fidl::InterfacePtr<fuchsia::ui::composition::Flatland> child = CreateFlatland(); |
| fidl::InterfacePtr<fuchsia::ui::composition::ParentViewportWatcher> parent_viewport_watcher; |
| child->CreateView(std::move(child_token), parent_viewport_watcher.NewRequest()); |
| |
| RunLoopUntilIdle(); |
| EXPECT_EQ(manager_->GetSessionCount(), 2ul); |
| RunLoopUntil([this] { return !link_system_->GetResolvedTopologyLinks().empty(); }); |
| |
| EXPECT_CALL(*mock_flatland_presenter_, RemoveSession(_, _)); |
| } |
| |
| RunLoopUntil([this] { return link_system_->GetResolvedTopologyLinks().empty(); }); |
| } |
| |
| TEST_F(FlatlandManagerTest, ClientDiesBeforeManager) { |
| scheduling::SessionId id; |
| { |
| fidl::InterfacePtr<fuchsia::ui::composition::Flatland> flatland = CreateFlatland(); |
| id = uber_struct_system_->GetLatestInstanceId(); |
| |
| RunLoopUntilIdle(); |
| |
| EXPECT_TRUE(flatland.is_bound()); |
| |
| // |flatland| falls out of scope, killing the session. |
| EXPECT_CALL(*mock_flatland_presenter_, RemoveSession(id, _)); |
| |
| // FlatlandManager::RemoveFlatlandInstance() will be posted on main thread and may not be run |
| // yet. |
| RunLoopUntilIdle(); |
| } |
| |
| // The session should show up in the set of removed sessions. |
| RunLoopUntil([this] { |
| std::lock_guard lock(removed_session_thread_checker_); |
| // It could be tempting to only check a single condition here, however, |
| // this will cause a race and won't work as expected, since FlatlandManager |
| // removing session from manager's session list and Flatland impl requests |
| // presenter to remove the session occur on different threads. |
| // See the comment at `TearDown()` for details. |
| return manager_->GetSessionCount() == 0 && removed_sessions_.size() == 1; |
| }); |
| { |
| std::lock_guard lock(removed_session_thread_checker_); |
| EXPECT_TRUE(removed_sessions_.contains(id)); |
| } |
| } |
| |
| TEST_F(FlatlandManagerTest, ManagerDiesBeforeClients) { |
| fidl::InterfacePtr<fuchsia::ui::composition::Flatland> flatland = CreateFlatland(); |
| const scheduling::SessionId id = uber_struct_system_->GetLatestInstanceId(); |
| |
| RunLoopUntilIdle(); |
| |
| EXPECT_TRUE(flatland.is_bound()); |
| EXPECT_EQ(manager_->GetSessionCount(), 1ul); |
| |
| // Explicitly kill the server. |
| EXPECT_CALL(*mock_flatland_presenter_, RemoveSession(id, _)); |
| manager_.reset(); |
| |
| EXPECT_EQ(uber_struct_system_->GetSessionCount(), 0ul); |
| |
| RunLoopUntil([this] { |
| std::lock_guard lock(removed_session_thread_checker_); |
| return removed_sessions_.size() == 1ul; |
| }); |
| |
| { |
| std::lock_guard lock(removed_session_thread_checker_); |
| EXPECT_TRUE(removed_sessions_.contains(id)); |
| } |
| |
| // Wait until unbound. |
| RunLoopUntil([&flatland] { return !flatland.is_bound(); }); |
| |
| // FlatlandManager::RemoveFlatlandInstance() will be posted on main thread and may not be run yet. |
| RunLoopUntilIdle(); |
| } |
| |
| TEST_F(FlatlandManagerTest, FirstPresentReturnsMaxPresentCredits) { |
| // Setup a Flatland instance with an OnNextFrameBegin() callback. |
| fidl::InterfacePtr<fuchsia::ui::composition::Flatland> flatland = CreateFlatland(); |
| const scheduling::SessionId id = uber_struct_system_->GetLatestInstanceId(); |
| |
| uint32_t returned_tokens = 0; |
| flatland.events().OnNextFrameBegin = [&returned_tokens](OnNextFrameBeginValues values) { |
| returned_tokens += values.additional_present_credits(); |
| EXPECT_TRUE(returned_tokens > 0); |
| EXPECT_FALSE(values.future_presentation_infos().empty()); |
| }; |
| |
| // Present once, but don't update sessions. |
| PRESENT(flatland, id, true); |
| |
| auto snapshot = uber_struct_system_->Snapshot(); |
| EXPECT_TRUE(snapshot.empty()); |
| |
| EXPECT_EQ(GetNumPendingSessionUpdates(id), 1ul); |
| |
| // Update the session, this should return max tokens through OnNextFrameBegin(). |
| const auto next_present_id = PopPendingPresent(id); |
| manager_->UpdateInstances({{id, next_present_id}}); |
| |
| EXPECT_CALL(*mock_flatland_presenter_, GetFuturePresentationInfos()); |
| manager_->SendHintsToStartRendering(); |
| |
| snapshot = uber_struct_system_->Snapshot(); |
| EXPECT_EQ(snapshot.size(), 1u); |
| EXPECT_TRUE(snapshot.contains(id)); |
| |
| RunLoopUntil([&returned_tokens] { return returned_tokens != 0; }); |
| EXPECT_EQ(returned_tokens, scheduling::FrameScheduler::kMaxPresentsInFlight); |
| EXPECT_EQ(GetNumPendingSessionUpdates(id), 0ul); |
| } |
| |
| TEST_F(FlatlandManagerTest, UpdateInstancesReturnsPresentCredits) { |
| // Setup two Flatland instances with OnNextFrameBegin() callbacks. |
| fidl::InterfacePtr<fuchsia::ui::composition::Flatland> flatland1 = CreateFlatland(); |
| const scheduling::SessionId id1 = uber_struct_system_->GetLatestInstanceId(); |
| |
| uint32_t returned_tokens1 = 0; |
| flatland1.events().OnNextFrameBegin = [&returned_tokens1](OnNextFrameBeginValues values) { |
| returned_tokens1 += values.additional_present_credits(); |
| EXPECT_TRUE(returned_tokens1 > 0); |
| EXPECT_FALSE(values.future_presentation_infos().empty()); |
| }; |
| |
| fidl::InterfacePtr<fuchsia::ui::composition::Flatland> flatland2 = CreateFlatland(); |
| const scheduling::SessionId id2 = uber_struct_system_->GetLatestInstanceId(); |
| |
| uint32_t returned_tokens2 = 0; |
| flatland2.events().OnNextFrameBegin = [&returned_tokens2](OnNextFrameBeginValues values) { |
| returned_tokens2 += values.additional_present_credits(); |
| EXPECT_TRUE(returned_tokens2 > 0); |
| EXPECT_FALSE(values.future_presentation_infos().empty()); |
| }; |
| |
| { // Go through the initial present so both instances have multiple credits. |
| PRESENT(flatland1, id1, true); |
| PRESENT(flatland2, id2, true); |
| const auto next_present_id1 = PopPendingPresent(id1); |
| const auto next_present_id2 = PopPendingPresent(id2); |
| manager_->UpdateInstances({{id1, next_present_id1}, {id2, next_present_id2}}); |
| EXPECT_CALL(*mock_flatland_presenter_, GetFuturePresentationInfos()); |
| manager_->SendHintsToStartRendering(); |
| RunLoopUntil([&] { |
| return returned_tokens1 == scheduling::FrameScheduler::kMaxPresentsInFlight && |
| returned_tokens2 == scheduling::FrameScheduler::kMaxPresentsInFlight; |
| }); |
| EXPECT_EQ(GetNumPendingSessionUpdates(id1), 0ul); |
| EXPECT_EQ(GetNumPendingSessionUpdates(id2), 0ul); |
| // Now forget about the returned tokens. |
| returned_tokens1 = 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); |
| |
| 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_->UpdateInstances({{id1, next_present_id1}}); |
| |
| EXPECT_CALL(*mock_flatland_presenter_, GetFuturePresentationInfos()); |
| manager_->SendHintsToStartRendering(); |
| |
| 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. |
| PopPendingPresent(id2); |
| const auto next_present_id2 = PopPendingPresent(id2); |
| |
| manager_->UpdateInstances({{id2, next_present_id2}}); |
| |
| EXPECT_CALL(*mock_flatland_presenter_, GetFuturePresentationInfos()); |
| manager_->SendHintsToStartRendering(); |
| |
| const auto snapshot = uber_struct_system_->Snapshot(); |
| EXPECT_EQ(snapshot.size(), 2u); |
| EXPECT_TRUE(snapshot.contains(id1)); |
| EXPECT_TRUE(snapshot.contains(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); |
| } |
| |
| // It is possible for the session to update multiple times in a row before |
| // SendHintsToStartRendering() is called. If that's the case, we need to ensure that present credits |
| // returned from the first update are not lost. |
| TEST_F(FlatlandManagerTest, ConsecutiveUpdateInstances_ReturnsCorrectPresentCredits) { |
| fidl::InterfacePtr<fuchsia::ui::composition::Flatland> flatland = CreateFlatland(); |
| const scheduling::SessionId id = uber_struct_system_->GetLatestInstanceId(); |
| |
| uint32_t returned_tokens = 0; |
| flatland.events().OnNextFrameBegin = [&returned_tokens](OnNextFrameBeginValues values) { |
| returned_tokens = values.additional_present_credits(); |
| EXPECT_TRUE(returned_tokens > 0); |
| EXPECT_FALSE(values.future_presentation_infos().empty()); |
| }; |
| |
| { // Receive the initial allotment of tokens, then forget those tokens. |
| PRESENT(flatland, id, true); |
| const auto next_present_id = PopPendingPresent(id); |
| manager_->UpdateInstances({{id, next_present_id}}); |
| EXPECT_CALL(*mock_flatland_presenter_, GetFuturePresentationInfos()); |
| manager_->SendHintsToStartRendering(); |
| RunLoopUntil( |
| [&] { return returned_tokens == scheduling::FrameScheduler::kMaxPresentsInFlight; }); |
| EXPECT_EQ(GetNumPendingSessionUpdates(id), 0ul); |
| returned_tokens = 0; |
| } |
| |
| // Present twice, but don't update the session yet. |
| PRESENT(flatland, id, true); |
| PRESENT(flatland, id, true); |
| EXPECT_EQ(GetNumPendingSessionUpdates(id), 2ul); |
| |
| // Update the session, but only with the first PresentId, which should push an UberStruct |
| // and return one token to the first instance. |
| auto next_present_id = PopPendingPresent(id); |
| manager_->UpdateInstances({{id, next_present_id}}); |
| |
| // Update again. |
| next_present_id = PopPendingPresent(id); |
| manager_->UpdateInstances({{id, next_present_id}}); |
| |
| // Finally, the work is done according to the frame scheduler. |
| EXPECT_CALL(*mock_flatland_presenter_, GetFuturePresentationInfos()); |
| manager_->SendHintsToStartRendering(); |
| |
| const auto snapshot = uber_struct_system_->Snapshot(); |
| EXPECT_EQ(snapshot.size(), 1u); |
| EXPECT_TRUE(snapshot.contains(id)); |
| |
| RunLoopUntil([&returned_tokens] { return returned_tokens != 0; }); |
| |
| EXPECT_EQ(returned_tokens, 2u); |
| |
| EXPECT_EQ(GetNumPendingSessionUpdates(id), 0ul); |
| } |
| |
| TEST_F(FlatlandManagerTest, PresentWithoutTokensClosesSession) { |
| // Setup a Flatland instance with an OnNextFrameBegin() callback. |
| fidl::InterfacePtr<fuchsia::ui::composition::Flatland> flatland = CreateFlatland(); |
| const scheduling::SessionId id = uber_struct_system_->GetLatestInstanceId(); |
| |
| FlatlandError error_returned; |
| uint32_t tokens_remaining = 1; |
| flatland.events().OnError = [&error_returned](FlatlandError error) { error_returned = error; }; |
| |
| // Present until no tokens remain. |
| PRESENT(flatland, id, true); |
| EXPECT_TRUE(flatland.is_bound()); |
| |
| // Present one more time and ensure the session is closed. |
| EXPECT_CALL(*mock_flatland_presenter_, RemoveSession(id, _)); |
| PRESENT(flatland, id, false); |
| |
| // The instance will eventually be unbound, but it takes a pair of thread hops to complete since |
| // the destroy_instance_function() posts a task from the worker to the main and that task |
| // ultimately posts the destruction back onto the worker. |
| RunLoopUntil([&flatland] { return !flatland.is_bound(); }); |
| EXPECT_EQ(error_returned, FlatlandError::NO_PRESENTS_REMAINING); |
| |
| // Wait until all Flatland threads are destroyed. |
| RunLoopUntil([this] { return manager_->GetAliveSessionCount() == 0; }); |
| |
| // FlatlandManager::RemoveFlatlandInstance() will be posted on main thread and may not be run yet. |
| RunLoopUntilIdle(); |
| } |
| |
| TEST_F(FlatlandManagerTest, ErrorClosesSession) { |
| // Setup a Flatland instance with an OnNextFrameBegin() callback. |
| fidl::InterfacePtr<fuchsia::ui::composition::Flatland> flatland = CreateFlatland(); |
| const scheduling::SessionId id = uber_struct_system_->GetLatestInstanceId(); |
| |
| FlatlandError error_returned; |
| flatland.events().OnError = [&error_returned](FlatlandError error) { error_returned = error; }; |
| EXPECT_TRUE(flatland.is_bound()); |
| |
| // Queue a bad SetRootTransform call ensure the session is closed. |
| EXPECT_CALL(*mock_flatland_presenter_, RemoveSession(id, _)); |
| flatland->SetRootTransform({2}); |
| PRESENT(flatland, id, false); |
| |
| // The instance will eventually be unbound, but it takes a pair of thread hops to complete since |
| // the destroy_instance_function() posts a task from the worker to the main and that task |
| // ultimately posts the destruction back onto the worker. |
| RunLoopUntil([&flatland] { return !flatland.is_bound(); }); |
| EXPECT_EQ(error_returned, FlatlandError::BAD_OPERATION); |
| |
| // Wait until all Flatland threads are destroyed. |
| RunLoopUntil([this] { return manager_->GetAliveSessionCount() == 0; }); |
| |
| // FlatlandManager::RemoveFlatlandInstance() will be posted on main thread and may not be run |
| // yet. |
| RunLoopUntilIdle(); |
| } |
| |
| TEST_F(FlatlandManagerTest, TokensAreReplenishedAfterRunningOut) { |
| // Setup a Flatland instance with an OnNextFrameBegin() callback. |
| fidl::InterfacePtr<fuchsia::ui::composition::Flatland> flatland = CreateFlatland(); |
| const scheduling::SessionId id = uber_struct_system_->GetLatestInstanceId(); |
| |
| uint32_t tokens_remaining = 0; |
| flatland.events().OnNextFrameBegin = [&tokens_remaining](OnNextFrameBeginValues values) { |
| tokens_remaining += values.additional_present_credits(); |
| EXPECT_TRUE(tokens_remaining > 0); |
| }; |
| |
| { // Receive the initial allotment of tokens. |
| PRESENT(flatland, id, true); |
| const auto next_present_id = PopPendingPresent(id); |
| manager_->UpdateInstances({{id, next_present_id}}); |
| EXPECT_CALL(*mock_flatland_presenter_, GetFuturePresentationInfos()); |
| manager_->SendHintsToStartRendering(); |
| RunLoopUntil( |
| [&] { return tokens_remaining == scheduling::FrameScheduler::kMaxPresentsInFlight; }); |
| } |
| |
| // Present until no tokens remain. |
| while (tokens_remaining > 0) { |
| PRESENT(flatland, id, true); |
| --tokens_remaining; |
| } |
| |
| // Process the first present. |
| auto next_present_id = PopPendingPresent(id); |
| manager_->UpdateInstances({{id, next_present_id}}); |
| |
| // Signal that the work is done, which should return present credits to the client. |
| EXPECT_CALL(*mock_flatland_presenter_, GetFuturePresentationInfos()); |
| manager_->SendHintsToStartRendering(); |
| |
| RunLoopUntil([&tokens_remaining] { return tokens_remaining != 0; }); |
| |
| // Present once more which should succeed. |
| PRESENT(flatland, id, true); |
| EXPECT_TRUE(flatland.is_bound()); |
| } |
| |
| TEST_F(FlatlandManagerTest, OnFramePresentedEvent) { |
| // Setup two Flatland instances with OnFramePresented() callbacks. |
| fidl::InterfacePtr<fuchsia::ui::composition::Flatland> flatland1 = CreateFlatland(); |
| 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::composition::Flatland> flatland2 = CreateFlatland(); |
| 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); }; |
| |
| { // Go through the initial present so both instances have multiple credits. |
| uint32_t returned_tokens1 = 0; |
| flatland1.events().OnNextFrameBegin = [&returned_tokens1](OnNextFrameBeginValues values) { |
| returned_tokens1 += values.additional_present_credits(); |
| }; |
| uint32_t returned_tokens2 = 0; |
| flatland2.events().OnNextFrameBegin = [&](OnNextFrameBeginValues values) { |
| returned_tokens2 += values.additional_present_credits(); |
| }; |
| PRESENT(flatland1, id1, true); |
| PRESENT(flatland2, id2, true); |
| const auto next_present_id1 = PopPendingPresent(id1); |
| const auto next_present_id2 = PopPendingPresent(id2); |
| manager_->UpdateInstances({{id1, next_present_id1}, {id2, next_present_id2}}); |
| EXPECT_CALL(*mock_flatland_presenter_, GetFuturePresentationInfos()); |
| manager_->SendHintsToStartRendering(); |
| RunLoopUntil([&] { |
| return returned_tokens1 == scheduling::FrameScheduler::kMaxPresentsInFlight && |
| returned_tokens2 == scheduling::FrameScheduler::kMaxPresentsInFlight; |
| }); |
| } |
| |
| // 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); |
| |
| // Call `OnFramePresented()` after the first session has terminated. |
| // |
| // Verify that Scenic does not crash, and that the second session still gets its |
| // `OnFramePresented` event. |
| // |
| // Note: The iteration order of sessions within the argument to `OnFramePresented()` |
| // is dependent on a hash function. Hence: if the hash ordering varies from one test |
| // run to another (for identical builds), and there is a bug in `OnFramePresented()`, |
| // this test could flake. |
| PRESENT(flatland1, id1, true); |
| PRESENT(flatland2, id2, true); |
| EXPECT_CALL(*mock_flatland_presenter_, RemoveSession(id1, _)); |
| flatland1 = {}; |
| FX_LOGS(INFO) << "Waiting for removal of session " << id1; |
| RunLoopUntil([this, id1] { |
| std::lock_guard lock(removed_session_thread_checker_); |
| return removed_sessions_.find(id1) != removed_sessions_.end(); |
| }); |
| info2 = {}; |
| manager_->OnFramePresented({{id1, {{PopPendingPresent(id1), zx::time(789)}}}, |
| {id2, {{PopPendingPresent(id2), zx::time(789)}}}}, |
| scheduling::PresentTimestamps({ |
| .presented_time = zx::time(777), |
| .vsync_interval = zx::duration(16), |
| })); |
| FX_LOGS(INFO) << "Waiting for event on session " << id2; |
| RunLoopUntil([&info2] { return info2.has_value(); }); |
| } |
| |
| TEST_F(FlatlandManagerTest, ViewBoundProtocolsAreRegistered) { |
| fuchsia::ui::views::ViewportCreationToken parent_token; |
| fuchsia::ui::views::ViewCreationToken child_token; |
| ASSERT_EQ(ZX_OK, zx::channel::create(0, &parent_token.value, &child_token.value)); |
| fidl::InterfacePtr<fuchsia::ui::composition::Flatland> parent = CreateFlatland(); |
| const fuchsia::ui::composition::ContentId kLinkId = {1}; |
| fidl::InterfacePtr<fuchsia::ui::composition::ChildViewWatcher> child_view_watcher; |
| fuchsia::ui::composition::ViewportProperties properties; |
| properties.set_logical_size({1, 2}); |
| parent->CreateViewport(kLinkId, std::move(parent_token), std::move(properties), |
| child_view_watcher.NewRequest()); |
| |
| fidl::InterfacePtr<fuchsia::ui::composition::Flatland> child = CreateFlatland(); |
| |
| fidl::InterfacePtr<fuchsia::ui::views::Focuser> view_focuser_ptr; |
| fidl::InterfacePtr<fuchsia::ui::views::ViewRefFocused> view_ref_focused_ptr; |
| fidl::InterfacePtr<fuchsia::ui::pointer::TouchSource> touch_source_ptr; |
| fidl::InterfacePtr<fuchsia::ui::pointer::MouseSource> mouse_source_ptr; |
| |
| fidl::InterfacePtr<fuchsia::ui::composition::ParentViewportWatcher> parent_viewport_watcher; |
| fuchsia::ui::composition::ViewBoundProtocols protocols; |
| protocols.set_view_focuser(view_focuser_ptr.NewRequest()) |
| .set_view_ref_focused(view_ref_focused_ptr.NewRequest()) |
| .set_touch_source(touch_source_ptr.NewRequest()) |
| .set_mouse_source(mouse_source_ptr.NewRequest()); |
| child->CreateView2(std::move(child_token), scenic::NewViewIdentityOnCreation(), |
| std::move(protocols), parent_viewport_watcher.NewRequest()); |
| |
| RunLoopUntil([this] { |
| return view_focuser_registered_ && view_ref_focused_registered_ && touch_source_registered_ && |
| mouse_source_registered_; |
| }); |
| } |
| |
| #undef PRESENT |
| |
| } // namespace flatland::test |