Skip to content

Commit

Permalink
[6.0] Reduce overhead of .expectationChecked event handling in `#ex…
Browse files Browse the repository at this point in the history
…pect()` (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
  • Loading branch information
grynspan authored Sep 4, 2024
1 parent 185d0db commit 5887703
Show file tree
Hide file tree
Showing 6 changed files with 65 additions and 9 deletions.
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
6 changes: 4 additions & 2 deletions Sources/Testing/Expectations/ExpectationChecking+Macro.swift
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\">")

0 comments on commit 5887703

Please sign in to comment.