Skip to content

Commit de7263a

Browse files
[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 #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 #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 #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 162ee50 commit de7263a

File tree

3 files changed

+112
-10
lines changed

3 files changed

+112
-10
lines changed

lldb/source/Plugins/Language/Swift/SwiftLanguage.cpp

Lines changed: 100 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1832,19 +1832,109 @@ 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+
};
18411842

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

lldb/test/API/lang/swift/async_breakpoints/TestSwiftAsyncBreakpoints.py

Lines changed: 6 additions & 0 deletions
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)

lldb/test/API/lang/swift/async_breakpoints/main.swift

Lines changed: 6 additions & 0 deletions
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)