From ce727982411973996498cf62e47a707ea4bc8f70 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 9 Feb 2026 09:57:46 +0900 Subject: [PATCH] BridgeJS: Add profiling instrumentation --- .../Sources/BridgeJSCore/ClosureCodegen.swift | 6 +- .../Sources/BridgeJSCore/ExportSwift.swift | 64 +++++----- .../Sources/BridgeJSCore/ImportTS.swift | 30 +++-- .../BridgeJS/Sources/BridgeJSCore/Misc.swift | 115 ++++++++++++++++++ .../Sources/BridgeJSTool/BridgeJSTool.swift | 102 ++++++++++------ 5 files changed, 236 insertions(+), 81 deletions(-) diff --git a/Plugins/BridgeJS/Sources/BridgeJSCore/ClosureCodegen.swift b/Plugins/BridgeJS/Sources/BridgeJSCore/ClosureCodegen.swift index 12e4a413c..3ec0418b3 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSCore/ClosureCodegen.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSCore/ClosureCodegen.swift @@ -328,7 +328,9 @@ public struct ClosureCodegen { decls.append(try renderClosureInvokeHandler(signature)) } - let format = BasicFormat() - return decls.map { $0.formatted(using: format).description }.joined(separator: "\n\n") + return withSpan("Format Closure Glue") { + let format = BasicFormat() + return decls.map { $0.formatted(using: format).description }.joined(separator: "\n\n") + } } } diff --git a/Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift b/Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift index acff21847..70ed8f8a8 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift @@ -47,45 +47,53 @@ public class ExportSwift { func renderSwiftGlue() throws -> String? { var decls: [DeclSyntax] = [] - let protocolCodegen = ProtocolCodegen() - for proto in skeleton.protocols { - decls.append(contentsOf: try protocolCodegen.renderProtocolWrapper(proto, moduleName: moduleName)) + try withSpan("Render Protocols") { [self] in + let protocolCodegen = ProtocolCodegen() + for proto in skeleton.protocols { + decls.append(contentsOf: try protocolCodegen.renderProtocolWrapper(proto, moduleName: moduleName)) + } } - let enumCodegen = EnumCodegen() - for enumDef in skeleton.enums { - if let enumHelpers = enumCodegen.renderEnumHelpers(enumDef) { - decls.append(enumHelpers) - } + try withSpan("Render Enums") { [self] in + let enumCodegen = EnumCodegen() + for enumDef in skeleton.enums { + if let enumHelpers = enumCodegen.renderEnumHelpers(enumDef) { + decls.append(enumHelpers) + } - for staticMethod in enumDef.staticMethods { - decls.append(try renderSingleExportedFunction(function: staticMethod)) - } + for staticMethod in enumDef.staticMethods { + decls.append(try renderSingleExportedFunction(function: staticMethod)) + } - for staticProperty in enumDef.staticProperties { - decls.append( - contentsOf: try renderSingleExportedProperty( - property: staticProperty, - context: .enumStatic(enumDef: enumDef) + for staticProperty in enumDef.staticProperties { + decls.append( + contentsOf: try renderSingleExportedProperty( + property: staticProperty, + context: .enumStatic(enumDef: enumDef) + ) ) - ) + } } } - let structCodegen = StructCodegen() - for structDef in skeleton.structs { - decls.append(contentsOf: structCodegen.renderStructHelpers(structDef)) - decls.append(contentsOf: try renderSingleExportedStruct(struct: structDef)) - } + try withSpan("Render Structs") { [self] in + let structCodegen = StructCodegen() + for structDef in skeleton.structs { + decls.append(contentsOf: structCodegen.renderStructHelpers(structDef)) + decls.append(contentsOf: try renderSingleExportedStruct(struct: structDef)) + } - for function in skeleton.functions { - decls.append(try renderSingleExportedFunction(function: function)) + for function in skeleton.functions { + decls.append(try renderSingleExportedFunction(function: function)) + } + for klass in skeleton.classes { + decls.append(contentsOf: try renderSingleExportedClass(klass: klass)) + } } - for klass in skeleton.classes { - decls.append(contentsOf: try renderSingleExportedClass(klass: klass)) + return withSpan("Format Export Glue") { + let format = BasicFormat() + return decls.map { $0.formatted(using: format).description }.joined(separator: "\n\n") } - let format = BasicFormat() - return decls.map { $0.formatted(using: format).description }.joined(separator: "\n\n") } class ExportedThunkBuilder { diff --git a/Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift b/Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift index 0424d8ca0..0709afe72 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift @@ -32,17 +32,23 @@ public struct ImportTS { var decls: [DeclSyntax] = [] for skeleton in self.skeleton.children { - for getter in skeleton.globalGetters { - let getterDecls = try renderSwiftGlobalGetter(getter, topLevelDecls: &decls) - decls.append(contentsOf: getterDecls) + try withSpan("Render Global Getters") { + for getter in skeleton.globalGetters { + let getterDecls = try renderSwiftGlobalGetter(getter, topLevelDecls: &decls) + decls.append(contentsOf: getterDecls) + } } - for function in skeleton.functions { - let thunkDecls = try renderSwiftThunk(function, topLevelDecls: &decls) - decls.append(contentsOf: thunkDecls) + try withSpan("Render Functions") { + for function in skeleton.functions { + let thunkDecls = try renderSwiftThunk(function, topLevelDecls: &decls) + decls.append(contentsOf: thunkDecls) + } } - for type in skeleton.types { - let typeDecls = try renderSwiftType(type, topLevelDecls: &decls) - decls.append(contentsOf: typeDecls) + try withSpan("Render Types") { + for type in skeleton.types { + let typeDecls = try renderSwiftType(type, topLevelDecls: &decls) + decls.append(contentsOf: typeDecls) + } } } if decls.isEmpty { @@ -50,8 +56,10 @@ public struct ImportTS { return nil } - let format = BasicFormat() - return decls.map { $0.formatted(using: format).description }.joined(separator: "\n\n") + return withSpan("Format Import Glue") { + let format = BasicFormat() + return decls.map { $0.formatted(using: format).description }.joined(separator: "\n\n") + } } func renderSwiftGlobalGetter( diff --git a/Plugins/BridgeJS/Sources/BridgeJSCore/Misc.swift b/Plugins/BridgeJS/Sources/BridgeJSCore/Misc.swift index e11bd4058..70dae3a82 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSCore/Misc.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSCore/Misc.swift @@ -1,3 +1,12 @@ +import class Foundation.FileHandle +import class Foundation.ProcessInfo +import func Foundation.open +import func Foundation.strerror +import var Foundation.errno +import var Foundation.O_WRONLY +import var Foundation.O_CREAT +import var Foundation.O_TRUNC + // MARK: - ProgressReporting public struct ProgressReporting { @@ -20,6 +29,112 @@ public struct ProgressReporting { } } +// MARK: - Profiling + +/// A simple time-profiler to emit `chrome://tracing` format +public final class Profiling { + nonisolated(unsafe) static var current: Profiling? + + let startTime: ContinuousClock.Instant + let clock = ContinuousClock() + let output: @Sendable (String) -> Void + var firstEntry = true + + init(output: @Sendable @escaping (String) -> Void) { + self.startTime = ContinuousClock.now + self.output = output + } + + public static func with(body: @escaping () throws -> Void) rethrows -> Void { + guard let outputPath = ProcessInfo.processInfo.environment["BRIDGE_JS_PROFILING"] else { + return try body() + } + let fd = open(outputPath, O_WRONLY | O_CREAT | O_TRUNC, 0o644) + guard fd >= 0 else { + let error = String(cString: strerror(errno)) + fatalError("Failed to open profiling output file \(outputPath): \(error)") + } + let output = FileHandle(fileDescriptor: fd, closeOnDealloc: true) + let profiling = Profiling(output: { output.write($0.data(using: .utf8) ?? Data()) }) + defer { + profiling.output("]\n") + } + Profiling.current = profiling + defer { + Profiling.current = nil + } + return try body() + } + + private func formatTimestamp(instant: ContinuousClock.Instant) -> Int { + let duration = self.startTime.duration(to: instant) + let (seconds, attoseconds) = duration.components + // Convert to microseconds + return Int(seconds * 1_000_000 + attoseconds / 1_000_000_000_000) + } + + func begin(_ label: String, _ instant: ContinuousClock.Instant) { + let entry = #"{"ph":"B","pid":1,"name":\#(JSON.serialize(label)),"ts":\#(formatTimestamp(instant: instant))}"# + if firstEntry { + firstEntry = false + output("[\n\(entry)") + } else { + output(",\n\(entry)") + } + } + + func end(_ label: String, _ instant: ContinuousClock.Instant) { + let entry = #"{"ph":"E","pid":1,"name":\#(JSON.serialize(label)),"ts":\#(formatTimestamp(instant: instant))}"# + output(",\n\(entry)") + } +} + +/// Mark a span of code with a label and measure the duration. +public func withSpan(_ label: String, body: @escaping () throws -> T) rethrows -> T { + guard let profiling = Profiling.current else { + return try body() + } + profiling.begin(label, profiling.clock.now) + defer { + profiling.end(label, profiling.clock.now) + } + return try body() +} + +/// Foundation-less JSON serialization +private enum JSON { + static func serialize(_ value: String) -> String { + // https://www.ietf.org/rfc/rfc4627.txt + var output = "\"" + for scalar in value.unicodeScalars { + switch scalar { + case "\"": + output += "\\\"" + case "\\": + output += "\\\\" + case "\u{08}": + output += "\\b" + case "\u{0C}": + output += "\\f" + case "\n": + output += "\\n" + case "\r": + output += "\\r" + case "\t": + output += "\\t" + case "\u{20}"..."\u{21}", "\u{23}"..."\u{5B}", "\u{5D}"..."\u{10FFFF}": + output.unicodeScalars.append(scalar) + default: + var hex = String(scalar.value, radix: 16, uppercase: true) + hex = String(repeating: "0", count: 4 - hex.count) + hex + output += "\\u" + hex + } + } + output += "\"" + return output + } +} + // MARK: - DiagnosticError import SwiftSyntax diff --git a/Plugins/BridgeJS/Sources/BridgeJSTool/BridgeJSTool.swift b/Plugins/BridgeJS/Sources/BridgeJSTool/BridgeJSTool.swift index 92e74e621..f9fa56c42 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSTool/BridgeJSTool.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSTool/BridgeJSTool.swift @@ -50,7 +50,9 @@ import BridgeJSUtilities static func main() throws { do { - try run() + try Profiling.with { + try run() + } } catch { printStderr("error: \(error)") exit(1) @@ -121,18 +123,22 @@ import BridgeJSUtilities let bridgeJSMacrosPath = outputDirectory.appending(path: "BridgeJS.Macros.swift") let primaryDtsPath = hasDts ? bridgeJsDtsPath.path : bridgeJsGlobalDtsPath.path let globalDtsFiles = (hasDts && hasGlobalDts) ? [bridgeJsGlobalDtsPath.path] : [] - _ = try invokeTS2Swift( - dtsFile: primaryDtsPath, - globalDtsFiles: globalDtsFiles, - tsconfigPath: tsconfigPath, - nodePath: nodePath, - progress: progress, - outputPath: bridgeJSMacrosPath.path - ) + try withSpan("invokeTS2Swift") { + _ = try invokeTS2Swift( + dtsFile: primaryDtsPath, + globalDtsFiles: globalDtsFiles, + tsconfigPath: tsconfigPath, + nodePath: nodePath, + progress: progress, + outputPath: bridgeJSMacrosPath.path + ) + } generatedMacrosPath = bridgeJSMacrosPath.path } - var inputFiles = inputSwiftFiles(targetDirectory: targetDirectory, positionalArguments: positionalArguments) + var inputFiles = withSpan("Collecting Swift files") { + return inputSwiftFiles(targetDirectory: targetDirectory, positionalArguments: positionalArguments) + } // BridgeJS.Macros.swift contains imported declarations (@JSFunction, @JSClass, etc.) that need // to be processed by SwiftToSkeleton to populate the imported skeleton. The command plugin // filters out Generated/ files, so we explicitly add it here after generation. @@ -148,22 +154,28 @@ import BridgeJSUtilities exposeToGlobal: config.exposeToGlobal ) for inputFile in inputFiles.sorted() { - let inputURL = URL(fileURLWithPath: inputFile) - // Skip directories (e.g. .docc catalogs included in target.sourceFiles) - var isDirectory: ObjCBool = false - if FileManager.default.fileExists(atPath: inputFile, isDirectory: &isDirectory), isDirectory.boolValue { - continue + try withSpan("Parsing \(inputFile)") { + let inputURL = URL(fileURLWithPath: inputFile) + // Skip directories (e.g. .docc catalogs included in target.sourceFiles) + var isDirectory: ObjCBool = false + if FileManager.default.fileExists(atPath: inputFile, isDirectory: &isDirectory), + isDirectory.boolValue + { + return + } + let content = try String(contentsOf: inputURL, encoding: .utf8) + if hasBridgeJSSkipComment(content) { + return + } + + let sourceFile = Parser.parse(source: content) + swiftToSkeleton.addSourceFile(sourceFile, inputFilePath: inputFile) } - let content = try String(contentsOf: inputURL, encoding: .utf8) - if hasBridgeJSSkipComment(content) { - continue - } - - let sourceFile = Parser.parse(source: content) - swiftToSkeleton.addSourceFile(sourceFile, inputFilePath: inputFile) } - let skeleton = try swiftToSkeleton.finalize() + let skeleton = try withSpan("SwiftToSkeleton.finalize") { + return try swiftToSkeleton.finalize() + } var exporter: ExportSwift? if let skeleton = skeleton.exported { @@ -183,10 +195,16 @@ import BridgeJSUtilities } // Generate unified closure support for both import/export to avoid duplicate symbols when concatenating. - let closureSupport = try ClosureCodegen().renderSupport(for: skeleton) + let closureSupport = try withSpan("ClosureCodegen.renderSupport") { + return try ClosureCodegen().renderSupport(for: skeleton) + } - let importResult = try importer?.finalize() - let exportResult = try exporter?.finalize() + let importResult = try withSpan("ImportTS.finalize") { + return try importer?.finalize() + } + let exportResult = try withSpan("ExportSwift.finalize") { + return try exporter?.finalize() + } // Combine and write unified Swift output let outputSwiftURL = outputDirectory.appending(path: "BridgeJS.swift") @@ -194,26 +212,30 @@ import BridgeJSUtilities let outputSwift = combineGeneratedSwift(combinedSwift) let shouldWrite = doubleDashOptions["always-write"] == "true" || !outputSwift.isEmpty if shouldWrite { + try withSpan("Writing output Swift") { + try FileManager.default.createDirectory( + at: outputSwiftURL.deletingLastPathComponent(), + withIntermediateDirectories: true, + attributes: nil + ) + try outputSwift.write(to: outputSwiftURL, atomically: true, encoding: .utf8) + } + } + + // Write unified skeleton + let outputSkeletonURL = outputDirectory.appending(path: "JavaScript/BridgeJS.json") + try withSpan("Writing output skeleton") { try FileManager.default.createDirectory( - at: outputSwiftURL.deletingLastPathComponent(), + at: outputSkeletonURL.deletingLastPathComponent(), withIntermediateDirectories: true, attributes: nil ) - try outputSwift.write(to: outputSwiftURL, atomically: true, encoding: .utf8) + let encoder = JSONEncoder() + encoder.outputFormatting = [.prettyPrinted, .sortedKeys] + let skeletonData = try encoder.encode(skeleton) + try skeletonData.write(to: outputSkeletonURL) } - // Write unified skeleton - let outputSkeletonURL = outputDirectory.appending(path: "JavaScript/BridgeJS.json") - try FileManager.default.createDirectory( - at: outputSkeletonURL.deletingLastPathComponent(), - withIntermediateDirectories: true, - attributes: nil - ) - let encoder = JSONEncoder() - encoder.outputFormatting = [.prettyPrinted, .sortedKeys] - let skeletonData = try encoder.encode(skeleton) - try skeletonData.write(to: outputSkeletonURL) - if skeleton.exported != nil || skeleton.imported != nil { progress.print("Generated BridgeJS code") }