-
Notifications
You must be signed in to change notification settings - Fork 133
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
RUMM-1278 VitalRefreshRateReader is added
- Loading branch information
Showing
4 changed files
with
224 additions
and
1 deletion.
There are no files selected for viewing
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
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
112 changes: 112 additions & 0 deletions
112
Sources/Datadog/RUM/RUMVitals/VitalRefreshRateReader.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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
/* | ||
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. | ||
* This product includes software developed at Datadog (https://www.datadoghq.com/). | ||
* Copyright 2019-2020 Datadog, Inc. | ||
*/ | ||
|
||
import Foundation | ||
import UIKit | ||
|
||
/// A class reading the refresh rate (frames per second) of the main screen | ||
internal class VitalRefreshRateReader { | ||
private var observers = [VitalObserver]() | ||
private var displayLink: CADisplayLink? | ||
private var lastFrameTimestamp: CFTimeInterval? | ||
private(set) var isRunning = false | ||
|
||
init(notificationCenter: NotificationCenter = .default) { | ||
notificationCenter.addObserver( | ||
self, | ||
selector: #selector(appWillResignActive), | ||
name: UIApplication.willResignActiveNotification, | ||
object: nil | ||
) | ||
notificationCenter.addObserver( | ||
self, | ||
selector: #selector(appDidBecomeActive), | ||
name: UIApplication.didBecomeActiveNotification, | ||
object: nil | ||
) | ||
} | ||
|
||
deinit { | ||
stop() | ||
} | ||
|
||
/// `VitalRefreshRateReader` keeps pushing data to its `observers` at every new frame. | ||
/// - Parameter observer: receiver of refresh rate per frame. | ||
func register(_ observer: VitalObserver) { | ||
DispatchQueue.main.async { | ||
self.observers.append(observer) | ||
} | ||
} | ||
|
||
/// `VitalRefreshRateReader` stops pushing data to `observer` once unregistered. | ||
/// - Parameter observer: already added observer; otherwise nothing happens. | ||
func unregister(_ observer: VitalObserver) { | ||
DispatchQueue.main.async { | ||
self.observers.removeAll { existingObserver in | ||
return existingObserver === observer | ||
} | ||
} | ||
} | ||
|
||
/// Starts listening to frame paints. | ||
/// - Throws: only if `UIScreen.main` cannot generate its `CADisplayLink` | ||
func start() throws { | ||
try private_start() | ||
isRunning = true | ||
} | ||
|
||
/// Stops listening frame paints. Automatically called at `deinit()`. | ||
func stop() { | ||
private_stop() | ||
isRunning = false | ||
} | ||
|
||
// MARK: - Private | ||
|
||
@objc | ||
private func displayTick(link: CADisplayLink) { | ||
if let lastTimestamp = self.lastFrameTimestamp { | ||
let frameDuration = link.timestamp - lastTimestamp | ||
let currentFPS = 1.0 / frameDuration | ||
// NOTE: RUMM-1278 `oldValue` is not used | ||
observers.forEach { | ||
$0.onValueChanged(oldValue: 0.0, newValue: currentFPS) | ||
} | ||
} | ||
lastFrameTimestamp = link.timestamp | ||
} | ||
|
||
@objc | ||
private func appWillResignActive() { | ||
private_stop() | ||
} | ||
|
||
@objc | ||
private func appDidBecomeActive() { | ||
if isRunning { | ||
try? private_start() | ||
} | ||
} | ||
|
||
private func private_start() throws { | ||
stop() | ||
|
||
guard let link = UIScreen.main.displayLink( | ||
withTarget: self, | ||
selector: #selector(displayTick(link:)) | ||
) else { | ||
throw InternalError(description: "CADisplayLink could not be created!") | ||
} | ||
link.add(to: .main, forMode: .default) | ||
self.displayLink = link | ||
} | ||
|
||
private func private_stop() { | ||
displayLink?.invalidate() | ||
displayLink = nil | ||
lastFrameTimestamp = nil | ||
} | ||
} |
103 changes: 103 additions & 0 deletions
103
Tests/DatadogTests/Datadog/RUM/RUMVitals/VitalRefreshRateReaderTests.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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
/* | ||
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. | ||
* This product includes software developed at Datadog (https://www.datadoghq.com/). | ||
* Copyright 2019-2020 Datadog, Inc. | ||
*/ | ||
|
||
import XCTest | ||
@testable import Datadog | ||
|
||
class VitalRefreshRateReaderTests: XCTestCase { | ||
private let mockNotificationCenter = NotificationCenter() | ||
|
||
func testRefreshRateReader() throws { | ||
let reader = VitalRefreshRateReader(notificationCenter: mockNotificationCenter) | ||
XCTAssertFalse(reader.isRunning) | ||
|
||
let observer_view1 = VitalObserver(listener: VitalListenerMock()) | ||
let observer_view2 = VitalObserver(listener: VitalListenerMock()) | ||
|
||
XCTAssertNoThrow(try reader.start()) | ||
XCTAssertTrue(reader.isRunning) | ||
|
||
reader.register(observer_view1) | ||
|
||
let expectation1 = expectation(description: "async expectation for first observer") | ||
DispatchQueue.global().async { | ||
Thread.sleep(forTimeInterval: 1.0) | ||
expectation1.fulfill() | ||
} | ||
|
||
waitForExpectations(timeout: 3.0) { _ in | ||
XCTAssertGreaterThan(observer_view1.vitalInfo.sampleCount, 0) | ||
XCTAssertGreaterThan(UIScreen.main.maximumFramesPerSecond, Int(observer_view1.vitalInfo.maxValue)) | ||
XCTAssertGreaterThan(observer_view1.vitalInfo.minValue, 0.0) | ||
} | ||
|
||
reader.register(observer_view1) | ||
|
||
let expectation2 = expectation(description: "async expectation for second observer") | ||
DispatchQueue.global().async { | ||
Thread.sleep(forTimeInterval: 1.0) | ||
expectation2.fulfill() | ||
} | ||
|
||
waitForExpectations(timeout: 3.0) { _ in | ||
XCTAssertGreaterThan(observer_view1.vitalInfo.sampleCount, observer_view2.vitalInfo.sampleCount) | ||
} | ||
} | ||
|
||
func testAppStateHandling() throws { | ||
let reader = VitalRefreshRateReader(notificationCenter: mockNotificationCenter) | ||
XCTAssertFalse(reader.isRunning) | ||
|
||
let observer = VitalObserver(listener: VitalListenerMock()) | ||
|
||
XCTAssertNoThrow(try reader.start()) | ||
XCTAssertTrue(reader.isRunning) | ||
|
||
mockNotificationCenter.post(name: UIApplication.willResignActiveNotification, object: nil) | ||
reader.register(observer) | ||
|
||
let expectation1 = expectation(description: "async expectation for first observer") | ||
DispatchQueue.global().async { | ||
Thread.sleep(forTimeInterval: 1.0) | ||
expectation1.fulfill() | ||
} | ||
|
||
waitForExpectations(timeout: 3.0) { _ in | ||
XCTAssertEqual(observer.vitalInfo.sampleCount, 0) | ||
} | ||
|
||
mockNotificationCenter.post(name: UIApplication.didBecomeActiveNotification, object: nil) | ||
|
||
let expectation2 = expectation(description: "async expectation for second observer") | ||
DispatchQueue.global().async { | ||
Thread.sleep(forTimeInterval: 1.0) | ||
expectation2.fulfill() | ||
} | ||
|
||
waitForExpectations(timeout: 3.0) { _ in | ||
XCTAssertGreaterThan(observer.vitalInfo.sampleCount, 0) | ||
} | ||
} | ||
|
||
func testReaderNotRestartIfNotAlreadyRunning() throws { | ||
let reader = VitalRefreshRateReader(notificationCenter: mockNotificationCenter) | ||
XCTAssertFalse(reader.isRunning) | ||
|
||
let observer = VitalObserver(listener: VitalListenerMock()) | ||
|
||
mockNotificationCenter.post(name: UIApplication.didBecomeActiveNotification, object: nil) | ||
|
||
let expectation = expectation(description: "async expectation for second observer") | ||
DispatchQueue.global().async { | ||
Thread.sleep(forTimeInterval: 1.0) | ||
expectation.fulfill() | ||
} | ||
|
||
waitForExpectations(timeout: 3.0) { _ in | ||
XCTAssertEqual(observer.vitalInfo.sampleCount, 0) | ||
} | ||
} | ||
} |