← All repositories

listless

A simple list app for Apple platforms

git clone https://code.inqk.net/listless.git


ListlessiOS/Views/PerfDebugView.swift (7261B)

1import SwiftUI
2import UIKit
3
4struct PerfDebugView: View {
5    @State private var refreshTick = 0
6
7    var body: some View {
8        List {
9            Section("Summary (current launch)") {
10                let stats = perLabelStats(for: PerfSampler.shared.samplesForCurrentLaunch())
11                if stats.isEmpty {
12                    Text("No samples yet. Interact with the app and return here.")
13                        .foregroundStyle(.secondary)
14                } else {
15                    ForEach(stats, id: \.label) { stat in
16                        StatRow(stat: stat)
17                    }
18                }
19            }
20
21            ForEach(launchesMostRecentFirst()) { launch in
22                Section(sectionTitle(for: launch)) {
23                    let launchSamples = samples(for: launch.launchID)
24                    if launchSamples.isEmpty {
25                        Text("No samples recorded.")
26                            .foregroundStyle(.secondary)
27                    } else {
28                        ForEach(launchSamples) { sample in
29                            SampleRow(sample: sample)
30                        }
31                    }
32                }
33            }
34        }
35        .id(refreshTick)
36        .navigationTitle("Perf Samples")
37        .navigationBarTitleDisplayMode(.inline)
38        .toolbar {
39            ToolbarItem(placement: .topBarTrailing) {
40                Menu {
41                    Button("Refresh") { refreshTick &+= 1 }
42                    Button("Copy Summary") { copySummary() }
43                    Button("Copy All Samples") { copyAllSamples() }
44                    Button("Clear All", role: .destructive) {
45                        PerfSampler.shared.clear()
46                        refreshTick &+= 1
47                    }
48                } label: {
49                    Image(systemName: "ellipsis.circle")
50                }
51            }
52        }
53    }
54
55    private func launchesMostRecentFirst() -> [PerfSampler.Launch] {
56        PerfSampler.shared.allLaunches().sorted { $0.startedAt > $1.startedAt }
57    }
58
59    private func samples(for launchID: UUID) -> [PerfSampler.Sample] {
60        PerfSampler.shared.allSamples()
61            .filter { $0.launchID == launchID }
62            .sorted { $0.msSinceLaunch < $1.msSinceLaunch }
63    }
64
65    private func sectionTitle(for launch: PerfSampler.Launch) -> String {
66        let formatter = DateFormatter()
67        formatter.dateFormat = "MMM d, HH:mm:ss"
68        let isCurrent = launch.launchID == PerfSampler.shared.currentLaunch.launchID
69        return formatter.string(from: launch.startedAt) + (isCurrent ? " (current)" : "")
70    }
71
72    private func perLabelStats(for samples: [PerfSampler.Sample]) -> [LabelStat] {
73        let grouped = Dictionary(grouping: samples, by: \.label)
74        return grouped.map { label, entries in
75            let sorted = entries.sorted(by: { $0.callIndex < $1.callIndex })
76            let durations = sorted.map(\.durationMs)
77            return LabelStat(
78                label: label,
79                count: sorted.count,
80                firstCallMs: durations.first ?? 0,
81                medianMs: median(durations),
82                maxMs: durations.max() ?? 0,
83                firstCallMsSinceLaunch: sorted.first?.msSinceLaunch ?? 0
84            )
85        }
86        .sorted { $0.firstCallMs > $1.firstCallMs }
87    }
88
89    private func median(_ values: [Double]) -> Double {
90        guard !values.isEmpty else { return 0 }
91        let sorted = values.sorted()
92        let mid = sorted.count / 2
93        if sorted.count % 2 == 0 {
94            return (sorted[mid - 1] + sorted[mid]) / 2
95        }
96        return sorted[mid]
97    }
98
99    private func copySummary() {
100        var lines: [String] = []
101        for launch in launchesMostRecentFirst() {
102            lines.append("=== Launch \(launch.startedAt) ===")
103            let stats = perLabelStats(for: samples(for: launch.launchID))
104            for stat in stats {
105                lines.append(String(
106                    format: "%@  n=%d  first=%.2fms  median=%.2fms  max=%.2fms  firstAt=%.0fms",
107                    stat.label,
108                    stat.count,
109                    stat.firstCallMs,
110                    stat.medianMs,
111                    stat.maxMs,
112                    stat.firstCallMsSinceLaunch
113                ))
114            }
115            lines.append("")
116        }
117        UIPasteboard.general.string = lines.joined(separator: "\n")
118    }
119
120    private func copyAllSamples() {
121        var lines = ["launchStart,msSinceLaunch,callIndex,durationMs,label"]
122        for launch in launchesMostRecentFirst() {
123            let iso = ISO8601DateFormatter().string(from: launch.startedAt)
124            for sample in samples(for: launch.launchID) {
125                lines.append(String(
126                    format: "%@,%.3f,%d,%.3f,%@",
127                    iso,
128                    sample.msSinceLaunch,
129                    sample.callIndex,
130                    sample.durationMs,
131                    sample.label
132                ))
133            }
134        }
135        UIPasteboard.general.string = lines.joined(separator: "\n")
136    }
137}
138
139private struct LabelStat {
140    let label: String
141    let count: Int
142    let firstCallMs: Double
143    let medianMs: Double
144    let maxMs: Double
145    let firstCallMsSinceLaunch: Double
146}
147
148private struct StatRow: View {
149    let stat: LabelStat
150
151    var body: some View {
152        VStack(alignment: .leading, spacing: 2) {
153            Text(stat.label)
154                .font(.callout.monospaced())
155            HStack(spacing: 12) {
156                Text("n=\(stat.count)")
157                Text(String(format: "1st=%.1fms", stat.firstCallMs))
158                    .foregroundStyle(stat.firstCallMs > 16 ? .red : .primary)
159                Text(String(format: "med=%.1f", stat.medianMs))
160                Text(String(format: "max=%.1f", stat.maxMs))
161            }
162            .font(.caption.monospaced())
163            .foregroundStyle(.secondary)
164            Text(String(format: "first call at %.0f ms since launch", stat.firstCallMsSinceLaunch))
165                .font(.caption2.monospaced())
166                .foregroundStyle(.tertiary)
167        }
168        .padding(.vertical, 2)
169    }
170}
171
172private struct SampleRow: View {
173    let sample: PerfSampler.Sample
174
175    var body: some View {
176        HStack(spacing: 8) {
177            Text(String(format: "%5.0fms", sample.msSinceLaunch))
178                .font(.caption.monospaced())
179                .foregroundStyle(.secondary)
180            Text("#\(sample.callIndex)")
181                .font(.caption.monospaced())
182                .foregroundStyle(.tertiary)
183                .frame(width: 32, alignment: .leading)
184            Text(shortLabel(sample.label))
185                .font(.caption.monospaced())
186                .lineLimit(1)
187                .truncationMode(.middle)
188            Spacer(minLength: 4)
189            Text(String(format: "%.2f ms", sample.durationMs))
190                .font(.caption.monospaced())
191                .foregroundStyle(sample.durationMs > 16 ? .red : .primary)
192        }
193    }
194
195    private func shortLabel(_ label: String) -> String {
196        if let range = label.range(of: ".") {
197            return String(label[range.upperBound...])
198        }
199        return label
200    }
201}