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

Instrumentation #36

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 9 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
108 changes: 108 additions & 0 deletions Sources/RxComposableArchitecture/Instrumentation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import Foundation

/// Interface to enable tracking/instrumenting the activity within TCA as ``Actions`` are sent into ``Store``s and
/// ``ViewStores``, ``Reducers`` are executed, and ``Effects`` are observed.
///
/// Additionally it can also track where `ViewStore` instances's are created.
///
/// The way the library will call the closures provided is identical to the way that the ``Actions`` and ``Effects`` are
/// handled internally. That means that there are likely to be ``Instrumentation.ViewStore`` `will|did` pairs contained
/// within the bounds of an ``Instrumentation.Store`` `will|did` pair. For example: Consider sending a simple ``Action``
/// into a ``ViewStore`` that does not produce any synchronous ``Effects`` from the ``Reducer``, and the ``ViewStore``
/// scoped off a parent ``Store``s state:
/// ```
/// ViewStore.send(.someAction)
/// .pre, .viewStoreSend
/// Store.send(.someAction)
/// .pre, .storeSend
/// The Store will begin processing .someAction
/// .pre, .storeProcessEvent
/// The Store's reducer handles .someAction
/// Any returned actions from the reducer are queued up
/// .post, .storeProcessEvent
/// The above(willProcess -> didProcess) is repeated for each queued up action within Store
/// .pre, .storeChangeState
/// The Store updates its state
/// For each child Store scoped off the Store using a scoped local state
/// The Store computes the scoped local state
/// .pre, .storeToLocal
/// .post, .storeToLocal
/// The Store determines if the scoped local state is has changed
/// .pre, .storeDeduplicate
/// .post, .storeDeduplicate
/// If the scoped local state has changed then the scoped child Store's state is updated, along with any further
/// downstream scoped Stores
/// For each ViewStore subscribed to a Store, if the state has changed will have their states updated at this too,
/// thus there may be multiple instances of the below
/// .pre, .viewStoreDeduplicate for impacted ViewStores
/// The existing state of the ViewStore instance is compared newly generated state and determined if it is a duplicate (this is unique to our branch of TCA)
/// .post, .viewStoreDeduplicate
/// .pre, .viewStoreChangeState
/// If the value for a ViewStores state was not a duplicate, then it is updated
/// .post, .viewStoreChangeState
/// .post, .storeChangeState
/// .post, .store.didSend
/// .post, .viewStoreSend
/// ```
public class Instrumentation {
/// Type indicating the action being taken by the store
public enum CallbackKind: CaseIterable, Hashable {
case storeSend
case storeToLocal
case storeDeduplicate
case storeChangeState
case storeProcessEvent
case viewStoreSend
case viewStoreChangeState
case viewStoreDeduplicate
}

/// Type indicating if the callback is before or after the action being taken by the store
public enum CallbackTiming {
case pre
case post
}

/// The method to implement if a user of ComposableArchitecture would like to be notified about the "life cycle" of
/// the various stores within the app as an action is acted upon.
/// - Parameter info: The store's type and action (optionally the originating action)
/// - Parameter timing: When this callback is being invoked (pre|post)
/// - Parameter kind: The store's activity that to which this callback relates (state update, deduplication, etc)
public typealias Callback = (_ info: CallbackInfo<Any, Any>, _ timing: CallbackTiming, _ kind: CallbackKind) -> Void
let callback: Callback?

/// Used to track when an instance of a `ViewStore` was created
public typealias ViewStoreCreatedCallback = (_ instance: AnyObject, _ file: StaticString, _ line: UInt) -> Void
let viewStoreCreated: ViewStoreCreatedCallback?

public static let noop = Instrumentation()
public static var shared: Instrumentation = .noop

public init(callback: Callback? = nil, viewStoreCreated: ViewStoreCreatedCallback? = nil) {
self.callback = callback
self.viewStoreCreated = viewStoreCreated
}
}

extension Instrumentation {
/// Object that holds the information that will be passed to any implementation that has provided a callback function
public struct CallbackInfo<StoreKind, Action> {
/// The ``Type`` of the store that the callback is being executed within/for; e.g. a ViewStore or Store.
public let storeKind: StoreKind
/// The action that was `sent` to the store.
public let action: Action?
/// In the case of a ``Store.send`` operation the ``action`` may be one returned from a reducer and thus have an
/// "originating" action (that action which was passed to the reducer that then returned the current ``action``)
public let originatingAction: Action?

init(storeKind: StoreKind, action: Action? = nil, originatingAction: Action? = nil) {
self.storeKind = storeKind
self.action = action
self.originatingAction = originatingAction
}

func eraseToAny() -> CallbackInfo<Any, Any> {
return .init(storeKind: (storeKind as Any), action: action.map { $0 as Any }, originatingAction: originatingAction.map { $0 as Any })
}
}
}
52 changes: 52 additions & 0 deletions Sources/RxComposableArchitecture/Internal/RuntimeWarnings.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#if DEBUG
import os

// NB: Xcode runtime warnings offer a much better experience than traditional assertions and
// breakpoints, but Apple provides no means of creating custom runtime warnings ourselves.
// To work around this, we hook into SwiftUI's runtime issue delivery mechanism, instead.
//
// Feedback filed: https://gist.github.com/stephencelis/a8d06383ed6ccde3e5ef5d1b3ad52bbc
private let rw = (
dso: { () -> UnsafeMutableRawPointer in
let count = _dyld_image_count()
for i in 0..<count {
if let name = _dyld_get_image_name(i) {
let swiftString = String(cString: name)
if swiftString.hasSuffix("/SwiftUI") {
if let header = _dyld_get_image_header(i) {
return UnsafeMutableRawPointer(mutating: UnsafeRawPointer(header))
}
}
}
}
return UnsafeMutableRawPointer(mutating: #dsohandle)
}(),
log: OSLog(subsystem: "com.apple.runtime-issues", category: "ComposableArchitecture")
)
#endif

@_transparent
@inline(__always)
func runtimeWarning(
_ message: @autoclosure () -> StaticString,
_ args: @autoclosure () -> [CVarArg] = []
) {
#if DEBUG
if _XCTIsTesting {
XCTFail(String(format: "\(message())", arguments: args()))
} else {
if #available(iOS 12.0, *) {
unsafeBitCast(
os_log as (OSLogType, UnsafeRawPointer, OSLog, StaticString, CVarArg...) -> Void,
to: ((OSLogType, UnsafeRawPointer, OSLog, StaticString, [CVarArg]) -> Void).self
)(.fault, rw.dso, rw.log, message(), args())
} else {
fputs(
"\(message())",
stderr
)
raise(SIGTRAP)
}
}
#endif
}
11 changes: 11 additions & 0 deletions Sources/RxComposableArchitecture/Internal/XCTIsTesting.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#if DEBUG
import Foundation

public let _XCTIsTesting: Bool = {
ProcessInfo.processInfo.environment.keys.contains("XCTestSessionIdentifier")
|| ProcessInfo.processInfo.arguments.first
.flatMap(URL.init(fileURLWithPath:))
.map { $0.lastPathComponent == "xctest" || $0.pathExtension == "xctest" }
?? false
}()
#endif
Loading