Skip to content

Commit 770ff07

Browse files
authored
Optimizations to Backtrace.current(). (#647)
This PR optimizes `Backtrace.current()`: - There are now fewer library symbols are captured in a backtrace. This particularly affects debug builds where nested scoped accesses aren't optimized away. - I've eliminated almost all copies of the backtrace's underlying data. On 64-bit targets, the only copy required is to final `Array` storage and is handled by the standard library. - I've eliminated runtime bounds checks on backtrace counts that need to be cast from one integer type to another. - I've renamed the internal symbol `_startCachingForThrownErrors` to `__SWIFT_TESTING_IS_CAPTURING_A_BACKTRACE_FOR_A_THROWN_ERROR__`. This symbol unavoidably shows up in backtraces captured when `swift_willThrow` is called, so I gave it a name that (hopefully) clearly explains why it's there. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated.
1 parent 8e121e4 commit 770ff07

File tree

1 file changed

+39
-28
lines changed

1 file changed

+39
-28
lines changed

Sources/Testing/SourceAttribution/Backtrace.swift

Lines changed: 39 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,7 @@ public struct Backtrace: Sendable {
3737
/// The pointers in `addresses` are converted to instances of ``Address``. Any
3838
/// `nil` addresses are represented as `0`.
3939
public init(addresses: some Sequence<UnsafeRawPointer?>) {
40-
self.init(
41-
addresses: addresses.lazy
42-
.map(UInt.init(bitPattern:))
43-
.map(Address.init)
44-
)
40+
self.addresses = addresses.map { Address(UInt(bitPattern: $0)) }
4541
}
4642

4743
/// Get the current backtrace.
@@ -61,33 +57,42 @@ public struct Backtrace: Sendable {
6157
public static func current(maximumAddressCount addressCount: Int = 128) -> Self {
6258
// NOTE: the exact argument/return types for backtrace() vary across
6359
// platforms, hence the use of .init() when calling it below.
64-
let addresses = [UnsafeRawPointer?](unsafeUninitializedCapacity: addressCount) { addresses, initializedCount in
65-
addresses.withMemoryRebound(to: UnsafeMutableRawPointer?.self) { addresses in
60+
withUnsafeTemporaryAllocation(of: UnsafeMutableRawPointer?.self, capacity: addressCount) { addresses in
61+
var initializedCount = 0
6662
#if SWT_TARGET_OS_APPLE
67-
if #available(_backtraceAsyncAPI, *) {
68-
initializedCount = backtrace_async(addresses.baseAddress!, addresses.count, nil)
69-
} else {
70-
initializedCount = .init(backtrace(addresses.baseAddress!, .init(addresses.count)))
71-
}
72-
#elseif os(Linux)
73-
initializedCount = .init(backtrace(addresses.baseAddress!, .init(addresses.count)))
63+
if #available(_backtraceAsyncAPI, *) {
64+
initializedCount = backtrace_async(addresses.baseAddress!, addresses.count, nil)
65+
} else {
66+
initializedCount = .init(clamping: backtrace(addresses.baseAddress!, .init(clamping: addresses.count)))
67+
}
7468
#elseif os(Android)
75-
addresses.withMemoryRebound(to: UnsafeMutableRawPointer.self) { addresses in
76-
initializedCount = .init(backtrace(addresses.baseAddress!, .init(addresses.count)))
77-
}
69+
initializedCount = addresses.withMemoryRebound(to: UnsafeMutableRawPointer.self) { addresses in
70+
.init(clamping: backtrace(addresses.baseAddress!, .init(clamping: addresses.count)))
71+
}
72+
#elseif os(Linux)
73+
initializedCount = .init(clamping: backtrace(addresses.baseAddress!, .init(clamping: addresses.count)))
7874
#elseif os(Windows)
79-
initializedCount = Int(RtlCaptureStackBackTrace(0, ULONG(addresses.count), addresses.baseAddress!, nil))
75+
initializedCount = Int(clamping: RtlCaptureStackBackTrace(0, ULONG(clamping: addresses.count), addresses.baseAddress!, nil))
8076
#elseif os(WASI)
81-
// SEE: https://github.com/WebAssembly/WASI/issues/159
82-
// SEE: https://github.com/swiftlang/swift/pull/31693
83-
initializedCount = 0
77+
// SEE: https://github.com/WebAssembly/WASI/issues/159
78+
// SEE: https://github.com/swiftlang/swift/pull/31693
8479
#else
8580
#warning("Platform-specific implementation missing: backtraces unavailable")
86-
initializedCount = 0
8781
#endif
82+
83+
let endIndex = addresses.index(addresses.startIndex, offsetBy: initializedCount)
84+
#if _pointerBitWidth(_64)
85+
// The width of a pointer equals the width of an `Address`, so we can just
86+
// bitcast the memory rather than mapping through UInt first.
87+
return addresses[..<endIndex].withMemoryRebound(to: Address.self) { addresses in
88+
Self(addresses: addresses)
8889
}
90+
#else
91+
return addresses[..<endIndex].withMemoryRebound(to: UnsafeRawPointer?.self) { addresses in
92+
Self(addresses: addresses)
93+
}
94+
#endif
8995
}
90-
return Self(addresses: addresses)
9196
}
9297
}
9398

@@ -164,12 +169,12 @@ extension Backtrace {
164169
/// - errorAddress: The error that is about to be thrown. This pointer
165170
/// refers to an instance of `SwiftError` or (on platforms with
166171
/// Objective-C interop) an instance of `NSError`.
167-
@Sendable private static func _willThrow(_ errorAddress: UnsafeMutableRawPointer) {
172+
/// - backtrace: The backtrace from where the error was thrown.
173+
private static func _willThrow(_ errorAddress: UnsafeMutableRawPointer, from backtrace: Backtrace) {
168174
_oldWillThrowHandler.rawValue?(errorAddress)
169175

170176
let errorObject = unsafeBitCast(errorAddress, to: (any AnyObject & Sendable).self)
171177
let errorID = ObjectIdentifier(errorObject)
172-
let backtrace = Backtrace.current()
173178
let newEntry = _ErrorMappingCacheEntry(errorObject: errorObject, backtrace: backtrace)
174179

175180
_errorMappingCache.withLock { cache in
@@ -183,9 +188,15 @@ extension Backtrace {
183188

184189
/// The implementation of ``Backtrace/startCachingForThrownErrors()``, run
185190
/// only once.
186-
private static let _startCachingForThrownErrors: Void = {
191+
///
192+
/// This value is named oddly so that it shows up clearly in symbolicated
193+
/// backtraces.
194+
private static let __SWIFT_TESTING_IS_CAPTURING_A_BACKTRACE_FOR_A_THROWN_ERROR__: Void = {
187195
_oldWillThrowHandler.withLock { oldWillThrowHandler in
188-
oldWillThrowHandler = swt_setWillThrowHandler { _willThrow($0) }
196+
oldWillThrowHandler = swt_setWillThrowHandler { errorAddress in
197+
let backtrace = Backtrace.current()
198+
_willThrow(errorAddress, from: backtrace)
199+
}
189200
}
190201
}()
191202

@@ -196,7 +207,7 @@ extension Backtrace {
196207
/// developer-supplied code to ensure that thrown errors' backtraces are
197208
/// always captured.
198209
static func startCachingForThrownErrors() {
199-
_startCachingForThrownErrors
210+
__SWIFT_TESTING_IS_CAPTURING_A_BACKTRACE_FOR_A_THROWN_ERROR__
200211
}
201212

202213
/// Flush stale entries from the error-mapping cache.

0 commit comments

Comments
 (0)