WWDC Quick Look đź’“ By SwiftGGTeam
Run, Break, Inspect: Explore effective debugging in LLDB

Run, Break, Inspect: Explore effective debugging in LLDB

Watch original video

Highlight

LLDB models debugging as a Run → Break → Inspect loop: after each pause, inspect state, then continue or re-run based on what you learn—narrowing the problem until you find the bug.


Core Content

Print debugging is what most people learn first: insert print statements, recompile, run, read output, repeat if unsatisfied. Three problems—every print change needs a recompile; prints linger in production; and you can only inspect what you thought to print ahead of time. LLDB offers a better path: set breakpoints at runtime, inspect variables, evaluate expressions—all without recompiling.

This session frames debugging as a search problem. Wrong behavior appears at some point; the bug lives somewhere between launch and that moment. Your goal is to find that code. Inspect program state at different times; each inspection moves you closer. LLDB provides four core tools: backtrace, variable inspection, breakpoints, and expression evaluation.

Using the multi-platform Destination Video app, the session walks a real scenario: adding “Watch Later,” understanding SwiftUI closure timing, fixing JSON decode errors, and customizing how custom types appear in the debugger. Each maps to a core LLDB capability.


Detailed Content

Offline crashlog analysis

When a program crashes, Apple platforms collect state at crash time into a crashlog. LLDB can consume crashlogs to simulate a debug session—locate issues without running the program (04:37).

In Xcode, right-click a crashlog and open with Xcode linked to the matching project. Xcode creates an LLDB session and highlights the crash line. Backtrace shows the full call chain: current frame is JSON loading, parent frame imports video metadata, above that is initialization. Sometimes one crashlog is enough to find the bug.

Prerequisites: checkout the project at the same commit as the crashlog, and have the matching dSYM bundle.

One SwiftUI line breakpoint resolves to multiple locations

A line breakpoint on a SwiftUI Button initializer resolves to multiple independent locations (08:29):

Button(action: { watchLater.toggle(video: video) }) {
  let inList = watchLater.isInList(video: video)
  Label(inList ? "In Watch Later" : "Add to Watch Later",
  systemImage: inList ? "checkmark" : "plus")
}

Key points:

  • LLDB resolves this line to 3 breakpoint locations: 1.1, 1.2, 1.3
  • 1.1 is the Button initializer call site—fires when UI is created
  • 1.2 is the action closure (watchLater.toggle(video: video))—fires only on tap
  • 1.3 is the trailing closure (label)—called by the initializer itself
  • Common in declarative frameworks like SwiftUI: same line, different code paths
  • breakpoint list shows line and column for each location
  • Each location can be enabled or disabled independently

Breakpoint actions and command-line control

In the Break/Inspect loop, repeating the same commands manually is tedious. Breakpoint actions run commands automatically on hit (13:17):

p "last video is \(watchLater.last?.name)"

In Xcode: right-click breakpoint → Edit Breakpoint → add Debugger Command action; check “Continue after evaluation” to auto-continue.

Same from the command line (14:42):

b DetailView.swift:70
break command add
p "last video is \(watchLater.last?.name)"
continue
DONE

Key points:

  • b is shorthand for breakpoint set
  • break command add attaches actions to the most recent breakpoint; type DONE to finish
  • Command-line actions override Xcode-configured actions
  • help and apropos search all LLDB commands

Conditional breakpoints and high-frequency hits

When a breakpoint fires too often in a loop, three filters help (16:09):

  1. Conditional breakpoint: break modify --condition with a Boolean expression, e.g. video.duration > 60
  2. Temporary breakpoint (tbreak): create a one-shot breakpoint inside an action—for “stop only after A reaches B”
  3. Ignore count: break modify --ignore-count 10 skips the first N hits

When a line runs millions of times, LLDB still pauses each time to evaluate conditions, slowing execution. Add an if in code that calls raise(SIGSTOP) inside to hand control to the debugger.

p command and expression evaluation

From Xcode 15, p is redefined as a “do what I mean” print alias covering most variable inspection and expression evaluation (19:57).

When debugging JSON decode errors, build complex expressions step by step with p (24:06):

p watchLater.count
p watchLater.last!.name

Key points:

  • p evaluates expressions in any backtrace frame
  • Build expressions incrementally; see intermediate results without recompiling
  • Variables also appear in Xcode’s variable viewer or source hover
  • Some types (URL, Image) have Quick Look for extra visualization
  • Swift Error breakpoint pauses automatically when Swift errors are thrown

@DebugDescription macro

Swift 6 adds @DebugDescription so custom types show readable summaries in the debugger (26:46):

@DebugDescription
struct WatchLaterItem {
    let video: Video
    let name: String
    let addedOn: Date

    var debugDescription: String {
        "\(name) - \(addedOn)"
    }
}

Key points:

  • Annotate with @DebugDescription and provide a debugDescription computed property
  • debugDescription must use string interpolation and stored properties
  • Variable viewer and p show the summary directly—no expanding every field
  • If you used CustomDebugStringConvertible with po and only string interpolation/computed properties, migrate to @DebugDescription for better debugger integration

Core Takeaways

  • What to do: Use breakpoint actions instead of print debugging. Why it’s worth it: No recompile; add and change dynamically; avoids prints shipping to production. How to start: Xcode → right-click breakpoint → Edit Breakpoint → Debugger Command action with p expression and auto-continue.

  • What to do: Add @DebugDescription to core data types that appear often in collections. Why it’s worth it: See record summaries in the variable viewer instead of expanding each entry. How to start: Annotate the type, implement debugDescription with string interpolation of key fields.

  • What to do: Archive dSYM bundles for every CI build. Why it’s worth it: Offline crashlog analysis needs dSYMs for symbolication; without them you only see addresses, not source lines. How to start: Ensure dSYM generation on Archive; App Store Connect handles uploads; manage archives for internal distribution.

  • What to do: Use conditional breakpoints and ignore counts for high-frequency hits—not give up on breakpoints. Why it’s worth it: Loop breakpoints can fire thousands of times; conditions pause only when needed; ignore count skips known-irrelevant iterations. How to start: Edit Breakpoint → Condition or Ignore Count; CLI: break modify --condition or --ignore-count.


Comments

GitHub Issues · utterances