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 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
10 changes: 5 additions & 5 deletions Examples/Examples/1-BasicUsage/BasicUsageVC.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,11 @@ class BasicUsageVC: UIScrollVC {
})
.disposed(by: disposeBag)

store.subscribe(\.errorMessage)
.subscribe(onNext: { [errorLabel] in
errorLabel.text = $0
})
.disposed(by: disposeBag)
// store.subscribe(\.errorMessage)
// .subscribe(onNext: { [errorLabel] in
// errorLabel.text = $0
// })
// .disposed(by: disposeBag)

plusButton.addTarget(self, action: #selector(didTapPlus), for: .touchUpInside)
minusButton.addTarget(self, action: #selector(didTapMinus), for: .touchUpInside)
Expand Down
184 changes: 180 additions & 4 deletions Examples/Examples/RouteVC.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,173 @@
import RxComposableArchitecture
import UIKit

final class Node<Value> {
var value: Value
private(set) var children: [Node]
weak var parent: Node?

// var count: Int {
// 1 + children.reduce(0) { $0 + $1.count }
// }

init(_ value: Value) {
self.value = value
children = []
}

init(_ value: Value, children: [Node]) {
self.value = value
self.children = children
}

func add(child: Node) {
children.append(child)
child.parent = self
}

func resolveDFS(_ tree: Node) -> [Value] {

var stackResult = [Value]()
var stackTree = [tree]

while !stackTree.isEmpty {

let current = stackTree.popLast() // remove the last one added, O(1)
guard let currentUnwrap = current else { return stackResult }
stackResult.append(currentUnwrap.value) // process node
if !currentUnwrap.children.isEmpty {
for tree in currentUnwrap.children {
stackTree.append(tree)
}
}
}

return stackResult
}
}

extension Node: CustomStringConvertible {
// 2
var description: String {
// 3
var text = "\(value)"

// 4
if !children.isEmpty {
text += " {" + children.map { $0.description }.joined(separator: ", ") + "} "
}
return text
}
}


extension Node: Equatable where Value: Equatable {
static func ==(lhs: Node, rhs: Node) -> Bool {
lhs.value == rhs.value && lhs.children == rhs.children
}
}
extension Node where Value: Equatable {
func find(_ value: Value) -> Node? {
if self.value == value {
return self
}

for child in children {
if let match = child.find(value) {
return match
}
}

return nil
}
}
struct InstrumentLog {
var info: Instrumentation.CallbackInfo<Any, Any>
var kind: Instrumentation.CallbackKind
var timing: Instrumentation.CallbackTiming
var time: DispatchTime
}

class MyInstrumentation {
var instrumentation: Instrumentation! // to have access on self
var allLogs: [InstrumentLog] = [] // Queue

init() {
instrumentation = Instrumentation(callback: { [unowned self] info, timing, kind in
let currentTime = DispatchTime.now()
let infoString = String(describing: info)
allLogs.append(InstrumentLog(info: info, kind: kind, timing: timing, time: currentTime))
})
}

func fireLog() {
guard let first = allLogs.first else { return }
let tree = Node<FinalData>(FinalData(name: String(describing: first.info.storeKind), kind: "Store Creation", startTime: first.time))
var latestLeaf: Node<FinalData>? = tree
allLogs.forEach { instrumentLog in
switch instrumentLog.timing {
case .pre:
let val = Node(FinalData(
name: String(describing: instrumentLog.info),
kind: "\(instrumentLog.kind) \(String(describing: instrumentLog.info.action))",
startTime: instrumentLog.time
))
latestLeaf!.add(child: val)
latestLeaf = val
case .post:
let comparing = FinalData(
name: String(describing: instrumentLog.info),
kind: "\(instrumentLog.kind) \(String(describing: instrumentLog.info.action))",
startTime: instrumentLog.time
)
var leaf: Node<FinalData>? = latestLeaf
while leaf?.value != comparing && leaf?.parent != nil {
leaf = leaf?.parent
}
leaf!.value.endTime = instrumentLog.time
latestLeaf = leaf?.parent
}
}

allLogs.removeAll()
// var stackResult = [Value]()
// var stackTree = [tree]
//
// while !stackTree.isEmpty {
//
// let current = stackTree.popLast() // remove the last one added, O(1)
// guard let currentUnwrap = current else { return stackResult }
// stackResult.append(currentUnwrap.value) // process node
// if !currentUnwrap.children.isEmpty {
// for tree in currentUnwrap.children {
// stackTree.append(tree)
// }
// }
// }
//
// return stackResult

print(tree.resolveDFS(tree))
}
}

struct FinalData: Equatable {
var name: String
var kind: String
var startTime: DispatchTime
var endTime: DispatchTime?

static func == (lhs: FinalData, rhs: FinalData) -> Bool {
return lhs.name == rhs.name && lhs.kind == rhs.kind
}
}

// Roadmap:
/// 1. dump data
/// 2. store dmn => userdefaults
/// 3. processing gmn
/// 4. dashboard / displaying

class RouteVC: UITableViewController {
internal enum Route: String, CaseIterable {
case basic = "1. State, Action, Reducer"
Expand All @@ -17,24 +184,33 @@ class RouteVC: UITableViewController {
case optionalIfLet = "5. IfLet & Reducer.optional"
case neverEqual = "6. Demo NeverEqual"
}

internal var routes: [Route] = Route.allCases

let instrumentLog = MyInstrumentation()
internal init() {
super.init(style: .insetGrouped)
title = "RxComposableArchitecture Examples"
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
Instrumentation.shared = instrumentLog.instrumentation
}

override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
instrumentLog.fireLog()
}

override internal func tableView(_: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)

let selectedRoute = routes[indexPath.row]
switch selectedRoute {
case .basic:
let viewController = BasicUsageVC(store: Store(
initialState: BasicState(number: 0),
reducer: basicUsageReducer,
environment: ()
environment: (),
useNewScope: true
))
navigationController?.pushViewController(viewController, animated: true)
case .environment:
Expand Down Expand Up @@ -73,7 +249,7 @@ class RouteVC: UITableViewController {
override func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int {
routes.count
}

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = routes[indexPath.row].rawValue
Expand Down
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 })
}
}
}
Loading