Skip to content

Commit a303423

Browse files
authored
[perf]: don't instantiate debugger info if not needed (#331)
refactors action propagation to no longer instantiate any `WorkflowDebugInfo` types unless there is a `WorkflowDebugger` set on the host. this should avoid some unnecessary string allocations and reflection metadata lookups.
1 parent 99f6f4f commit a303423

File tree

7 files changed

+148
-24
lines changed

7 files changed

+148
-24
lines changed

Workflow/Sources/Debugging.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,3 +148,23 @@ extension WorkflowHierarchyDebugSnapshot {
148148
}
149149
}
150150
}
151+
152+
// MARK: - Compatibility
153+
154+
/// These extensions are utilities to support conditionally emitting debug info only when a
155+
/// `debugger` is set.
156+
extension WorkflowUpdateDebugInfo? {
157+
var unwrappedOrErrorDefault: WorkflowUpdateDebugInfo {
158+
self ?? .unexpectedlyMissing
159+
}
160+
}
161+
162+
extension WorkflowUpdateDebugInfo {
163+
fileprivate static let unexpectedlyMissing = {
164+
assertionFailure("Creation of actual WorkflowUpdateDebugInfo failed unexpectedly")
165+
return WorkflowUpdateDebugInfo(
166+
workflowType: "BUG IN WORKFLOW",
167+
kind: .didUpdate(source: .external)
168+
)
169+
}()
170+
}

Workflow/Sources/SubtreeManager.swift

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,10 +131,29 @@ extension WorkflowNode {
131131
}
132132
}
133133

134+
/// Carries information about the provenance of an event handled by the runtime.
135+
enum EventSource: Equatable {
136+
/// An event received from an external source.
137+
case external
138+
139+
/// An event that comes from a descendent in the subtree.
140+
/// Will contain associated debug info iff the `debugger` property of the `WorkflowHost`
141+
/// is set.
142+
case subtree(WorkflowUpdateDebugInfo?)
143+
144+
/// Compatibility method to convert to the public representation of this data
145+
func toDebugInfoSource() -> WorkflowUpdateDebugInfo.Source {
146+
switch self {
147+
case .external: .external
148+
case .subtree(let maybeInfo): .subtree(maybeInfo.unwrappedOrErrorDefault)
149+
}
150+
}
151+
}
152+
134153
extension WorkflowNode.SubtreeManager {
135154
enum Output {
136-
case update(any WorkflowAction<WorkflowType>, source: WorkflowUpdateDebugInfo.Source)
137-
case childDidUpdate(WorkflowUpdateDebugInfo)
155+
case update(any WorkflowAction<WorkflowType>, source: EventSource)
156+
case childDidUpdate(WorkflowUpdateDebugInfo?)
138157
}
139158
}
140159

Workflow/Sources/WorkflowHost.swift

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,12 @@ public final class WorkflowHost<WorkflowType: Workflow> {
9595
// Treat the update as an "output" from the workflow originating from an external event to force a render pass.
9696
let output = WorkflowNode<WorkflowType>.Output(
9797
outputEvent: nil,
98-
debugInfo: WorkflowUpdateDebugInfo(
99-
workflowType: "\(WorkflowType.self)",
100-
kind: .didUpdate(source: .external)
101-
)
98+
debugInfo: context.ifDebuggerEnabled {
99+
WorkflowUpdateDebugInfo(
100+
workflowType: "\(WorkflowType.self)",
101+
kind: .didUpdate(source: .external)
102+
)
103+
}
102104
)
103105
handle(output: output)
104106
}
@@ -112,7 +114,7 @@ public final class WorkflowHost<WorkflowType: Workflow> {
112114

113115
debugger?.didUpdate(
114116
snapshot: rootNode.makeDebugSnapshot(),
115-
updateInfo: output.debugInfo
117+
updateInfo: output.debugInfo.unwrappedOrErrorDefault
116118
)
117119

118120
rootNode.enableEvents()
@@ -140,3 +142,11 @@ final class HostContext {
140142
self.debugger = debugger
141143
}
142144
}
145+
146+
extension HostContext {
147+
func ifDebuggerEnabled<T>(
148+
_ perform: () -> T
149+
) -> T? {
150+
debugger != nil ? perform() : nil
151+
}
152+
}

Workflow/Sources/WorkflowNode.swift

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -96,19 +96,23 @@ final class WorkflowNode<WorkflowType: Workflow> {
9696
/// Finally, we tell the outside world that our state has changed (including an output event if it exists).
9797
output = Output(
9898
outputEvent: outputEvent,
99-
debugInfo: WorkflowUpdateDebugInfo(
100-
workflowType: "\(WorkflowType.self)",
101-
kind: .didUpdate(source: source)
102-
)
99+
debugInfo: hostContext.ifDebuggerEnabled {
100+
WorkflowUpdateDebugInfo(
101+
workflowType: "\(WorkflowType.self)",
102+
kind: .didUpdate(source: source.toDebugInfoSource())
103+
)
104+
}
103105
)
104106

105107
case .childDidUpdate(let debugInfo):
106108
output = Output(
107109
outputEvent: nil,
108-
debugInfo: WorkflowUpdateDebugInfo(
109-
workflowType: "\(WorkflowType.self)",
110-
kind: .childDidUpdate(debugInfo)
111-
)
110+
debugInfo: hostContext.ifDebuggerEnabled {
111+
WorkflowUpdateDebugInfo(
112+
workflowType: "\(WorkflowType.self)",
113+
kind: .childDidUpdate(debugInfo.unwrappedOrErrorDefault)
114+
)
115+
}
112116
)
113117
}
114118

