Skip to content

Instantly share code, notes, and snippets.

@adam-zethraeus
Created December 18, 2021 11:30
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 adam-zethraeus/bb55915c27b6ebb0a3061ed05e998544 to your computer and use it in GitHub Desktop.
Save adam-zethraeus/bb55915c27b6ebb0a3061ed05e998544 to your computer and use it in GitHub Desktop.
StartupTimer.swift
import Foundation
import UIKit
StartupTimer.initSharedAtMain()
let appDelegateType: UIApplicationDelegate.Type = AppDelegate.self
let argc = CommandLine.argc
let argv = CommandLine.unsafeArgv
let delegateString = NSStringFromClass(appDelegateType) as String
_ = UIApplicationMain(argc, argv, nil, delegateString)
extension AppDelegate {
func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
StartupTimer.shared.registerEvent(.didFinishLaunching)
// whatever else
return true
}
}
extension MyMainThing {
func didFinishConstructingDependencies() {
StartupTimer.shared.registerEvent(.setupComplete)
StartupTimer.shared.setRecord { timer in
timer.measurements.forEach { measurement in
let number = NSNumber(floatLiteral: measurement.time)
NewRelic.setAttribute(measurement.type.rawValue, value: number)
}
timer.osLogDump()
}
}
}
import Foundation
import QuartzCore
import os.log
extension OSLog {
private static var subsystem = Bundle.main.bundleIdentifier!
fileprivate static let osLogCategoryString = "StartupTimerEvents"
static let startupTimer = OSLog(subsystem: subsystem, category: osLogCategoryString)
}
enum StartupTimerEvent: String, CaseIterable {
case main
case didFinishLaunching
case setupComplete
case firstViewDidAppear
}
struct StartupTimer: CustomStringConvertible {
struct Measurement: CustomStringConvertible {
var description: String {
"""
## \(type.rawValue)
* time: \(time)
* file: \(file.split(separator: "/").last.map(String.init) ?? file)
* function: \(function)
"""
}
let type: StartupTimerEvent
let time: CFTimeInterval
let function: String
let file: String
}
var description: String {
(
["# ⏱ StartupTimer Record (\(manualRecord ? "manual" : "automatic"))"] +
measurements
.sorted { lhs, rhs in
lhs.time < rhs.time
}
.map { measurement in
String(describing: measurement)
}
).joined(separator: "\n")
}
static func initSharedAtMain(
function: String = #function,
file: String = #file
) {
Self.instance = StartupTimer(function: function, file: file)
}
private static var instance = StartupTimer()
static var shared: StartupTimer {
set {
instance = newValue
}
get {
instance
}
}
private let initTime: CFTimeInterval
private let preMainSeconds: CFTimeInterval
private(set) var manualRecord: Bool = false
private(set) var measurements: [Measurement]
private init(function: String = #function, file: String = #file) {
let preMainSeconds = Self.secondsSinceProcessStart
self.preMainSeconds = preMainSeconds
self.measurements = [Measurement(type: .main, time: preMainSeconds, function: function, file: file)]
self.initTime = CACurrentMediaTime()
assertMain(after: nil)
}
private var record: ((StartupTimer) -> ())?
private var finishedRecording = false
private var invokedRecord = false
func osLogDump() {
if #available(iOS 14, *) {
let startupTimerEvents = Logger(subsystem: Bundle.main.bundleIdentifier!,
category: OSLog.osLogCategoryString)
measurements
.sorted { lhs, rhs in
lhs.time < rhs.time
}
.forEach { measurement in
startupTimerEvents.info("\(String(describing: measurement))")
}
} else {
measurements
.sorted { lhs, rhs in
lhs.time < rhs.time
}
.forEach { measurement in
os_log("%@", log: .startupTimer, type: .info, String(describing: measurement))
}
}
}
mutating func setRecord(callback: @escaping (StartupTimer)->()) {
assertMain(after: nil)
// only set once
record = record ?? callback
invokeRecordIfNeeded()
}
mutating func forceManualRecord() {
assertMain(after: nil)
manualRecord = true
record?(self)
}
private mutating func invokeRecordIfNeeded() {
guard !invokedRecord,
measurements.count == StartupTimerEvent.allCases.count else {
return
}
invokedRecord = true
manualRecord = false
assertMain(after: nil)
record?(self)
}
mutating func registerEvent(
_ type: StartupTimerEvent,
after: StartupTimerEvent? = nil,
function: String = #function,
file: String = #file
) {
assertMain(after: after)
guard !has(type: type) else {
return
}
measurements.append(
Measurement(type: type, time: elapsed, function: function, file: file)
)
invokeRecordIfNeeded()
}
private func has(type: StartupTimerEvent) -> Bool {
measurements.contains(where: { measurement in
measurement.type == type
})
}
private func assertMain(after type: StartupTimerEvent? = nil) {
#if DEBUG
dispatchPrecondition(condition: DispatchPredicate.onQueue(DispatchQueue.main))
guard let type = type else { return }
assert(has(type: type))
#endif
}
private var elapsed: CFTimeInterval {
preMainSeconds + (CACurrentMediaTime() - initTime)
}
private static var secondsSinceProcessStart: CFTimeInterval {
var kinfo = kinfo_proc()
var size = MemoryLayout<kinfo_proc>.stride
var mib : [Int32] = [CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()]
sysctl(&mib, u_int(mib.count), &kinfo, &size, nil, 0)
let start_time = kinfo.kp_proc.p_starttime
var time : timeval = timeval(tv_sec: 0, tv_usec: 0)
gettimeofday(&time, nil)
let currentTimeMilliseconds = Double(Int64(time.tv_sec) * 1000) + Double(time.tv_usec) / 1000.0
let processTimeMilliseconds = Double(Int64(start_time.tv_sec) * 1000) + Double(start_time.tv_usec) / 1000.0
return (currentTimeMilliseconds - processTimeMilliseconds) / 1000.0
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment