Performance and Instruments

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production; see the section bibliography for the reference material consulted while preparing these pages.

"It feels slow" is not a bug report you can fix by reading code. Performance work starts with a measurement, finds the one place that dominates it, changes that place, and measures again. Instruments, the profiler that ships with Xcode, is the tool for the middle two steps. This page applies it to FieldNotes with the 2026 baseline of this section: iOS, iPadOS, macOS, watchOS and visionOS 27, Xcode 27 and Swift 6.4, with a deployment minimum of 26. It follows one method through CPU time, responsiveness (hangs, hitches, SwiftUI updates and Swift concurrency), blocking, memory, energy, launch time and app size, and uses the Instruments 27 additions: the Top Functions mode, Run Comparisons and the Swift Executors instrument.

Field data (the Organizer’s metrics and MetricKit reports) tells you which of these areas to work on; it is set up on Logging, MetricKit, Crash Reports and Organizer, together with the FieldNotesSignposts.loading signposter this page records. Correctness bugs and leaks found in the debugger are on Debugging with Xcode and LLDB, and automated performance tests (measure(metrics:)) are on UI Tests, Test Plans, Performance Tests and Coverage.

The Method: Measure, Identify, Fix, Verify

Every performance fix on this page goes around the same loop. Measure the symptom on a real device, in a Release build, around a reproducible interaction. Identify the single biggest cause in the trace. Fix one thing. Verify by recording the same interaction again and comparing, then either stop or go around again.

Figure: The performance loop

flowchart LR M["Measure
Release build on a device,
Product > Profile (Command-I)"] --> I["Identify
the biggest cause:
track, call tree, top function"] I --> F["Fix
one change:
less work, off the main actor, no blocking"] F --> V["Verify
record again,
Run Comparison on the same signpost"] V -- "still over budget" --> M V -- "within budget" --> Done(["Ship, and watch the
Organizer and MetricKit"]) Field["Organizer, MetricKit,
tester reports"] --> M

Three rules keep the loop honest:

  • Profile Release builds. Product > Profile (Command-I) builds with the Release configuration by default and opens Instruments. A Debug build is unoptimized and full of checks, so its hot spots are often not the real ones.

  • Profile on a device. The Simulator runs on the Mac’s CPU and memory and has no watchdog, thermal limits or real GPU. Use the slowest device you support.

  • Mark the interaction. Wrap the interaction in a signpost interval so every trace can be filtered to exactly that time range. FieldNotes' FieldNotesSignposts.loading intervals appear in the Points of Interest track.

Instruments can also record from the command line with xctrace, which is how a CI job or a script captures the same trace repeatedly:

xcrun xctrace list templates
xcrun xctrace list devices

# Record 30 seconds of the Time Profiler template while launching FieldNotes on a paired iPhone
xcrun xctrace record --template 'Time Profiler' --device 'Field iPhone' \
  --time-limit 30s --output FieldNotes-search.trace --launch -- com.example.fieldnotes

# Export the table of contents of a trace, restricted to its first 10 seconds (xctrace 27)
xcrun xctrace export --input FieldNotes-search.trace --toc --time-end 10s

xctrace in Xcode 27 also accepts recording options as JSON (--show-recording-options, --recording-options) and can export a time range directly. Instruments 27 needs a target device with at least iOS 17 or watchOS 10.

CPU: Time Profiler

The Time Profiler samples the call stack of every thread about once a millisecond. A function’s weight is the number of samples in which it appeared; its self weight counts only samples where it was the function actually running. Instruments 27 shows the samples three ways, switched with the control above the detail view:

  • Call Tree: the classic outline, from main down to the leaves. Use Invert Call Tree and Hide System Libraries to bring your own heaviest functions to the top.

  • Flame Graph: the same tree as nested bars, callers on top, width proportional to time. It shows expensive paths at a glance, but spreads the cost of a function called from many places across many small bars.

  • Top Functions (new in 27): discards the hierarchy and merges every occurrence of a function into one entry, sorted by self weight, with a flame graph of its callers next to it. It finds overhead that is scattered, such as Swift runtime functions, retain and release, or existential unboxing.

FieldNotes' search field got slow with a few thousand notes. The first version filtered in the view’s body, so every keystroke re-ran a case- and diacritic-insensitive comparison of every note, on the main actor:

// FieldNotes/Views/NoteSearchResults.swift (first version: filters in body)
import SwiftUI
import FieldNotesModel
import FieldNotesServices

struct NoteSearchResults: View {
    @Environment(NoteStore.self) private var store
    let query: String

    var body: some View {
        let signposter = FieldNotesSignposts.loading
        let results = signposter.withIntervalSignpost("Search") {
            store.notes.filter {
                $0.title.localizedStandardContains(query) || $0.body.localizedStandardContains(query)
                    || $0.tags.contains { $0.localizedStandardContains(query) }
            }
        }
        List(results) { NoteRow(note: $0) }
    }
}

Filtered to the Search signpost, the main thread is at 100% CPU and Top Functions lists string comparison and Unicode normalization at the top. The fix does the expensive part once per note instead of once per keystroke, and moves each search off the main actor. The views then only show a ready array:

// FieldNotes/Search/NoteSearchMatcher.swift (page-local; the word index on Swift Concurrency in Apps is a different type)
import Foundation
import FieldNotesModel

/// Folded, lowercased text per note, built once and searched off the main actor.
nonisolated struct NoteSearchMatcher: Sendable {
    private let entries: [(id: Note.ID, text: String)]

    init(notes: [Note]) {
        entries = notes.map { note in
            let text = ([note.title, note.body] + note.tags).joined(separator: " ")
            return (note.id, text.folding(options: [.caseInsensitive, .diacriticInsensitive], locale: nil))
        }
    }

    @concurrent
    func matches(_ query: String) async -> Set<Note.ID> {
        let folded = query.folding(options: [.caseInsensitive, .diacriticInsensitive], locale: nil)
        return Set(entries.lazy.filter { $0.text.contains(folded) }.map(\.id))
    }
}

// FieldNotes/Views/NoteSearchResults.swift (fixed)
struct NoteSearchResults: View {
    /// What the results depend on: the query and the notes' contents. `Note` is `Hashable`.
    private struct SearchInput: Hashable {
        var query: String
        var notes: [Note]
    }

    @Environment(NoteStore.self) private var store
    @State private var matchingIDs: Set<Note.ID> = []
    let query: String

    var body: some View {
        List(store.notes.filter { matchingIDs.contains($0.id) }) { NoteRow(note: $0) }
            .task(id: SearchInput(query: query, notes: store.notes), name: "Search") {
                let index = NoteSearchMatcher(notes: store.notes)   // cheap enough; cache it for larger stores
                matchingIDs = await index.matches(query)
            }
    }
}

#Preview {
    NoteSearchResults(query: "beetle")
        .environment(NoteStore(notes: [Note(title: "Beetle traps, site A", tags: ["beetles"])]))
}

