Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 6a811c3

Browse files
committedMar 14, 2025
[lldb][swift] Filter unnecessary funclets when setting line breakpoints
Prior to this commit, line breakpoints that match multiple funclets were being filtered out to remove "Q" funclets from them. This generally works for simple "await foo()" expressions, but it is not a comprehensive solution, as it does not address the patterns emerging from `async let` and `await <async_let_variable>` statements. This commit generalizes the filtering algorithm: 1. Locations are bundled together based on the async function that generated them. 2. For each bundle, choose the funclet with the smallest "number" as per its mangling. To see why this is helpful, consider an `async let` statement like: `async let timestamp3 = getTimestamp(i: 44)` It creates 4 funclets: ``` 2.1: function = (3) suspend resume partial function for test.some_other_async() async -> () mangled function = $s4test16some_other_asyncyyYaFTY2_ 2.2: function = implicit closure swiftlang#1 @sendable () async -> Swift.Int in test.some_other_async() async -> () mangled function = $s4test16some_other_asyncyyYaFSiyYaYbcfu_ 2.3: function = (1) await resume partial function for implicit closure swiftlang#1 @sendable () async -> Swift.Int in test.some_other_async() async -> () mangled function = $s4test16some_other_asyncyyYaFSiyYaYbcfu_TQ0_ 2.4: function = (2) suspend resume partial function for implicit closure swiftlang#1 @sendable () async -> Swift.Int in test.some_other_async() async -> () mangled function = $s4test16some_other_asyncyyYaFSiyYaYbcfu_TY1_ ``` The first is for the LHS, the others are for the RHS expression and are only executed by the new Task. Only 2.1 and 2.2 should receive breakpoints. Likewise, a breakpoint on an `await <async_let_variable>` line would create 3 funclets: ``` 3.1: function = (3) suspend resume partial function for test.some_other_async() async -> () mangled function = $s4test16some_other_asyncyyYaFTY2_ 3.2: function = (4) suspend resume partial function for test.some_other_async() async -> () mangled function = $s4test16some_other_asyncyyYaFTY3_ 3.3: function = (5) suspend resume partial function for test.some_other_async() async -> () mangled function = $s4test16some_other_asyncyyYaFTY4_ ``` The first is for "before" the await, the other two for "after". Only the first should receive a breakpoint.
1 parent 1888085 commit 6a811c3

File tree

3 files changed

+113
-10
lines changed

3 files changed

+113
-10
lines changed
 

Diff for: ‎lldb/source/Plugins/Language/Swift/SwiftLanguage.cpp

+101-10
Original file line numberDiff line numberDiff line change
@@ -1832,19 +1832,110 @@ SwiftLanguage::GetDemangledFunctionNameWithoutArguments(Mangled mangled) const {
18321832
return mangled_name;
18331833
}
18341834

