Skip to content

Commit ce72798

Browse files
BridgeJS: Add profiling instrumentation
1 parent 180c010 commit ce72798

File tree

5 files changed

+236
-81
lines changed

5 files changed

+236
-81
lines changed

Plugins/BridgeJS/Sources/BridgeJSCore/ClosureCodegen.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,9 @@ public struct ClosureCodegen {
328328
decls.append(try renderClosureInvokeHandler(signature))
329329
}
330330

331-
let format = BasicFormat()
332-
return decls.map { $0.formatted(using: format).description }.joined(separator: "\n\n")
331+
return withSpan("Format Closure Glue") {
332+
let format = BasicFormat()
333+
return decls.map { $0.formatted(using: format).description }.joined(separator: "\n\n")
334+
}
333335
}
334336
}

Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift

Lines changed: 36 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -47,45 +47,53 @@ public class ExportSwift {
4747
func renderSwiftGlue() throws -> String? {
4848
var decls: [DeclSyntax] = []
4949

50-
let protocolCodegen = ProtocolCodegen()
51-
for proto in skeleton.protocols {
52-
decls.append(contentsOf: try protocolCodegen.renderProtocolWrapper(proto, moduleName: moduleName))
50+
try withSpan("Render Protocols") { [self] in
51+
let protocolCodegen = ProtocolCodegen()
52+
for proto in skeleton.protocols {
53+
decls.append(contentsOf: try protocolCodegen.renderProtocolWrapper(proto, moduleName: moduleName))
54+
}
5355
}
5456

55-
let enumCodegen = EnumCodegen()
56-
for enumDef in skeleton.enums {
57-
if let enumHelpers = enumCodegen.renderEnumHelpers(enumDef) {
58-
decls.append(enumHelpers)
59-
}
57+
try withSpan("Render Enums") { [self] in
58+
let enumCodegen = EnumCodegen()
59+
for enumDef in skeleton.enums {
60+
if let enumHelpers = enumCodegen.renderEnumHelpers(enumDef) {
61+
decls.append(enumHelpers)
62+
}
6063

61-
for staticMethod in enumDef.staticMethods {
62-
decls.append(try renderSingleExportedFunction(function: staticMethod))
63-
}
64+
for staticMethod in enumDef.staticMethods {
65+
decls.append(try renderSingleExportedFunction(function: staticMethod))
66+
}
6467

65-
for staticProperty in enumDef.staticProperties {
66-
decls.append(
67-
contentsOf: try renderSingleExportedProperty(
68-
property: staticProperty,
69-
context: .enumStatic(enumDef: enumDef)
68+
for staticProperty in enumDef.staticProperties {
69+
decls.append(
70+
contentsOf: try renderSingleExportedProperty(
71+
property: staticProperty,
72+
context: .enumStatic(enumDef: enumDef)
73+
)
7074
)
71-
)
75+
}
7276
}
7377
}
7478

75-
let structCodegen = StructCodegen()
76-
for structDef in skeleton.structs {
77-
decls.append(contentsOf: structCodegen.renderStructHelpers(structDef))
78-
decls.append(contentsOf: try renderSingleExportedStruct(struct: structDef))
79-
}
79+
try withSpan("Render Structs") { [self] in
80+
let structCodegen = StructCodegen()
81+
for structDef in skeleton.structs {
82+
decls.append(contentsOf: structCodegen.renderStructHelpers(structDef))
83+
decls.append(contentsOf: try renderSingleExportedStruct(struct: structDef))
84+
}
8085

81-
for function in skeleton.functions {
82-
decls.append(try renderSingleExportedFunction(function: function))
86+
for function in skeleton.functions {
87+
decls.append(try renderSingleExportedFunction(function: function))
88+
}
89+
for klass in skeleton.classes {
90+
decls.append(contentsOf: try renderSingleExportedClass(klass: klass))
91+
}
8392
}
84-
for klass in skeleton.classes {
85-
decls.append(contentsOf: try renderSingleExportedClass(klass: klass))
93+
return withSpan("Format Export Glue") {
94+
let format = BasicFormat()
95+
return decls.map { $0.formatted(using: format).description }.joined(separator: "\n\n")
8696
}
87-
let format = BasicFormat()
88-
return decls.map { $0.formatted(using: format).description }.joined(separator: "\n\n")
8997
}
9098

9199
class ExportedThunkBuilder {

Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,26 +32,34 @@ public struct ImportTS {
3232
var decls: [DeclSyntax] = []
3333

3434
for skeleton in self.skeleton.children {
35-
for getter in skeleton.globalGetters {
36-
let getterDecls = try renderSwiftGlobalGetter(getter, topLevelDecls: &decls)
37-
decls.append(contentsOf: getterDecls)
35+
try withSpan("Render Global Getters") {
36+
for getter in skeleton.globalGetters {
37+
let getterDecls = try renderSwiftGlobalGetter(getter, topLevelDecls: &decls)
38+
decls.append(contentsOf: getterDecls)
39+
}
3840
}
39-
for function in skeleton.functions {
40-
let thunkDecls = try renderSwiftThunk(function, topLevelDecls: &decls)
41-
decls.append(contentsOf: thunkDecls)
41+
try withSpan("Render Functions") {
42+
for function in skeleton.functions {
43+
let thunkDecls = try renderSwiftThunk(function, topLevelDecls: &decls)
44+
decls.append(contentsOf: thunkDecls)
45+
}
4246
}
43-
for type in skeleton.types {
44-
let typeDecls = try renderSwiftType(type, topLevelDecls: &decls)
45-
decls.append(contentsOf: typeDecls)
47+
try withSpan("Render Types") {
48+
for type in skeleton.types {
49+
let typeDecls = try renderSwiftType(type, topLevelDecls: &decls)
50+
decls.append(contentsOf: typeDecls)
51+
}
4652
}
4753
}
4854
if decls.isEmpty {
4955
// No declarations to import
5056
return nil
5157
}
5258

53-
let format = BasicFormat()
54-
return decls.map { $0.formatted(using: format).description }.joined(separator: "\n\n")
59+
return withSpan("Format Import Glue") {
60+
let format = BasicFormat()
61+
return decls.map { $0.formatted(using: format).description }.joined(separator: "\n\n")
62+
}
5563
}
5664

5765
func renderSwiftGlobalGetter(

Plugins/BridgeJS/Sources/BridgeJSCore/Misc.swift

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
import class Foundation.FileHandle
2+
import class Foundation.ProcessInfo
3+
import func Foundation.open
4+
import func Foundation.strerror
5+
import var Foundation.errno
6+
import var Foundation.O_WRONLY
7+
import var Foundation.O_CREAT
8+
import var Foundation.O_TRUNC
9+
110
// MARK: - ProgressReporting
211

312
public struct ProgressReporting {
@@ -20,6 +29,112 @@ public struct ProgressReporting {
2029
}
2130
}
2231

32+
// MARK: - Profiling
33+
34+
/// A simple time-profiler to emit `chrome://tracing` format
35+
public final class Profiling {
36+
nonisolated(unsafe) static var current: Profiling?
37+
38+
let startTime: ContinuousClock.Instant
39+
let clock = ContinuousClock()
40+
let output: @Sendable (String) -> Void
41+
var firstEntry = true
42+
43+
init(output: @Sendable @escaping (String) -> Void) {
44+
self.startTime = ContinuousClock.now
45+
self.output = output
46+
}
47+
48+
public static func with(body: @escaping () throws -> Void) rethrows -> Void {
49+
guard let outputPath = ProcessInfo.processInfo.environment["BRIDGE_JS_PROFILING"] else {
50+
return try body()
51+
}
52+
let fd = open(outputPath, O_WRONLY | O_CREAT | O_TRUNC, 0o644)
53+
guard fd >= 0 else {
54+
let error = String(cString: strerror(errno))
55+
fatalError("Failed to open profiling output file \(outputPath): \(error)")
56+
}
57+
let output = FileHandle(fileDescriptor: fd, closeOnDealloc: true)
58+
let profiling = Profiling(output: { output.write($0.data(using: .utf8) ?? Data()) })
59+
defer {
60+
profiling.output("]\n")
61+
}
62+
Profiling.current = profiling
63+
defer {
64+
Profiling.current = nil
65+
}
66+
return try body()
67+
}
68+
69+
private func formatTimestamp(instant: ContinuousClock.Instant) -> Int {
70+
let duration = self.startTime.duration(to: instant)
71+
let (seconds, attoseconds) = duration.components
72+
// Convert to microseconds
73+
return Int(seconds * 1_000_000 + attoseconds / 1_000_000_000_000)
74+
}
75+
76+
func begin(_ label: String, _ instant: ContinuousClock.Instant) {
77+
let entry = #"{"ph":"B","pid":1,"name":\#(JSON.serialize(label)),"ts":\#(formatTimestamp(instant: instant))}"#
78+
if firstEntry {
79+
firstEntry = false
80+
output("[\n\(entry)")
81+
} else {
82+
output(",\n\(entry)")
83+
}
84+
}
85+
86+
func end(_ label: String, _ instant: ContinuousClock.Instant) {
87+
let entry = #"{"ph":"E","pid":1,"name":\#(JSON.serialize(label)),"ts":\#(formatTimestamp(instant: instant))}"#
88+
output(",\n\(entry)")
89+
}
90+
}
91+
92+
/// Mark a span of code with a label and measure the duration.
93+
public func withSpan<T>(_ label: String, body: @escaping () throws -> T) rethrows -> T {
94+
guard let profiling = Profiling.current else {
95+
return try body()
96+
}
97+
profiling.begin(label, profiling.clock.now)
98+
defer {
99+
profiling.end(label, profiling.clock.now)
100+
}
101+
return try body()
102+
}
103+
104+
/// Foundation-less JSON serialization
105+
private enum JSON {
106+
static func serialize(_ value: String) -> String {
107+
// https://www.ietf.org/rfc/rfc4627.txt
108+
var output = "\""
109+
for scalar in value.unicodeScalars {
110+
switch scalar {
111+
case "\"":
112+
output += "\\\""
113+
case "\\":
114+
output += "\\\\"
115+
case "\u{08}":
116+
output += "\\b"
117+
case "\u{0C}":
118+
output += "\\f"
119+
case "\n":
120+
output += "\\n"
121+
case "\r":
122+
output += "\\r"
123+
case "\t":
124+
output += "\\t"
125+
case "\u{20}"..."\u{21}", "\u{23}"..."\u{5B}", "\u{5D}"..."\u{10FFFF}":
126+
output.unicodeScalars.append(scalar)
127+
default:
128+
var hex = String(scalar.value, radix: 16, uppercase: true)
129+
hex = String(repeating: "0", count: 4 - hex.count) + hex
130+
output += "\\u" + hex
131+
}
132+
}
133+
output += "\""
134+
return output
135+
}
136+
}
137+
23138
// MARK: - DiagnosticError
24139

25140
import SwiftSyntax

0 commit comments

Comments
 (0)