Skip to content

Commit 9de2a60

Browse files
committed
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
1 parent 85add72 commit 9de2a60

File tree

3 files changed

+511
-0
lines changed

3 files changed

+511
-0
lines changed

services/surfaceflinger/tests/end2end/Android.bp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ cc_test {
3636
"test_framework/fake_hwc3/Hwc3Composer.cpp",
3737
"test_framework/fake_hwc3/Hwc3Controller.cpp",
3838
"test_framework/surfaceflinger/SFController.cpp",
39+
40+
// Internal tests
41+
"tests/internal/AsyncFunction_test.cpp",
42+
43+
// SurfaceFlinger tests
3944
"tests/Placeholder_test.cpp",
4045
],
4146
tidy: true,
Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
/*
2+
* Copyright 2025 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#pragma once
18+
19+
#include <cstddef>
20+
#include <functional>
21+
#include <memory>
22+
#include <mutex>
23+
#include <optional>
24+
#include <type_traits>
25+
#include <utility>
26+
27+
#include <android-base/thread_annotations.h>
28+
#include <ftl/finalizer.h>
29+
#include <ftl/function.h>
30+
31+
namespace android::surfaceflinger::tests::end2end::test_framework::core {
32+
33+
// Define a function wrapper class that has some special features to make it async safe.
34+
//
35+
// 1) The contained function is only called on one thread at a time.
36+
// 2) The contained function can be safely replaced at any time.
37+
// 3) This wrapper helps ensure that after replacement, all calls to the replaced function are
38+
// complete by a well-defined synchronization point, which can be deferred to happen outside of
39+
// mutex locks that might otherwise cause a deadlock.
40+
//
41+
// To achieve the last feature, the `set` function to perform replacement returns a special
42+
// `Finalizer` instance which is either automatically invoked on destruction, or on demand via
43+
// `operator()` with no arguments (and returning no value). When invoked, the finalizer waits for
44+
// any calls to the replaced function to complete, and as the finalizer can be moved if needed, this
45+
// wait can be done without other mutexes being held that might cause a deadlock in the replaced
46+
// function.
47+
//
48+
// Once the finalizer completes, any resources needed only by the previous function can be
49+
// safely destroyed. The finalizer also destroys any captured state that was part of the
50+
// previous function.
51+
//
52+
// Note that the target function that is called by this wrapper is allowed to replace the function
53+
// this wrapper contains. When this happens there is no synchronization point, as waiting for the
54+
// replaced function to complete as part of what is executed while it is invoked would be a
55+
// deadlock. For this case, the returned finalizer instance is a no-op if invoked. Instead the
56+
// capture for the prior function will be destroyed when the control returns back to the wrapper,
57+
// before control returns to the code that invoked the wrapper.
58+
//
59+
// Instances of this class can be default constructed, and invoking the contained function in this
60+
// state does nothing, and also requires no synchronization point when replaced by an actual target.
61+
// After being set, this state can be entered again by using the `clear()` member function.
62+
//
63+
// If the contained function has a return type T other than void, the return type of the wrapper
64+
// will be std::optional<T>. If there is no target set, invoking the wrapper will return a
65+
// std::nullopt value, otherwise it will return an optional with the value set to the value returned
66+
// by the contained function.
67+
//
68+
// Usage:
69+
//
70+
// AsyncFunctionStd<void()> function = [this](){
71+
// std_lock_guard lock(mutex);
72+
// someMemberFunction();
73+
// };
74+
//
75+
// function(); // Invokes someMemberFunction();
76+
//
77+
// // May invoke someMemberFunction or otherMemberFunction (set below).
78+
// std::async(std::launch::async, function);
79+
//
80+
// ftl::AsyncFunctionStd<void()>::Finalizer finalizer;
81+
// {
82+
// std_lock_guard lock(mutex);
83+
// finalizer = function.set([this](){
84+
// std_lock_guard lock(mutex);
85+
// otherMemberFunction();
86+
// });
87+
// // do not invoke the finalizer with locks held, unless there is no chance of a deadlock.
88+
// }
89+
// function() // Invokes instance2->otherMemberFunction();
90+
// finalizer(); // Waits for calls to someMemberFunction to complete
91+
// // It is now safe to destroy resources that are used by someMemberFunction.
92+
//
93+
// std::ignore = function.clear(); // Clear the function and implicitly invoke the returned
94+
// finalizer.
95+
// // It is now safe to destroy resource that used used by otherMemberFunction, including
96+
// // 'this' if desired.
97+
//
98+
template <typename Function>
99+
class AsyncFunction final {
100+
struct SharedFunction;
101+
102+
// Turns some return type `T` into `std::optional<T>`, unless `T` is `void`.
103+
template <typename T>
104+
using AddOptionalUnlessVoid = std::conditional_t<std::is_void_v<T>, T, std::optional<T>>;
105+
106+
public:
107+
using Finalizer = ftl::FinalizerStd;
108+
109+
// The return type from `operator()`.
110+
using result_type = AddOptionalUnlessVoid<typename Function::result_type>;
111+
112+
// Default construct an empty state.
113+
AsyncFunction() = default;
114+
115+
~AsyncFunction() {
116+
// Ensure any outstanding calls complete before teardown by clearing the shared_ptr. Note
117+
// however if there are any, there would likely be other problems since the owning class is
118+
// in the process of being destroyed.
119+
setInternal(nullptr)();
120+
}
121+
122+
// For simplicity, copying and moving are not possible.
123+
AsyncFunction(const AsyncFunction&) = delete;
124+
auto operator=(const AsyncFunction&) = delete;
125+
AsyncFunction(AsyncFunction&&) = delete;
126+
auto operator=(AsyncFunction&&) = delete;
127+
128+
// Constructs an AsyncFunction from the function type.
129+
template <typename NewFunction>
130+
requires(!std::is_same_v<std::remove_cvref_t<NewFunction>, AsyncFunction> &&
131+
std::is_constructible_v<Function, NewFunction>)
132+
// NOLINTNEXTLINE(google-explicit-constructor)
133+
explicit(false) AsyncFunction(NewFunction&& function)
134+
: mShared(std::make_shared<SharedFunction>(std::forward<NewFunction>(function))) {}
135+
136+
// Replaces the contained function value with a new one.
137+
//
138+
// Returns a finalizer which when invoked waits for calls to the old function value
139+
// complete. This is done so that the caller can invoke the caller without locks held
140+
// that might block the call from completing,
141+
template <typename NewFunction>
142+
requires(std::is_constructible_v<Function, NewFunction>)
143+
[[nodiscard]] auto set(NewFunction&& function) -> Finalizer {
144+
return setInternal(std::make_shared<SharedFunction>(std::forward<NewFunction>(function)));
145+
}
146+
147+
// Clears the contained function value.
148+
//
149+
// Returns a finalizer which when invoked waits for calls to the old function value
150+
// complete. This is done so that the caller can invoke the caller without locks held
151+
// that might block the call from completing,
152+
[[nodiscard]] auto clear() -> Finalizer { return setInternal(nullptr); }
153+
154+
// Invoke the contained function, if set.
155+
template <typename... Args>
156+
requires(std::is_invocable_v<Function, Args...>)
157+
auto operator()(Args&&... args) const -> result_type {
158+
// We might need to retry the process to forward the call if we happen to obtain a zombie
159+
// shared_ptr. We try at least once.
160+
bool retry = true;
161+
while (std::exchange(retry, false)) {
162+
// To avoid deadlocks, the call to the contained function must be made on a copy of the
163+
// shared_ptr without the internal locks on the source shared_ptr member data.
164+
const auto shared = copy();
165+
166+
// Confirm we got a non-null pointer before continuing. The pointer can be null if no
167+
// target is set.
168+
if (shared == nullptr) {
169+
break;
170+
}
171+
172+
// We must hold shared->callingMutex before accessing the other fields it contains.
173+
std::lock_guard lock(shared->callingMutex);
174+
175+
// Now that the calling mutex is held, confirm it is valid to use.
176+
if (!shared->valid) [[unlikely]] {
177+
// If the pointer isn't valid, we must retry to get a new copy.
178+
// It indicates another thread set a new target by setting a new pointer after we
179+
// made our copy, but before we acquired `callingMutex`. Our pointer is effectively
180+
// a zombie, and must not be used.
181+
retry = true;
182+
continue;
183+
}
184+
185+
// If `Function` can be (possibly explicitly) converted to bool, it is used as a check
186+
// at runtime that the function is safe to invoke.
187+
if constexpr (std::is_constructible_v<bool, Function>) {
188+
if (!shared->function) {
189+
break;
190+
}
191+
}
192+
193+
// Forward the call. Note that callingMutex must be held for the duration of the call.
194+
return std::invoke(shared->function, std::forward<Args>(args)...);
195+
}
196+
197+
// If we reached this point, we had no target to invoke.
198+
if constexpr (!std::is_void_v<std::invoke_result_t<Function, Args...>>) {
199+
// If the function was supposed to return a value, we explicitly return `std::nullopt`
200+
// rather than manufacturing a value of some arbitrary type (perhaps by default
201+
// construction).
202+
return std::nullopt;
203+
}
204+
}
205+
206+
private:
207+
struct SharedFunction final {
208+
SharedFunction() = default;
209+
210+
template <typename NewFunction>
211+
requires(!std::is_same_v<NewFunction, SharedFunction>)
212+
explicit SharedFunction(NewFunction&& newFunction)
213+
: function(std::forward<NewFunction>(newFunction)) {}
214+
215+
// NOLINTBEGIN(misc-non-private-member-variables-in-classes)
216+
217+
// `callingMutex` must be held for the duration that `function` is invoked.
218+
mutable std::recursive_mutex callingMutex;
219+
const Function function; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members)
220+
// If valid is false, it means the shared pointer was exchanged to point at a new value, and
221+
// the function here is no longer safe to invoke.
222+
bool valid = true; // GUARDED_BY(callingMutex)
223+
224+
// NOLINTEND(misc-non-private-member-variables-in-classes)
225+
};
226+
227+
[[nodiscard]] auto setInternal(std::shared_ptr<SharedFunction> newShared) -> Finalizer {
228+
// To avoid deadlocks, the old instance MUST be destroyed outside of all locks, including
229+
// locks held by the caller. It is the caller's responsibility to invoke the returned
230+
// finalizer outside of any locks being holds.
231+
std::shared_ptr<SharedFunction> prior = exchange(std::move(newShared));
232+
233+
return Finalizer([prior = std::move(prior)]() {
234+
if (prior) {
235+
// Wait for any call to complete.
236+
// Note that callingMutex is a recursive_mutex so that we won't deadlock if the
237+
// current thread is already holding the same lock.
238+
std::lock_guard lock(prior->callingMutex);
239+
// Mark the function as no longer valid to call, on the off chance another thread
240+
// obtained a copy just before this thread did the exchange.
241+
prior->valid = false;
242+
}
243+
});
244+
}
245+
246+
[[nodiscard]] auto exchange(std::shared_ptr<SharedFunction>&& newShared)
247+
-> std::shared_ptr<SharedFunction> {
248+
std::lock_guard lock(mMutex);
249+
return std::exchange(mShared, std::move(newShared));
250+
}
251+
252+
[[nodiscard]] auto copy() const -> std::shared_ptr<SharedFunction> {
253+
std::lock_guard lock(mMutex);
254+
return mShared;
255+
}
256+
257+
// In the future, maybe this can become `std::atomic<std::shared_ptr<Function>>`.
258+
mutable std::mutex mMutex;
259+
std::shared_ptr<SharedFunction> mShared GUARDED_BY(mMutex);
260+
};
261+
262+
template <typename Function>
263+
AsyncFunction(Function&&) -> AsyncFunction<std::decay_t<Function>>;
264+
265+
template <typename Signature>
266+
using AsyncFunctionStd = AsyncFunction<std::function<Signature>>;
267+
268+
template <typename Signature>
269+
using AsyncFunctionFtl = AsyncFunction<ftl::Function<Signature>>;
270+
271+
template <typename Signature>
272+
using AsyncFunctionFtl1 = AsyncFunction<ftl::Function<Signature, 1>>;
273+
274+
template <typename Signature>
275+
using AsyncFunctionFtl2 = AsyncFunction<ftl::Function<Signature, 2>>;
276+
277+
template <typename Signature>
278+
using AsyncFunctionFtl3 = AsyncFunction<ftl::Function<Signature, 3>>;
279+
280+
} // namespace android::surfaceflinger::tests::end2end::test_framework::core

0 commit comments

Comments
 (0)