Skip to content

Commit 1ee49f2

Browse files
authored
[performance]: disable logging by default & add logging configuration (#151)
## Background - during some recent investigations into rendering performance, we found that leaving the runtime os logging enabled (the current implicit behavior) had a noticeable adverse effect. ## Changes - logging is now opt-in, and disabled by default - added additional configuration options to control which categories of runtime data are logged - added synthetic performance tests to gut check execution time changes under various logging configurations
1 parent fb20bb1 commit 1ee49f2

File tree

7 files changed

+280
-26
lines changed

7 files changed

+280
-26
lines changed

Workflow/Sources/SubtreeManager.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -406,7 +406,7 @@ extension WorkflowNode.SubtreeManager {
406406
}
407407

408408
func render() -> W.Rendering {
409-
return node.render()
409+
return node.render(isRootNode: false)
410410
}
411411

412412
func update(workflow: W, outputMap: @escaping (W.Output) -> AnyWorkflowAction<WorkflowType>, eventPipe: EventPipe) {

Workflow/Sources/WorkflowHost.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ public final class WorkflowHost<WorkflowType: Workflow> {
5454

5555
self.rootNode = WorkflowNode(workflow: workflow)
5656

57-
self.mutableRendering = MutableProperty(rootNode.render())
57+
self.mutableRendering = MutableProperty(rootNode.render(isRootNode: true))
5858
self.rendering = Property(mutableRendering)
5959
rootNode.enableEvents()
6060

@@ -81,7 +81,7 @@ public final class WorkflowHost<WorkflowType: Workflow> {
8181
}
8282

8383
private func handle(output: WorkflowNode<WorkflowType>.Output) {
84-
mutableRendering.value = rootNode.render()
84+
mutableRendering.value = rootNode.render(isRootNode: true)
8585

8686
if let outputEvent = output.outputEvent {
8787
outputEventObserver.send(value: outputEvent)

Workflow/Sources/WorkflowLogger.swift

Lines changed: 112 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,69 @@
1717
import os.signpost
1818

1919
private extension OSLog {
20+
/// Logging will use this log handle when enabled
2021
static let workflow = OSLog(subsystem: "com.squareup.Workflow", category: "Workflow")
22+
23+
/// The active log handle to use when logging. Defaults to the shared `.disabled` handle.
24+
static var active: OSLog = .disabled
25+
}
26+
27+
// MARK: -
28+
29+
/// Namespace for specifying logging configuration data.
30+
public enum WorkflowLogging {}
31+
32+
extension WorkflowLogging {
33+
public struct Config {
34+
/// Configuration options to control logging during a render pass.
35+
public enum RenderLoggingMode {
36+
/// No data will be recorded for WorkflowNode render timings.
37+
case none
38+
39+
/// Render timings will only be recorded for root nodes in a Workflow tree.
40+
case rootsOnly
41+
42+
/// Render timings will be recorded for all nodes in a Workflow tree.
43+
/// N.B. performance may be noticeably impacted when using this option.
44+
case allNodes
45+
}
46+
47+
public var renderLoggingMode: RenderLoggingMode = .allNodes
48+
49+
/// When `true`, the interval spanning a WorkflowNode's lifetime will be recorded.
50+
public var logLifetimes = true
51+
52+
/// When `true`, action events will be recorded.
53+
public var logActions = true
54+
}
55+
56+
/// Global setting to enable or disable logging.
57+
/// Note, this is independent of the specified `config` value, and simply governs whether
58+
/// the runtime should emit any logs.
59+
///
60+
/// To enable logging, at a minimum you must set:
61+
/// `WorkflowLogging.enabled = true`
62+
///
63+
/// If you wish for more control over what the runtime will log, you may additionally specify
64+
/// a custom value for `WorkflowLogging.config`.
65+
public static var enabled: Bool {
66+
get { OSLog.active === OSLog.workflow }
67+
set { OSLog.active = newValue ? .workflow : .disabled }
68+
}
69+
70+
/// Configuration options used to determine which activities are logged.
71+
public static var config: Config = .rootRendersAndActions
72+
}
73+
74+
extension WorkflowLogging.Config {
75+
/// Logging config that will output the most information.
76+
/// Will also have the most noticeable effect on performance.
77+
public static let debug: Self = .init(renderLoggingMode: .allNodes, logLifetimes: true, logActions: true)
78+
79+
/// Logging config that will record render timings for root nodes as well as action events.
80+
/// This provides a reasonable performance tradeoff if you're interested in the runtime's behavior
81+
/// but don't wan to pay the price of logging everything.
82+
public static let rootRendersAndActions: Self = .init(renderLoggingMode: .rootsOnly, logLifetimes: false, logActions: true)
2183
}
2284

2385
// MARK: -
@@ -32,10 +94,12 @@ final class WorkflowLogger {
3294

3395
static func logWorkflowStarted<WorkflowType>(ref: WorkflowNode<WorkflowType>) {
3496
if #available(iOS 12.0, macOS 10.14, *) {
35-
let signpostID = OSSignpostID(log: .workflow, object: ref)
97+
guard WorkflowLogging.config.logLifetimes else { return }
98+
99+
let signpostID = OSSignpostID(log: .active, object: ref)
36100
os_signpost(
37101
.begin,
38-
log: .workflow,
102+
log: .active,
39103
name: "Alive",
40104
signpostID: signpostID,
41105
"Workflow: %{public}@",
@@ -46,17 +110,21 @@ final class WorkflowLogger {
46110

47111
static func logWorkflowFinished<WorkflowType>(ref: WorkflowNode<WorkflowType>) {
48112
if #available(iOS 12.0, macOS 10.14, *) {
49-
let signpostID = OSSignpostID(log: .workflow, object: ref)
50-
os_signpost(.end, log: .workflow, name: "Alive", signpostID: signpostID)
113+
guard WorkflowLogging.config.logLifetimes else { return }
114+
115+
let signpostID = OSSignpostID(log: .active, object: ref)
116+
os_signpost(.end, log: .active, name: "Alive", signpostID: signpostID)
51117
}
52118
}
53119

54120
static func logSinkEvent<Action: WorkflowAction>(ref: AnyObject, action: Action) {
55121
if #available(iOS 12.0, macOS 10.14, *) {
56-
let signpostID = OSSignpostID(log: .workflow, object: ref)
122+
guard WorkflowLogging.config.logActions else { return }
123+
124+
let signpostID = OSSignpostID(log: .active, object: ref)
57125
os_signpost(
58126
.event,
59-
log: .workflow,
127+
log: .active,
60128
name: "Sink Event",
61129
signpostID: signpostID,
62130
"Event for workflow: %{public}@",
@@ -67,12 +135,20 @@ final class WorkflowLogger {
67135

68136
// MARK: Rendering
69137

70-
static func logWorkflowStartedRendering<WorkflowType>(ref: WorkflowNode<WorkflowType>) {
138+
static func logWorkflowStartedRendering<WorkflowType>(
139+
ref: WorkflowNode<WorkflowType>,
140+
isRootNode: Bool
141+
) {
71142
if #available(iOS 12.0, macOS 10.14, *) {
72-
let signpostID = OSSignpostID(log: .workflow, object: ref)
143+
guard shouldLogRenderTimingsForMode(
144+
WorkflowLogging.config.renderLoggingMode,
145+
isRootNode: isRootNode
146+
) else { return }
147+
148+
let signpostID = OSSignpostID(log: .active, object: ref)
73149
os_signpost(
74150
.begin,
75-
log: .workflow,
151+
log: .active,
76152
name: "Render",
77153
signpostID: signpostID,
78154
"Render Workflow: %{public}@",
@@ -81,10 +157,34 @@ final class WorkflowLogger {
81157
}
82158
}
83159

84-
static func logWorkflowFinishedRendering<WorkflowType>(ref: WorkflowNode<WorkflowType>) {
160+
static func logWorkflowFinishedRendering<WorkflowType>(
161+
ref: WorkflowNode<WorkflowType>,
162+
isRootNode: Bool
163+
) {
85164
if #available(iOS 12.0, macOS 10.14, *) {
86-
let signpostID = OSSignpostID(log: .workflow, object: ref)
87-
os_signpost(.end, log: .workflow, name: "Render", signpostID: signpostID)
165+
guard shouldLogRenderTimingsForMode(
166+
WorkflowLogging.config.renderLoggingMode,
167+
isRootNode: isRootNode
168+
) else { return }
169+
170+
let signpostID = OSSignpostID(log: .active, object: ref)
171+
os_signpost(.end, log: .active, name: "Render", signpostID: signpostID)
172+
}
173+
}
174+
175+
// MARK: - Utilities
176+
177+
private static func shouldLogRenderTimingsForMode(
178+
_ renderLoggingMode: WorkflowLogging.Config.RenderLoggingMode,
179+
isRootNode: Bool
180+
) -> Bool {
181+
switch WorkflowLogging.config.renderLoggingMode {
182+
case .none:
183+
return false
184+
case .rootsOnly where !isRootNode:
185+
return false
186+
default:
187+
return true
88188
}
89189
}
90190
}

Workflow/Sources/WorkflowNode.swift

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,16 @@ final class WorkflowNode<WorkflowType: Workflow> {
7474
onOutput?(output)
7575
}
7676

77-
func render() -> WorkflowType.Rendering {
78-
WorkflowLogger.logWorkflowStartedRendering(ref: self)
77+
/// Internal method that forwards the render call through the underlying `subtreeManager`,
78+
/// and eventually to the client-specified `Workflow` instance.
79+
/// - Parameter isRootNode: whether or not this is the root node of the tree. Note, this
80+
/// is currently only used as a hint for the logging infrastructure, and is up to callers to correctly specify.
81+
/// - Returns: A `Rendering` of appropriate type
82+
func render(isRootNode: Bool = false) -> WorkflowType.Rendering {
83+
WorkflowLogger.logWorkflowStartedRendering(ref: self, isRootNode: isRootNode)
7984

8085
defer {
81-
WorkflowLogger.logWorkflowFinishedRendering(ref: self)
86+
WorkflowLogger.logWorkflowFinishedRendering(ref: self, isRootNode: isRootNode)
8287
}
8388

8489
return subtreeManager.render { context in

Workflow/Tests/PerformanceTests.swift

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/*
2+
* Copyright 2022 Square Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import XCTest
18+
19+
@testable import Workflow
20+
21+
class PerformanceTests: XCTestCase {
22+
override func setUp() {
23+
super.setUp()
24+
WorkflowLogging.enabled = false
25+
}
26+
27+
override func tearDown() {
28+
super.tearDown()
29+
WorkflowLogging.enabled = false
30+
}
31+
32+
func test_render_wideShallowTree() throws {
33+
measure {
34+
let node = WorkflowNode(workflow: WideShallowParentWorkflow())
35+
_ = node.render(isRootNode: true)
36+
}
37+
}
38+
39+
func test_render_narrowDeepTree() throws {
40+
measure {
41+
let node = WorkflowNode(workflow: NarrowDeepParentWorkflow())
42+
_ = node.render(isRootNode: true)
43+
}
44+
}
45+
46+
func test_debugLogging_render_wideShallowTree() throws {
47+
WorkflowLogging.enabled = true
48+
WorkflowLogging.config = .debug
49+
50+
measure {
51+
let node = WorkflowNode(workflow: WideShallowParentWorkflow())
52+
_ = node.render(isRootNode: true)
53+
}
54+
}
55+
56+
func test_rootNodeLogging_render_wideShallowTree() throws {
57+
WorkflowLogging.enabled = true
58+
WorkflowLogging.config = .rootRendersAndActions
59+
60+
measure {
61+
let node = WorkflowNode(workflow: WideShallowParentWorkflow())
62+
_ = node.render(isRootNode: true)
63+
}
64+
}
65+
66+
func test_rootNodeLogging_render_narrowDeepTree() throws {
67+
WorkflowLogging.enabled = true
68+
WorkflowLogging.config = .rootRendersAndActions
69+
70+
measure {
71+
let node = WorkflowNode(workflow: NarrowDeepParentWorkflow())
72+
_ = node.render(isRootNode: true)
73+
}
74+
}
75+
}
76+
77+
private extension PerformanceTests {
78+
struct WideShallowParentWorkflow: Workflow {
79+
typealias State = Void
80+
typealias Rendering = Int
81+
82+
func render(state: Void, context: RenderContext<WideShallowParentWorkflow>) -> Int {
83+
var sum = 0
84+
for i in 1 ... 1000 {
85+
sum += ChildWorkflow()
86+
.rendered(in: context, key: "child-\(i)")
87+
}
88+
89+
return sum
90+
}
91+
}
92+
93+
struct NarrowDeepParentWorkflow: Workflow {
94+
typealias State = Void
95+
typealias Rendering = Int
96+
97+
func render(state: Void, context: RenderContext<NarrowDeepParentWorkflow>) -> Int {
98+
var sum = 0
99+
sum += ChildWorkflow(remainingChildren: 999)
100+
.rendered(in: context)
101+
102+
return sum
103+
}
104+
}
105+
106+
struct ChildWorkflow: Workflow {
107+
typealias State = Void
108+
typealias Rendering = Int
109+
110+
var remainingChildren: UInt = 0
111+
112+
func render(state: Void, context: RenderContext<ChildWorkflow>) -> Int {
113+
let rendering: Int
114+
115+
if remainingChildren > 0 {
116+
rendering = ChildWorkflow(remainingChildren: remainingChildren - 1)
117+
.rendered(in: context)
118+
} else {
119+
rendering = 42
120+
}
121+
122+
return rendering
123+
}
124+
}
125+
}

0 commit comments

Comments
 (0)