Skip to content

Commit 5887703

Browse files
authored
[6.0] Reduce overhead of .expectationChecked event handling in #expect() (take 2) (#659)
**Explanation:** Optimizes the implementation of `#expect()`, in particular the parts that ask for fully-qualified type names and generate `.expectationChecked` events. **Scope:** 6.0 branch **Issue:** N/A **Original PR:** #610, also includes fixup commit 6ba948a **Risk:** Moderate—refactors code inside `#expect()` and introduces a new lock and atomic value used by them. **Testing:** New unit test coverage, existing coverage. **Reviewer:** @briancroom @suzannaratcliff
1 parent 185d0db commit 5887703

File tree

6 files changed

+65
-9
lines changed

6 files changed

+65
-9
lines changed

Package.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ extension Array where Element == PackageDescription.SwiftSetting {
148148
.enableExperimentalFeature("AvailabilityMacro=_clockAPI:macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0"),
149149
.enableExperimentalFeature("AvailabilityMacro=_regexAPI:macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0"),
150150
.enableExperimentalFeature("AvailabilityMacro=_swiftVersionAPI:macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0"),
151+
.enableExperimentalFeature("AvailabilityMacro=_synchronizationAPI:macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0"),
151152

152153
.enableExperimentalFeature("AvailabilityMacro=_distantFuture:macOS 99.0, iOS 99.0, watchOS 99.0, tvOS 99.0, visionOS 99.0"),
153154
]

