Lesson 11 / 12
Debugging Basics
Measuring the union of breakpoints, the intersection of log filters, a scheme environment variable taking precedence, and the rejection of a breakpoint placed on a line with no code.
Published Updated
In this lesson
- Measures that breakpoints from two sources merge
- Shows the intersection of the level and category filters
- Measures that an environment variable in the scheme takes precedence over the code's default
- Shows that the order of stepping stays outside the model
Contents
When something unexpected happens while the app runs in the simulator or on a device, a debugger is used to find the problem. The most basic tool is the breakpoint: it stops execution at a particular line of code and lets you inspect the values at that moment. After stopping, the code is advanced line by line, which is called stepping, and the output is read from the messages written to the log.
The measurement axis stays the same: when two definitions meet, how is it resolved? In this lesson the meeting definitions are sets of breakpoints, log filters and debugging settings.
The Classifier
// Meeting.swift — classifies how two definitions are resolved when they meet
struct Meeting {
let label: String
let first: Set<String>
let second: Set<String>
let result: Set<String>?
init(_ label: String, first: Set<String>, second: Set<String>, result: Set<String>?) {
self.label = label
self.first = first
self.second = second
self.result = result
}
var resolution: String {
if first.isEmpty || second.isEmpty
|| first.isSubset(of: second) || second.isSubset(of: first) {
return "out-of-scope"
}
guard let result, !result.isEmpty else { return "rejection" }
if result == first.union(second) { return "union" }
if result == first.intersection(second) { return "intersection" }
if result == first || result == second { return "precedence" }
return "off-rule"
}
static func list(_ values: Set<String>) -> String {
values.isEmpty ? "empty" : values.sorted().joined(separator: ",")
}
func report() {
let outcome = result.map(Meeting.list) ?? "none"
print("\(label): first \(Meeting.list(first)) | second \(Meeting.list(second)) | result \(outcome) -> \(resolution)")
}
}
func summarize(_ meetings: [Meeting]) {
for resolution in ["precedence", "intersection", "union", "rejection"] {
let count = meetings.filter { $0.resolution == resolution }.count
print("\(resolution): \(count)")
}
}
Breakpoints
A breakpoint is placed on a line of a file. In practice breakpoints can come from two places: those placed by clicking in the margin of the development environment, and those added from the debugger’s command line with breakpoint set. When the two come together in the same project, the set of active breakpoints is the union of the two sources. A point placed on the same line from both sources is counted once.
The model shows each breakpoint with a file:line identity. The first source places points on lines twelve and thirty of the cart file; the second source places points on line thirty of the cart file and line eight of the order file.
// main.swift — the breakpoints of two sources merge
let gutter: Set<String> = ["Cart.swift:12", "Cart.swift:30"]
let console: Set<String> = ["Cart.swift:30", "Order.swift:8"]
let breakpoints = Meeting(
"breakpoint-sources",
first: gutter,
second: console,
result: gutter.union(console)
)
breakpoints.report()
breakpoint-sources: first Cart.swift:12,Cart.swift:30 | second Cart.swift:30,Order.swift:8 | result Cart.swift:12,Cart.swift:30,Order.swift:8 -> union
The resolution is union: three separate points are active. The shared Cart.swift:30 was counted once. For this reason the number of stops is not the sum of the source counts: a point defined in both sources stops once.
DE17. The measurement treated breakpoints as file and line identities. Conditional breakpoints, debugger-specific actions and disabling a point were not measured.
Breakpoint Kinds
A plain breakpoint stops every time it is reached. In a loop or in a frequently called function this leads to hundreds of needless stops. Two kinds solve this.
- Conditional breakpoint: It stops only when the given expression is true. On the command line the condition is given with the
-cflag:breakpoint set -f Cart.swift -l 30 -c 'count > 3'. The condition is evaluated every time the point is reached; so on a line that is passed very often, the condition itself can cause slowness. - Action breakpoint: It runs an action before stopping or instead of stopping. The action can print the value of an expression, run a command or play a sound. If an automatic continue option is added to the point, the app does not stop; the point behaves like a temporary log line without touching the code. On the command line
breakpoint command adddoes this job.
There are two more kinds of breakpoint. An exception breakpoint stops at the moment the app throws an exception, before the crash; it lets you see not only the result but the cause in the crash trace. A symbolic breakpoint is placed on the name of a function instead of a line, so that every call of a system function whose source you do not have can be stopped at. Because neither carries a file and line identity, both are outside the measurement below.
It is also possible to disable a point without deleting it. A disabled point keeps its definition but does not stop; when the problem is solved it is deleted, and if it is needed again it is turned back on.
None of these kinds is the subject of the measurement above. The measurement looks only at the location of the point; whether the condition turns out true belongs to run time and cannot be expressed with a set.
Log Filters
While running, an app writes a great many messages. To keep them readable, two filters are used together: the log level filter drops messages below a certain severity, and the category filter keeps the messages of a certain subsystem. A message is visible only if it passes both filters. The code that writes to the log depends on the platform’s logging framework, so it stands only as source.
// log — SOURCE
import os
let logger = Logger(subsystem: "com.example.shop", category: "cart")
logger.info("cart updated")
The model shows each message with an identity. The level filter keeps warnings and above; the category filter keeps the messages of the cart subsystem.
// main.swift — the intersection of the level and category filters
let levelPass: Set<String> = ["m2", "m3", "m5", "m6"]
let categoryPass: Set<String> = ["m3", "m4", "m5"]
let filters = Meeting(
"log-filters",
first: levelPass,
second: categoryPass,
result: levelPass.intersection(categoryPass)
)
filters.report()
log-filters: first m2,m3,m5,m6 | second m3,m4,m5 | result m3,m5 -> intersection
The resolution is intersection: only the two messages that pass both filters are visible. If one of the filters is loosened, the visible set grows; if one is tightened, it shrinks. A message not being visible does not mean it was not written; it says only that it could not pass one of the filters.
DE18. The measurement took the two filters as separate sets. Cases where the filters depend on each other and a message belonging to more than one category were not measured.
Filters alone are not enough; the level at which a message is written also matters. The platform’s logging framework separates messages in order of severity.
- Debug level (debug): Details useful only during development. They are usually not kept in the release build.
- Info level (info): Events that are useful to know in the ordinary flow.
- Notice level (notice): The default level; situations that are not a problem but worth recording.
- Error level (error): Situations in which an operation failed.
- Fault level (fault): Situations that break the consistency of the app or the system and call for immediate action.
A category is the name of a subsystem: network, database, interface. The app defines its own subsystems and categories. A good naming makes it possible to look only at network messages when a problem arises. Messages containing personal data are hidden by default; unless the developer states otherwise, their values do not appear in the log.
The Environment Variable in the Scheme
Run settings are kept in the scheme. The scheme can supply environment variables when the app starts. If the code carries a default value for a setting and the scheme gives a value for the same setting, at run time the value in the scheme takes the place of the code’s. This replacement is a precedence: this is why verbose logging can be turned on and off without changing the code.
// main.swift — a scheme environment variable takes precedence over the code's default
func effectiveLogging(codeDefault: String, schemeValue: String?) -> String {
schemeValue ?? codeDefault
}
let chosen = effectiveLogging(codeDefault: "verbose-off", schemeValue: "verbose-on")
let environment = Meeting(
"scheme-environment",
first: ["verbose-off"],
second: ["verbose-on"],
result: [chosen]
)
environment.report()
scheme-environment: first verbose-off | second verbose-on | result verbose-on -> precedence
The resolution is precedence: the result is the same as the second definition. Had there been no value in the scheme, the code’s default would have applied. Because a scheme can be developer-specific, an environment variable in a scheme that is not shared does not appear on another developer’s machine; differences of the “it works on mine” kind sometimes come from here.
DE19. The measurement covered a single pair of settings. Cases where several environment variables affect one another, and the app re-reading a setting within itself, were not measured.
A Line With No Code
A breakpoint cannot be placed on every line. An empty line, a comment line or a lone curly brace produces no executable code; the debugger moves the point to the nearest line of code or does not make it active at all. The model measures this behavior not as a fixed rule but as a meeting: the requested line meets the set of lines that contain code.
// main.swift — a breakpoint placed on a line with no code
let codeLines: Set<String> = ["10", "11", "13"]
let requested: Set<String> = ["12"]
let emptyLine = Meeting(
"breakpoint-on-empty-line",
first: requested,
second: codeLines,
result: requested.intersection(codeLines)
)
emptyLine.report()
breakpoint-on-empty-line: first 12 | second 10,11,13 | result empty -> rejection
There is no common line and the resolution is rejection: there is no code to stop at on line twelve. The debugger moving the point to a neighboring line is outside this model; the model counts whether the point stays on the requested line.
Stepping Commands and Inspecting Variables
When the app stops at a point, three basic commands manage progress. Their counterparts on the debugger command line are given in code font.
- Step into: the
stepcommand. It enters the body of the call on the current line and stops at its first line. - Step over: the
nextcommand. It runs the call in one step and stops at the next line. If there is a breakpoint inside the call, it still stops there. - Step out: the
finishcommand. It runs until the current function returns and stops at the caller’s line.
If you are interested in what is inside a call you step into it; if you are not, you step over it; if you went too deep by mistake, you step out. Stepping into code you do not know, such as the standard library, often wastes time.
After stopping, values are inspected. frame variable lists the local variables in the current frame. print evaluates an expression. po gives the printable description of the expression as an object; this is more readable if the type provides its own description. The variables view shows the same information graphically. By walking the call chain the variables of the upper frames can also be seen. Changing a value is possible too; an expression such as expression count = 0 tests an assumption without rebuilding the app.
All of these commands talk to a running process, so they have no counterpart in the model. The measurement below counts only the trace stepping leaves behind, that is, which lines were visited.
The Bounding Measurement
Stepping advances the code line by line. Step over passes a call in one step, step into enters the call. Two steppings that visit the same lines can advance in a different order. The model counts the set of visited lines; it cannot count the order. The code below prints two visit sequences and then meets their sets.
// main.swift — the order of stepping is invisible to the model
let overOrder = ["10", "11", "13"]
let intoOrder = ["13", "10", "11"]
print("step-over: " + overOrder.joined(separator: ","))
print("step-into: " + intoOrder.joined(separator: ","))
let stepping = Meeting(
"stepping-order",
first: Set(overOrder),
second: Set(intoOrder),
result: Set(overOrder)
)
stepping.report()
step-over: 10,11,13 step-into: 13,10,11 stepping-order: first 10,11,13 | second 10,11,13 | result 10,11,13 -> out-of-scope
The two sequences advanced in different orders but visited the same three lines. Because the sets are the same, the classifier saw no meeting: the result is out-of-scope. Order can matter in determining where a fault appears; the model does not count it.
DE20. The measurement counted the set of visited lines. The order of steps, repeated visits and the time spent inside a call were not measured.
Summary
- Breakpoints from two sources merge; a shared point is counted once.
- Messages that pass both the level and the category filter are visible; the resolution is intersection.
- An environment variable in the scheme takes the place of the code’s default.
- A breakpoint requested on a line with no code is rejected within the model.
- This lesson’s resolution: union. The order of stepping stays outside the measurement.
Next Step
In this lesson a fault was searched for inside the running app. The last lesson takes up the meeting of two languages in the same project: the result of two Swift methods that fall onto the same Objective-C selector is measured.
To keep your progress and take notes, Log in
My notes
Log in to take notes.