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

Improve perception performance #24

Merged
merged 15 commits into from
Jan 20, 2024
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ jobs:
strategy:
matrix:
xcode: ['15.1']
config: ['debug']
config: ['debug', 'release']
runs-on: macos-13
steps:
- uses: actions/checkout@v3
- name: Select Xcode ${{ matrix.xcode }}
run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app
- name: Run ${{ matrix.config }} tests
run: swift test
run: swift test -c ${{ matrix.config }}
2 changes: 2 additions & 0 deletions Example/Example/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ class CounterModel {
var count = 0
var isDisplayingCount = true
var isPresentingSheet = false
var text = ""
func decrementButtonTapped() {
count -= 1
}
Expand All @@ -24,6 +25,7 @@ struct ContentView: View {
WithPerceptionTracking {
let _ = print("\(Self.self): tracked change.")
Form {
TextField("Text", text: $model.text)
if model.isDisplayingCount {
Text(model.count.description)
} else {
Expand Down
45 changes: 0 additions & 45 deletions Sources/Perception/Internal/Memoization.swift

This file was deleted.

148 changes: 95 additions & 53 deletions Sources/Perception/PerceptionRegistrar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,18 @@ import Foundation
@available(watchOS, deprecated: 10, renamed: "ObservationRegistrar")
public struct PerceptionRegistrar: Sendable {
private let _rawValue: AnySendable
#if DEBUG
private let isPerceptionCheckingEnabled: Bool
fileprivate let perceptionChecks = LockIsolated<[Location: Bool]>([:])
#endif

/// Creates an instance of the observation registrar.
///
/// You don't need to create an instance of
/// ``PerceptionRegistrar`` when using the
/// ``Perception/Perceptible()`` macro to indicate observably
/// of a type.
public init() {
public init(isPerceptionCheckingEnabled: Bool = Perception.isPerceptionCheckingEnabled) {
if #available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) {
#if canImport(Observation)
self._rawValue = AnySendable(ObservationRegistrar())
Expand All @@ -31,6 +35,9 @@ public struct PerceptionRegistrar: Sendable {
} else {
self._rawValue = AnySendable(_PerceptionRegistrar())
}
#if DEBUG
self.isPerceptionCheckingEnabled = isPerceptionCheckingEnabled
#endif
}

#if canImport(Observation)
Expand Down Expand Up @@ -78,10 +85,13 @@ extension PerceptionRegistrar {
@_disfavoredOverload
public func access<Subject: Perceptible, Member>(
_ subject: Subject,
keyPath: KeyPath<Subject, Member>
keyPath: KeyPath<Subject, Member>,
file: StaticString = #file,
line: UInt = #line
) {
perceptionCheck()

#if DEBUG
self.perceptionCheck(file: file, line: line)
#endif
#if canImport(Observation)
if #available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) {
func `open`<T: Observable>(_ subject: T) {
Expand Down Expand Up @@ -194,37 +204,46 @@ extension PerceptionRegistrar: Hashable {
}

#if DEBUG
private func perceptionCheck() {
if
isPerceptionCheckingEnabled,
!_PerceptionLocals.isInPerceptionTracking,
!_PerceptionLocals.skipPerceptionChecking,
isInSwiftUIBody()
{
runtimeWarn(
"""
Perceptible state was accessed but is not being tracked. Track changes to state by \
wrapping your view in a 'WithPerceptionTracking' view.
"""
)
extension PerceptionRegistrar {
fileprivate func perceptionCheck(file: StaticString, line: UInt) {
if self.isPerceptionCheckingEnabled,
Perception.isPerceptionCheckingEnabled,
!_PerceptionLocals.isInPerceptionTracking,
!_PerceptionLocals.skipPerceptionChecking,
self.isInSwiftUIBody(file: file, line: line)
{
runtimeWarn(
"""
Perceptible state was accessed but is not being tracked. Track changes to state by \
wrapping your view in a 'WithPerceptionTracking' view.
"""
)
}
}
}

private let isInSwiftUIBody: () -> Bool = memoize {
for callStackSymbol in Thread.callStackSymbols {
let mangledSymbol = callStackSymbol.utf8
.drop(while: { $0 != .init(ascii: "$") })
.prefix(while: { $0 != .init(ascii: " ") })
guard
mangledSymbol.isMangledViewBodyGetter,
let demangled = String(Substring(mangledSymbol)).demangled,
!demangled.isActionClosure
else {
continue
fileprivate func isInSwiftUIBody(file: StaticString, line: UInt) -> Bool {
self.perceptionChecks.withValue { perceptionChecks in
if let result = perceptionChecks[Location(file: file, line: line)] {
return result
}
for callStackSymbol in Thread.callStackSymbols {
let mangledSymbol = callStackSymbol.utf8
.drop(while: { $0 != .init(ascii: "$") })
.prefix(while: { $0 != .init(ascii: " ") })

guard
mangledSymbol.isMangledViewBodyGetter,
let demangled = String(Substring(mangledSymbol)).demangled,
!demangled.isActionClosure
else {
continue
}
return true
}
perceptionChecks[Location(file: file, line: line)] = false
return false
}
return true
}
return false
}

extension String {
Expand Down Expand Up @@ -263,10 +282,6 @@ extension PerceptionRegistrar: Hashable {
outputBufferSize: UnsafeMutablePointer<UInt>?,
flags: UInt32
) -> UnsafeMutablePointer<CChar>?
#else
@_transparent
@inline(__always)
private func perceptionCheck() {}
#endif

#if DEBUG
Expand Down Expand Up @@ -295,25 +310,52 @@ extension PerceptionRegistrar: Hashable {
}
#endif

extension Substring.UTF8View {
fileprivate var isMangledViewBodyGetter: Bool {
self._contains("V4bodyQrvg".utf8)
#if DEBUG
extension Substring.UTF8View {
fileprivate var isMangledViewBodyGetter: Bool {
self._contains("V4bodyQrvg".utf8)
}
fileprivate func _contains(_ other: String.UTF8View) -> Bool {
guard let first = other.first
else { return false }
let otherCount = other.count
var input = self
while let index = input.firstIndex(where: { first == $0 }) {
input = input[index...]
if input.count >= otherCount,
zip(input, other).allSatisfy(==)
{
return true
}
input.removeFirst()
}
return false
}
}
fileprivate func _contains(_ other: String.UTF8View) -> Bool {
guard let first = other.first
else { return false }
let otherCount = other.count
var input = self
while let index = input.firstIndex(where: { first == $0 }) {
input = input[index...]
if
input.count >= otherCount,
zip(input, other).allSatisfy(==)
{
return true

private final class LockIsolated<Value>: @unchecked Sendable {
private var _value: Value
private let lock = NSRecursiveLock()
init(_ value: @autoclosure @Sendable () throws -> Value) rethrows {
self._value = try value()
}
func withValue<T: Sendable>(
_ operation: @Sendable (inout Value) throws -> T
) rethrows -> T {
try self.lock.withLock {
var value = self._value
defer { self._value = value }
return try operation(&value)
}
input.removeFirst()
}
return false
}
}

private struct Location: Hashable {
let file: String
let line: UInt
init(file: StaticString, line: UInt) {
self.file = file.description
self.line = line
}
}
#endif
6 changes: 4 additions & 2 deletions Sources/PerceptionMacros/PerceptibleMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,11 @@ public struct PerceptibleMacro {
return
"""
internal nonisolated func access<Member>(
keyPath: KeyPath<\(perceptibleType), Member>
keyPath: KeyPath<\(perceptibleType), Member>,
file: StaticString = #file,
line: UInt = #line
) {
\(raw: registrarVariableName).access(self, keyPath: keyPath)
\(raw: registrarVariableName).access(self, keyPath: keyPath, file: file, line: line)
}
"""
}
Expand Down
6 changes: 4 additions & 2 deletions Tests/PerceptionMacrosTests/PerceptionMacrosTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,11 @@
private let _$perceptionRegistrar = Perception.PerceptionRegistrar()

internal nonisolated func access<Member>(
keyPath: KeyPath<Feature , Member>
keyPath: KeyPath<Feature , Member>,
file: StaticString = #file,
line: UInt = #line
) {
_$perceptionRegistrar.access(self, keyPath: keyPath)
_$perceptionRegistrar.access(self, keyPath: keyPath, file: file, line: line)
}

internal nonisolated func withMutation<Member, MutationResult>(
Expand Down
Loading