Debugging Swift is just…awful

I do a lot of Apple platform development (iOS & macOS primarily) in Swift. For a long time now (at least since the introduction of async/await, but I think longer), debugging Swift code has been pretty bad. It was never great, even in other languages, but it has definitely gotten worse in the past couple of years. And I’m not the only one experiencing this. A few people chimed in on a recent post over on the Swift forums. I’ve reported these issues to Apple via Radar, but generally gotten “can’t reproduce” responses.

There are two major issues:

  • Local variables are often not available for inspection
  • Single-stepping through async code rarely works.

In general, the behaviors are as if the code were heavily optimized, but all of this is built with -Onone (debug builds in Xcode, verified by looking at the build transcripts). All of these issues persist in the latest Xcode 14.2.

Local Variables

It’s frequently the case that some or all local variable values are not available to the debugger. The Xcode debugger pane will show all of the variables in the function, but it won’t display their values. So it knows they exist, but thinks they’re gone. Depending on how one attempts to inspect, either nothing is displayed (Xcode variables pane), or two different messages are reported. e.g.:

(lldb) v req
(Foundation.URLRequest) req = <no location, value may have been optimized out>

(lldb) po req
error: expression failed to parse:
error: <EXPR>:8:1: error: cannot find 'req' in scope
req
^~~

frame v will also show all the same variables Xcode sees should exist, but won’t show values for some. e.g.:

(lldb) frame v
(String) inLogin = "rmann@test.com"
(String) inPassword = "testpass"
(<redacted>) self = 0x0000600001ebce80 {
  session = 0x000000012fd07160 {…}
  baseURL = "http://localhost:8080/api"
  apiKey = "aldknca98s457qo24nfpo9u8a9hartap9a0s8ejl4kysdrt"
  config = {
    deviceID = 181CD0FD-D242-4794-BD48-8C4837C336E5
    token = "a2999a27017bded26f11d18dc4647fae3970eccf0c25e6f7608028a5d0d09578"
    selectedOrgID = nil
  }
  deviceName = "API Unit Test"
}
(Foundation.Data) json = 148 bytes
([String : Any]) obj = 4 key/value pairs {
  [0] = {
    key = "login"
    value = "rmann@test.com"
  }
  [1] = {
    key = "password"
    value = "testpass"
  }
  [2] = {
    key = "deviceName"
    value = "API Unit Test"
  }
  [3] = {
    key = "deviceID"
    value = "181CD0FD-D242-4794-BD48-8C4837C336E5"
  }
}
(<MyApp>.LoginRequest) loginRequest = <no location, value may have been optimized out>

(Foundation.URLRequest) req = <no location, value may have been optimized out>

(Foundation.URL) url = <no location, value may have been optimized out>

(Int) statusCode = <variable not available>

(URLResponse) resp = <variable not available>

(Foundation.Data) data = <variable not available>

(lldb)

Stepping Through Async Code

I don’t think I’ve ever been able to successfully step into or over an await call. I can set a breakpoint inside that call, and it will hit it. I also can’t reliably step out; that usually results in the debugger stopping in some assembly code somewhere, if you then try to step out, you can’t resume execution, step, or anything other than kill the target.

Is it just that most people never experience these behaviors? I’ve had at least half-a-dozen people chime in that they also experience these issues, and are generally relegated to using print() to debug.

Please know that I write this out of frustration but not with malintent toward anyone. It just seems like such a core functionality, and Apple should but a microscopic portion of their vast wealth toward making their tools best-in-class. I wish I knew enough about the Swift toolchain to actually get in there and fix this. But it would take me months to get anywhere, and I’ve got other things I want to build using those tools.

7 Likes

I wholeheartedly agree with this assessment. It doesn’t seem to follow a scheme or pattern, sometimes (rarely) it works, most of the time it breaks though.

1 Like

While I sympathize, I don’t think this is the correct forum. I think posting this to the Swift discussion forums would be better. I bet it would get better traction there.

I did post there. It was pointed out to me that “Swift support is part of mainline lldb,” which is why I made this post.

1 Like

There is a lot more that goes into debugging than just the debugger. The quality of the debug info is a huge part of that. I am not a Swift expert, and for someone to explain these issues, you probably need a Swift expert. Looking at the git history, it seems like @JDevlieghere has done quite a bit of work on lldb+swift - so maybe he can better understand the problems.

1 Like

Yes and no. The LLVM lldb has no support for Swift. The LLDB shipped with MacOS has support for Swift.

They develop LLDB with Swift support
here.

1 Like

I did post there. It was pointed out to me that “Swift support is part of mainline lldb,” which is why I made this post.

At this point in time the LLDB plugin for Swift is not actually part of the LLVM project. You did the right thing by posting on the Swift forums.

3 Likes

Did you consider that part of await is an excursion though the JavaScript thread
scheduler. This enables all waiting threads to wait at similar priority and stay out
of the way while waiting. See first drawing 30% down from the top at::

in particular arc # 6 and arc #7

Your application (JS[you]) lives in its own address space, contains its own control
stack, contains its own registers, modes, and a few other do-dads.

When control passes into await, control may flow into JavaScript thread scheduler;
and another application (JS[him]) may receive control before you get it again. [The
debugger is a proxy for you here,] you (JS[you]) are not allowed to look at the virtual
address space of him.
{The debugger may know where your registers are, your current IP, and minutia
concerning your application but it can’t see into the virtual address space because
you are not in control at that instant in time.}

When control passes back, you are now back in your own little world (JS[you]) and
at this point the debugger can see your virtual address space, again.

I can see why you cannot step through such a thing. There are other kinds of code
where one cannot single step through and have any semblance of what would happen
with a full-speed traversal of the same code sequence.

I don’t know how similar the .NET or JavaScript implementation are compared to the Swift one, but at least for Swift, it is expected that you can step in or over those calls with LLDB.

Let’s continue this discussion on the Swift forums.

1 Like

Note that this is also an issue for C++ with coroutines. Afaik, lldb is currently lacking a way to expose green-threads from language plugins.

Is my understanding correct that the debug info for Swift async functions is actually generated by LLVM itself (and not the swift frontend), inside the “coroutine splitting” pass?

1 Like

| avogelsgesang
January 24 |

  • | - |

JetForMe:

Stepping Through Async Code

Note that this is also an issue for C++ with coroutines. Afaik, lldb is currently lacking a way to expose green-threads from language plugins.

lldb provides the “OperatingSystem” plugin interface as a way to expose non-system threading entities as native thread peers. The non-system threads can either be backed by system threads or be stand-alone. So there should be a pretty straightforward way to expose these green threads, but somebody would have to implement it.

Jim

1 Like

Would you mind posting a link to where you posted in the Swift forums? I’d be interested in reading that thread, too.

Here you go! I also updated the OP to include it.

Since we’ve determined this should be discussed on the Swift forums, I’m just going to close this thread so it doesn’t keep getting bumped. If you have more comments please add them on the Swift forum, which is linked above.