.task(id:) cancels the previous search and starts a new one whenever its ID changes. Keying it on SearchInput rather than on query alone means an edited, added or deleted note refreshes the results too; comparing the array is a linear pass over values already in memory, far cheaper than the folding and matching it guards. Task identity and cancellation are explained on Swift Concurrency in Apps; @concurrent runs the matching on the global concurrent executor. The filter left in body is a cheap Set lookup.

Verifying with Run Comparisons (27)

To verify, record the same search again into the same trace document, filter both runs to the same Search signpost interval, select the main thread track, and click Compare in the middle bar to choose the baseline run. Instruments 27 matches each function in the baseline with its counterpart in the new run and shows the difference: red for regressions, green for improvements, in the call tree, the flame graph or Top Functions. Comparisons are saved in the trace document, so a teammate can review the evidence. Recording both runs from the command line keeps the conditions identical:

# Baseline, then the fixed build, recorded the same way
xcrun xctrace record --template 'Time Profiler' --device 'Field iPhone' --time-limit 30s \
  --output Search-baseline.trace --launch -- com.example.fieldnotes
xcrun xctrace record --template 'Time Profiler' --device 'Field iPhone' --time-limit 30s \
  --output Search-fixed.trace --launch -- com.example.fieldnotes
# Drag both onto the Instruments sidebar (27) to get both runs in one document, then Compare.

Expect a comparison to show some red: a fix often introduces new, cheaper functions, which appear as regressions. Judge the total of the interval, not individual rows.

Responsiveness: Hangs and Hitches

Hangs

