Skip to content

Commit

Permalink
RUMM-1278 VitalRefreshRateReader is added
Browse files Browse the repository at this point in the history
  • Loading branch information
buranmert committed Jun 23, 2021
1 parent 01e4c85 commit 67f4237
Show file tree
Hide file tree
Showing 4 changed files with 225 additions and 1 deletion.
8 changes: 8 additions & 0 deletions Datadog/Datadog.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -461,7 +461,9 @@
9E58E8E324615EDA008E5063 /* JSONEncoderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E58E8E224615EDA008E5063 /* JSONEncoderTests.swift */; };
9E68FB55244707FD0013A8AA /* ObjcExceptionHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 9E68FB53244707FD0013A8AA /* ObjcExceptionHandler.m */; };
9E68FB56244707FD0013A8AA /* ObjcExceptionHandler.h in Headers */ = {isa = PBXBuildFile; fileRef = 9E68FB54244707FD0013A8AA /* ObjcExceptionHandler.h */; settings = {ATTRIBUTES = (Public, ); }; };
9E986C302677B91400D62490 /* VitalRefreshRateReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E986C2F2677B91400D62490 /* VitalRefreshRateReaderTests.swift */; };
9E989A4225F640D100235FC3 /* AppStateListenerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E989A4125F640D100235FC3 /* AppStateListenerTests.swift */; };
9EA3CA6926775A3500B16871 /* VitalRefreshRateReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EA3CA6826775A3500B16871 /* VitalRefreshRateReader.swift */; };
9EC8B5DA2668197B000F7529 /* VitalCPUReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EC8B5D92668197B000F7529 /* VitalCPUReader.swift */; };
9EC8B5EE2668E4DB000F7529 /* VitalCPUReaderTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EC8B5ED2668E4DB000F7529 /* VitalCPUReaderTest.swift */; };
9ED6A6B425F2901800CB2E29 /* AppStateListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ED6A6B325F2901800CB2E29 /* AppStateListener.swift */; };
Expand Down Expand Up @@ -1074,8 +1076,10 @@
9E58E8E224615EDA008E5063 /* JSONEncoderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONEncoderTests.swift; sourceTree = "<group>"; };
9E68FB53244707FD0013A8AA /* ObjcExceptionHandler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ObjcExceptionHandler.m; sourceTree = "<group>"; };
9E68FB54244707FD0013A8AA /* ObjcExceptionHandler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ObjcExceptionHandler.h; sourceTree = "<group>"; };
9E986C2F2677B91400D62490 /* VitalRefreshRateReaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VitalRefreshRateReaderTests.swift; sourceTree = "<group>"; };
9E989A4125F640D100235FC3 /* AppStateListenerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStateListenerTests.swift; sourceTree = "<group>"; };
9E9EB37624468CE90002C80B /* Datadog.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = Datadog.modulemap; sourceTree = "<group>"; };
9EA3CA6826775A3500B16871 /* VitalRefreshRateReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VitalRefreshRateReader.swift; sourceTree = "<group>"; };
9EC8B5D92668197B000F7529 /* VitalCPUReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VitalCPUReader.swift; sourceTree = "<group>"; };
9EC8B5ED2668E4DB000F7529 /* VitalCPUReaderTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VitalCPUReaderTest.swift; sourceTree = "<group>"; };
9ED6A6B325F2901800CB2E29 /* AppStateListener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStateListener.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3070,6 +3074,7 @@
B3FC3C0426526EE900DEED9E /* RUMVitals */ = {
isa = PBXGroup;
children = (
9EA3CA6826775A3500B16871 /* VitalRefreshRateReader.swift */,
9EC8B5D92668197B000F7529 /* VitalCPUReader.swift */,
B3BBBCB0265E71C600943419 /* VitalMemoryReader.swift */,
B3BBBCB1265E71C600943419 /* VitalReader.swift */,
Expand All @@ -3086,6 +3091,7 @@
9EC8B5ED2668E4DB000F7529 /* VitalCPUReaderTest.swift */,
B3BBBCBB265E71D100943419 /* VitalMemoryReaderTest.swift */,
B3FC3C3B2653A97700DEED9E /* VitalObserverTest.swift */,
9E986C2F2677B91400D62490 /* VitalRefreshRateReaderTests.swift */,
);
path = RUMVitals;
sourceTree = "<group>";
Expand Down Expand Up @@ -3783,6 +3789,7 @@
614E9EB3244719FA007EE3E1 /* BundleType.swift in Sources */,
61F3CDA72512144600C816E5 /* UIKitRUMViewsPredicate.swift in Sources */,
61133BCE2423979B00786299 /* BatteryStatusProvider.swift in Sources */,
9EA3CA6926775A3500B16871 /* VitalRefreshRateReader.swift in Sources */,
E13A880C257922EC004FB174 /* EnvironmentSpanIntegration.swift in Sources */,
61B038602527247200518F3C /* URLSessionTracingHandler.swift in Sources */,
61122ED425B1B84D00F9C7F5 /* RUMEventSanitizer.swift in Sources */,
Expand Down Expand Up @@ -3834,6 +3841,7 @@
61411B1024EC15AC0012EAB2 /* Casting+RUM.swift in Sources */,
61133C622423990D00786299 /* InternalLoggersTests.swift in Sources */,
61FF283024BC5E2D000B3D9B /* RUMEventFileOutputTests.swift in Sources */,
9E986C302677B91400D62490 /* VitalRefreshRateReaderTests.swift in Sources */,
61133C582423990D00786299 /* FileWriterTests.swift in Sources */,
61E917D3246546BF00E6C631 /* TracerConfigurationTests.swift in Sources */,
61C5A89D24509C1100DA608C /* DDSpanTests.swift in Sources */,
Expand Down
2 changes: 1 addition & 1 deletion Sources/Datadog/RUM/RUMVitals/VitalObserver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import Foundation
internal class VitalObserver: ValueObserver {
let listener: VitalListener

private var vitalInfo = VitalInfo(
private(set) var vitalInfo = VitalInfo(
sampleCount: 0,
minValue: Double.greatestFiniteMagnitude,
maxValue: -Double.greatestFiniteMagnitude,
Expand Down
112 changes: 112 additions & 0 deletions Sources/Datadog/RUM/RUMVitals/VitalRefreshRateReader.swift
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
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
* 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
import UIKit
@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 expectation1 = expectation(description: "async expectation for second observer")
DispatchQueue.global().async {
Thread.sleep(forTimeInterval: 1.0)
expectation1.fulfill()
}

waitForExpectations(timeout: 3.0) { _ in
XCTAssertEqual(observer.vitalInfo.sampleCount, 0)
}
}
}

0 comments on commit 67f4237

Please sign in to comment.