1835-
void SwiftLanguage::FilterForLineBreakpoints(
1836-
llvm::SmallVectorImpl<SymbolContext> &sc_list) const {
1837-
llvm::erase_if(sc_list, [](const SymbolContext &sc) {
1838-
// If we don't have a function, conservatively keep this sc.
1839-
if (!sc.function)
1840-
return false;
1835+
namespace {
1836+
using namespace swift::Demangle;
1837+
struct AsyncInfo {
1838+
const Function *function;
1839+
NodePointer demangle_node;
1840+
std::optional<uint64_t> funclet_number;
1841+
1842+
/// Helper function for logging.
1843+
std::string to_str() const {
1844+
StreamString stream_str;
1845+
llvm::raw_ostream &str = stream_str.AsRawOstream();
1846+
str << "function = ";
1847+
if (function)
1848+
str << function->GetMangled().GetMangledName();
1849+
else
1850+
str << "nullptr";
1851+
str << ", demangle_node: " << demangle_node;
1852+
str << ", funclet_number = ";
1853+
if (funclet_number)
1854+
str << *funclet_number;
1855+
else
1856+
str << "nullopt";
1857+
return stream_str.GetString().str();
1858+
}
1859+
};
18411860

1842-
// In async functions, ignore await resume ("Q") funclets, these only
1843-
// deallocate the async context and task_switch back to user code.
1861+
/// Map each unique Function in sc_list to a Demangle::NodePointer, or null if
1862+
/// demangling is not possible.
1863+
llvm::SmallVector<AsyncInfo> GetAsyncInfo(llvm::ArrayRef<SymbolContext> sc_list,
1864+
swift::Demangle::Context &ctx) {
1865+
Log *log(GetLog(LLDBLog::Demangle));
1866+
llvm::SmallSet<Function *, 8> seen_functions;
1867+
llvm::SmallVector<AsyncInfo> async_infos;
1868+
for (const SymbolContext &sc : sc_list) {
1869+
if (!sc.function || seen_functions.contains(sc.function))
1870+
continue;
1871+
seen_functions.insert(sc.function);
18441872
llvm::StringRef name =
18451873
sc.function->GetMangled().GetMangledName().GetStringRef();
1846-
return SwiftLanguageRuntime::IsSwiftAsyncAwaitResumePartialFunctionSymbol(
1847-
name);
1874+
NodePointer node = SwiftLanguageRuntime::DemangleSymbolAsNode(name, ctx);
1875+
async_infos.push_back(
1876+
{sc.function, node, SwiftLanguageRuntime::GetFuncletNumber(node)});
1877+
1878+
if (log) {
1879+
std::string as_str = async_infos.back().to_str();
1880+
LLDB_LOGF(log, "%s: %s", __FUNCTION__, as_str.c_str());
1881+
}
1882+
}
1883+
return async_infos;
1884+
}
1885+
} // namespace
1886+
1887+
void SwiftLanguage::FilterForLineBreakpoints(
1888+
llvm::SmallVectorImpl<SymbolContext> &sc_list) const {
1889+
using namespace swift::Demangle;
1890+
Context ctx;
1891+
1892+
llvm::SmallVector<AsyncInfo> async_infos = GetAsyncInfo(sc_list, ctx);
1893+
1894+
// Vector containing one representative funclet of each unique async function
1895+
// in sc_list. The representative is always the one with the smallest funclet
1896+
// number seen so far.
1897+
llvm::SmallVector<AsyncInfo> unique_async_funcs;
1898+
1899+
// Note the subtlety: this deletes based on functions, not SymbolContexts, as
1900+
// there might be multiple SCs with the same Function at this point.
1901+
llvm::SmallPtrSet<const Function *, 4> to_delete;
1902+
1903+
for (const auto &async_info : async_infos) {
1904+
// If we can't find a funclet number, don't delete this.
1905+
if (!async_info.funclet_number)
1906+
continue;
1907+
1908+
// Have we found other funclets of the same async function?
1909+
auto *representative =
1910+
llvm::find_if(unique_async_funcs, [&](AsyncInfo &other_info) {
1911+
// This looks quadratic, but in practice it is not. We should have at
1912+
// most 2 different async functions in the same line, unless a user
1913+
// writes many closures on the same line.
1914+
return SwiftLanguageRuntime::AreFuncletsOfSameAsyncFunction(
1915+
async_info.demangle_node, other_info.demangle_node) ==
1916+
SwiftLanguageRuntime::FuncletComparisonResult::
1917+
SameAsyncFunction;
1918+
});
1919+
1920+
// We found a new async function.
1921+
if (representative == unique_async_funcs.end()) {
1922+
unique_async_funcs.push_back(async_info);
1923+
continue;
1924+
}
1925+
1926+
// This is another funclet of the same async function. Keep the one with the
1927+
// smallest number, erase the other. If they have the same number, don't
1928+
// erase it.
1929+
if (async_info.funclet_number > representative->funclet_number)
1930+
to_delete.insert(async_info.function);
1931+
else if (async_info.funclet_number < representative->funclet_number) {
1932+
to_delete.insert(representative->function);
1933+
*representative = async_info;
1934+
}
1935+
}
1936+
1937+
llvm::erase_if(sc_list, [&](const SymbolContext &sc) {
1938+
return to_delete.contains(sc.function);
18481939
});
18491940
}
18501941

Diff for: ‎lldb/test/API/lang/swift/async_breakpoints/TestSwiftAsyncBreakpoints.py

+6
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,15 @@ def test(self):
1919
)
2020
breakpoint2 = target.BreakpointCreateBySourceRegex("Breakpoint2", filespec)
2121
breakpoint3 = target.BreakpointCreateBySourceRegex("Breakpoint3", filespec)
22+
breakpoint4 = target.BreakpointCreateBySourceRegex("Breakpoint4", filespec)
23+
breakpoint5 = target.BreakpointCreateBySourceRegex("Breakpoint5", filespec)
2224
self.assertEquals(breakpoint1.GetNumLocations(), 1)
2325
self.assertEquals(breakpoint2.GetNumLocations(), 1)
2426
self.assertEquals(breakpoint3.GetNumLocations(), 1)
27+
# FIXME: there should be two breakpoints here, but the "entry" funclet of the
28+
# implicit closure is mangled slightly differently. rdar://147035260
29+
self.assertEquals(breakpoint4.GetNumLocations(), 3)
30+
self.assertEquals(breakpoint5.GetNumLocations(), 1)
2531

2632
location11 = breakpoint1.GetLocationAtIndex(0)
2733
self.assertEquals(location11.GetHitCount(), 1)

Diff for: ‎lldb/test/API/lang/swift/async_breakpoints/main.swift

+6
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ func foo() async {
1010
work() // Breakpoint2
1111
let timestamp2 = await getTimestamp(i:43) // Breakpoint3
1212
work()
13+
// There should be two breakpoints below in an async let:
14+
// One for the code in the "callee", i.e., foo.
15+
// One for the implicit closure in the RHS.
16+
async let timestamp3 = getTimestamp(i: 44) // Breakpoint4
17+
// There should be one breakpoint in an await of an async let variable
18+
await timestamp3 // Breakpoint5
1319
}
1420

1521
await foo()

0 commit comments

Comments
 (0)
Please sign in to comment.