A hang is a period in which the main run loop is too busy to handle input. Apple’s tools start reporting when the main run loop is unresponsive for more than 250 ms; below about 100 ms a delay after a tap is rarely noticeable, and a few hundred milliseconds already feels broken. The Hangs instrument, included in the Time Profiler and Swift Concurrency templates, marks each hang on the timeline and can use a lower threshold. The Organizer and MetricKit report the hang rate from the field.

Hitches

A hitch is a frame that appears later than expected during an animation or scroll. Each frame has a deadline: the app must commit its view changes, and the render server must render them, before the next display refresh. A commit hitch means the app’s main thread was late (the same long main-thread work that causes hangs); a render hitch means the render server was late, usually because the frame was too expensive to draw: many offscreen passes, large blurs, too many Liquid Glass effects at once, or a shader compiled on first use. The Animation Hitches instrument records each frame’s lifetime and hitch time; in Instruments 27 it also works on Apple Vision Pro. The Organizer’s new Hitches metric covers all animations, not only scrolling.

# Scroll the notes list for 20 seconds while recording frame lifetimes
xcrun xctrace record --template 'Animation Hitches' --device 'Field iPhone' \
  --time-limit 20s --output FieldNotes-scroll.trace --attach FieldNotes

Specific causes are covered where the feature is: shaders can be precompiled with Shader.compile(as:) (Drawing, Animation and Gestures), list rows should be cheap and have a stable identity (Lists, Tables and Collections), and the number of glass effects on screen should stay small (Liquid Glass, Materials and Icons).

Figure: High CPU or a blocked main thread?

When the app hangs or hitches, the first question is what the main thread is doing during that time. The answer picks the instrument and the kind of fix:

flowchart TD S(["Hang or hitch in the trace"]) --> Q1{"Main thread CPU
during the hang?"} Q1 -- "high, near 100%" --> Q2{"Is the work needed
on the main actor?"} Q2 -- "no" --> OFF["Move it off the main actor:
@concurrent, a model actor,
Swift Executors instrument to confirm"] Q2 -- "yes, it is UI work" --> OPT["Do less: Time Profiler, Top Functions,
SwiftUI instrument for long body updates"] Q1 -- "low, mostly idle" --> Q3{"What is it waiting for?
System Trace"} Q3 -- "file I/O, a system call" --> IO["Do the I/O off the main actor"] Q3 -- "a lock or semaphore" --> LOCK["Remove the synchronous wait:
await instead of blocking"] Q3 -- "another process (IPC)" --> IPC["Call the async API,
or call it earlier"] Q3 -- "runnable but not running" --> PRIO["Too many threads competing:
check QoS and thread priority"]

The WWDC26 session Profile, fix, and verify walks through this flow with a note-taking app very close to FieldNotes: a CPU-bound hang fixed with Top Functions, a hang caused by thumbnail tasks on the main actor, and a hang caused by a synchronous file write.

The SwiftUI Instrument

The SwiftUI template records every update SwiftUI performs next to a Time Profiler track. Its lanes show View Body Updates, Platform View Updates and Other Updates per module, and long updates are colored so they stand out. Two kinds of problem show up:

  • Long updates: a single body that takes too long. Select it, set the inspection range, and use the Time Profiler track (or its flame graph) to see what the body runs.

  • Too many updates: each short, but so many that SwiftUI is busy for a long time. Choose Show Causes on an update to open the cause-and-effect graph: nodes are your views, observable properties, environment values and transactions (blue for your code, gray for the system), and edges say which change caused which update.

Xcode 27 adds a Summary of Updates focus action on the view hierarchy detail and records layout passes with the reason a layout wasn’t cached. A typical FieldNotes finding was a row that created a formatter in body and redrew whenever any note changed, because it read the whole store.notes array:

// Before: runs a DateFormatter per row per update, and depends on every note in the store.
struct PinnedBadgeRow: View {
    @Environment(NoteStore.self) private var store
    let note: Note
    var body: some View {
        let formatter = DateFormatter()
        formatter.dateStyle = .medium
        let pinnedCount = store.notes.filter(\.isPinned).count
        return Text("\(note.title), \(formatter.string(from: note.createdAt)) (\(pinnedCount) pinned)")
    }
}

// After: the row depends only on its own note; the count is read once by the list header.
struct PinnedBadgeRowFixed: View {
    let note: Note
    var body: some View {
        Text("\(note.title), \(note.createdAt, format: .dateTime.day().month().year())")
    }
}

#Preview {
    List { PinnedBadgeRowFixed(note: Note(title: "Beetle traps, site A")) }
}

