From ddd607f5196911fbb6878ffdc85941079a04ebc5 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Wed, 4 Feb 2026 19:54:13 +0900 Subject: [PATCH] BridgeJS: Perf-tune `SwiftToSkeleton` by avoiding unnecessary Syntax node allocation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `trimmedDescription` allocates two new Syntax nodes just to get the trimmed description of a node. ```swift extension SyntaxProtocol { public var trimmedDescription: String { // TODO: We shouldn't need to create to copies just to get the trimmed // description. return self.trimmed.description } public var trimmed: Self { // TODO: Should only need one new node here return self.with(\.leadingTrivia, []).with(\.trailingTrivia, []) } } ``` ``` $ cat check.d.ts export function getCanvas(x: string): HTMLCanvasElement; $ node Plugins/BridgeJS/Sources/TS2Swift/JavaScript/bin/ts2swift.js check.d.ts -p tsconfig.json -o out.swift $ hyperfine "./Plugins/BridgeJS/.build/debug/BridgeJSToolInternal.before emit-skeleton out.swift" "./Plugins/BridgeJS/.build/debug/BridgeJSToolInternal.after emit-skeleton out.swift" Benchmark 1: ./Plugins/BridgeJS/.build/debug/BridgeJSToolInternal.before emit-skeleton out.swift Time (mean ± σ): 16.531 s ± 0.317 s [User: 16.355 s, System: 0.107 s] Range (min … max): 16.026 s … 17.252 s 10 runs Benchmark 2: ./Plugins/BridgeJS/.build/debug/BridgeJSToolInternal.after emit-skeleton out.swift Time (mean ± σ): 4.068 s ± 0.134 s [User: 3.976 s, System: 0.039 s] Range (min … max): 4.000 s … 4.445 s 10 runs Summary ./Plugins/BridgeJS/.build/debug/BridgeJSToolInternal.after emit-skeleton out.swift ran 4.06 ± 0.15 times faster than ./Plugins/BridgeJS/.build/debug/BridgeJSToolInternal.before emit-skeleton out.swift ``` --- .../BridgeJSCore/SwiftToSkeleton.swift | 36 ++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift b/Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift index 32b9f641..d32d85f5 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift @@ -394,22 +394,28 @@ private enum ExportSwiftConstants { static let supportedRawTypes = SwiftEnumRawType.allCases.map { $0.rawValue } } +extension AttributeSyntax { + /// The attribute name as text when it is a simple identifier (e.g. "JS", "JSFunction"). + /// Prefer this over `attributeName.trimmedDescription` for name checks to avoid unnecessary string work. + fileprivate var attributeNameText: String? { + attributeName.as(IdentifierTypeSyntax.self)?.name.text + } +} + extension AttributeListSyntax { func hasJSAttribute() -> Bool { firstJSAttribute != nil } var firstJSAttribute: AttributeSyntax? { - first(where: { - $0.as(AttributeSyntax.self)?.attributeName.trimmedDescription == "JS" - })?.as(AttributeSyntax.self) + first(where: { $0.as(AttributeSyntax.self)?.attributeNameText == "JS" })?.as(AttributeSyntax.self) } /// Returns true if any attribute has the given name (e.g. "JSClass"). func hasAttribute(name: String) -> Bool { contains { attribute in guard let syntax = attribute.as(AttributeSyntax.self) else { return false } - return syntax.attributeName.trimmedDescription == name + return syntax.attributeNameText == name } } } @@ -1916,9 +1922,7 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor { } static func firstJSFunctionAttribute(_ attributes: AttributeListSyntax?) -> AttributeSyntax? { - attributes?.first { attribute in - attribute.as(AttributeSyntax.self)?.attributeName.trimmedDescription == "JSFunction" - }?.as(AttributeSyntax.self) + firstAttribute(attributes, named: "JSFunction") } static func hasJSGetterAttribute(_ attributes: AttributeListSyntax?) -> Bool { @@ -1926,9 +1930,7 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor { } static func firstJSGetterAttribute(_ attributes: AttributeListSyntax?) -> AttributeSyntax? { - attributes?.first { attribute in - attribute.as(AttributeSyntax.self)?.attributeName.trimmedDescription == "JSGetter" - }?.as(AttributeSyntax.self) + firstAttribute(attributes, named: "JSGetter") } static func hasJSSetterAttribute(_ attributes: AttributeListSyntax?) -> Bool { @@ -1936,9 +1938,7 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor { } static func firstJSSetterAttribute(_ attributes: AttributeListSyntax?) -> AttributeSyntax? { - attributes?.first { attribute in - attribute.as(AttributeSyntax.self)?.attributeName.trimmedDescription == "JSSetter" - }?.as(AttributeSyntax.self) + firstAttribute(attributes, named: "JSSetter") } static func hasJSClassAttribute(_ attributes: AttributeListSyntax?) -> Bool { @@ -1946,16 +1946,18 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor { } static func firstJSClassAttribute(_ attributes: AttributeListSyntax?) -> AttributeSyntax? { - attributes?.first { attribute in - attribute.as(AttributeSyntax.self)?.attributeName.trimmedDescription == "JSClass" - }?.as(AttributeSyntax.self) + firstAttribute(attributes, named: "JSClass") + } + + static func firstAttribute(_ attributes: AttributeListSyntax?, named name: String) -> AttributeSyntax? { + attributes?.first { $0.as(AttributeSyntax.self)?.attributeNameText == name }?.as(AttributeSyntax.self) } static func hasAttribute(_ attributes: AttributeListSyntax?, name: String) -> Bool { guard let attributes else { return false } return attributes.contains { attribute in guard let syntax = attribute.as(AttributeSyntax.self) else { return false } - return syntax.attributeName.trimmedDescription == name + return syntax.attributeNameText == name } }