Skip to content

Instantly share code, notes, and snippets.

@zwaldowski
Last active February 15, 2024 18:49
Show Gist options
  • Star 71 You must be signed in to star a gist
  • Fork 10 You must be signed in to fork a gist
  • Save zwaldowski/49f61292757f86d7d036a529f2d04f0c to your computer and use it in GitHub Desktop.
Save zwaldowski/49f61292757f86d7d036a529f2d04f0c to your computer and use it in GitHub Desktop.
os_activity_t for Swift 3
//
// 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)
}
}
}
@dhoerl
Copy link

dhoerl commented Nov 9, 2016

Thank you so much for doing this!!! Big huge help!

@lilyball
Copy link

lilyball commented Jan 10, 2017

<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?

@zwaldowski
Copy link
Author

zwaldowski commented Jan 10, 2017

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.

@zwaldowski
Copy link
Author

Note to self: rewrite to use dlsym instead of @_silgen_name.

@lilyball
Copy link

@zwaldowski Where's the (half-assed) overlay for os_log? I skimmed through your public gists but don't see anything.

@lilyball
Copy link

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
Copy link

What is the answer fixing these warnings?

screen shot 2017-08-31 at 4 36 58 pm

@alemar11
Copy link

alemar11 commented Sep 8, 2017

@kdawgwilk try this:
let str = buf.baseAddress!.withMemoryRebound(to: Int8.self, capacity: 8, { $0 })

@vgorloff
Copy link

vgorloff commented May 6, 2018

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)
      }
   }
}

@LinMaris
Copy link

@vgorloff What is OS_os_activity?

@sstadelman
Copy link

sstadelman commented Nov 2, 2018

@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

@Oyvindkg
Copy link

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)
    }
}

@OdNairy
Copy link

OdNairy commented Jun 25, 2019

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() }.

@zwaldowski
Copy link
Author

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.

@matis-schotte
Copy link

matis-schotte commented May 29, 2020

@vgorloff Works great even in Swift 5.2/Xcode 11, thanks!

@LinMaris Its just a protocol, same as os_activity_t. The piece works perfectly!

public protocol OS_os_activity : NSObjectProtocol {
}

public typealias os_activity_t = OS_os_activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment