Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 9de2a60f authored by Lloyd Pique's avatar Lloyd Pique
Browse files

end2end: Introduce AsyncFunction [2/N]

AsyncFunction is a function object wrapper that provides some critical
thread safety guarantees while also striving to allow dynamic
connections between parts of the tests and framework/

In particular:

* Calls to an AsyncFunction target are serialized. The target will only
  be called by one thread at a time.
* The target the AsyncFunction will call can be changed at any time.
* AsyncFunction provides a synchronization point via a returned
  finalizer object from the call to set a new target. When this
  finalizer is explicitly invoked or destroyed, the finalizer waits for
  any calls to the replaced target to complete. After that finishes, any
  resources used by the replaced target can be freed if no longer
  needed.

This implementation is tricky enough that an internal test is needed,
which for simplicity runs with all the SurfaceFlinger integration tests.

Flag: TEST_ONLY
Bug: 372735083
Test: atest surfaceflinger_end2end_tests

Change-Id: I896ff759df514d18e5f3e079bce5f697f90124ea
parent 85add72b
Loading
Loading
Loading
Loading
+5 −0
Original line number Diff line number Diff line
@@ -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,
+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
+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