With @Observable, a view depends exactly on the properties its body reads, so the cure for too many updates is usually to read less: pass a value instead of the store, move derived values into the model, and avoid storing closures that capture the parent view. The WWDC25 session Optimize SwiftUI performance with Instruments works through both problem kinds.

Swift Concurrency and the Swift Executors Instrument (27)

The Swift Concurrency template records tasks and actors: when each task is created, running, suspended or waiting, and how long work waits in an actor’s queue. Task names from Task(name:) and .task(name:) label the tracks (see Swift Concurrency in Apps); in Instruments 27, tasks are grouped into Swift Task Collection tracks by name or place of creation, and selecting an interval of an actor’s queue lists the tasks waiting in it. Recorded together with Time Profiler, tasks, actors and executors gain a Profile detail, a call tree of the samples taken while they were running. Instruments 27 also adds the Swift Executors instrument, with one track for the main actor, one for the cooperative thread pool and one for each custom SerialExecutor or TaskExecutor, so you see where each task’s work ran.

That is how FieldNotes found why scrolling a notebook of photo notes hitched. The previews were rendered in a view’s .task, which inherits the main actor, so every render competed with scrolling on the main actor track:

// FieldNotes/Views/NotePreviewImage.swift
import SwiftUI
import FieldNotesModel

nonisolated struct NotePreviewRenderer: Sendable {
    // Before: a synchronous function, called from a main-actor .task, so it ran on the main actor.
    // func render(_ note: Note, size: CGSize) -> CGImage? { ... }

    // After: @concurrent always runs on the global concurrent executor, and renders in parallel.
    @concurrent
    func render(_ note: Note, size: CGSize) async -> CGImage? {
        guard !Task.isCancelled else { return nil }
        return drawPreview(of: note, size: size)     // Core Graphics drawing, several ms per note
    }

    private func drawPreview(of note: Note, size: CGSize) -> CGImage? { /* ... */ nil }
}

struct NotePreviewImage: View {
    let note: Note
    @State private var image: CGImage?

    var body: some View {
        Group {
            if let image { Image(decorative: image, scale: 2) } else { Color.secondary.opacity(0.1) }
        }
        .frame(width: 60, height: 60)
        .task(id: note.id, name: "Render preview") {
            image = await NotePreviewRenderer().render(note, size: CGSize(width: 120, height: 120))
        }
    }
}

In the new trace the Render preview tasks move from the main actor track to the cooperative pool track of the Swift Executors instrument, and the hitches disappear. Secondary sources also describe Swift actor data inside Time Profiler and Allocations in Instruments 27; the Xcode 27 release notes confirm the Time Profiler side (the Profile detail above) but say nothing about Allocations, so this page doesn’t rely on it.

A Blocked Main Thread: System Trace

When a hang shows low CPU on the main thread, the thread isn’t slow, it is waiting. Time Profiler only samples running code, so it can’t see this. The System Trace template records thread states (running, runnable, blocked), system calls, virtual memory faults and scheduling. Instruments 27 draws system calls, VM faults and thread states in one plot, follows the chain of events that made a thread runnable, graphs thread priority over time, and shows each system call’s arguments in the inspector.

FieldNotes' Back Up Notebook command froze the UI for half a second. System Trace showed the main thread blocked inside a single write system call of a large file:

// FieldNotes/Export/NotebookBackup.swift
import Foundation
import FieldNotesModel

nonisolated struct NotebookBackup: Sendable {
    let destination: URL

    // Before: called from a Button action on the main actor.
    // func write(_ notes: [Note]) throws {
    //     try PropertyListEncoder().encode(notes.map(BackupRecord.init)).write(to: destination, options: .atomic)
    // }

    // After: encoding and the blocking write run off the main actor.
    @concurrent
    func write(_ notes: [Note]) async throws {
        let data = try PropertyListEncoder().encode(notes.map(BackupRecord.init))
        try Task.checkCancellation()
        try data.write(to: destination, options: .atomic)
    }
}

