Skip to content

Instantly share code, notes, and snippets.

@LK-Simon
Created June 19, 2022 14:48
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save LK-Simon/d1f5979c54a064871e4eea7ff2e4abf8 to your computer and use it in GitHub Desktop.
Save LK-Simon/d1f5979c54a064871e4eea7ff2e4abf8 to your computer and use it in GitHub Desktop.
High Precision "Mach Timer" for MacOS and iOS (Swift implementation)
import Foundation
enum MachTimerError: Error {
case TimebaseInfoError // Only thrown where we cannot initialize the Mach Timer (shouldn't ever happen though)
}
struct MachTimer {
// We need to know whether or not the Timer is still running or Stopped at the point of requesting a Result
enum MachTimerState {
case NotRunning
case Running
}
// Object to encapsulate a Result and provide basic conversion to Milliseconds and Seconds from NAnoseconds
struct MachTimerResult {
var nanos: UInt64 // Original Result is in Nanoseconds
init(nanos: UInt64) { // We must provide a Nanosecond Result on Initialization
self.nanos = nanos
}
var millis: Double { // Return Result in Milliseconds
get {
return Double(self.nanos) / 1_000_00
}
}
var seconds: Double { // Return Result in Seconds
get {
return Double(self.nanos) / 1_000_000_000
}
}
}
var state: MachTimerState = .NotRunning
var startTime: UInt64 = 0
var stopTime: UInt64 = 0
let numer: UInt64
let denom: UInt64
init() throws {
var info = mach_timebase_info(numer: 0, denom: 0)
let status = mach_timebase_info(&info)
if status != KERN_SUCCESS {
throw MachTimerError.TimebaseInfoError
}
self.numer = UInt64(info.numer)
self.denom = UInt64(info.denom)
}
mutating func start(refTime: UInt64? = nil) {
let now: UInt64 = mach_absolute_time() // Always get the Reference Time (we might need to rely on it if no refTime is given)
let ref: UInt64 = refTime ?? now // Use refTime if given, otherwise fall back to now
self.startTime = ref
self.state = .Running
}
mutating func stop(refTime: UInt64? = nil) -> MachTimerResult {
let now: UInt64 = mach_absolute_time() // Always get the Reference Time (we might need to rely on it if no refTime is given)
let ref: UInt64 = refTime ?? now // Use refTime if given, otherwise fall back to now
self.stopTime = ref
self.state = .NotRunning
return self.result(refTime: self.stopTime)
}
mutating func result(refTime: UInt64? = nil) -> MachTimerResult {
let now: UInt64 = mach_absolute_time() // Always get the Reference Time (we might need to rely on it if no refTime is given)
let ref: UInt64 = self.state == .Running ? refTime ?? now : stopTime // If Running, use Now or refTime. If NOT Running, use stopTime
return MachTimerResult(nanos: ((ref - self.startTime) * self.numer) / self.denom)
}
}
@LK-Simon
Copy link
Author

LK-Simon commented Jun 19, 2022

The reason for all of the now variables is that we want to get the mach_absolute_time() value as early as physically possible. In this case, the precision of the result is more important than wasting cycles asking for the value in cases where we don't end up using it.

The best way to use this timer would be to provide refTime parameter values wherever available, and for that value to be from mach_absolute_time()

This timer allows you to call result = myTimer.result(refTime: mach_absolute_time()) even when the Timer is still running. This is good for situations where you want to take "Checkpoint" times from an overall process using a single Timer.

The stop function will return the result by default.

@LK-Simon
Copy link
Author

LK-Simon commented Jun 19, 2022

Simple usage example:

do {
  var myTimer = try MachTimer()
  myTimer.start() // or myTimer.start(mach_absolute_time()) if you care about drift from the call stack
  // Do something here
  let pointInTime = myTimer.result() // or let pointInTime = myTimer.result(mach_absolute_time()) if you care about drift from the call stack
  // Do something else
  let totalTime = myTimer.stop() // or let totalTime = myTimer.stop(mach_absolute_time()) if you care about drift from the call stack

To use pointInTime and totalTime you can do the following:

  print("Point In Time was \(pointInTime.nanos)ns [\(pointInTime.millis)ms]")
  print("Total Time was \(totalTime.nanos)ns [\(totalTime.millis)ms]")

Don't forget to close the do block and handle the possible Error:

}
catch MachTimerError.TimebaseInfoError {
    print("Couldn't get Timebase Info!")
    exit(1)
}

You can wrap the do and catch block as close to the initialisation of your MachTimer object as you wish. Just remember that it is possible (albeit extremely unlikely) to encounter an error when the MachTimer attempts to read the mach_timebase_info(&info) which is why we have the throw and catch in the first place.

@LK-Simon
Copy link
Author

LK-Simon commented Jun 19, 2022

If you need to persist the instance of your MachTimer without encapsulating all of its referencing code inside of a do/catch block, you can do this:

var myTimer: MachTimer? = nil
do {
    myTimer = try MachTimer()
}
catch MachTimerError.TimebaseInfoError {
  print("Couldn't get Timebase Info!")
}

You can then do nil checks before using the Timer in the many ways Swift language supports, e.g:

if myTimer {
    // Use the Timer
}

or

if !myTimer {
    return // Early return pattern
}
myTimer!.start() // Example of asserting that myTimer cannot be nil, and call its start method

or:

myTimer?.start() // Will only invoke the start method if myTimer is not nil

or (for some versions of Swift that don't like the above)

if let actualTimer = myTimer {
    actualTimer.start()
    // Use the timer referencing actualTimer from here on
}

or (finally) you can do something similar to above, but reusing the same variable name:

if let myTimer = myTimer {
    myTimer.start()
    // Use the timer referencing myTimer
}

@NilaxanN
Copy link

can you please tell me how to call in the view (like in the content view, or you need to make a class???
)

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