Skip to content

Commit

Permalink
Improve perception performance (#24)
Browse files Browse the repository at this point in the history
* Registrar-configurable perception checking

* perception performance

* wip

* clean up and tests

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* fix test

* fix release builds.

* wrap runtime warnings tests in debug

* cleanup

---------

Co-authored-by: Stephen Celis <[email protected]>
  • Loading branch information
mbrandonw and stephencelis authored Jan 20, 2024
1 parent 41581ed commit 4224012
Show file tree
Hide file tree
Showing 7 changed files with 495 additions and 370 deletions.
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

0 comments on commit 4224012

Please sign in to comment.