/// The on-disk shape of one note in a backup.
nonisolated struct BackupRecord: Codable, Sendable {
    let id: UUID, title: String, body: String, createdAt: Date, tags: [String]
    init(_ note: Note) {
        id = note.id; title = note.title; body = note.body; createdAt = note.createdAt; tags = note.tags
    }
}

// The command (main actor):
// Button("Back Up Notebook") {
//     Task(name: "Back up notebook") {
//         do { try await NotebookBackup(destination: url).write(notes) }
//         catch { backupError = error }          // shown in an alert, never dropped
//     }
// }
xcrun xctrace record --template 'System Trace' --device 'Field iPhone' \
  --time-limit 15s --output FieldNotes-backup.trace --attach FieldNotes

After the fix, the write call appears on a pool thread and the main thread stays runnable. The same template finds waits on locks, semaphores and synchronous IPC; the fix is always to stop waiting synchronously on the main actor, not to make the waited-for work faster.

Memory: Allocations and Leaks

The Allocations instrument records every heap and virtual memory allocation with its size, category and allocating backtrace. Its most useful feature for an app is generations: click Mark Generation before an interaction, perform it, return to the starting screen and mark again. Everything that survives in the new generation was allocated during the interaction and never freed. For FieldNotes, opening and closing the note composer should leave nothing behind; the first version of DraftAutosaver left one autosaver per cycle, the retain cycle traced on Debugging with Xcode and LLDB. The Leaks instrument checks the heap periodically for unreachable objects and shows each leaked cycle as a graph, which is why this generations-and-Leaks workflow has superseded hand-driven leak hunting with breakpoints and ad-hoc heap scripts in LLDB; malloc stack logging with leaks, heap and vmmap (see Debugging with Xcode and LLDB) remains the command-line complement to it.

# Record Allocations and Leaks together while opening and closing the composer
xcrun xctrace record --template 'Leaks' --device 'Field iPhone' \
  --time-limit 60s --output FieldNotes-composer.trace --launch -- com.example.fieldnotes

Leaks only finds memory nothing references. Memory that is still referenced but no longer needed (an ever-growing cache, the whole store’s photos decoded at full size) is abandoned memory, and only generations show it. Put regenerable data in URL.cachesDirectory or an NSCache, decode images at display size, and respond to memory warnings. Extensions, especially widgets, have much lower memory limits than the app; MetricKit’s new memory exception diagnostic reports when one was terminated for it.

Energy: Power Profiler

The Power Profiler instrument shows the system’s power use and, for the app, its power impact per subsystem: CPU, GPU, display and networking, plus the thermal state and charging status. It needs an iPhone or iPad running 26 or later. Because a cable charges the device (and Xcode keeps it awake), the most realistic traces come from the device itself: turn on Developer Mode, enable Settings > Developer > Performance Trace in Power Profiler mode for FieldNotes, start and stop the trace from Control Center, and open the file in Instruments later. From a paired device over Wi-Fi:

xcrun xctrace record --instrument 'Power Profiler' --device 'Field iPhone' \
  --time-limit 10m --output FieldNotes-fieldtrip.trace --attach FieldNotes

For FieldNotes the expensive scenario is a field trip: location updates, photos and background sync for hours. Compare traces of the same scenario before and after a change, on the same device model; power impact values aren’t comparable across models. The usual savings are fewer, batched network requests (see Networking: URLSession, Network Framework and ATS), coarser location accuracy when precision isn’t needed, and letting the system schedule deferrable work (Background Execution).

Launch Time

Launch time is measured from the tap on the icon to the first frame. A cold launch (after a reboot, or after the system evicted the app and its dependencies) is much slower than a warm one, so measure several kinds. The App Launch template records a time profile and thread states for the launch, split into phases (process initialization, system interface initialization, initial scene rendering, first frame):

xcrun xctrace record --template 'App Launch' --device 'Field iPhone' \
  --output FieldNotes-launch.trace --launch -- com.example.fieldnotes

Before the first frame, FieldNotes' AppServices opens the SwiftData store, and every scene injects the store. That is required; loading all notes is not. FieldNotes shows its window immediately and loads the notes in the first scene’s .task with loadNotesAtLaunch(_:), which the Points of Interest track shows as Load notes and MetricKit reports as an extended launch task (both defined on Logging, MetricKit, Crash Reports and Organizer):

