├── 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 | ![Timelane Icon](etc/Icon_128x128@2x.png) 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 | ![Timelane Instrument](etc/timelane.png) 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 | ![Timelane Instrument Template](etc/timelane-template.png) 51 | 52 | Inspect your operations on the interactive timeline: 53 | 54 | ![Timelane Live Recording](etc/timelane-recording.gif) 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 | ![Timelane demo app](etc/demo.png) 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 | --------------------------------------------------------------------------------