Skip to content

Commit dd19baa

Browse files
committed
Add subcommand to visualize the requests running concurrently using a trace file
Showing the list of active requests didn’t turn out to be very useful for debugging. What is more useful, however, is to visualize the requests as the get enqueued and handled in a trace chart. This gives a very intuitive view of which request is blocking another request from getting executed.
1 parent 4e53c01 commit dd19baa

File tree

4 files changed

+260
-112
lines changed

4 files changed

+260
-112
lines changed

Sources/Diagnose/ActiveRequestsCommand.swift

Lines changed: 0 additions & 109 deletions
This file was deleted.

Sources/Diagnose/CMakeLists.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
add_library(Diagnose STATIC
2-
ActiveRequestsCommand.swift
32
CommandLineArgumentsReducer.swift
43
DebugCommand.swift
54
DiagnoseCommand.swift
@@ -19,7 +18,8 @@ add_library(Diagnose STATIC
1918
SourceReducer.swift
2019
StderrStreamConcurrencySafe.swift
2120
SwiftFrontendCrashScraper.swift
22-
Toolchain+SwiftFrontend.swift)
21+
Toolchain+SwiftFrontend.swift
22+
TraceFromSignpostsCommand.swift)
2323

2424
set_target_properties(Diagnose PROPERTIES
2525
INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY})