// FieldNotes/ContentView.swift (excerpt, updated: load after the first frame)
NavigationSplitView { /* ... */ }
    .task(name: "Launch load") { await loadNotesAtLaunch(store) }

Other launch costs to look for: many dynamic frameworks (consider static linking or mergeable libraries, see Swift Packages and Modularizing an App), static initializers in Objective-C or C++ dependencies (the dyld Activity instrument measures them), and synchronous work in the app delegate. If launch takes too long, the system’s watchdog terminates the app, which shows up as a crash report with a watchdog termination reason.

App Size and Thinning

Download and install size matter on cellular connections and full devices. The App Store thins the app: each device downloads only its own architecture and asset variants. Build products (the .app, the .xcarchive, the uploaded .ipa) are not representative sizes. To measure, export the archive for development or ad hoc distribution with thinning for all variants; the export folder then contains App Thinning Size Report.txt with compressed (download) and uncompressed (install) sizes per variant:

<?xml version="1.0" encoding="UTF-8"?>
<!-- SizeReportExportOptions.plist: a development export used only for the size report -->
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>method</key>
    <string>development</string>
    <key>teamID</key>
    <string>ABCDE12345</string>
    <key>thinning</key>
    <string>&lt;thin-for-all-variants&gt;</string>
</dict>
</plist>
xcodebuild -exportArchive -archivePath build/FieldNotes.xcarchive \
  -exportPath build/SizeReport -exportOptionsPlist SizeReportExportOptions.plist
head -20 "build/SizeReport/App Thinning Size Report.txt"

The angle brackets of <thin-for-all-variants> must be escaped in the plist, as shown. The App Store export uses the canonical ExportOptions.plist from App Store Connect, TestFlight and App Review; App Store Connect then shows the real size of each variant. Typical savings are asset catalogs instead of loose images (so they can be thinned), removing unused resources, compressing large media, and moving rarely used content to Background Assets. The Organizer’s new Storage metrics in Xcode 27 track app size and Documents & Data across releases.

Specialized Instruments

A few templates belong to a single framework and are covered with it:

  • Foundation Models: prompts, tool calls, token counts and latency of on-device model requests; see Apple Intelligence and Machine Learning.

  • RealityKit Trace: frame time, entity counts and rendering cost of RealityKit scenes such as FieldNotes' notes map; profile on the device, since the Simulator doesn’t reflect GPU cost:

xcrun xctrace record --template 'RealityKit Trace' --device 'Field Vision Pro' \
  --time-limit 30s --output FieldNotes-map.trace --attach FieldNotes

Availability and Requirements

Platform availability

API or feature iOS iPadOS macOS watchOS visionOS

Time Profiler, call tree and flame graph

Yes

Yes

Yes

Yes

Yes

Top Functions and Run Comparisons (Instruments 27)

New in 27

New in 27

New in 27

New in 27

New in 27

Hangs instrument

Yes

Yes

Yes

Yes

Yes

Animation Hitches instrument

Yes

Yes

Yes

Not documented

New in 27 (device)

SwiftUI instrument

Yes

Yes

Yes

Yes

Yes

Swift Executors instrument (executor names)

New in 27

New in 27

New in 27

New in 27

New in 27

System Trace, Allocations, Leaks

Yes

Yes

Yes

Yes

Yes

Power Profiler

Yes (26 or later)

Yes (26 or later)

Not available

Not available

Not available

App Launch template

Yes

Yes

Yes

Not documented

Not documented

App Thinning Size Report

Yes

Yes

Not applicable

Yes

Yes

Requirements

  • Profiling on a physical device needs Developer Mode and a signing team; Instruments 27 supports devices with iOS 17, watchOS 10, tvOS 17 or later.

  • Power Profiler traces need an iPhone or iPad with 26 or later; on-device Performance Trace needs Developer Mode.

  • The Swift Executors instrument names executors only on 27 systems; older systems show "Unknown executor".

  • The Animation Hitches instrument on visionOS needs a device running visionOS 27.

Where to Go Next

Guard the gains with automated measurements: XCTClockMetric and XCTApplicationLaunchMetric performance tests on UI Tests, Test Plans, Performance Tests and Coverage, run on every change by the pipeline on CI/CD with Xcode Cloud and the Command Line. Watch the field results in the Organizer and MetricKit, set up on Logging, MetricKit, Crash Reports and Organizer.