├── etc
├── demo.png
├── timelane.png
├── Icon_128x128@2x.png
├── timelane-template.png
└── timelane-recording.gif
├── Tests
├── LinuxMain.swift
└── OperationTimelaneTests
│ ├── XCTestManifests.swift
│ └── OpertionTimelaneTests.swift
├── Package.swift
├── LICENSE
├── OperationTimelane.podspec
├── .gitignore
├── README.md
└── Sources
└── OperationTimelane
└── OperationTimelane.swift
/etc/demo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/icanzilb/OperationTimelane/HEAD/etc/demo.png
--------------------------------------------------------------------------------
/etc/timelane.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/icanzilb/OperationTimelane/HEAD/etc/timelane.png
--------------------------------------------------------------------------------
/etc/Icon_128x128@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/icanzilb/OperationTimelane/HEAD/etc/Icon_128x128@2x.png
--------------------------------------------------------------------------------
/etc/timelane-template.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/icanzilb/OperationTimelane/HEAD/etc/timelane-template.png
--------------------------------------------------------------------------------
/etc/timelane-recording.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/icanzilb/OperationTimelane/HEAD/etc/timelane-recording.gif
--------------------------------------------------------------------------------
/Tests/LinuxMain.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | import OperationTimelaneTests
4 |
5 | var tests = [XCTestCaseEntry]()
6 | tests += OperationTimelaneTests.allTests()
7 | XCTMain(tests)
8 |
--------------------------------------------------------------------------------
/Tests/OperationTimelaneTests/XCTestManifests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | #if !canImport(ObjectiveC)
4 | public func allTests() -> [XCTestCaseEntry] {
5 | return [
6 | testCase(OperationTimelaneTests.allTests),
7 | ]
8 | }
9 | #endif
10 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.1
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "OperationTimelane",
8 | platforms: [
9 | .macOS(.v10_14),
10 | .iOS(.v12),
11 | .tvOS(.v12),
12 | .watchOS(.v5)
13 | ],
14 | products: [
15 | .library(
16 | name: "OperationTimelane",
17 | targets: ["OperationTimelane"]),
18 | ],
19 | dependencies: [
20 | .package(url: "https://github.com/icanzilb/TimelaneCore", from: "1.0.1")
21 | ],
22 | targets: [
23 | .target(
24 | name: "OperationTimelane",
25 | dependencies: ["TimelaneCore"]),
26 | .testTarget(
27 | name: "OperationTimelaneTests",
28 | dependencies: ["OperationTimelane", "TimelaneCoreTestUtils"]),
29 | ],
30 | swiftLanguageVersions: [.v5]
31 | )
32 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Marin Todorov
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/OperationTimelane.podspec:
--------------------------------------------------------------------------------
1 | Pod::Spec.new do |s|
2 | s.name = 'OperationTimelane'
3 | s.version = '0.9.0'
4 | s.summary = 'OperationTimelane provides operations bindings for profiling asynchronous code with the Timelane Instrument. Consult the README for the specific APIs to use in order to make the most out of OperationTimelane.'
5 |
6 | s.description = <<-DESC
7 | OperationTimelane provides operations bindings for profiling asynchronous code with the Timelane Instrument.
8 | DESC
9 |
10 | s.homepage = 'https://github.com/icanzilb/OperationTimelane'
11 | s.license = { :type => 'MIT', :file => 'LICENSE' }
12 | s.author = { 'Marin Todorov' => 'touch-code-magazine@underplot.com' }
13 | s.source = { :git => 'https://github.com/icanzilb/OperationTimelane.git', :tag => s.version.to_s }
14 | s.social_media_url = 'https://twitter.com/icanzilb'
15 |
16 | s.source_files = 'Sources/**/*.swift'
17 |
18 | s.swift_versions = ['5.0']
19 | s.requires_arc = true
20 | s.ios.deployment_target = '13.0'
21 | s.osx.deployment_target = '10.15'
22 | s.watchos.deployment_target = '6.0'
23 | s.tvos.deployment_target = '13.0'
24 |
25 | s.source_files = 'Sources/**/*.swift'
26 | s.frameworks = 'Foundation'
27 |
28 | s.dependency 'TimelaneCore', '~> 1'
29 | end
30 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## User settings
6 | xcuserdata/
7 |
8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
9 | *.xcscmblueprint
10 | *.xccheckout
11 |
12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
13 | build/
14 | DerivedData/
15 | *.moved-aside
16 | *.pbxuser
17 | !default.pbxuser
18 | *.mode1v3
19 | !default.mode1v3
20 | *.mode2v3
21 | !default.mode2v3
22 | *.perspectivev3
23 | !default.perspectivev3
24 |
25 | ## Obj-C/Swift specific
26 | *.hmap
27 |
28 | ## App packaging
29 | *.ipa
30 | *.dSYM.zip
31 | *.dSYM
32 |
33 | ## Playgrounds
34 | timeline.xctimeline
35 | playground.xcworkspace
36 |
37 | # Swift Package Manager
38 | #
39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
40 | # Packages/
41 | # Package.pins
42 | # Package.resolved
43 | # *.xcodeproj
44 | #
45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
46 | # hence it is not needed unless you have added a package configuration file to your project
47 | # .swiftpm
48 |
49 | .build/
50 |
51 | # CocoaPods
52 | #
53 | # We recommend against adding the Pods directory to your .gitignore. However
54 | # you should judge for yourself, the pros and cons are mentioned at:
55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
56 | #
57 | # Pods/
58 | #
59 | # Add this line if you want to avoid checking in source code from the Xcode workspace
60 | # *.xcworkspace
61 |
62 | # Carthage
63 | #
64 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
65 | # Carthage/Checkouts
66 |
67 | Carthage/Build/
68 |
69 | # Accio dependency management
70 | Dependencies/
71 | .accio/
72 |
73 | # fastlane
74 | #
75 | # It is recommended to not store the screenshots in the git repo.
76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed.
77 | # For more information about the recommended setup visit:
78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
79 |
80 | fastlane/report.xml
81 | fastlane/Preview.html
82 | fastlane/screenshots/**/*.png
83 | fastlane/test_output
84 |
85 | # Code Injection
86 | #
87 | # After new code Injection tools there's a generated folder /iOSInjectionProject
88 | # https://github.com/johnno1962/injectionforxcode
89 |
90 | iOSInjectionProject/
91 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # OperationTimelane ß
2 |
3 | 
4 |
5 | > Note: Pre 1.0 software.
6 |
7 | **OperationTimelane** provides an API allowing you to debug your `Operation` based asynchronous code visually in the Timelane Instrument.
8 |
9 | 
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | #### Contents:
18 |
19 | - [Usage](#Usage)
20 | - [API Reference](#Reference)
21 | - [Installation](#Installation)
22 | - [Demo](#Demo)
23 | - [License](#License)
24 |
25 | # Usage
26 |
27 | ## Before getting started
28 |
29 | 1. Download the latest release of the Timelane Instrument and install it on your computer: http://timelane.tools/#download.
30 | 2. Add the `OperationTimelane` package to your Xcode project like described in the [Installation](#installation) section.
31 |
32 | ## Debugging specific operation
33 |
34 | Import the OperationTimelane framework in your code:
35 |
36 | ```swift
37 | import OperationTimelane
38 | ```
39 |
40 | Use the `lane(_)` function to "activate" an operation for debugging in Timelane when you are creating the operation:
41 |
42 | ```swift
43 | let op = MyOperation().lane("My Operation")
44 | ```
45 |
46 | Then profile your project by clicking **Product > Profile** in Xcode's main menu.
47 |
48 | Select the Timelane Instrument template:
49 |
50 | 
51 |
52 | Inspect your operations on the interactive timeline:
53 |
54 | 
55 |
56 | ## Debugging specific operation queue
57 |
58 | `OperationTimelane` offers a custom operation queue which automatically logs any added operations in Timelane. (No swizzling in favor of a custom type.)
59 |
60 | Create a `LaneOperationQueue` (in other words replace your existing queue with) like so and then add operations as usual:
61 |
62 | ```swift
63 | let myQueue = LaneOperationQueue(name: "My Queue")
64 |
65 | myQueue.addOperation(...)
66 | myQueue.addOperation(...)
67 | myQueue.addOperation(...)
68 | ```
69 |
70 | All of the added operations will be visualized in Timelane like in the previous section.
71 |
72 | For a more detailed walkthrough go to [http://timelane.tools](http://timelane.tools).
73 |
74 | # API Reference
75 |
76 | ### `Operation.lane(_:filter:)`
77 |
78 | Use `lane("Lane name")` to send data to both the subscriptions and events lanes in the Timelane Instrument.
79 |
80 | `lane("Lane name", filter: [.subscriptions])` sends begin/completion events to the Subscriptions lane.
81 |
82 | `lane("Lane name", filter: [.events])` sends events and values to the Events lane.
83 |
84 | ### `Operation.laneValue(_)`
85 |
86 | Use this function if you want to log a value for the current operation in Timelane.
87 |
88 | ### `LaneOperationQueue`
89 |
90 | Temporarily replace your own operation queue with this type to debug its operations in Timelane.
91 |
92 | # Installation
93 |
94 | ## Swift Package Manager
95 |
96 | I . Automatically in Xcode:
97 |
98 | - Click **File > Swift Packages > Add Package Dependency...**
99 | - Use the package URL `https://github.com/icanzilb/OperationTimelane` to add TimelaneCombine to your project.
100 |
101 | II . Manually in your **Package.swift** file add:
102 |
103 | ```swift
104 | .package(url: "https://github.com/icanzilb/OperationTimelane", from: "1.0.0")
105 | ```
106 |
107 | ## CocoaPods
108 |
109 | [CocoaPods](https://cocoapods.org) is a dependency manager for Cocoa projects. For usage and installation instructions, visit their website. To integrate **OperationTimelane** into your Xcode project using CocoaPods, add it to your `Podfile`:
110 |
111 | ```ruby
112 | pod 'OperationTimelane', '~> 1.0'
113 | ```
114 |
115 | # Demo
116 |
117 | TODO: This repo contains a simple demo app. To give it a try open **OperationTimelaneExample/OperationTimelane.xcodeproj** and run the "OperationTimelaneDemo" scheme.
118 |
119 | 
120 |
121 | # License
122 |
123 | Copyright (c) Marin Todorov 2020
124 | This package is provided under the MIT License.
125 |
--------------------------------------------------------------------------------
/Tests/OperationTimelaneTests/OpertionTimelaneTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import TimelaneCore
3 | import TimelaneCoreTestUtils
4 | @testable import OperationTimelane
5 |
6 | final class OperationTimelaneTests: XCTestCase {
7 | /// Test subscription
8 | func testEmitsSubscription() {
9 | let recorder = TestLog()
10 | Timelane.Subscription.didEmitVersion = true
11 |
12 | let op = BlockOperation { sleep(1) }
13 | .lane("Sleep operation", filter: [.subscription], logger: recorder.log)
14 |
15 | op.start()
16 |
17 | XCTAssertEqual(recorder.logged.count, 2)
18 | guard recorder.logged.count == 2 else {
19 | return
20 | }
21 |
22 | XCTAssertEqual(recorder.logged[0].signpostType, "begin")
23 | XCTAssertEqual(recorder.logged[0].subscribe, "Sleep operation")
24 | XCTAssertEqual(recorder.logged[1].signpostType, "end")
25 | }
26 |
27 | /// Test cancelled
28 | func testEmitsCancelled() {
29 | let recorder = TestLog()
30 | Timelane.Subscription.didEmitVersion = true
31 |
32 | let op = BlockOperation { sleep(1) }
33 | .lane("Sleep operation", filter: [.subscription], logger: recorder.log)
34 | op.cancel()
35 | op.start()
36 |
37 | XCTAssertEqual(recorder.logged.count, 1)
38 | guard recorder.logged.count == 1 else {
39 | return
40 | }
41 |
42 | XCTAssertEqual(recorder.logged[0].signpostType, "end")
43 | }
44 |
45 | /// Test operation queue
46 | func testVanillaOperationQueue() {
47 | let recorder = TestLog()
48 | Timelane.Subscription.didEmitVersion = true
49 |
50 | let queue = OperationQueue()
51 | let op = BlockOperation { sleep(1) }
52 | .lane("Operation", filter: [.subscription], logger: recorder.log)
53 |
54 | queue.addOperation(op)
55 |
56 | XCTAssertEqual(recorder.logged.count, 0)
57 |
58 | queue.waitUntilAllOperationsAreFinished()
59 |
60 | // Wait asynchronously to give the queue a chance to finish
61 | let expectation1 = expectation(description: "Finished")
62 | DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
63 | expectation1.fulfill()
64 | }
65 | wait(for: [expectation1], timeout: 5.0)
66 |
67 | XCTAssertEqual(recorder.logged.count, 2)
68 | guard recorder.logged.count == 2 else {
69 | return
70 | }
71 |
72 | XCTAssertEqual(recorder.logged[0].signpostType, "begin")
73 | XCTAssertEqual(recorder.logged[0].subscribe, "Operation")
74 | XCTAssertEqual(recorder.logged[1].signpostType, "end")
75 | }
76 |
77 | /// Test operation queue
78 | func testLaneOperationQueue() {
79 | let recorder = TestLog()
80 | Timelane.Subscription.didEmitVersion = true
81 |
82 | let queue = LaneOperationQueue("Queue", filter: [.subscription], logger: recorder.log)
83 | let op = BlockOperation { sleep(1) }
84 |
85 | queue.addOperation(op)
86 |
87 | XCTAssertEqual(recorder.logged.count, 0)
88 |
89 | queue.waitUntilAllOperationsAreFinished()
90 |
91 | // Wait asynchronously to give the queue a chance to finish
92 | let expectation1 = expectation(description: "Finished")
93 | DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
94 | expectation1.fulfill()
95 | }
96 | wait(for: [expectation1], timeout: 5.0)
97 |
98 | XCTAssertEqual(recorder.logged.count, 2)
99 | guard recorder.logged.count == 2 else {
100 | return
101 | }
102 |
103 | XCTAssertEqual(recorder.logged[0].signpostType, "begin")
104 | XCTAssertEqual(recorder.logged[0].subscribe, "Queue")
105 | XCTAssertEqual(recorder.logged[1].signpostType, "end")
106 | }
107 |
108 | class TestValuesOperation: Operation {
109 | private var _finished = false
110 | override var isFinished: Bool {
111 | get { return _finished }
112 | set {
113 | willChangeValue(forKey: "isFinished")
114 | _finished = newValue
115 | didChangeValue(forKey: "isFinished")
116 | }
117 | }
118 |
119 | private var _executing = false
120 | override var isExecuting: Bool{
121 | get { return _executing }
122 | set {
123 | willChangeValue(forKey: "isExecuting")
124 | _executing = newValue
125 | didChangeValue(forKey: "isExecuting")
126 | }
127 | }
128 |
129 | override func start() {
130 | // Initialize
131 | isExecuting = true
132 | isFinished = false
133 |
134 | self.laneValue(1)
135 | sleep(1)
136 | self.laneValue(2)
137 |
138 | isFinished = true
139 | isExecuting = false
140 | }
141 | }
142 |
143 | /// Test values
144 | func testEmitsValues() {
145 | let recorder = TestLog()
146 | Timelane.Subscription.didEmitVersion = true
147 |
148 | let op = TestValuesOperation()
149 | .lane("Test operation", filter: [.event], logger: recorder.log)
150 |
151 | op.start()
152 |
153 | XCTAssertEqual(recorder.logged.count, 3)
154 | guard recorder.logged.count == 3 else {
155 | return
156 | }
157 |
158 | XCTAssertEqual(recorder.logged[0].signpostType, "event")
159 | XCTAssertEqual(recorder.logged[0].value, "1")
160 | XCTAssertEqual(recorder.logged[0].type, "Output")
161 | XCTAssertEqual(recorder.logged[1].signpostType, "event")
162 | XCTAssertEqual(recorder.logged[1].value, "2")
163 | XCTAssertEqual(recorder.logged[1].type, "Output")
164 | XCTAssertEqual(recorder.logged[2].signpostType, "event")
165 | XCTAssertEqual(recorder.logged[2].type, "Completed")
166 | }
167 |
168 | static var allTests = [
169 | ("testEmitsSubscription", testEmitsSubscription),
170 | ("testEmitsCancelled", testEmitsCancelled),
171 | ("testVanillaOperationQueue", testVanillaOperationQueue),
172 | ("testLaneOperationQueue", testLaneOperationQueue),
173 | ("testEmitsValues", testEmitsValues),
174 | ]
175 | }
176 |
177 |
--------------------------------------------------------------------------------
/Sources/OperationTimelane/OperationTimelane.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright(c) Marin Todorov 2020
3 | // For the license agreement for this code check the LICENSE file.
4 | //
5 |
6 | import Foundation
7 | import TimelaneCore
8 |
9 | fileprivate let lock = NSLock()
10 | fileprivate let operationTimelane = OperationTimelane()
11 |
12 | class OperationTimelane: NSObject {
13 |
14 | /// Private wrapper to ensure operations emit begin event only once.
15 | private class OperationWrapper: NSObject {
16 | weak var op: Operation?
17 | var started = false
18 |
19 | let filter: Set
20 | let source: String
21 | let subscription: Timelane.Subscription
22 |
23 | init(_ op: Operation, name: String, filter: Set, source: String, logger: @escaping Timelane.Logger) {
24 | self.op = op
25 |
26 | self.filter = filter
27 | self.source = source
28 | self.subscription = Timelane.Subscription(name: name, logger: logger)
29 | }
30 | }
31 |
32 | /// List of actively tracked operations
33 | private var operations: [OperationWrapper] = []
34 |
35 | /// The states of an operation Timelane tracks
36 | private enum State: String {
37 | case executing, cancelled, finished
38 | }
39 |
40 | /// Start tracking an operation
41 | func addOperation(_ operation: Operation, name: String, filter: Set, source: String, logger: @escaping Timelane.Logger) {
42 | lock.lock()
43 | defer { lock.unlock() }
44 |
45 | operation.addObserver(self, forKeyPath: State.executing.rawValue, options: [.initial, .new], context: nil)
46 | operation.addObserver(self, forKeyPath: State.cancelled.rawValue, options: .new, context: nil)
47 | operation.addObserver(self, forKeyPath: State.finished.rawValue, options: .new, context: nil)
48 | operations.append(OperationWrapper(operation, name: name, filter: filter, source: source, logger: logger))
49 | }
50 |
51 | /// Stop tracking an operation
52 | func removeOperation(_ op: Operation) {
53 | lock.lock()
54 | defer { lock.unlock() }
55 |
56 | op.removeObserver(self, forKeyPath: State.executing.rawValue)
57 | op.removeObserver(self, forKeyPath: State.cancelled.rawValue)
58 | op.removeObserver(self, forKeyPath: State.finished.rawValue)
59 |
60 | if let index = operations.firstIndex(where: { wrapper -> Bool in
61 | return wrapper.op == op
62 | }) {
63 | operations.remove(at: index)
64 | }
65 | }
66 |
67 | /// Receives an update event for a tracked operation.
68 | override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
69 |
70 | guard let op = object as? Operation,
71 | let keyPath = keyPath,
72 | let newValue = change?[NSKeyValueChangeKey.newKey] as? Bool,
73 | let wrapper = operations.first(where: { wrapper -> Bool in
74 | return wrapper.op == op
75 | })
76 | else { return }
77 |
78 | switch keyPath {
79 | case State.executing.rawValue where newValue && !wrapper.started:
80 | // Should happen only once, remove observer for this event
81 | lock.lock()
82 | defer { lock.unlock() }
83 |
84 | // Begin the operation
85 | wrapper.started = true
86 | if wrapper.filter.contains(.subscription) {
87 | wrapper.subscription.begin(source: wrapper.source)
88 | }
89 |
90 | case State.cancelled.rawValue where newValue && wrapper.started:
91 | // Should happen only once, remove observer for this event
92 | lock.lock()
93 | defer { lock.unlock() }
94 |
95 | wrapper.subscription.end(state: .cancelled)
96 | removeOperation(op)
97 |
98 | case State.finished.rawValue where newValue:
99 | // Should happen only once, remove observer for this event
100 | if wrapper.filter.contains(.subscription) {
101 | wrapper.subscription.end(state: .completed)
102 | }
103 | if wrapper.filter.contains(.event) {
104 | wrapper.subscription.event(value: .completion, source: wrapper.source)
105 | }
106 |
107 | removeOperation(op)
108 | default:
109 | return
110 | }
111 | }
112 |
113 | func emitValue(_ value: String, op: Operation, source: String) {
114 | guard let wrapper = operations.first(where: { wrapper -> Bool in
115 | return wrapper.op == op
116 | })
117 | else { return }
118 | wrapper.subscription.event(value: .value(value), source: source)
119 | }
120 | }
121 |
122 | /// An operation queue that logs its operations in Timelane.
123 | public class LaneOperationQueue: OperationQueue {
124 | public override init() {
125 | super.init()
126 | }
127 |
128 | var filter: Set = Set(Timelane.LaneType.allCases)
129 | var logger: Timelane.Logger = Timelane.defaultLogger
130 |
131 |
132 | /// Creates an operation queue that logs its operations in Timelane.
133 | /// - Parameters:
134 | /// - name: The name to use for creating a lane in Timelane.
135 | /// - name: A name for the lane when visualized in Instruments
136 | /// - filter: Which events to log, subscriptions or data events.
137 | public init(_ name: String, filter: Set = Set(Timelane.LaneType.allCases), logger: @escaping Timelane.Logger = Timelane.defaultLogger) {
138 | super.init()
139 | self.name = name
140 | self.filter = filter
141 | self.logger = logger
142 | }
143 |
144 | private static let unnamed = "Unnamed Operation Queue"
145 |
146 | public override func addOperation(_ op: Operation) {
147 | super.addOperation(op.lane(name ?? LaneOperationQueue.unnamed, filter: filter, logger: logger))
148 | }
149 |
150 | public override func addOperations(_ ops: [Operation], waitUntilFinished wait: Bool) {
151 | super.addOperations(ops.map { operation in
152 | return operation.lane(name ?? LaneOperationQueue.unnamed, filter: filter, logger: logger)
153 | }, waitUntilFinished: wait)
154 | }
155 |
156 | public override func addOperation(_ block: @escaping () -> Void) {
157 | self.addOperation(BlockOperation(block: block).lane(name ?? LaneOperationQueue.unnamed, filter: filter, logger: logger))
158 | }
159 | }
160 |
161 | public extension Operation {
162 |
163 | /// Logs a value event for the current operation in Timelane.
164 | /// - Parameters:
165 | /// - value: A value to log in Timelane
166 | @available(macOS 10.14, iOS 12, tvOS 12, watchOS 5, *)
167 | func laneValue(_ value: Any,
168 | file: StaticString = #file,
169 | function: StaticString = #function, line: UInt = #line) {
170 |
171 | let fileName = file.description.components(separatedBy: "/").last!
172 | let source = "\(fileName):\(line) - \(function)"
173 |
174 | let string = (value as? String) ?? String(describing: value)
175 | operationTimelane.emitValue(string, op: self, source: source)
176 | }
177 |
178 | /// The `lane` method logs the operation and its events to the Timelane Instrument.
179 | ///
180 | /// - Note: You can download the Timelane Instrument from http://timelane.tools
181 | /// - Parameters:
182 | /// - name: A name for the lane when visualized in Instruments
183 | /// - filter: Which events to log subscriptions or data events.
184 | @discardableResult
185 | @available(macOS 10.14, iOS 12, tvOS 12, watchOS 5, *)
186 | func lane(_ name: String,
187 | filter: Set = Set(Timelane.LaneType.allCases),
188 | file: StaticString = #file,
189 | function: StaticString = #function, line: UInt = #line,
190 | logger: @escaping Timelane.Logger = Timelane.defaultLogger) -> Self {
191 |
192 | let fileName = file.description.components(separatedBy: "/").last!
193 | let source = "\(fileName):\(line) - \(function)"
194 |
195 | // Start tracking the operation in Timelane
196 | operationTimelane.addOperation(self, name: name, filter: filter, source: source, logger: logger)
197 |
198 | return self
199 | }
200 | }
201 |
--------------------------------------------------------------------------------