Skip to content

Commit 512ef2e

Browse files
authored
Merge pull request #85698 from carlpeto/eng/PR-136978398
Move JSON formatting code to Runtime Module
2 parents 4d4b82a + 1c3684f commit 512ef2e

File tree

12 files changed

+1354
-456
lines changed

12 files changed

+1354
-456
lines changed
Lines changed: 388 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,388 @@
1+
//===--- BacktraceJSONFormatter.swift -------------------------*- swift -*-===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2025 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+
// Provides functionality to format JSON backtraces.
14+
//
15+
//===----------------------------------------------------------------------===//
16+
17+
import Swift
18+
19+
@_spi(Formatting)
20+
public struct BacktraceJSONFormatterOptions: OptionSet {
21+
public let rawValue: Int
22+
23+
public init(rawValue: Int) {
24+
self.rawValue = rawValue
25+
}
26+
27+
public static let allRegisters = BacktraceJSONFormatterOptions(rawValue: 1<<0)
28+
public static let demangle = BacktraceJSONFormatterOptions(rawValue: 1<<1)
29+
public static let sanitize = BacktraceJSONFormatterOptions(rawValue: 1<<2)
30+
public static let mentionedImages = BacktraceJSONFormatterOptions(rawValue: 1<<3)
31+
public static let allThreads = BacktraceJSONFormatterOptions(rawValue: 1<<4)
32+
public static let images = BacktraceJSONFormatterOptions(rawValue: 1<<5)
33+
}
34+
35+
internal extension BacktraceJSONFormatterOptions {
36+
// this is not the form we want but will do for now
37+
// we are going to turn this into an OptionSet or similar
38+
var showAllRegisters: Bool { contains(.allRegisters) }
39+
var shouldDemangle: Bool { contains(.demangle) }
40+
var shouldSanitize: Bool { contains(.sanitize) }
41+
var showMentionedImages: Bool { contains(.mentionedImages) }
42+
var showAllThreads: Bool { contains(.allThreads) }
43+
var showImages: Bool { contains(.images) }
44+
}
45+
46+
@_spi(Formatting)
47+
public protocol BacktraceJSONWriter {
48+
func write(_ string: String, flush: Bool)
49+
func writeln(_ string: String, flush: Bool)
50+
}
51+
52+
@_spi(Formatting)
53+
public struct BacktraceJSONFormatter<
54+
Address: FixedWidthInteger,
55+
Writer: BacktraceJSONWriter> {
56+
57+
var writer: Writer
58+
var options: BacktraceJSONFormatterOptions
59+
60+
typealias Log = CrashLog<Address>
61+
62+
var crashLog: Log
63+
var imageMap: ImageMap?
64+
65+
var mentionedImages: Set<Int> = []
66+
67+
@_spi(Formatting)
68+
public init(
69+
crashLog: CrashLog<Address>,
70+
writer: Writer,
71+
options: BacktraceJSONFormatterOptions)
72+
{
73+
self.crashLog = crashLog
74+
self.writer = writer
75+
self.options = options
76+
self.imageMap = crashLog.imageMap()
77+
}
78+
}
79+
80+
internal extension BacktraceJSONFormatter {
81+
func write(_ string: String, flush: Bool) {
82+
writer.write(string, flush: flush)
83+
}
84+
func writeln(_ string: String, flush: Bool) {
85+
writer.writeln(string, flush: flush)
86+
}
87+
88+
func getDescription() -> String? {
89+
crashLog.description
90+
}
91+
92+
func getArchitecture() -> String? {
93+
crashLog.architecture
94+
}
95+
96+
func getPlatform() -> String? {
97+
crashLog.platform
98+
}
99+
100+
func getFaultAddress() -> String? {
101+
crashLog.faultAddress
102+
}
103+
}
104+
105+
@_spi(Formatting)
106+
public extension BacktraceJSONFormatter {
107+
mutating func writeCrashLog(now: String) {
108+
writePreamble(now: now)
109+
writeThreads()
110+
writeCapturedMemory()
111+
writeImages()
112+
writeFooter()
113+
}
114+
}
115+
116+
@_spi(Formatting)
117+
public extension BacktraceJSONFormatter {
118+
func writePreamble(now: String) {
119+
guard let description = getDescription(),
120+
let faultAddress = getFaultAddress(),
121+
let platform = getPlatform(),
122+
let architecture = getArchitecture() else { return }
123+
124+
write("""
125+
{ \
126+
"timestamp": "\(now)", \
127+
"kind": "crashReport", \
128+
"description": "\(escapeJSON(description))", \
129+
"faultAddress": "\(faultAddress)", \
130+
"platform": "\(escapeJSON(platform))", \
131+
"architecture": "\(escapeJSON(architecture))"
132+
""",
133+
flush: false)
134+
}
135+
136+
// this updates the mentionedImages and omittedImages properties
137+
// it's done here rather than in capture for efficiency, as at that
138+
// point we are not sure if we are limiting to mentioned images or not
139+
mutating func writeThreads() {
140+
141+
write(#", "threads": [ "#, flush: false)
142+
143+
let threads = crashLog.threads.filter { options.showAllThreads || $0.crashed }
144+
145+
var first = true
146+
for thread in threads {
147+
if first {
148+
first = false
149+
} else {
150+
write(", ", flush: false)
151+
}
152+
153+
writeThread(thread: thread)
154+
}
155+
156+
write(" ]", flush: false)
157+
158+
if !options.showAllThreads && crashLog.threads.count > 1 {
159+
write(", \"omittedThreads\": \(crashLog.threads.count - 1)", flush: false)
160+
}
161+
162+
// if omittedImages is nil, try to calculate it.
163+
if let images = crashLog.images,
164+
options.showMentionedImages,
165+
self.crashLog.omittedImages == nil {
166+
167+
self.crashLog.omittedImages = images.count - mentionedImages.count
168+
169+
}
170+
}
171+
172+
func writeCapturedMemory() {
173+
// suppress writing captured memory if we should sanitize
174+
if !options.shouldSanitize, let capturedMemory = crashLog.capturedMemory {
175+
write(#", "capturedMemory": {"#, flush: false)
176+
var first = true
177+
for (address, bytes) in capturedMemory.sorted(by: <) {
178+
if first {
179+
first = false
180+
} else {
181+
write(",", flush: false)
182+
}
183+
write(" \"\(address)\": \"\(bytes)\"", flush: false)
184+
}
185+
write(" }", flush: false)
186+
}
187+
}
188+
189+
// note: this updates the crash log with the ommitted image count
190+
func writeImages() {
191+
if options.showImages, let images = crashLog.images {
192+
var imagesToWrite = images
193+
194+
if options.showMentionedImages {
195+
let mentioned =
196+
images.enumerated()
197+
.filter { mentionedImages.contains($0.0) }
198+
.map { $0.1 }
199+
200+
imagesToWrite = mentioned
201+
202+
write(", \"omittedImages\": \(self.crashLog.omittedImages ?? 0)",
203+
flush: false)
204+
}
205+
206+
write(", \"images\": [ ", flush: false)
207+
var first = true
208+
for image in imagesToWrite {
209+
writeImage(image, first: first)
210+
if first {
211+
first = false
212+
}
213+
}
214+
write(" ] ", flush: false)
215+
}
216+
}
217+
218+
func writeFooter() {
219+
write(", \"backtraceTime\": \(crashLog.backtraceTime) ", flush: false)
220+
221+
write("}", flush: false)
222+
}
223+
}
224+
225+
internal extension BacktraceJSONFormatter {
226+
func writeThreadRegisters(thread: Log.Thread) {
227+
guard let registers = thread.registers else { return }
228+
229+
var first = true
230+
let registerOrder = HostContext.registerDumpOrder
231+
for registerName in registerOrder {
232+
if let value = registers[registerName] {
233+
if first {
234+
first = false
235+
} else {
236+
write(", ", flush: false)
237+
}
238+
239+
write("\"\(registerName)\": \"\(value)\"", flush: false)
240+
}
241+
}
242+
}
243+
244+
func imageByAddress(_ address: Address) -> Int? {
245+
guard let address = Backtrace.Address(address) else { return nil }
246+
return imageMap?.indexOfImage(at: address)
247+
}
248+
249+
// note: this updates the mentioned images
250+
mutating func writeThread(
251+
thread: Log.Thread) {
252+
253+
write("{ ", flush: false)
254+
255+
if let name = thread.name, !name.isEmpty {
256+
write("\"name\": \"\(escapeJSON(name))\", ", flush: false)
257+
}
258+
259+
let isCrashingThread = thread.crashed
260+
261+
if isCrashingThread {
262+
write(#""crashed": true, "#, flush: false)
263+
}
264+
265+
if options.showAllRegisters || isCrashingThread {
266+
write(#""registers": {"#, flush: false)
267+
writeThreadRegisters(thread: thread)
268+
write(" }, ", flush: false)
269+
}
270+
271+
write(#""frames": ["#, flush: false)
272+
var first = true
273+
for frame in thread.frames {
274+
if first {
275+
first = false
276+
} else {
277+
write(",", flush: false)
278+
}
279+
280+
write(" { ", flush: false)
281+
282+
write(frame.jsonBody, flush: false)
283+
284+
if frame.inlined {
285+
write(#", "inlined": true"#, flush: false)
286+
}
287+
if frame.isSwiftRuntimeFailure {
288+
write(#", "runtimeFailure": true"#, flush: false)
289+
}
290+
if frame.isSwiftThunk {
291+
write(#", "thunk": true"#, flush: false)
292+
}
293+
if frame.isSystem {
294+
write(#", "system": true"#, flush: false)
295+
}
296+
297+
if let symbol = frame.symbol {
298+
write("""
299+
, "symbol": "\(escapeJSON(symbol))"\
300+
, "offset": \(frame.offset ?? 0)
301+
""", flush: false)
302+
303+
if options.shouldDemangle, let demangledName = frame.demangledName {
304+
let formattedOffset: String
305+
if (frame.offset ?? 0) > 0 {
306+
formattedOffset = " + \(frame.offset!)"
307+
} else if (frame.offset ?? 0) < 0 {
308+
formattedOffset = " - \(frame.offset!)"
309+
} else {
310+
formattedOffset = ""
311+
}
312+
313+
write("""
314+
, "description": \"\(escapeJSON(demangledName))\(formattedOffset)\"
315+
""", flush: false)
316+
}
317+
318+
if let image = frame.image {
319+
write(", \"image\": \"\(image)\"", flush: false)
320+
}
321+
322+
if var sourceLocation = frame.sourceLocation {
323+
if options.shouldSanitize {
324+
sourceLocation.file = sanitizePath(sourceLocation.file)
325+
}
326+
write(#", "sourceLocation": { "#, flush: false)
327+
328+
write("""
329+
"file": "\(escapeJSON(sourceLocation.file))", \
330+
"line": \(sourceLocation.line), \
331+
"column": \(sourceLocation.column)
332+
""", flush: false)
333+
334+
write(" }", flush: false)
335+
}
336+
}
337+
write(" }", flush: false)
338+
}
339+
340+
write(" ] ", flush: false)
341+
342+
write("}", flush: false)
343+
344+
if options.showMentionedImages {
345+
for frame in thread.frames {
346+
if let imageName = frame.image,
347+
let imageIndex = crashLog.images?
348+
.firstIndex(where: { $0.name == imageName }) {
349+
mentionedImages.insert(imageIndex)
350+
} else if let addressString = frame.address,
351+
let address = Log.addressFromString(addressString),
352+
let imageIndex = imageByAddress(address) {
353+
mentionedImages.insert(imageIndex)
354+
}
355+
}
356+
}
357+
}
358+
359+
func writeImage(_ image: Log.Image, first: Bool) {
360+
if !first {
361+
write(", ", flush: false)
362+
}
363+
364+
write("{ ", flush: false)
365+
366+
if let name = image.name {
367+
write("\"name\": \"\(escapeJSON(name))\", ", flush: false)
368+
}
369+
370+
if let buildId = image.buildId {
371+
write("\"buildId\": \"\(buildId)\", ", flush: false)
372+
}
373+
374+
if var path = image.path {
375+
if options.shouldSanitize {
376+
path = sanitizePath(path)
377+
}
378+
write("\"path\": \"\(path)\", ", flush: false)
379+
}
380+
381+
write("""
382+
"baseAddress": "\(image.baseAddress)", \
383+
"endOfText": "\(image.endOfText)"
384+
""", flush: false)
385+
386+
write(" }", flush: false)
387+
}
388+
}

0 commit comments

Comments
 (0)