Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

❇️ added slow frozen rendering instrumentation #496

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
//

import Foundation

public class SlowRenderingConfiguration {
public var slowFrameThreshold: CFTimeInterval = 16.7
public var frozenFrameThreshold: CFTimeInterval = 700

public init () {}

public init(slowFrameThreshold: CFTimeInterval, frozenFrameThreshold: CFTimeInterval) {
self.slowFrameThreshold = slowFrameThreshold
self.frozenFrameThreshold = frozenFrameThreshold
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
//
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
//

import Foundation
import UIKit
import OpenTelemetrySdk
import OpenTelemetryApi

extension UIViewController {
static func visibleViewController(from viewController: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
if let navigationController = viewController as? UINavigationController {
return visibleViewController(from: navigationController.visibleViewController)
} else if let tabBarController = viewController as? UITabBarController {
return visibleViewController(from: tabBarController.selectedViewController)
} else if let presentedViewController = viewController?.presentedViewController {
return visibleViewController(from: presentedViewController)
} else {
return viewController
}
}
}

class SlowRenderingDetector {

private var displayLink: CADisplayLink?
private var previousTimestamp: CFTimeInterval = 0.0
private var slowFrames: [String: Int] = [:]
private var frozenFrames: [String: Int] = [:]

private var configuration: SlowRenderingConfiguration

public private(set) var tracer: Tracer

private var activityName: String
private var timer = Timer()

public init(configuration: SlowRenderingConfiguration) {
self.tracer = OpenTelemetry.instance.tracerProvider.get(instrumentationName: "io.opentelemetry.slow-rendering", instrumentationVersion: "0.0.1")
self.activityName = SlowRenderingDetector.getActivityName()
self.configuration = configuration
start()
}

func start() {
if(self.displayLink != nil) {
return
}

NotificationCenter.default.addObserver(self, selector: #selector(self.appPaused(notification:)), name: UIApplication.willResignActiveNotification, object: nil)

NotificationCenter.default.addObserver(self, selector: #selector(self.appResumed(notification:)), name: UIApplication.didBecomeActiveNotification, object: nil)

let displayLink = CADisplayLink(target: self, selector: #selector(displayLinkCallback))
displayLink.add(to: .main, forMode: .common)
self.displayLink = displayLink

self.timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { _ in
self.flushFrames()
})
}

@objc func displayLinkCallback(displayLink: CADisplayLink) {
if(previousTimestamp == 0.0) {
previousTimestamp = displayLink.timestamp
return
}

let duration = displayLink.timestamp - previousTimestamp
previousTimestamp = displayLink.timestamp

let slowThresholdInSecs = configuration.slowFrameThreshold / 1e3
let frozenThresholdInSecs = configuration.frozenFrameThreshold / 1e3

if duration >= frozenThresholdInSecs {
if let count = self.frozenFrames[activityName] {
self.frozenFrames[activityName] = count + 1
} else {
self.frozenFrames[activityName] = 1
}
} else if(duration >= slowThresholdInSecs) {
if let count = self.slowFrames[activityName] {
self.slowFrames[activityName] = count + 1
} else {
self.slowFrames[activityName] = 1
}
}
}

@objc func appPaused(notification: Notification) {
self.displayLink?.isPaused = true
flushFrames()
}

@objc func appResumed(notification: Notification) {
previousTimestamp = 0.0
self.displayLink?.isPaused = false
}

func flushFrames() {
for (activityName, count) in self.slowFrames {
reportFrame("slowRenders", activityName, count)
}

for (activityName, count) in self.frozenFrames {
reportFrame("frozenRenders", activityName, count)
}
self.slowFrames.removeAll()
self.frozenFrames.removeAll()
}

class func getActivityName() -> String {
if let currentViewController = UIViewController.visibleViewController() {
let name = NSStringFromClass(type(of: currentViewController))
return name
}
return "unknown"

}

func reportFrame(_ type: String, _ activityName: String, _ count: Int) {
let now = Date()
let span = tracer.spanBuilder(spanName: type).setStartTime(time: now).startSpan()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be more appropriate to utilize an event rather than a span to collecting this data. What do you think?

span.setAttribute(key: "component", value: "ui")
span.setAttribute(key: "count", value: count)
span.setAttribute(key: "activity.name", value: activityName)
span.end(time: now)
}
}
Loading