Created
December 18, 2021 11:30
-
-
Save adam-zethraeus/bb55915c27b6ebb0a3061ed05e998544 to your computer and use it in GitHub Desktop.
StartupTimer.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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