Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[6.0] Reduce overhead of .expectationChecked event handling in #expect() (take 2) #659

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ extension Array where Element == PackageDescription.SwiftSetting {
.enableExperimentalFeature("AvailabilityMacro=_clockAPI:macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0"),
.enableExperimentalFeature("AvailabilityMacro=_regexAPI:macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0"),
.enableExperimentalFeature("AvailabilityMacro=_swiftVersionAPI:macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0"),
.enableExperimentalFeature("AvailabilityMacro=_synchronizationAPI:macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0"),

.enableExperimentalFeature("AvailabilityMacro=_distantFuture:macOS 99.0, iOS 99.0, watchOS 99.0, tvOS 99.0, visionOS 99.0"),
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,10 @@ public func __checkValue(
// Post an event for the expectation regardless of whether or not it passed.
// If the current event handler is not configured to handle events of this
// kind, this event is discarded.
var expectation = Expectation(evaluatedExpression: expression, isPassing: condition, isRequired: isRequired, sourceLocation: sourceLocation)
Event.post(.expectationChecked(expectation))
lazy var expectation = Expectation(evaluatedExpression: expression, isPassing: condition, isRequired: isRequired, sourceLocation: sourceLocation)
if Configuration.deliverExpectationCheckedEvents {
Event.post(.expectationChecked(expectation))
}

// Early exit if the expectation passed.
if condition {
Expand Down
11 changes: 11 additions & 0 deletions Sources/Testing/Parameterization/TypeInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ public struct TypeInfo: Sendable {
// MARK: - Name

extension TypeInfo {
/// An in-memory cache of fully-qualified type name components.
private static let _fullyQualifiedNameComponentsCache = Locked<[ObjectIdentifier: [String]]>()

/// The complete name of this type, with the names of all referenced types
/// fully-qualified by their module names when possible.
///
Expand All @@ -92,6 +95,10 @@ extension TypeInfo {
public var fullyQualifiedNameComponents: [String] {
switch _kind {
case let .type(type):
if let cachedResult = Self._fullyQualifiedNameComponentsCache.rawValue[ObjectIdentifier(type)] {
return cachedResult
}

var result = String(reflecting: type)
.split(separator: ".")
.map(String.init)
Expand All @@ -109,6 +116,10 @@ extension TypeInfo {
// those out as they're uninteresting to us.
result = result.filter { !$0.starts(with: "(unknown context at") }

Self._fullyQualifiedNameComponentsCache.withLock { fullyQualifiedNameComponentsCache in
fullyQualifiedNameComponentsCache[ObjectIdentifier(type)] = result
}

return result
case let .nameOnly(fullyQualifiedNameComponents, _, _):
return fullyQualifiedNameComponents
Expand Down
44 changes: 37 additions & 7 deletions Sources/Testing/Running/Runner.RuntimeState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
//

private import Synchronization

extension Runner {
/// A type which collects the task-scoped runtime state for a running
/// ``Runner`` instance, the tests it runs, and other objects it interacts
Expand Down Expand Up @@ -111,7 +113,10 @@ extension Configuration {
/// - Returns: A unique number identifying `self` that can be
/// passed to `_removeFromAll(identifiedBy:)`` to unregister it.
private func _addToAll() -> UInt64 {
Self._all.withLock { all in
if deliverExpectationCheckedEvents, #available(_synchronizationAPI, *) {
Self._deliverExpectationCheckedEventsCount.add(1, ordering: .sequentiallyConsistent)
}
return Self._all.withLock { all in
let id = all.nextID
all.nextID += 1
all.instances[id] = self
Expand All @@ -123,12 +128,37 @@ extension Configuration {
///
/// - Parameters:
/// - id: The unique identifier of this instance, as previously returned by
/// `_addToAll()`. If `nil`, this function has no effect.
private func _removeFromAll(identifiedBy id: UInt64?) {
if let id {
Self._all.withLock { all in
_ = all.instances.removeValue(forKey: id)
}
/// `_addToAll()`.
private func _removeFromAll(identifiedBy id: UInt64) {
let configuration = Self._all.withLock { all in
all.instances.removeValue(forKey: id)
}
if let configuration, configuration.deliverExpectationCheckedEvents, #available(_synchronizationAPI, *) {
Self._deliverExpectationCheckedEventsCount.subtract(1, ordering: .sequentiallyConsistent)
}
}

/// An atomic counter that tracks the number of "current" configurations that
/// have set ``deliverExpectationCheckedEvents`` to `true`.
///
/// On older Apple platforms, this property is not available and ``all`` is
/// directly consulted instead (which is less efficient.)
@available(_synchronizationAPI, *)
private static let _deliverExpectationCheckedEventsCount = Atomic(0)

/// Whether or not events of the kind
/// ``Event/Kind-swift.enum/expectationChecked(_:)`` should be delivered to
/// the event handler of _any_ configuration set as current for a task in the
/// current process.
///
/// To determine if an individual instance of ``Configuration`` is listening
/// for these events, consult the per-instance
/// ``Configuration/deliverExpectationCheckedEvents`` property.
static var deliverExpectationCheckedEvents: Bool {
if #available(_synchronizationAPI, *) {
_deliverExpectationCheckedEventsCount.load(ordering: .sequentiallyConsistent) > 0
} else {
all.contains(where: \.deliverExpectationCheckedEvents)
}
}
}
Expand Down
11 changes: 11 additions & 0 deletions Tests/TestingTests/MiscellaneousTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -532,4 +532,15 @@ struct MiscellaneousTests {
failureBreakpoint()
#expect(failureBreakpointValue == 1)
}

@available(_clockAPI, *)
@Test("Repeated calls to #expect() run in reasonable time", .disabled("time-sensitive"))
func repeatedlyExpect() {
let duration = Test.Clock().measure {
for _ in 0 ..< 1_000_000 {
#expect(true as Bool)
}
}
#expect(duration < .seconds(1))
}
}
1 change: 1 addition & 0 deletions cmake/modules/shared/AvailabilityDefinitions.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ add_compile_options(
"SHELL:$<$<COMPILE_LANGUAGE:Swift>:-Xfrontend -define-availability -Xfrontend \"_clockAPI:macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0\">"
"SHELL:$<$<COMPILE_LANGUAGE:Swift>:-Xfrontend -define-availability -Xfrontend \"_regexAPI:macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0\">"
"SHELL:$<$<COMPILE_LANGUAGE:Swift>:-Xfrontend -define-availability -Xfrontend \"_swiftVersionAPI:macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0\">"
"SHELL:$<$<COMPILE_LANGUAGE:Swift>:-Xfrontend -define-availability -Xfrontend \"_synchronizationAPI:macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0\">"
"SHELL:$<$<COMPILE_LANGUAGE:Swift>:-Xfrontend -define-availability -Xfrontend \"_distantFuture:macOS 99.0, iOS 99.0, watchOS 99.0, tvOS 99.0, visionOS 99.0\">")