Sources/Testing/Expectations/ExpectationChecking+Macro.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,10 @@ public func __checkValue(
9595
// Post an event for the expectation regardless of whether or not it passed.
9696
// If the current event handler is not configured to handle events of this
9797
// kind, this event is discarded.
98-
var expectation = Expectation(evaluatedExpression: expression, isPassing: condition, isRequired: isRequired, sourceLocation: sourceLocation)
99-
Event.post(.expectationChecked(expectation))
98+
lazy var expectation = Expectation(evaluatedExpression: expression, isPassing: condition, isRequired: isRequired, sourceLocation: sourceLocation)
99+
if Configuration.deliverExpectationCheckedEvents {
100+
Event.post(.expectationChecked(expectation))
101+
}
100102

101103
// Early exit if the expectation passed.
102104
if condition {

Sources/Testing/Parameterization/TypeInfo.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ public struct TypeInfo: Sendable {
7474
// MARK: - Name
7575

7676
extension TypeInfo {
77+
/// An in-memory cache of fully-qualified type name components.
78+
private static let _fullyQualifiedNameComponentsCache = Locked<[ObjectIdentifier: [String]]>()
79+
7780
/// The complete name of this type, with the names of all referenced types
7881
/// fully-qualified by their module names when possible.
7982
///
@@ -92,6 +95,10 @@ extension TypeInfo {
9295
public var fullyQualifiedNameComponents: [String] {
9396
switch _kind {
9497
case let .type(type):
98+
if let cachedResult = Self._fullyQualifiedNameComponentsCache.rawValue[ObjectIdentifier(type)] {
99+
return cachedResult
100+
}
101+
95102
var result = String(reflecting: type)
96103
.split(separator: ".")
97104
.map(String.init)
@@ -109,6 +116,10 @@ extension TypeInfo {
109116
// those out as they're uninteresting to us.
110117
result = result.filter { !$0.starts(with: "(unknown context at") }
111118

119+
Self._fullyQualifiedNameComponentsCache.withLock { fullyQualifiedNameComponentsCache in
120+
fullyQualifiedNameComponentsCache[ObjectIdentifier(type)] = result
121+
}
122+
112123
return result
113124
case let .nameOnly(fullyQualifiedNameComponents, _, _):
114125
return fullyQualifiedNameComponents

Sources/Testing/Running/Runner.RuntimeState.swift

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
99
//
1010

11+
private import Synchronization
12+
1113
extension Runner {
1214
/// A type which collects the task-scoped runtime state for a running
1315
/// ``Runner`` instance, the tests it runs, and other objects it interacts
@@ -111,7 +113,10 @@ extension Configuration {
111113
/// - Returns: A unique number identifying `self` that can be
112114
/// passed to `_removeFromAll(identifiedBy:)`` to unregister it.
113115
private func _addToAll() -> UInt64 {
114-
Self._all.withLock { all in
116+
if deliverExpectationCheckedEvents, #available(_synchronizationAPI, *) {
117+
Self._deliverExpectationCheckedEventsCount.add(1, ordering: .sequentiallyConsistent)
118+
}
119+
return Self._all.withLock { all in
115120
let id = all.nextID
116121
all.nextID += 1
117122
all.instances[id] = self
@@ -123,12 +128,37 @@ extension Configuration {
123128
///
124129
/// - Parameters:
125130
/// - id: The unique identifier of this instance, as previously returned by
126-
/// `_addToAll()`. If `nil`, this function has no effect.
127-
private func _removeFromAll(identifiedBy id: UInt64?) {
128-
if let id {
129-
Self._all.withLock { all in
130-
_ = all.instances.removeValue(forKey: id)
131-
}
131+
/// `_addToAll()`.
132+
private func _removeFromAll(identifiedBy id: UInt64) {
133+
let configuration = Self._all.withLock { all in
134+
all.instances.removeValue(forKey: id)
135+
}
136+
if let configuration, configuration.deliverExpectationCheckedEvents, #available(_synchronizationAPI, *) {
137+
Self._deliverExpectationCheckedEventsCount.subtract(1, ordering: .sequentiallyConsistent)
138+
}
139+
}
140+
141+
/// An atomic counter that tracks the number of "current" configurations that
142+
/// have set ``deliverExpectationCheckedEvents`` to `true`.
143+
///
144+
/// On older Apple platforms, this property is not available and ``all`` is
145+
/// directly consulted instead (which is less efficient.)
146+
@available(_synchronizationAPI, *)
147+
private static let _deliverExpectationCheckedEventsCount = Atomic(0)
148+
149+
/// Whether or not events of the kind
150+
/// ``Event/Kind-swift.enum/expectationChecked(_:)`` should be delivered to
151+
/// the event handler of _any_ configuration set as current for a task in the
152+
/// current process.
153+
///
154+
/// To determine if an individual instance of ``Configuration`` is listening
155+
/// for these events, consult the per-instance
156+
/// ``Configuration/deliverExpectationCheckedEvents`` property.
157+
static var deliverExpectationCheckedEvents: Bool {
158+
if #available(_synchronizationAPI, *) {
159+
_deliverExpectationCheckedEventsCount.load(ordering: .sequentiallyConsistent) > 0
160+
} else {
161+
all.contains(where: \.deliverExpectationCheckedEvents)
132162
}
133163
}
134164
}

Tests/TestingTests/MiscellaneousTests.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -532,4 +532,15 @@ struct MiscellaneousTests {
532532
failureBreakpoint()
533533
#expect(failureBreakpointValue == 1)
534534
}
535+
536+
@available(_clockAPI, *)
537+
@Test("Repeated calls to #expect() run in reasonable time", .disabled("time-sensitive"))
538+
func repeatedlyExpect() {
539+
let duration = Test.Clock().measure {
540+
for _ in 0 ..< 1_000_000 {
541+
#expect(true as Bool)
542+
}
543+
}
544+
#expect(duration < .seconds(1))
545+
}
535546
}

cmake/modules/shared/AvailabilityDefinitions.cmake

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,5 @@ add_compile_options(
1313
"SHELL:$<$<COMPILE_LANGUAGE:Swift>:-Xfrontend -define-availability -Xfrontend \"_clockAPI:macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0\">"
1414
"SHELL:$<$<COMPILE_LANGUAGE:Swift>:-Xfrontend -define-availability -Xfrontend \"_regexAPI:macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0\">"
1515
"SHELL:$<$<COMPILE_LANGUAGE:Swift>:-Xfrontend -define-availability -Xfrontend \"_swiftVersionAPI:macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0\">"
16+
"SHELL:$<$<COMPILE_LANGUAGE:Swift>:-Xfrontend -define-availability -Xfrontend \"_synchronizationAPI:macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0\">"
1617
"SHELL:$<$<COMPILE_LANGUAGE:Swift>:-Xfrontend -define-availability -Xfrontend \"_distantFuture:macOS 99.0, iOS 99.0, watchOS 99.0, tvOS 99.0, visionOS 99.0\">")

0 commit comments

Comments
 (0)