Skip to content

Commit f6eac86

Browse files
authored
Merge pull request #1724 from ahoppen/tracing-file
Add subcommand to visualize the requests running concurrently using a trace file
2 parents 3c7aa6f + dd19baa commit f6eac86

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)