Sources/Diagnose/DebugCommand.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,11 @@ package struct DebugCommand: ParsableCommand {
2121
commandName: "debug",
2222
abstract: "Commands to debug sourcekit-lsp. Intended for developers of sourcekit-lsp",
2323
subcommands: [
24-
ActiveRequestsCommand.self,
2524
IndexCommand.self,
2625
ReduceCommand.self,
2726
ReduceFrontendCommand.self,
2827
RunSourceKitdRequestCommand.self,
28+
TraceFromSignpostsCommand.self,
2929
]
3030
)
3131

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
#if compiler(>=6)
14+
package import ArgumentParser
15+
import Foundation
16+
import RegexBuilder
17+
18+
import class TSCBasic.Process
19+
#else
20+
import ArgumentParser
21+
import Foundation
22+
import RegexBuilder
23+
24+
import class TSCBasic.Process
25+
#endif
26+
27+
/// Shared instance of the regex that is used to extract Signpost lines from `log stream --signpost`.
28+
fileprivate struct LogParseRegex {
29+
@MainActor static let shared = LogParseRegex()
30+
31+
let dateComponent = Reference(Substring.self)
32+
let processIdComponent = Reference(Substring.self)
33+
let signpostIdComponent = Reference(Substring.self)
34+
let eventTypeComponent = Reference(Substring.self)
35+
let categoryComponent = Reference(Substring.self)
36+
let messageComponent = Reference(Substring.self)
37+
private(set) var regex:
38+
Regex<Regex<(Substring, Substring, Substring, Substring, Substring, Substring, Substring, Substring)>.RegexOutput>!
39+
40+
private init() {
41+
regex = Regex {
42+
Capture(as: dateComponent) {
43+
#/[-0-9]+ [0-9:.-]+/#
44+
}
45+
" "
46+
#/[0-9a-fx]+/# // Thread ID
47+
ZeroOrMore(.whitespace)
48+
"Signpost"
49+
ZeroOrMore(.whitespace)
50+
#/[0-9a-fx]+/# // Activity
51+
ZeroOrMore(.whitespace)
52+
Capture(as: processIdComponent) {
53+
ZeroOrMore(.digit)
54+
}
55+
ZeroOrMore(.whitespace)
56+
ZeroOrMore(.digit) // TTL
57+
ZeroOrMore(.whitespace)
58+
"[spid 0x"
59+
Capture(as: signpostIdComponent) {
60+
OneOrMore(.hexDigit)
61+
}
62+
", process, "
63+
ZeroOrMore(.whitespace)
64+
Capture(as: eventTypeComponent) {
65+
#/(begin|event|end)/#
66+
}
67+
"]"
68+
ZeroOrMore(.whitespace)
69+
ZeroOrMore(.whitespace.inverted) // Process name
70+
ZeroOrMore(.whitespace)
71+
"["
72+
ZeroOrMore(.any) // subsystem
73+
":"
74+
Capture(as: categoryComponent) {
75+
ZeroOrMore(.any)
76+
}
77+
"]"
78+
Capture(as: messageComponent) {
79+
ZeroOrMore(.any)
80+
}
81+
}
82+
}
83+
}
84+
85+
/// A signpost event extracted from a log.
86+
fileprivate struct Signpost {
87+
/// ID that identifies the signpost across the log.
88+
///
89+
/// There might be multiple signposts with the same `signpostId` across multiple processes.
90+
struct ID: Hashable {
91+
let processId: Int
92+
let signpostId: Int
93+
}
94+
95+
enum EventType: String {
96+
case begin
97+
case event
98+
case end
99+
}
100+
101+
let date: Date
102+
let processId: Int
103+
let signpostId: Int
104+
let eventType: EventType
105+
let category: String
106+
let message: String
107+
108+
var id: ID {
109+
ID(processId: processId, signpostId: signpostId)
110+
}
111+
112+
@MainActor
113+
init?(logLine line: Substring) {
114+
let regex = LogParseRegex.shared
115+
let dateFormatter = DateFormatter()
116+
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSSZ"
117+
guard let match = try? regex.regex.wholeMatch(in: line) else {
118+
return nil
119+
}
120+
guard let date = dateFormatter.date(from: String(match[regex.dateComponent])),
121+
let processId = Int(match[regex.processIdComponent]),
122+
let signpostId = Int(match[regex.signpostIdComponent], radix: 16),
123+
let eventType = Signpost.EventType(rawValue: String(match[regex.eventTypeComponent]))
124+
else {
125+
return nil
126+
}
127+
self.date = date
128+
self.processId = processId
129+
self.signpostId = signpostId
130+
self.eventType = eventType
131+
self.category = String(match[regex.categoryComponent])
132+
self.message = String(match[regex.messageComponent])
133+
}
134+
}
135+
136+
/// A trace event in the *Trace Event Format* that can be opened using Perfetto.
137+
/// https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/mobilebasic
138+
fileprivate struct TraceEvent: Codable {
139+
enum EventType: String, Codable {
140+
case begin = "B"
141+
case end = "E"
142+
}
143+
144+
/// The name of the event, as displayed in Trace Viewer
145+
let name: String?
146+
/// The event categories.
147+
///
148+
/// This is a comma separated list of categories for the event.
149+
/// The categories can be used to hide events in the Trace Viewer UI.
150+
let cat: String
151+
152+
/// The event type.
153+
///
154+
/// This is a single character which changes depending on the type of event being output.
155+
let ph: EventType
156+
157+
/// The process ID for the process that output this event.
158+
let pid: Int
159+
160+
/// The thread ID for the thread that output this event.
161+
///
162+
/// We use the signpost IDs as thread IDs to show each signpost on a single lane in the trace.
163+
let tid: Int
164+
165+
/// The tracing clock timestamp of the event. The timestamps are provided at microsecond granularity.
166+
let ts: Double
167+
168+
init(beginning signpost: Signpost) {
169+
self.name = signpost.message
170+
self.cat = signpost.category
171+
self.ph = .begin
172+
self.pid = signpost.processId
173+
self.tid = signpost.signpostId
174+
self.ts = signpost.date.timeIntervalSince1970 * 1_000_000
175+
}
176+
177+
init(ending signpost: Signpost) {
178+
self.name = nil
179+
self.cat = signpost.category
180+
self.ph = .end
181+
self.pid = signpost.processId
182+
self.tid = signpost.signpostId
183+
self.ts = signpost.date.timeIntervalSince1970 * 1_000_000
184+
}
185+
}
186+
187+
package struct TraceFromSignpostsCommand: AsyncParsableCommand {
188+
package static let configuration: CommandConfiguration = CommandConfiguration(
189+
commandName: "trace-from-signposts",
190+
abstract: "Generate a Trace Event Format file from signposts captured using OS Log",
191+
discussion: """
192+
Extracts signposts captured using 'log stream --signpost ..' and generates a trace file that can be opened using \
193+
Perfetto to visualize which requests were running concurrently.
194+
"""
195+
)
196+
197+
@Option(name: .customLong("log-file"), help: "The log file that was captured using 'log stream --signpost ...'")
198+
var logFile: String
199+
200+
@Option(help: "The trace output file to generate")
201+
var output: String
202+
203+
@Option(
204+
name: .customLong("category-filter"),
205+
help: "If specified, only include signposts from this logging category in the output file"
206+
)
207+
var categoryFilter: String?
208+
209+
package init() {}
210+
211+
private func traceEvents(from signpostsById: [Signpost.ID: [Signpost]]) -> [TraceEvent] {
212+
var traceEvents: [TraceEvent] = []
213+
for signposts in signpostsById.values {
214+
guard let begin = signposts.filter({ $0.eventType == .begin }).only else {
215+
continue
216+
}
217+
// Each begin event should to be paired with an end event.
218+
// If a begin event exists before the previous begin event is ended, a nested timeline is shown.
219+
// We display signpost events to last until the next signpost event.
220+
let events = signposts.filter { $0.eventType == .event }
221+
traceEvents.append(TraceEvent(beginning: begin))
222+
var hadPreviousEvent = false
223+
for event in events {
224+
if hadPreviousEvent {
225+
traceEvents.append(TraceEvent(ending: event))
226+
}
227+
hadPreviousEvent = true
228+
traceEvents.append(TraceEvent(beginning: event))
229+
}
230+
if let end = signposts.filter({ $0.eventType == .end }).only {
231+
if hadPreviousEvent {
232+
traceEvents.append(TraceEvent(ending: end))
233+
}
234+
traceEvents.append(TraceEvent(ending: end))
235+
}
236+
}
237+
return traceEvents
238+
}
239+
240+
@MainActor
241+
package func run() async throws {
242+
let log = try String(contentsOf: URL(fileURLWithPath: logFile), encoding: .utf8)
243+
244+
var signpostsById: [Signpost.ID: [Signpost]] = [:]
245+
for line in log.split(separator: "\n") {
246+
guard let signpost = Signpost(logLine: line) else {
247+
continue
248+
}
249+
if let categoryFilter, signpost.category != categoryFilter {
250+
continue
251+
}
252+
signpostsById[signpost.id, default: []].append(signpost)
253+
}
254+
let traceEvents = traceEvents(from: signpostsById)
255+
try JSONEncoder().encode(traceEvents).write(to: URL(fileURLWithPath: output))
256+
}
257+
}

0 commit comments

Comments
 (0)