@@ -179,7 +183,7 @@ final class WorkflowNode<WorkflowType: Workflow> {
179183
extension WorkflowNode {
180184
struct Output {
181185
var outputEvent: WorkflowType.Output?
182-
var debugInfo: WorkflowUpdateDebugInfo
186+
var debugInfo: WorkflowUpdateDebugInfo?
183187
}
184188
}
185189

Workflow/Tests/HostContextTests.swift

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import XCTest
2+
3+
@testable import Workflow
4+
5+
final class HostContextTests: XCTestCase {
6+
func test_conditional_debug_info_no_debugger() {
7+
let subject = HostContext.testing(debugger: nil)
8+
subject.ifDebuggerEnabled {
9+
XCTFail("should not be called")
10+
}
11+
}
12+
13+
func test_conditional_debug_info_with_debugger() {
14+
let subject = HostContext.testing(debugger: TestDebugger())
15+
let expectation = expectation(description: "debugger block invoked")
16+
17+
subject.ifDebuggerEnabled {
18+
expectation.fulfill()
19+
}
20+
21+
wait(for: [expectation], timeout: 0.001)
22+
}
23+
}

Workflow/Tests/TestUtilities.swift

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,15 +57,29 @@ struct StateTransitioningWorkflow: Workflow {
5757
}
5858
}
5959

60-
// MARK: -
60+
// MARK: - HostContext
6161

6262
extension HostContext {
6363
static func testing(
64-
observer: WorkflowObserver? = nil
64+
observer: WorkflowObserver? = nil,
65+
debugger: WorkflowDebugger? = nil
6566
) -> HostContext {
6667
HostContext(
6768
observer: observer,
68-
debugger: nil
69+
debugger: debugger
6970
)
7071
}
7172
}
73+
74+
// MARK: - WorkflowDebugger
75+
76+
struct TestDebugger: WorkflowDebugger {
77+
func didEnterInitialState(
78+
snapshot: WorkflowHierarchyDebugSnapshot
79+
) {}
80+
81+
func didUpdate(
82+
snapshot: WorkflowHierarchyDebugSnapshot,
83+
updateInfo: WorkflowUpdateDebugInfo
84+
) {}
85+
}

Workflow/Tests/WorkflowNodeTests.swift

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -110,12 +110,13 @@ final class WorkflowNodeTests: XCTestCase {
110110
b: SimpleWorkflow(string: "World")
111111
)
112112

113-
let node = WorkflowNode(workflow: workflow)
113+
let context = HostContext.testing(debugger: TestDebugger())
114+
let node = WorkflowNode(workflow: workflow, hostContext: context)
114115

115116
let rendering = node.render()
116117
node.enableEvents()
117118

118-
var emittedDebugInfo: [WorkflowUpdateDebugInfo] = []
119+
var emittedDebugInfo: [WorkflowUpdateDebugInfo?] = []
119120

120121
let expectation = XCTestExpectation(description: "Output")
121122
node.onOutput = { value in
@@ -130,11 +131,12 @@ final class WorkflowNodeTests: XCTestCase {
130131
XCTAssertEqual(emittedDebugInfo.count, 1)
131132

132133
let debugInfo = emittedDebugInfo[0]
133-
134-
XCTAssert(debugInfo.workflowType == "\(WorkflowType.self)")
134+
XCTAssert(debugInfo?.workflowType == "\(WorkflowType.self)")
135135

136136
/// Test the shape of the emitted debug info
137-
switch debugInfo.kind {
137+
switch debugInfo?.kind {
138+
case .none:
139+
XCTFail()
138140
case .childDidUpdate:
139141
XCTFail()
140142
case .didUpdate(let source):
@@ -158,6 +160,38 @@ final class WorkflowNodeTests: XCTestCase {
158160
}
159161
}
160162

163+
func test_noDebugUpdateInfoIfNoDebugger() {
164+
typealias WorkflowType = CompositeWorkflow<EventEmittingWorkflow, SimpleWorkflow>
165+
166+
let workflow = CompositeWorkflow(
167+
a: EventEmittingWorkflow(string: "Hello"),
168+
b: SimpleWorkflow(string: "World")
169+
)
170+
171+
let context = HostContext.testing(debugger: nil)
172+
let node = WorkflowNode(workflow: workflow, hostContext: context)
173+
174+
let rendering = node.render()
175+
node.enableEvents()
176+
177+
var emittedDebugInfo: [WorkflowUpdateDebugInfo?] = []
178+
179+
let expectation = XCTestExpectation(description: "Output")
180+
node.onOutput = { value in
181+
emittedDebugInfo.append(value.debugInfo)
182+
expectation.fulfill()
183+
}
184+
185+
rendering.aRendering.someoneTappedTheButton()
186+
187+
wait(for: [expectation], timeout: 1.0)
188+
189+
XCTAssertEqual(emittedDebugInfo.count, 1)
190+
191+
let debugInfo = emittedDebugInfo[0]
192+
XCTAssertNil(debugInfo)
193+
}
194+
161195
func test_debugTreeSnapshots() {
162196
typealias WorkflowType = CompositeWorkflow<EventEmittingWorkflow, SimpleWorkflow>
163197

0 commit comments

Comments
 (0)