Loading services/surfaceflinger/tests/end2end/Android.bp +5 −0 Original line number Diff line number Diff line Loading @@ -36,6 +36,11 @@ cc_test { "test_framework/fake_hwc3/Hwc3Composer.cpp", "test_framework/fake_hwc3/Hwc3Controller.cpp", "test_framework/surfaceflinger/SFController.cpp", // Internal tests "tests/internal/AsyncFunction_test.cpp", // SurfaceFlinger tests "tests/Placeholder_test.cpp", ], tidy: true, Loading services/surfaceflinger/tests/end2end/test_framework/core/AsyncFunction.h 0 → 100644 +280 −0 Original line number Diff line number Diff line /* * Copyright 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include <cstddef> #include <functional> #include <memory> #include <mutex> #include <optional> #include <type_traits> #include <utility> #include <android-base/thread_annotations.h> #include <ftl/finalizer.h> #include <ftl/function.h> namespace android::surfaceflinger::tests::end2end::test_framework::core { // Define a function wrapper class that has some special features to make it async safe. // // 1) The contained function is only called on one thread at a time. // 2) The contained function can be safely replaced at any time. // 3) This wrapper helps ensure that after replacement, all calls to the replaced function are // complete by a well-defined synchronization point, which can be deferred to happen outside of // mutex locks that might otherwise cause a deadlock. // // To achieve the last feature, the `set` function to perform replacement returns a special // `Finalizer` instance which is either automatically invoked on destruction, or on demand via // `operator()` with no arguments (and returning no value). When invoked, the finalizer waits for // any calls to the replaced function to complete, and as the finalizer can be moved if needed, this // wait can be done without other mutexes being held that might cause a deadlock in the replaced // function. // // Once the finalizer completes, any resources needed only by the previous function can be // safely destroyed. The finalizer also destroys any captured state that was part of the // previous function. // // Note that the target function that is called by this wrapper is allowed to replace the function // this wrapper contains. When this happens there is no synchronization point, as waiting for the // replaced function to complete as part of what is executed while it is invoked would be a // deadlock. For this case, the returned finalizer instance is a no-op if invoked. Instead the // capture for the prior function will be destroyed when the control returns back to the wrapper, // before control returns to the code that invoked the wrapper. // // Instances of this class can be default constructed, and invoking the contained function in this // state does nothing, and also requires no synchronization point when replaced by an actual target. // After being set, this state can be entered again by using the `clear()` member function. // // If the contained function has a return type T other than void, the return type of the wrapper // will be std::optional<T>. If there is no target set, invoking the wrapper will return a // std::nullopt value, otherwise it will return an optional with the value set to the value returned // by the contained function. // // Usage: // // AsyncFunctionStd<void()> function = [this](){ // std_lock_guard lock(mutex); // someMemberFunction(); // }; // // function(); // Invokes someMemberFunction(); // // // May invoke someMemberFunction or otherMemberFunction (set below). // std::async(std::launch::async, function); // // ftl::AsyncFunctionStd<void()>::Finalizer finalizer; // { // std_lock_guard lock(mutex); // finalizer = function.set([this](){ // std_lock_guard lock(mutex); // otherMemberFunction(); // }); // // do not invoke the finalizer with locks held, unless there is no chance of a deadlock. // } // function() // Invokes instance2->otherMemberFunction(); // finalizer(); // Waits for calls to someMemberFunction to complete // // It is now safe to destroy resources that are used by someMemberFunction. // // std::ignore = function.clear(); // Clear the function and implicitly invoke the returned // finalizer. // // It is now safe to destroy resource that used used by otherMemberFunction, including // // 'this' if desired. // template <typename Function> class AsyncFunction final { struct SharedFunction; // Turns some return type `T` into `std::optional<T>`, unless `T` is `void`. template <typename T> using AddOptionalUnlessVoid = std::conditional_t<std::is_void_v<T>, T, std::optional<T>>; public: using Finalizer = ftl::FinalizerStd; // The return type from `operator()`. using result_type = AddOptionalUnlessVoid<typename Function::result_type>; // Default construct an empty state. AsyncFunction() = default; ~AsyncFunction() { // Ensure any outstanding calls complete before teardown by clearing the shared_ptr. Note // however if there are any, there would likely be other problems since the owning class is // in the process of being destroyed. setInternal(nullptr)(); } // For simplicity, copying and moving are not possible. AsyncFunction(const AsyncFunction&) = delete; auto operator=(const AsyncFunction&) = delete; AsyncFunction(AsyncFunction&&) = delete; auto operator=(AsyncFunction&&) = delete; // Constructs an AsyncFunction from the function type. template <typename NewFunction> requires(!std::is_same_v<std::remove_cvref_t<NewFunction>, AsyncFunction> && std::is_constructible_v<Function, NewFunction>) // NOLINTNEXTLINE(google-explicit-constructor) explicit(false) AsyncFunction(NewFunction&& function) : mShared(std::make_shared<SharedFunction>(std::forward<NewFunction>(function))) {} // Replaces the contained function value with a new one. // // Returns a finalizer which when invoked waits for calls to the old function value // complete. This is done so that the caller can invoke the caller without locks held // that might block the call from completing, template <typename NewFunction> requires(std::is_constructible_v<Function, NewFunction>) [[nodiscard]] auto set(NewFunction&& function) -> Finalizer { return setInternal(std::make_shared<SharedFunction>(std::forward<NewFunction>(function))); } // Clears the contained function value. // // Returns a finalizer which when invoked waits for calls to the old function value // complete. This is done so that the caller can invoke the caller without locks held // that might block the call from completing, [[nodiscard]] auto clear() -> Finalizer { return setInternal(nullptr); } // Invoke the contained function, if set. template <typename... Args> requires(std::is_invocable_v<Function, Args...>) auto operator()(Args&&... args) const -> result_type { // We might need to retry the process to forward the call if we happen to obtain a zombie // shared_ptr. We try at least once. bool retry = true; while (std::exchange(retry, false)) { // To avoid deadlocks, the call to the contained function must be made on a copy of the // shared_ptr without the internal locks on the source shared_ptr member data. const auto shared = copy(); // Confirm we got a non-null pointer before continuing. The pointer can be null if no // target is set. if (shared == nullptr) { break; } // We must hold shared->callingMutex before accessing the other fields it contains. std::lock_guard lock(shared->callingMutex); // Now that the calling mutex is held, confirm it is valid to use. if (!shared->valid) [[unlikely]] { // If the pointer isn't valid, we must retry to get a new copy. // It indicates another thread set a new target by setting a new pointer after we // made our copy, but before we acquired `callingMutex`. Our pointer is effectively // a zombie, and must not be used. retry = true; continue; } // If `Function` can be (possibly explicitly) converted to bool, it is used as a check // at runtime that the function is safe to invoke. if constexpr (std::is_constructible_v<bool, Function>) { if (!shared->function) { break; } } // Forward the call. Note that callingMutex must be held for the duration of the call. return std::invoke(shared->function, std::forward<Args>(args)...); } // If we reached this point, we had no target to invoke. if constexpr (!std::is_void_v<std::invoke_result_t<Function, Args...>>) { // If the function was supposed to return a value, we explicitly return `std::nullopt` // rather than manufacturing a value of some arbitrary type (perhaps by default // construction). return std::nullopt; } } private: struct SharedFunction final { SharedFunction() = default; template <typename NewFunction> requires(!std::is_same_v<NewFunction, SharedFunction>) explicit SharedFunction(NewFunction&& newFunction) : function(std::forward<NewFunction>(newFunction)) {} // NOLINTBEGIN(misc-non-private-member-variables-in-classes) // `callingMutex` must be held for the duration that `function` is invoked. mutable std::recursive_mutex callingMutex; const Function function; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) // If valid is false, it means the shared pointer was exchanged to point at a new value, and // the function here is no longer safe to invoke. bool valid = true; // GUARDED_BY(callingMutex) // NOLINTEND(misc-non-private-member-variables-in-classes) }; [[nodiscard]] auto setInternal(std::shared_ptr<SharedFunction> newShared) -> Finalizer { // To avoid deadlocks, the old instance MUST be destroyed outside of all locks, including // locks held by the caller. It is the caller's responsibility to invoke the returned // finalizer outside of any locks being holds. std::shared_ptr<SharedFunction> prior = exchange(std::move(newShared)); return Finalizer([prior = std::move(prior)]() { if (prior) { // Wait for any call to complete. // Note that callingMutex is a recursive_mutex so that we won't deadlock if the // current thread is already holding the same lock. std::lock_guard lock(prior->callingMutex); // Mark the function as no longer valid to call, on the off chance another thread // obtained a copy just before this thread did the exchange. prior->valid = false; } }); } [[nodiscard]] auto exchange(std::shared_ptr<SharedFunction>&& newShared) -> std::shared_ptr<SharedFunction> { std::lock_guard lock(mMutex); return std::exchange(mShared, std::move(newShared)); } [[nodiscard]] auto copy() const -> std::shared_ptr<SharedFunction> { std::lock_guard lock(mMutex); return mShared; } // In the future, maybe this can become `std::atomic<std::shared_ptr<Function>>`. mutable std::mutex mMutex; std::shared_ptr<SharedFunction> mShared GUARDED_BY(mMutex); }; template <typename Function> AsyncFunction(Function&&) -> AsyncFunction<std::decay_t<Function>>; template <typename Signature> using AsyncFunctionStd = AsyncFunction<std::function<Signature>>; template <typename Signature> using AsyncFunctionFtl = AsyncFunction<ftl::Function<Signature>>; template <typename Signature> using AsyncFunctionFtl1 = AsyncFunction<ftl::Function<Signature, 1>>; template <typename Signature> using AsyncFunctionFtl2 = AsyncFunction<ftl::Function<Signature, 2>>; template <typename Signature> using AsyncFunctionFtl3 = AsyncFunction<ftl::Function<Signature, 3>>; } // namespace android::surfaceflinger::tests::end2end::test_framework::core services/surfaceflinger/tests/end2end/tests/internal/AsyncFunction_test.cpp 0 → 100644 +226 −0 Original line number Diff line number Diff line /* * Copyright 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include <chrono> #include <future> #include <memory> #include <mutex> #include <thread> #include <tuple> #include <type_traits> #include <utility> #include <gtest/gtest.h> #include "test_framework/core/AsyncFunction.h" namespace android::surfaceflinger::tests::end2end { namespace { template <typename T> using AsyncFunction = test_framework::core::AsyncFunction<T>; template <typename T> using AsyncFunctionStd = test_framework::core::AsyncFunctionStd<T>; template <typename T> using AsyncFunctionFtl = test_framework::core::AsyncFunctionFtl<T>; TEST(AsyncFunction, DefaultConstructionOfVoidFunctionIsANoOpWhenInvoked) { const AsyncFunctionStd<void(int)> afn; afn(123); } TEST(AsyncFunction, DefaultConstructionOfIntFunctionIsANoOpWhenInvoked) { const AsyncFunctionStd<int(int)> afn; const auto result = afn(123); EXPECT_FALSE(result.has_value()); } TEST(AsyncFunction, NulledVoidFunctionIsANoOpWhenInvoked) { int callArg = 0; AsyncFunctionStd<void(int)> afn{[&](auto value) { callArg = value; }}; afn.set(nullptr)(); EXPECT_EQ(callArg, 0); afn(345); EXPECT_EQ(callArg, 0); EXPECT_TRUE(std::is_void_v<decltype(afn(345))>); } TEST(AsyncFunction, NulledIntFunctionIsANoOpWhenInvoked) { int callArg = 0; AsyncFunctionStd<int(int)> afn{[&](auto value) { callArg = value; return value + 123; }}; afn.set(nullptr)(); EXPECT_EQ(callArg, 0); const auto result = afn(345); EXPECT_EQ(callArg, 0); EXPECT_FALSE(result.has_value()); } TEST(AsyncFunction, VoidFunctionWorksWhenInvoked) { int callArg = 0; const AsyncFunctionStd<void(int)> afn{[&](auto value) { callArg = value; }}; EXPECT_EQ(callArg, 0); afn(345); EXPECT_EQ(callArg, 345); EXPECT_TRUE(std::is_void_v<decltype(afn(345))>); } TEST(AsyncFunction, IntFunctionWorksWhenInvoked) { int callArg = 0; const AsyncFunctionStd<int(int)> afn{[&](auto value) { callArg = value; return value + 123; }}; EXPECT_EQ(callArg, 0); const auto result = afn(345); EXPECT_EQ(callArg, 345); EXPECT_TRUE(result == 468); } TEST(AsyncFunction, NulledIntFtlFunctionIsANoOpWhenInvoked) { int callArg = 0; AsyncFunctionFtl<int(int)> afn{[&](auto value) { callArg = value; return value + 123; }}; afn.set(nullptr)(); EXPECT_EQ(callArg, 0); const auto result = afn(345); EXPECT_EQ(callArg, 0); EXPECT_FALSE(result.has_value()); } TEST(AsyncFunction, IntFtlFunctionWorksWhenInvoked) { int callArg = 0; const AsyncFunctionFtl<int(int)> afn{[&](auto value) { callArg = value; return value + 123; }}; EXPECT_EQ(callArg, 0); const auto result = afn(345); EXPECT_EQ(callArg, 345); EXPECT_TRUE(result == 468); } TEST(AsyncFunction, CalledFunctionCanReplaceItselfWithoutHanging) { AsyncFunctionStd<void()> afn; afn.set([&] { afn.set([] {})(); })(); afn(); } TEST(AsyncFunction, FinalizerDestroysCaptureWhenManuallyInvoked) { auto shared = std::make_shared<int>(0); const std::weak_ptr<int> weak = shared; AsyncFunctionStd<void()> afn = [context = std::move(shared)] {}; EXPECT_FALSE(weak.expired()); auto finalizer = afn.clear(); EXPECT_FALSE(weak.expired()); finalizer(); EXPECT_TRUE(weak.expired()); } TEST(AsyncFunction, FinalizerDestroysCaptureWhenDestroyed) { auto shared = std::make_shared<int>(0); const std::weak_ptr<int> weak = shared; { AsyncFunctionStd<void()> afn = [context = std::move(shared)] {}; EXPECT_FALSE(weak.expired()); std::ignore = afn.clear(); EXPECT_TRUE(weak.expired()); } } TEST(AsyncFunctionStd, CanSafelyReplaceFunctionViaAssignmentWhileBeingCalled) { // This has a good but not guaranteed chance of catching a problem, but with continuous runs of // this test we should eventually see a failure. Increase this value to increase the chance of // catching errors when running locally. static constexpr auto kRunFor = std::chrono::milliseconds(10); // This test ensures that the wrapper does what it says it does: // 1) Allows the function to be replaced at any time. // 2) Includes a barrier that the replaced function is no longer used past a point that can be // controlled by the caller so that a wait can be done outside of any locks that might be // acquired by the replaced function, // We intentionally use a mutex to guard `state` rather than using an atomic integer. std::mutex mutex; AsyncFunctionStd<void()> afn; // GUARDED_BY(mutex) int state = 0; // GUARDED_BY(mutex) auto setState = [&](int value) { const std::lock_guard lock(mutex); state = value; }; auto setStateTemporarily = [&](int value) { static constexpr auto kDelayBeforeSwitching = std::chrono::microseconds(10); setState(value); std::this_thread::sleep_for(kDelayBeforeSwitching); setState(0); }; auto getState = [&] -> int { const std::lock_guard lock(mutex); return state; }; auto expectStateIsZeroOr = [&](int expected) { const auto value = getState(); EXPECT_TRUE(value == 0 || value == expected) << "Expected zero or " << expected << " but observed " << value; }; auto setAsyncFunction = [&](auto newFunction) -> AsyncFunctionStd<void()>::Finalizer { const std::lock_guard lock(mutex); return afn.set(newFunction); }; // Continuously invoke the current function via the wrapper in a separate thread, for kRunFor // time. const std::future<void> future = std::async(std::launch::async, [&afn] { const auto endAt = std::chrono::steady_clock::now() + kRunFor; while (std::chrono::steady_clock::now() < endAt) { afn(); } }); // While the callback is being called, switch the callback between two functions. // The first callback sets the state to either 0 or 1. // The second callback sets the state to either 0 or 2. constexpr auto kWaitFor = std::chrono::microseconds(0); while (future.wait_for(kWaitFor) != std::future_status::ready) { auto cleanup1 = setAsyncFunction([&] { setStateTemporarily(1); }); cleanup1(); expectStateIsZeroOr(1); setAsyncFunction([&] { setStateTemporarily(2); })(); expectStateIsZeroOr(2); } } } // namespace } // namespace android::surfaceflinger::tests::end2end Loading
services/surfaceflinger/tests/end2end/Android.bp +5 −0 Original line number Diff line number Diff line Loading @@ -36,6 +36,11 @@ cc_test { "test_framework/fake_hwc3/Hwc3Composer.cpp", "test_framework/fake_hwc3/Hwc3Controller.cpp", "test_framework/surfaceflinger/SFController.cpp", // Internal tests "tests/internal/AsyncFunction_test.cpp", // SurfaceFlinger tests "tests/Placeholder_test.cpp", ], tidy: true, Loading
services/surfaceflinger/tests/end2end/test_framework/core/AsyncFunction.h 0 → 100644 +280 −0 Original line number Diff line number Diff line /* * Copyright 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include <cstddef> #include <functional> #include <memory> #include <mutex> #include <optional> #include <type_traits> #include <utility> #include <android-base/thread_annotations.h> #include <ftl/finalizer.h> #include <ftl/function.h> namespace android::surfaceflinger::tests::end2end::test_framework::core { // Define a function wrapper class that has some special features to make it async safe. // // 1) The contained function is only called on one thread at a time. // 2) The contained function can be safely replaced at any time. // 3) This wrapper helps ensure that after replacement, all calls to the replaced function are // complete by a well-defined synchronization point, which can be deferred to happen outside of // mutex locks that might otherwise cause a deadlock. // // To achieve the last feature, the `set` function to perform replacement returns a special // `Finalizer` instance which is either automatically invoked on destruction, or on demand via // `operator()` with no arguments (and returning no value). When invoked, the finalizer waits for // any calls to the replaced function to complete, and as the finalizer can be moved if needed, this // wait can be done without other mutexes being held that might cause a deadlock in the replaced // function. // // Once the finalizer completes, any resources needed only by the previous function can be // safely destroyed. The finalizer also destroys any captured state that was part of the // previous function. // // Note that the target function that is called by this wrapper is allowed to replace the function // this wrapper contains. When this happens there is no synchronization point, as waiting for the // replaced function to complete as part of what is executed while it is invoked would be a // deadlock. For this case, the returned finalizer instance is a no-op if invoked. Instead the // capture for the prior function will be destroyed when the control returns back to the wrapper, // before control returns to the code that invoked the wrapper. // // Instances of this class can be default constructed, and invoking the contained function in this // state does nothing, and also requires no synchronization point when replaced by an actual target. // After being set, this state can be entered again by using the `clear()` member function. // // If the contained function has a return type T other than void, the return type of the wrapper // will be std::optional<T>. If there is no target set, invoking the wrapper will return a // std::nullopt value, otherwise it will return an optional with the value set to the value returned // by the contained function. // // Usage: // // AsyncFunctionStd<void()> function = [this](){ // std_lock_guard lock(mutex); // someMemberFunction(); // }; // // function(); // Invokes someMemberFunction(); // // // May invoke someMemberFunction or otherMemberFunction (set below). // std::async(std::launch::async, function); // // ftl::AsyncFunctionStd<void()>::Finalizer finalizer; // { // std_lock_guard lock(mutex); // finalizer = function.set([this](){ // std_lock_guard lock(mutex); // otherMemberFunction(); // }); // // do not invoke the finalizer with locks held, unless there is no chance of a deadlock. // } // function() // Invokes instance2->otherMemberFunction(); // finalizer(); // Waits for calls to someMemberFunction to complete // // It is now safe to destroy resources that are used by someMemberFunction. // // std::ignore = function.clear(); // Clear the function and implicitly invoke the returned // finalizer. // // It is now safe to destroy resource that used used by otherMemberFunction, including // // 'this' if desired. // template <typename Function> class AsyncFunction final { struct SharedFunction; // Turns some return type `T` into `std::optional<T>`, unless `T` is `void`. template <typename T> using AddOptionalUnlessVoid = std::conditional_t<std::is_void_v<T>, T, std::optional<T>>; public: using Finalizer = ftl::FinalizerStd; // The return type from `operator()`. using result_type = AddOptionalUnlessVoid<typename Function::result_type>; // Default construct an empty state. AsyncFunction() = default; ~AsyncFunction() { // Ensure any outstanding calls complete before teardown by clearing the shared_ptr. Note // however if there are any, there would likely be other problems since the owning class is // in the process of being destroyed. setInternal(nullptr)(); } // For simplicity, copying and moving are not possible. AsyncFunction(const AsyncFunction&) = delete; auto operator=(const AsyncFunction&) = delete; AsyncFunction(AsyncFunction&&) = delete; auto operator=(AsyncFunction&&) = delete; // Constructs an AsyncFunction from the function type. template <typename NewFunction> requires(!std::is_same_v<std::remove_cvref_t<NewFunction>, AsyncFunction> && std::is_constructible_v<Function, NewFunction>) // NOLINTNEXTLINE(google-explicit-constructor) explicit(false) AsyncFunction(NewFunction&& function) : mShared(std::make_shared<SharedFunction>(std::forward<NewFunction>(function))) {} // Replaces the contained function value with a new one. // // Returns a finalizer which when invoked waits for calls to the old function value // complete. This is done so that the caller can invoke the caller without locks held // that might block the call from completing, template <typename NewFunction> requires(std::is_constructible_v<Function, NewFunction>) [[nodiscard]] auto set(NewFunction&& function) -> Finalizer { return setInternal(std::make_shared<SharedFunction>(std::forward<NewFunction>(function))); } // Clears the contained function value. // // Returns a finalizer which when invoked waits for calls to the old function value // complete. This is done so that the caller can invoke the caller without locks held // that might block the call from completing, [[nodiscard]] auto clear() -> Finalizer { return setInternal(nullptr); } // Invoke the contained function, if set. template <typename... Args> requires(std::is_invocable_v<Function, Args...>) auto operator()(Args&&... args) const -> result_type { // We might need to retry the process to forward the call if we happen to obtain a zombie // shared_ptr. We try at least once. bool retry = true; while (std::exchange(retry, false)) { // To avoid deadlocks, the call to the contained function must be made on a copy of the // shared_ptr without the internal locks on the source shared_ptr member data. const auto shared = copy(); // Confirm we got a non-null pointer before continuing. The pointer can be null if no // target is set. if (shared == nullptr) { break; } // We must hold shared->callingMutex before accessing the other fields it contains. std::lock_guard lock(shared->callingMutex); // Now that the calling mutex is held, confirm it is valid to use. if (!shared->valid) [[unlikely]] { // If the pointer isn't valid, we must retry to get a new copy. // It indicates another thread set a new target by setting a new pointer after we // made our copy, but before we acquired `callingMutex`. Our pointer is effectively // a zombie, and must not be used. retry = true; continue; } // If `Function` can be (possibly explicitly) converted to bool, it is used as a check // at runtime that the function is safe to invoke. if constexpr (std::is_constructible_v<bool, Function>) { if (!shared->function) { break; } } // Forward the call. Note that callingMutex must be held for the duration of the call. return std::invoke(shared->function, std::forward<Args>(args)...); } // If we reached this point, we had no target to invoke. if constexpr (!std::is_void_v<std::invoke_result_t<Function, Args...>>) { // If the function was supposed to return a value, we explicitly return `std::nullopt` // rather than manufacturing a value of some arbitrary type (perhaps by default // construction). return std::nullopt; } } private: struct SharedFunction final { SharedFunction() = default; template <typename NewFunction> requires(!std::is_same_v<NewFunction, SharedFunction>) explicit SharedFunction(NewFunction&& newFunction) : function(std::forward<NewFunction>(newFunction)) {} // NOLINTBEGIN(misc-non-private-member-variables-in-classes) // `callingMutex` must be held for the duration that `function` is invoked. mutable std::recursive_mutex callingMutex; const Function function; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) // If valid is false, it means the shared pointer was exchanged to point at a new value, and // the function here is no longer safe to invoke. bool valid = true; // GUARDED_BY(callingMutex) // NOLINTEND(misc-non-private-member-variables-in-classes) }; [[nodiscard]] auto setInternal(std::shared_ptr<SharedFunction> newShared) -> Finalizer { // To avoid deadlocks, the old instance MUST be destroyed outside of all locks, including // locks held by the caller. It is the caller's responsibility to invoke the returned // finalizer outside of any locks being holds. std::shared_ptr<SharedFunction> prior = exchange(std::move(newShared)); return Finalizer([prior = std::move(prior)]() { if (prior) { // Wait for any call to complete. // Note that callingMutex is a recursive_mutex so that we won't deadlock if the // current thread is already holding the same lock. std::lock_guard lock(prior->callingMutex); // Mark the function as no longer valid to call, on the off chance another thread // obtained a copy just before this thread did the exchange. prior->valid = false; } }); } [[nodiscard]] auto exchange(std::shared_ptr<SharedFunction>&& newShared) -> std::shared_ptr<SharedFunction> { std::lock_guard lock(mMutex); return std::exchange(mShared, std::move(newShared)); } [[nodiscard]] auto copy() const -> std::shared_ptr<SharedFunction> { std::lock_guard lock(mMutex); return mShared; } // In the future, maybe this can become `std::atomic<std::shared_ptr<Function>>`. mutable std::mutex mMutex; std::shared_ptr<SharedFunction> mShared GUARDED_BY(mMutex); }; template <typename Function> AsyncFunction(Function&&) -> AsyncFunction<std::decay_t<Function>>; template <typename Signature> using AsyncFunctionStd = AsyncFunction<std::function<Signature>>; template <typename Signature> using AsyncFunctionFtl = AsyncFunction<ftl::Function<Signature>>; template <typename Signature> using AsyncFunctionFtl1 = AsyncFunction<ftl::Function<Signature, 1>>; template <typename Signature> using AsyncFunctionFtl2 = AsyncFunction<ftl::Function<Signature, 2>>; template <typename Signature> using AsyncFunctionFtl3 = AsyncFunction<ftl::Function<Signature, 3>>; } // namespace android::surfaceflinger::tests::end2end::test_framework::core
services/surfaceflinger/tests/end2end/tests/internal/AsyncFunction_test.cpp 0 → 100644 +226 −0 Original line number Diff line number Diff line /* * Copyright 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include <chrono> #include <future> #include <memory> #include <mutex> #include <thread> #include <tuple> #include <type_traits> #include <utility> #include <gtest/gtest.h> #include "test_framework/core/AsyncFunction.h" namespace android::surfaceflinger::tests::end2end { namespace { template <typename T> using AsyncFunction = test_framework::core::AsyncFunction<T>; template <typename T> using AsyncFunctionStd = test_framework::core::AsyncFunctionStd<T>; template <typename T> using AsyncFunctionFtl = test_framework::core::AsyncFunctionFtl<T>; TEST(AsyncFunction, DefaultConstructionOfVoidFunctionIsANoOpWhenInvoked) { const AsyncFunctionStd<void(int)> afn; afn(123); } TEST(AsyncFunction, DefaultConstructionOfIntFunctionIsANoOpWhenInvoked) { const AsyncFunctionStd<int(int)> afn; const auto result = afn(123); EXPECT_FALSE(result.has_value()); } TEST(AsyncFunction, NulledVoidFunctionIsANoOpWhenInvoked) { int callArg = 0; AsyncFunctionStd<void(int)> afn{[&](auto value) { callArg = value; }}; afn.set(nullptr)(); EXPECT_EQ(callArg, 0); afn(345); EXPECT_EQ(callArg, 0); EXPECT_TRUE(std::is_void_v<decltype(afn(345))>); } TEST(AsyncFunction, NulledIntFunctionIsANoOpWhenInvoked) { int callArg = 0; AsyncFunctionStd<int(int)> afn{[&](auto value) { callArg = value; return value + 123; }}; afn.set(nullptr)(); EXPECT_EQ(callArg, 0); const auto result = afn(345); EXPECT_EQ(callArg, 0); EXPECT_FALSE(result.has_value()); } TEST(AsyncFunction, VoidFunctionWorksWhenInvoked) { int callArg = 0; const AsyncFunctionStd<void(int)> afn{[&](auto value) { callArg = value; }}; EXPECT_EQ(callArg, 0); afn(345); EXPECT_EQ(callArg, 345); EXPECT_TRUE(std::is_void_v<decltype(afn(345))>); } TEST(AsyncFunction, IntFunctionWorksWhenInvoked) { int callArg = 0; const AsyncFunctionStd<int(int)> afn{[&](auto value) { callArg = value; return value + 123; }}; EXPECT_EQ(callArg, 0); const auto result = afn(345); EXPECT_EQ(callArg, 345); EXPECT_TRUE(result == 468); } TEST(AsyncFunction, NulledIntFtlFunctionIsANoOpWhenInvoked) { int callArg = 0; AsyncFunctionFtl<int(int)> afn{[&](auto value) { callArg = value; return value + 123; }}; afn.set(nullptr)(); EXPECT_EQ(callArg, 0); const auto result = afn(345); EXPECT_EQ(callArg, 0); EXPECT_FALSE(result.has_value()); } TEST(AsyncFunction, IntFtlFunctionWorksWhenInvoked) { int callArg = 0; const AsyncFunctionFtl<int(int)> afn{[&](auto value) { callArg = value; return value + 123; }}; EXPECT_EQ(callArg, 0); const auto result = afn(345); EXPECT_EQ(callArg, 345); EXPECT_TRUE(result == 468); } TEST(AsyncFunction, CalledFunctionCanReplaceItselfWithoutHanging) { AsyncFunctionStd<void()> afn; afn.set([&] { afn.set([] {})(); })(); afn(); } TEST(AsyncFunction, FinalizerDestroysCaptureWhenManuallyInvoked) { auto shared = std::make_shared<int>(0); const std::weak_ptr<int> weak = shared; AsyncFunctionStd<void()> afn = [context = std::move(shared)] {}; EXPECT_FALSE(weak.expired()); auto finalizer = afn.clear(); EXPECT_FALSE(weak.expired()); finalizer(); EXPECT_TRUE(weak.expired()); } TEST(AsyncFunction, FinalizerDestroysCaptureWhenDestroyed) { auto shared = std::make_shared<int>(0); const std::weak_ptr<int> weak = shared; { AsyncFunctionStd<void()> afn = [context = std::move(shared)] {}; EXPECT_FALSE(weak.expired()); std::ignore = afn.clear(); EXPECT_TRUE(weak.expired()); } } TEST(AsyncFunctionStd, CanSafelyReplaceFunctionViaAssignmentWhileBeingCalled) { // This has a good but not guaranteed chance of catching a problem, but with continuous runs of // this test we should eventually see a failure. Increase this value to increase the chance of // catching errors when running locally. static constexpr auto kRunFor = std::chrono::milliseconds(10); // This test ensures that the wrapper does what it says it does: // 1) Allows the function to be replaced at any time. // 2) Includes a barrier that the replaced function is no longer used past a point that can be // controlled by the caller so that a wait can be done outside of any locks that might be // acquired by the replaced function, // We intentionally use a mutex to guard `state` rather than using an atomic integer. std::mutex mutex; AsyncFunctionStd<void()> afn; // GUARDED_BY(mutex) int state = 0; // GUARDED_BY(mutex) auto setState = [&](int value) { const std::lock_guard lock(mutex); state = value; }; auto setStateTemporarily = [&](int value) { static constexpr auto kDelayBeforeSwitching = std::chrono::microseconds(10); setState(value); std::this_thread::sleep_for(kDelayBeforeSwitching); setState(0); }; auto getState = [&] -> int { const std::lock_guard lock(mutex); return state; }; auto expectStateIsZeroOr = [&](int expected) { const auto value = getState(); EXPECT_TRUE(value == 0 || value == expected) << "Expected zero or " << expected << " but observed " << value; }; auto setAsyncFunction = [&](auto newFunction) -> AsyncFunctionStd<void()>::Finalizer { const std::lock_guard lock(mutex); return afn.set(newFunction); }; // Continuously invoke the current function via the wrapper in a separate thread, for kRunFor // time. const std::future<void> future = std::async(std::launch::async, [&afn] { const auto endAt = std::chrono::steady_clock::now() + kRunFor; while (std::chrono::steady_clock::now() < endAt) { afn(); } }); // While the callback is being called, switch the callback between two functions. // The first callback sets the state to either 0 or 1. // The second callback sets the state to either 0 or 2. constexpr auto kWaitFor = std::chrono::microseconds(0); while (future.wait_for(kWaitFor) != std::future_status::ready) { auto cleanup1 = setAsyncFunction([&] { setStateTemporarily(1); }); cleanup1(); expectStateIsZeroOr(1); setAsyncFunction([&] { setStateTemporarily(2); })(); expectStateIsZeroOr(2); } } } // namespace } // namespace android::surfaceflinger::tests::end2end