Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Impeller] Test FenceWaiterVK and fix termination bugs #45870

Merged
merged 25 commits into from
Sep 15, 2023
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
2ff38d2
[Impeller] transitioned mock vulkan context to the builder pattern
gaaclarke Sep 14, 2023
60b871d
made creating a context with validation layers work
gaaclarke Sep 14, 2023
67e9055
added capabilties asserts
gaaclarke Sep 14, 2023
ba4bc1e
moved to fml thread_local
gaaclarke Sep 14, 2023
004f79b
removed strcpy
gaaclarke Sep 14, 2023
da1aada
switched to strncpy
gaaclarke Sep 14, 2023
630f915
moved to sizeof since its a bit more robust
gaaclarke Sep 14, 2023
10b7cdc
Merge branch 'mock-vulkan-context-builder' of https://github.com/gaac…
matanlurey Sep 14, 2023
c89d30d
Add MockFence and MockFence::SetStatus.
matanlurey Sep 15, 2023
ce6a1c5
Remove unused code.
matanlurey Sep 15, 2023
ac485e1
Add stub unit test, remove unused IsValid() method.
matanlurey Sep 15, 2023
50ef1c0
Add unit tests, including those that fail.
matanlurey Sep 15, 2023
94500af
Fix fence waiter and add tests.
matanlurey Sep 15, 2023
3a01df4
Merge branch 'main' into impeller-vk-fence-contract
matanlurey Sep 15, 2023
5540542
Fix buggy fence waiter test.
matanlurey Sep 15, 2023
2efd5e8
Make mocks thread-safe and tweak.
matanlurey Sep 15, 2023
79998e7
Merge.
matanlurey Sep 15, 2023
4e1861c
Use atomics, delete unused code.
matanlurey Sep 15, 2023
530af8d
Merge branch 'main' into impeller-vk-fence-contract
matanlurey Sep 15, 2023
1460841
Tweak fence termination contract.
matanlurey Sep 15, 2023
5bdcb65
false -> true
matanlurey Sep 15, 2023
e5ab21b
Address some comments and add a TODO.
matanlurey Sep 15, 2023
110f745
Add FML_DCHECK.
matanlurey Sep 15, 2023
d973574
Address feedback.
matanlurey Sep 15, 2023
fe3c926
Address comments.
matanlurey Sep 15, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ci/licenses_golden/excluded_files
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@
../../../flutter/impeller/renderer/backend/vulkan/blit_command_vk_unittests.cc
../../../flutter/impeller/renderer/backend/vulkan/command_encoder_vk_unittests.cc
../../../flutter/impeller/renderer/backend/vulkan/context_vk_unittests.cc
../../../flutter/impeller/renderer/backend/vulkan/fence_waiter_vk_unittests.cc
../../../flutter/impeller/renderer/backend/vulkan/pass_bindings_cache_unittests.cc
../../../flutter/impeller/renderer/backend/vulkan/resource_manager_vk_unittests.cc
../../../flutter/impeller/renderer/backend/vulkan/test
Expand Down
1 change: 1 addition & 0 deletions impeller/renderer/backend/vulkan/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ impeller_component("vulkan_unittests") {
"blit_command_vk_unittests.cc",
"command_encoder_vk_unittests.cc",
"context_vk_unittests.cc",
"fence_waiter_vk_unittests.cc",
"pass_bindings_cache_unittests.cc",
"resource_manager_vk_unittests.cc",
"test/mock_vulkan.cc",
Expand Down
4 changes: 0 additions & 4 deletions impeller/renderer/backend/vulkan/context_vk.cc
Original file line number Diff line number Diff line change
Expand Up @@ -377,10 +377,6 @@ void ContextVK::Setup(Settings settings) {
///
auto fence_waiter =
std::shared_ptr<FenceWaiterVK>(new FenceWaiterVK(device_holder));
matanlurey marked this conversation as resolved.
Show resolved Hide resolved
if (!fence_waiter->IsValid()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An invalid fence waiter will no longer invalidate the context now. In the future, if fence waiters cannot be created reliably, the call sites of the users will have to be patched.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no such thing as an invalid fence waiter, that is, there is no state that ever marks the fence waiter as invalid. I found it misleading that we check a boolean that is effectively constant true - let's it add it back if/when there is such a case?

VALIDATION_LOG << "Could not create fence waiter.";
return;
}

//----------------------------------------------------------------------------
/// Create the resource manager.
Expand Down
134 changes: 71 additions & 63 deletions impeller/renderer/backend/vulkan/fence_waiter_vk.cc
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

#include <algorithm>
#include <chrono>
#include <utility>

#include "flutter/fml/thread.h"
#include "flutter/fml/trace_event.h"
Expand Down Expand Up @@ -47,24 +48,17 @@ class WaitSetEntry {
FenceWaiterVK::FenceWaiterVK(std::weak_ptr<DeviceHolder> device_holder)
: device_holder_(std::move(device_holder)) {
waiter_thread_ = std::make_unique<std::thread>([&]() { Main(); });
is_valid_ = true;
}

FenceWaiterVK::~FenceWaiterVK() {
Terminate();
if (waiter_thread_) {
waiter_thread_->join();
}
}

bool FenceWaiterVK::IsValid() const {
return is_valid_;
waiter_thread_->join();
}

bool FenceWaiterVK::AddFence(vk::UniqueFence fence,
const fml::closure& callback) {
TRACE_EVENT0("flutter", "FenceWaiterVK::AddFence");
if (!IsValid() || !fence || !callback) {
if (!fence || !callback || terminate_) {
matanlurey marked this conversation as resolved.
Show resolved Hide resolved
return false;
}
{
Expand Down Expand Up @@ -101,76 +95,90 @@ void FenceWaiterVK::Main() {
// critical section. Copy the array of entries and immediately unlock the
// mutex.
WaitSet wait_set = wait_set_;

const auto terminate = terminate_;

lock.unlock();

const auto terminate = terminate_;
matanlurey marked this conversation as resolved.
Show resolved Hide resolved
if (terminate) {
WaitUntilEmpty();
break;
}

// Check if the context had died in the meantime.
auto device_holder = device_holder_.lock();
if (!device_holder) {
if (!Wait(wait_set)) {
break;
}
}
}

const auto& device = device_holder->GetDevice();
void FenceWaiterVK::WaitUntilEmpty() {
matanlurey marked this conversation as resolved.
Show resolved Hide resolved
while (!wait_set_.empty() && Wait(wait_set_)) {
matanlurey marked this conversation as resolved.
Show resolved Hide resolved
matanlurey marked this conversation as resolved.
Show resolved Hide resolved
// Intentionally empty.
}
}

// Wait for one or more fences to be signaled. Any additional fences added
// to the waiter will be serviced in the next pass. If a fence that is going
// to be signaled at an abnormally long deadline is the only one in the set,
// a timeout will bail out the wait.
auto fences = GetFencesForWaitSet(wait_set);
if (fences.empty()) {
continue;
}
bool FenceWaiterVK::Wait(WaitSet& wait_set) {
using namespace std::literals::chrono_literals;

auto result = device.waitForFences(
fences.size(), // fences count
fences.data(), // fences
false, // wait for all
std::chrono::nanoseconds{100ms}.count() // timeout (ns)
);
if (!(result == vk::Result::eSuccess || result == vk::Result::eTimeout)) {
VALIDATION_LOG << "Fence waiter encountered an unexpected error. Tearing "
"down the waiter thread.";
break;
}
// Check if the context had died in the meantime.
auto device_holder = device_holder_.lock();
if (!device_holder) {
return false;
}

// One or more fences have been signaled. Find out which ones and update
// their signaled statuses.
{
TRACE_EVENT0("impeller", "CheckFenceStatus");
for (auto& entry : wait_set) {
entry->UpdateSignalledStatus(device);
}
wait_set.clear();
}
const auto& device = device_holder->GetDevice();
// Wait for one or more fences to be signaled. Any additional fences added
// to the waiter will be serviced in the next pass. If a fence that is going
// to be signaled at an abnormally long deadline is the only one in the set,
// a timeout will bail out the wait.
auto fences = GetFencesForWaitSet(wait_set);
if (fences.empty()) {
return true;
}

// Quickly acquire the wait set lock and erase signaled entries. Make sure
// the mutex is unlocked before calling the destructors of the erased
// entries. These might touch allocators.
WaitSet erased_entries;
{
static auto is_signalled = [](const auto& entry) {
return entry->IsSignalled();
};
std::scoped_lock lock(wait_set_mutex_);
std::copy_if(wait_set_.begin(), wait_set_.end(),
std::back_inserter(erased_entries), is_signalled);
wait_set_.erase(
std::remove_if(wait_set_.begin(), wait_set_.end(), is_signalled),
wait_set_.end());
}
auto result = device.waitForFences(
fences.size(), // fences count
matanlurey marked this conversation as resolved.
Show resolved Hide resolved
fences.data(), // fences
false, // wait for all
std::chrono::nanoseconds{100ms}.count() // timeout (ns)
);
if (!(result == vk::Result::eSuccess || result == vk::Result::eTimeout)) {
VALIDATION_LOG << "Fence waiter encountered an unexpected error. Tearing "
"down the waiter thread.";
return false;
}

{
TRACE_EVENT0("impeller", "ClearSignaledFences");
// Erase the erased entries which will invoke callbacks.
erased_entries.clear(); // Bit redundant because of scope but hey.
// One or more fences have been signaled. Find out which ones and update
// their signaled statuses.
{
TRACE_EVENT0("impeller", "CheckFenceStatus");
for (auto& entry : wait_set) {
entry->UpdateSignalledStatus(device);
}
wait_set.clear();
}

// Quickly acquire the wait set lock and erase signaled entries. Make sure
// the mutex is unlocked before calling the destructors of the erased
// entries. These might touch allocators.
WaitSet erased_entries;
{
static auto is_signalled = [](const auto& entry) {
matanlurey marked this conversation as resolved.
Show resolved Hide resolved
return entry->IsSignalled();
};
std::scoped_lock lock(wait_set_mutex_);
std::copy_if(wait_set_.begin(), wait_set_.end(),
std::back_inserter(erased_entries), is_signalled);
wait_set_.erase(
matanlurey marked this conversation as resolved.
Show resolved Hide resolved
std::remove_if(wait_set_.begin(), wait_set_.end(), is_signalled),
wait_set_.end());
}

{
TRACE_EVENT0("impeller", "ClearSignaledFences");
// Erase the erased entries which will invoke callbacks.
erased_entries.clear(); // Bit redundant because of scope but hey.
}

return true;
}

void FenceWaiterVK::Terminate() {
Expand Down
4 changes: 3 additions & 1 deletion impeller/renderer/backend/vulkan/fence_waiter_vk.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,14 @@ class FenceWaiterVK {
std::condition_variable wait_set_cv_;
WaitSet wait_set_;
bool terminate_ = false;
bool is_valid_ = false;

explicit FenceWaiterVK(std::weak_ptr<DeviceHolder> device_holder);

void Main();

bool Wait(WaitSet& wait_set);
void WaitUntilEmpty();

FML_DISALLOW_COPY_AND_ASSIGN(FenceWaiterVK);
};

Expand Down
130 changes: 130 additions & 0 deletions impeller/renderer/backend/vulkan/fence_waiter_vk_unittests.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// 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 "fml/synchronization/waitable_event.h"
#include "gtest/gtest.h" // IWYU pragma: keep
#include "impeller/renderer/backend/vulkan/fence_waiter_vk.h" // IWYU pragma: keep
#include "impeller/renderer/backend/vulkan/test/mock_vulkan.h"

namespace impeller {
namespace testing {

TEST(FenceWaiterVKTest, IgnoresNullFence) {
auto const context = MockVulkanContextBuilder().Build();
auto const waiter = context->GetFenceWaiter();
EXPECT_FALSE(waiter->AddFence(vk::UniqueFence(), []() {}));
}

TEST(FenceWaiterVKTest, IgnoresNullCallback) {
auto const context = MockVulkanContextBuilder().Build();
auto const device = context->GetDevice();
auto const waiter = context->GetFenceWaiter();

auto fence = device.createFenceUnique({}).value;
EXPECT_FALSE(waiter->AddFence(std::move(fence), nullptr));
}

TEST(FenceWaiterVKTest, ExecutesFenceCallback) {
auto const context = MockVulkanContextBuilder().Build();
auto const device = context->GetDevice();
auto const waiter = context->GetFenceWaiter();

auto signal = fml::ManualResetWaitableEvent();
auto fence = device.createFenceUnique({}).value;
waiter->AddFence(std::move(fence), [&signal]() { signal.Signal(); });

signal.Wait();
}

TEST(FenceWaiterVKTest, ExecutesFenceCallbackX2) {
auto const context = MockVulkanContextBuilder().Build();
auto const device = context->GetDevice();
auto const waiter = context->GetFenceWaiter();

auto signal = fml::ManualResetWaitableEvent();
auto fence = device.createFenceUnique({}).value;
waiter->AddFence(std::move(fence), [&signal]() { signal.Signal(); });

auto signal2 = fml::ManualResetWaitableEvent();
auto fence2 = device.createFenceUnique({}).value;
waiter->AddFence(std::move(fence2), [&signal2]() { signal2.Signal(); });

signal.Wait();
signal2.Wait();
}

TEST(FenceWaiterVKTest, ExecutesNewFenceThenOldFence) {
auto const context = MockVulkanContextBuilder().Build();
auto const device = context->GetDevice();
auto const waiter = context->GetFenceWaiter();

auto signal = fml::ManualResetWaitableEvent();
auto fence = device.createFenceUnique({}).value;
MockFence::SetStatus(fence, vk::Result::eNotReady);
auto raw_fence = MockFence::GetRawPointer(fence);
waiter->AddFence(std::move(fence), [&signal]() { signal.Signal(); });

// The easiest way to verify that the callback was _not_ called is to wait
// for a timeout, but that could introduce flakiness. Instead, we'll add a
// second fence that will signal immediately, and wait for that one instead.
{
auto signal2 = fml::ManualResetWaitableEvent();
auto fence2 = device.createFenceUnique({}).value;
MockFence::SetStatus(fence2, vk::Result::eSuccess);
waiter->AddFence(std::move(fence2), [&signal2]() { signal2.Signal(); });
signal2.Wait();
}

// Now, we'll signal the first fence, and wait for the callback to be called.
raw_fence->SetStatus(vk::Result::eSuccess);

// Now, we'll signal the first fence, and wait for the callback to be called.
signal.Wait();
}

TEST(FenceWaiterVKTest, AddFenceDoesNothingIfTerminating) {
auto signal = fml::ManualResetWaitableEvent();

{
auto const context = MockVulkanContextBuilder().Build();
auto const device = context->GetDevice();
auto const waiter = context->GetFenceWaiter();
waiter->Terminate();

auto fence = device.createFenceUnique({}).value;
waiter->AddFence(std::move(fence), [&signal]() { signal.Signal(); });
}

// Ensure the fence did _not_ signal.
EXPECT_FALSE(signal.WaitWithTimeout(fml::TimeDelta::FromMilliseconds(100)));
}

TEST(FenceWaiterVKTest, InProgressFencesStillWaitIfTerminated) {
MockFence* raw_fence = nullptr;
auto signal = fml::ManualResetWaitableEvent();

auto const context = MockVulkanContextBuilder().Build();
auto const device = context->GetDevice();
auto const waiter = context->GetFenceWaiter();

// Add a fence that isn't signalled yet.
auto fence = device.createFenceUnique({}).value;

// Even if the fence is eSuccess, it's not guaranteed to be called in time.
MockFence::SetStatus(fence, vk::Result::eNotReady);
raw_fence = MockFence::GetRawPointer(fence);
waiter->AddFence(std::move(fence), [&signal]() { signal.Signal(); });

// Terminate the waiter.
waiter->Terminate();

// Signal the fence.
raw_fence->SetStatus(vk::Result::eSuccess);

// This will hang if the fence was not signalled.
signal.Wait();
}

} // namespace testing
} // namespace impeller
Loading