-
-
Save zwaldowski/49f61292757f86d7d036a529f2d04f0c to your computer and use it in GitHub Desktop.
// | |
// Activity.swift | |
// | |
// Created by Zachary Waldowski on 8/21/16. | |
// Copyright © 2016 Zachary Waldowski. Licensed under MIT. | |
// | |
import os.activity | |
private final class LegacyActivityContext { | |
let dsoHandle: UnsafeRawPointer? | |
let description: UnsafePointer<CChar> | |
let flags: os_activity_flag_t | |
init(dsoHandle: UnsafeRawPointer?, description: UnsafePointer<CChar>, flags: os_activity_flag_t) { | |
self.dsoHandle = dsoHandle | |
self.description = description | |
self.flags = flags | |
} | |
} | |
@available(OSX 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) | |
@_silgen_name("_os_activity_create") private func _os_activity_create(_ dso: UnsafeRawPointer?, _ description: UnsafePointer<Int8>, _ parent : Unmanaged<AnyObject>?, _ flags: os_activity_flag_t) -> AnyObject! | |
@available(OSX 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) | |
@_silgen_name("os_activity_apply") private func _os_activity_apply(_ storage: AnyObject, _ block: @convention(block) () -> ()) | |
@_silgen_name("_os_activity_initiate") private func __os_activity_initiate(_ dso: UnsafeRawPointer?, _ description: UnsafePointer<Int8>, _ flags: os_activity_flag_t, _ block: @convention(block) () -> ()) | |
@available(OSX 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) | |
@_silgen_name("os_activity_scope_enter") private func _os_activity_scope_enter(_ storage: AnyObject, _ state: UnsafeMutablePointer<os_activity_scope_state_s>) | |
@_silgen_name("_os_activity_start") private func __os_activity_start(_ dso: UnsafeRawPointer?, _ description: UnsafePointer<Int8>, _ flags: os_activity_flag_t) -> UInt64 | |
@available(OSX 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) | |
@_silgen_name("os_activity_scope_leave") private func _os_activity_scope_leave(_ state: UnsafeMutablePointer<os_activity_scope_state_s>) | |
@_silgen_name("os_activity_end") private func __os_activity_end(_ state: UInt64) | |
@available(OSX 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) | |
private let OS_ACTIVITY_NONE = unsafeBitCast(dlsym(UnsafeMutableRawPointer(bitPattern: -2), "_os_activity_none"), to: Unmanaged<AnyObject>.self) | |
@available(OSX 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) | |
private let OS_ACTIVITY_CURRENT = unsafeBitCast(dlsym(UnsafeMutableRawPointer(bitPattern: -2), "_os_activity_current"), to: Unmanaged<AnyObject>.self) | |
public struct Activity { | |
/// Support flags for OSActivity. | |
public struct Options: OptionSet { | |
public let rawValue: UInt32 | |
public init(rawValue: UInt32) { | |
self.rawValue = rawValue | |
} | |
/// Detach a newly created activity from a parent activity, if any. | |
/// | |
/// If passed in conjunction with a parent activity, the activity will | |
/// only note what activity "created" the new one, but will make the | |
/// new activity a top level activity. This allows seeing what | |
/// activity triggered work without actually relating the activities. | |
public static let detached = Options(rawValue: OS_ACTIVITY_FLAG_DETACHED.rawValue) | |
/// Will only create a new activity if none present. | |
/// | |
/// If an activity ID is already present, a new activity will be | |
/// returned with the same underlying activity ID. | |
@available(OSX 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) | |
public static let ifNonePresent = Options(rawValue: OS_ACTIVITY_FLAG_IF_NONE_PRESENT.rawValue) | |
} | |
private let opaque: AnyObject | |
/// Creates an activity. | |
public init(_ description: StaticString, dso: UnsafeRawPointer? = #dsohandle, options: Options = []) { | |
self.opaque = description.withUTF8Buffer { (buf: UnsafeBufferPointer<UInt8>) -> AnyObject in | |
let str = unsafeBitCast(buf.baseAddress!, to: UnsafePointer<Int8>.self) | |
let flags = os_activity_flag_t(rawValue: options.rawValue) | |
if #available(OSX 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *), OS_ACTIVITY_OBJECT_API != 0 { | |
return _os_activity_create(dso, str, OS_ACTIVITY_CURRENT, flags) | |
} else { | |
return LegacyActivityContext(dsoHandle: dso, description: str, flags: flags) | |
} | |
} | |
} | |
private func active(execute body: @convention(block) () -> ()) { | |
if #available(OSX 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *), OS_ACTIVITY_OBJECT_API != 0 { | |
_os_activity_apply(opaque, body) | |
} else { | |
let context = opaque as! LegacyActivityContext | |
__os_activity_initiate(context.dsoHandle, context.description, context.flags, body) | |
} | |
} | |
/// Executes a function body within the context of the activity. | |
public func active<Return>(execute body: () throws -> Return) rethrows -> Return { | |
func impl(execute work: () throws -> Return, recover: (Error) throws -> Return) rethrows -> Return { | |
var result: Return? | |
var error: Error? | |
active { | |
do { | |
result = try work() | |
} catch let e { | |
error = e | |
} | |
} | |
if let e = error { | |
return try recover(e) | |
} else { | |
return result! | |
} | |
} | |
return try impl(execute: body, recover: { throw $0 }) | |
} | |
/// Opaque structure created by `Activity.enter()` and restored using | |
/// `leave()`. | |
public struct Scope { | |
fileprivate var state = os_activity_scope_state_s() | |
fileprivate init() {} | |
/// Pops activity state to `self`. | |
public mutating func leave() { | |
if #available(OSX 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *), OS_ACTIVITY_OBJECT_API != 0 { | |
_os_activity_scope_leave(&state) | |
} else { | |
UnsafeRawPointer(bitPattern: Int(state.opaque.0)).map(Unmanaged<AnyObject>.fromOpaque)?.release() | |
__os_activity_end(state.opaque.1) | |
} | |
} | |
} | |
/// Changes the current execution context to the activity. | |
/// | |
/// An activity can be created and applied to the current scope by doing: | |
/// | |
/// var scope = OSActivity("my new activity").enter() | |
/// defer { scope.leave() } | |
/// ... do some work ... | |
/// | |
public func enter() -> Scope { | |
var scope = Scope() | |
if #available(OSX 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *), OS_ACTIVITY_OBJECT_API != 0 { | |
_os_activity_scope_enter(opaque, &scope.state) | |
} else { | |
let context = opaque as! LegacyActivityContext | |
scope.state.opaque.0 = numericCast(Int(bitPattern: Unmanaged.passRetained(context).toOpaque())) | |
scope.state.opaque.1 = __os_activity_start(context.dsoHandle, context.description, context.flags) | |
} | |
return scope | |
} | |
/// Creates an activity. | |
@available(OSX 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) | |
public init(_ description: StaticString, dso: UnsafeRawPointer? = #dsohandle, parent: Activity, options: Options = []) { | |
self.opaque = description.withUTF8Buffer { (buf: UnsafeBufferPointer<UInt8>) -> AnyObject in | |
let str = unsafeBitCast(buf.baseAddress!, to: UnsafePointer<Int8>.self) | |
let flags = os_activity_flag_t(rawValue: options.rawValue) | |
return _os_activity_create(dso, str, Unmanaged.passRetained(parent.opaque), flags) | |
} | |
} | |
private init(_ opaque: AnyObject) { | |
self.opaque = opaque | |
} | |
/// An activity with no traits; as a parent, it is equivalent to a | |
/// detached activity. | |
@available(OSX 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) | |
public static var none: Activity { | |
return Activity(OS_ACTIVITY_NONE.takeUnretainedValue()) | |
} | |
/// The running activity. | |
/// | |
/// As a parent, the new activity is linked to the current activity, if one | |
/// is present. If no activity is present, it behaves the same as `.none`. | |
@available(OSX 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) | |
public static var current: Activity { | |
return Activity(OS_ACTIVITY_CURRENT.takeUnretainedValue()) | |
} | |
/// Label an activity auto-generated by UI with a name that is useful for | |
/// debugging macro-level user actions. | |
/// | |
/// This function should be called early within the scope of an `IBAction`, | |
/// before any sub-activities are created. The name provided will be shown | |
/// in tools in addition to the system-provided name. This API should only | |
/// be called once, and only on an activity created by the system. These | |
/// actions help determine workflow of the user in order to reproduce | |
/// problems that occur. | |
/// | |
/// For example, a control press and/or menu item selection can be labeled: | |
/// | |
/// OSActivity.labelUserAction("New mail message") | |
/// OSActivity.labelUserAction("Empty trash") | |
/// | |
/// Where the underlying name will be "gesture:" or "menuSelect:". | |
@available(OSX 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) | |
public static func labelUserAction(_ description: StaticString, dso: UnsafeRawPointer? = #dsohandle) { | |
description.withUTF8Buffer { (buf: UnsafeBufferPointer<UInt8>) in | |
let str = unsafeBitCast(buf.baseAddress!, to: UnsafePointer<Int8>.self) | |
_os_activity_label_useraction(UnsafeMutableRawPointer(mutating: dso), str) | |
} | |
} | |
} |
<os/activity.h>
stuffs the static strings into the section __TEXT,__oslogstring,cstring_literals
. Your gist here doesn't do that (since there's no way to do this in Swift). Isn't that a problem?
Putting it in a special section in ObjC guarantees a stable in-this-binary pointer; the OS uses this and the DSO handle to get a UUID for the binary. Not sure what that UUID is used for; it's LaunchServices-related. The relevant facility in libsystem_trace
looks in __TEXT,__cstring
and __TEXT,__const
as well; StaticString
seems to do the trick for Swift. The (half-assed) overlay done for os_log
does the same thing.
Note to self: rewrite to use dlsym
instead of @_silgen_name
.
@zwaldowski Where's the (half-assed) overlay for os_log
? I skimmed through your public gists but don't see anything.
Whoa wait, the system frameworks actually include os_log APIs for Swift? I thought they didn't have that, like they don't have os_activity. That's interesting.
@kdawgwilk try this:
let str = buf.baseAddress!.withMemoryRebound(to: Int8.self, capacity: 8, { $0 })
Update for Xcode 9.4 / Swift 4.1
:
import Foundation
import os.activity
// Bridging Obj-C variabled defined as c-macroses. See `activity.h` header.
private let OS_ACTIVITY_NONE = unsafeBitCast(dlsym(UnsafeMutableRawPointer(bitPattern: -2), "_os_activity_none"), to: OS_os_activity.self)
private let OS_ACTIVITY_CURRENT = unsafeBitCast(dlsym(UnsafeMutableRawPointer(bitPattern: -2), "_os_activity_current"), to: OS_os_activity.self)
public struct Activity {
private let activity: OS_os_activity!
public init(_ description: StaticString, dso: UnsafeRawPointer? = #dsohandle, options: Options = []) {
activity = description.withUTF8Buffer {
if let dso = UnsafeMutableRawPointer(mutating: dso), let address = $0.baseAddress {
let str = UnsafeRawPointer(address).assumingMemoryBound(to: Int8.self)
return _os_activity_create(dso, str, OS_ACTIVITY_CURRENT, os_activity_flag_t(rawValue: options.rawValue))
} else {
return nil
}
}
}
public init(_ description: StaticString, dso: UnsafeRawPointer? = #dsohandle, parent: Activity, options: Options = []) {
let parentActivity: OS_os_activity = parent.activity != nil ? parent.activity : OS_ACTIVITY_CURRENT
activity = description.withUTF8Buffer {
if let dso = UnsafeMutableRawPointer(mutating: dso), let address = $0.baseAddress {
let str = UnsafeRawPointer(address).assumingMemoryBound(to: Int8.self)
return _os_activity_create(dso, str, parentActivity, os_activity_flag_t(rawValue: options.rawValue))
} else {
return nil
}
}
}
private init(_ activity: OS_os_activity) {
self.activity = activity
}
}
extension Activity {
public static var none: Activity {
return Activity(OS_ACTIVITY_NONE)
}
public static var current: Activity {
return Activity(OS_ACTIVITY_CURRENT)
}
public static func initiate(_ description: StaticString, dso: UnsafeRawPointer? = #dsohandle, options: Options = [],
execute body: @convention(block) () -> ()) {
description.withUTF8Buffer {
if let dso = UnsafeMutableRawPointer(mutating: dso), let address = $0.baseAddress {
let str = UnsafeRawPointer(address).assumingMemoryBound(to: Int8.self)
_os_activity_initiate(dso, str, os_activity_flag_t(rawValue: options.rawValue), body)
}
}
}
public func apply(execute body: @convention(block) () -> ()) {
if activity != nil {
os_activity_apply(activity, body)
}
}
public func enter() -> Scope {
var scope = Scope()
if activity != nil {
os_activity_scope_enter(activity, &scope.state)
}
return scope
}
/**
* Label an activity that is auto-generated by AppKit/UIKit with a name that is
* useful for debugging macro-level user actions. The API should be called
* early within the scope of the IBAction and before any sub-activities are
* created.
* This API can only be called once and only on the activity created by AppKit/UIKit.
*/
public static func labelUserAction(_ description: StaticString, dso: UnsafeRawPointer? = #dsohandle) {
description.withUTF8Buffer {
if let dso = UnsafeMutableRawPointer(mutating: dso), let address = $0.baseAddress {
let str = UnsafeRawPointer(address).assumingMemoryBound(to: Int8.self)
_os_activity_label_useraction(dso, str)
}
}
}
}
extension Activity {
public struct Options: OptionSet {
public let rawValue: UInt32
public init(rawValue: UInt32) {
self.rawValue = rawValue
}
public static let `default` = Options(rawValue: OS_ACTIVITY_FLAG_DEFAULT.rawValue)
public static let detached = Options(rawValue: OS_ACTIVITY_FLAG_DETACHED.rawValue)
public static let ifNonePresent = Options(rawValue: OS_ACTIVITY_FLAG_IF_NONE_PRESENT.rawValue)
}
public struct Scope {
fileprivate var state = os_activity_scope_state_s()
public mutating func leave() {
os_activity_scope_leave(&state)
}
}
}
@vgorloff What is OS_os_activity?
@zwaldowski (all), Do you occasionally get blocked by the error 'String' is not convertible to 'StaticString'
?
EDIT: This seems to be the Swift compiler behavior, when a Return
type is not specified when wrapping a function expecting a return. Makes sense.
Fix:
func applyMainExclusionPath() -> CGRect {
// would be invoked from within Activity
return Activity("apply main exclusion path").active { () -> CGRect in
I haven't been able to try this out yet, but it looks interesting.
Compared to the native API, the process of entering and leaving scopes is a bit verbose, and when someone forgets to leave it, I imagine it may cause problems.
To avoid the deferred leave boilerplate, would it be possible to define the scope as a class instead and leave the scope on deinit?
public class Scope {
fileprivate var state = os_activity_scope_state_s()
deinit {
os_activity_scope_leave(&state)
}
}
To avoid the deferred leave boilerplate, would it be possible to define the scope as a class instead and leave the scope on deinit?
public class Scope { fileprivate var state = os_activity_scope_state_s() deinit { os_activity_scope_leave(&state) } }
This approach doesn't work on Swift 5.0. Unfortunately, we still have to dance around the defer { scope.leave() }
.
Re: class Scope
, Swift ARC deinitialization isn't defined to happen at the end of scope like in C++. It's free to be deallocated (and thus end scope) as soon as you last use it.
Thank you so much for doing this!!! Big huge help!