Performance-Profiling Swift on Linux: Getting Started
Learn how to profile Server-Side Swift with perf on Linux. You’ll discover the basic principles of profiling and how to view events, call-graph-traces and perform basic analysis. By kelvin ma.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Performance-Profiling Swift on Linux: Getting Started
35 mins
- Getting Started
- The Data File
- The Trading Application
- The Supporting Code
- Running the Application
- Measuring Performance
- Profiling With Perf
- Loading Samples With Perf Script
- Getting Task Information
- Getting Timeline Information
- Getting Landmark Information
- Working With Perf Events
- Collecting Call Graph Traces
- Tracing With Frame Pointers
- Viewing Call Graph Traces
- Working With Call Graph Traces
- Configuring Perf for Swift
- Enabling DWARF Metadata
- Demangling Swift Function Names
- Putting It All Together
- Counting Function Names
- Observing Changes in Performance
- Where to Go From Here?
It happens to app developers. Sooner or later, every application you build hits its natural limits, and you’ll have to optimize for performance. If you’re developing on macOS, Instruments is the tool for the job. But if you’re developing on Linux, you might be wondering what to use instead.
Fortunately, Linux developers aren’t strangers to performance optimization. In this tutorial, you’ll profile a cryptocurrency trading application, using the same workflow you might apply to one of your own projects. You’ll use the perf
tool to collect data about the performance of the application and generate a performance profile that shows which parts of the application are slow, and why they are slow. Along the way, you’ll learn:
- How to use
perf record
to gather data about an application. - How to use
perf script
to view columns of performamce data. - How to read and interpret performance data.
- How to configure
perf
to produce performance profiles tailored specifically for Swift binaries. - How to use
swift demangle
to post-process a performance profile.
Let’s get started!
Getting Started
First, click the Download Materials button at the top or bottom of this tutorial. This archive contains a simple application called crypto-bot
. Unzip the file, and navigate to the starter project directory. It contains a Swift Package Manager project with a Package.swift manifest file. It also contains the dockerfile you’ll use to build the Docker image for this tutorial.
The Data File
Alongside the manifest and the dockerfile is the ftx.json data file. It contains serialized market data from the FTX cryptocurrency exchange. Open this file in a text file viewer. For example, to view it with the cat
tool, run the following command in a terminal:
cat ftx.json
You should see a stream of cryptocurrency market data formatted as JSON. This is real data, captured from FTX’s public market data API. Although the file is over 8 MB in size, it represents just a few seconds of trading activity from a few markets in a single exchange. This should give you a sense of how much data real-world trading applications handle!
The Trading Application
Start building the Docker image by running the following command in the starter project directory:
docker build . --file dockerfile --tag swift-perf
This might take a few minutes to complete.
Meanwhile, look at the files in Sources/crypto-bot. For the purposes of this tutorial, we’ve removed all the components of crypto-bot
not related to parsing or decoding market data. Open example.swift and find Main.main()
, reproduced below:
static func main() throws {
// 1
let data = try File.read([UInt8].self, from: "ftx.json")
// 2
var input = ParsingInput<Grammar.NoDiagnostics<[UInt8]>>(data)
var updates: [String: Int] = [:]
// 3
while let json = input.parse(as: JSON.Rule<Array<UInt8>.Index>.Root?.self) {
// 4
guard let message = decode(message: json) else {
continue
}
updates[message.market, default: 0] +=
message.orderbook.bids.count + message.orderbook.asks.count
}
// 5
try input.parse(as: Grammar.End<Array<UInt8>.Index, UInt8>.self)
// 6
for (market, updates) in updates.sorted(by: { $0.value > $1.value }) {
print("\(market): \(updates) update(s)")
}
}
Key points about this function:
- This loads the market data file as a
[UInt8]
array. It’s faster than loading it as aString
because the file is UTF-8-encoded, and theJSON
module supports parsing JSON directly from UTF-8 data. - This creates a
ParsingInput
view over the[UInt8]
array and disables parsing diagnostics. These APIs are part of theJSON
module. - This loop tries to parse one complete JSON message at a time until it exhausts the parsing input. These APIs are also part of the
JSON
module. - This attempts to decode the fields of an
FTX.Message
instance from a parsed JSON value, skipping it if it’s not a validFTX.Message
. - This asserts that the parsing loop exited because the parser reached the end of the input — not because it encountered a parsing error.
- This prints out the number of events recorded in each cryptocurrency market.
Below main()
is a function called decode(message:)
, reproduced below:
@inline(never)
func decode(message json: JSON) -> FTX.Message? {
try? .init(from: json)
}
This function takes a parameter of type JSON
, which is a Decoder
, and passes it to FTX.Message
’s Decodable
implementation. It’s marked @inline(never)
to make it easier to measure its performance.
Below decode(message:)
is a similar function named decodeFast(message:)
. As its name suggests, it contains a significantly more efficient JSON decoding implementation. At the end of this tutorial, you will compare its performance to that of decode(message:)
.
The Supporting Code
The other files in Sources/crypto-bot are less important to this tutorial. Here’s an overview:
-
ftx.swift: This contains the definition of
FTX.Message
, which models a kind of market event called an orderbook update. Most of the market events in ftx.json are orderbook updates. The ftx.swift file also directs the compiler to synthesize aCodable
implementation forFTX.Message
. -
decimal.swift: This defines a simple decimal type and directs the compiler to synthesize a
Codable
implementation for it. -
system.swift: This contains helper code that
main()
uses to load ftx.json from disk.
Running the Application
If the Docker image has finished building, start a container with it:
docker run -it --rm --privileged --name swift-perf-instance -v $PWD:/crypto-bot swift-perf
This is a normal Docker run
command, except it contains the option --privileged
. You will need this because performance profiling uses specialized hardware in your CPU, which the default Docker containers can’t access.
Your prompt should look like this:
/crypto-bot$
Note the shell has already cd
in the project directory.
Within the container, build crypto-bot
with the Swift Package Manager. Ensure you compile it in release
mode, because running performance benchmarks on unoptimized code would not be meaningful.
swift build -c release
Try running crypto-bot
in the container. It should run for a second or two, and then print a long list of markets and orderbook update counts:
.build/release/crypto-bot
Output:
FTM-PERP: 7094 update(s)
AXS-PERP: 3595 update(s)
FTT-PERP: 3458 update(s)
...
MEDIA-PERP: 64 update(s)
That’s fast, but remember that ftx.json only holds a few seconds of market data. The application is nowhere near fast enough to keep up with the exchange in real time, which means you are going to have to optimize.