Skip to content

Commit d63aa3a

Browse files
Treehugger RobotAndroid (Google) Code Review
authored andcommitted
Merge "end2end: Introduce AsyncFunction [2/N]" into main
2 parents 1b7198f + 9de2a60 commit d63aa3a

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)