Skip to content

Commit

Permalink
Merge pull request #876 from DataDog/maxep/RUMM-2077/vitals-frequency
Browse files Browse the repository at this point in the history
RUMM-2077 Vitals Frequency Configuration
  • Loading branch information
maxep authored Jun 7, 2022
2 parents e2a961d + cf900c6 commit cdc9526
Show file tree
Hide file tree
Showing 17 changed files with 192 additions and 64 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Unreleased

### Changes
* [IMPROVEMENT] Add mobile vitals frequency configuration. See [#876][]

# 1.11.0-rc1 / 18-05-2022

Expand Down Expand Up @@ -367,6 +368,7 @@
[#837]: https://github.com/DataDog/dd-sdk-ios/issues/837
[#832]: https://github.com/DataDog/dd-sdk-ios/issues/832
[#851]: https://github.com/DataDog/dd-sdk-ios/issues/851
[#876]: https://github.com/DataDog/dd-sdk-ios/issues/876
[@00FA9A]: https://github.com/00FA9A
[@Britton-Earnin]: https://github.com/Britton-Earnin
[@Hengyu]: https://github.com/Hengyu
Expand Down
15 changes: 14 additions & 1 deletion Sources/Datadog/Core/FeaturesConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ internal struct FeaturesConfiguration {
let backgroundEventTrackingEnabled: Bool
let onSessionStart: RUMSessionListener?
let firstPartyHosts: Set<String>
let vitalsFrequency: TimeInterval?
}

struct URLSessionAutoInstrumentation {
Expand Down Expand Up @@ -219,7 +220,8 @@ extension FeaturesConfiguration {
instrumentation: instrumentation,
backgroundEventTrackingEnabled: configuration.rumBackgroundEventTrackingEnabled,
onSessionStart: configuration.rumSessionsListener,
firstPartyHosts: sanitizedHosts
firstPartyHosts: sanitizedHosts,
vitalsFrequency: configuration.mobileVitalsFrequency.timeInterval
)
} else {
let error = ProgrammerError(
Expand Down Expand Up @@ -288,6 +290,17 @@ extension FeaturesConfiguration {
}
}

extension Datadog.Configuration.VitalsFrequency {
var timeInterval: TimeInterval? {
switch self {
case .frequent: return 0.1
case .average: return 0.5
case .rare: return 1
case .never: return nil
}
}
}

private func ifValid(environment: String) throws -> String {
/// 1. cannot be more than 200 chars (including `env:` prefix)
/// 2. cannot end with `:`
Expand Down
22 changes: 22 additions & 0 deletions Sources/Datadog/DatadogConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,19 @@ extension Datadog {
case rare
}

/// Defines the frequency at which Datadog SDK will collect mobile vitals, such as CPU
/// and memory usage.
public enum VitalsFrequency {
/// Collect mobile vitals every 100ms.
case frequent
/// Collect mobile vitals every 500ms.
case average
/// Collect mobile vitals every 1000ms.
case rare
/// Don't provide mobile vitals.
case never
}

public enum DatadogEndpoint {
/// US based servers.
/// Sends data to [app.datadoghq.com](https://app.datadoghq.com/).
Expand Down Expand Up @@ -265,6 +278,7 @@ extension Datadog {
private(set) var rumResourceAttributesProvider: URLSessionRUMAttributesProvider?
private(set) var rumBackgroundEventTrackingEnabled: Bool
private(set) var rumTelemetrySamplingRate: Float
private(set) var mobileVitalsFrequency: VitalsFrequency
private(set) var batchSize: BatchSize
private(set) var uploadFrequency: UploadFrequency
private(set) var additionalConfiguration: [String: Any]
Expand Down Expand Up @@ -339,6 +353,7 @@ extension Datadog {
rumResourceAttributesProvider: nil,
rumBackgroundEventTrackingEnabled: false,
rumTelemetrySamplingRate: 20,
mobileVitalsFrequency: .rare,
batchSize: .medium,
uploadFrequency: .average,
additionalConfiguration: [:],
Expand Down Expand Up @@ -708,6 +723,13 @@ extension Datadog {
return self
}

/// Sets the preferred frequency for collecting mobile vitals.
/// - Parameter mobileVitalsFrequency: `.average` by default.
public func set(mobileVitalsFrequency: VitalsFrequency) -> Builder {
configuration.mobileVitalsFrequency = mobileVitalsFrequency
return self
}

// MARK: - Crash Reporting Configuration

/// Enables the crash reporting feature.
Expand Down
22 changes: 6 additions & 16 deletions Sources/Datadog/RUM/RUMFeature.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,8 @@ internal final class RUMFeature {
let networkConnectionInfoProvider: NetworkConnectionInfoProviderType
let carrierInfoProvider: CarrierInfoProviderType
let launchTimeProvider: LaunchTimeProviderType

let vitalCPUReader: SamplingBasedVitalReader
let vitalMemoryReader: SamplingBasedVitalReader
let vitalRefreshRateReader: ContinuousVitalReader

let onSessionStart: RUMSessionListener?
let telemetry: Telemetry?

// MARK: - Components

Expand Down Expand Up @@ -147,10 +143,8 @@ internal final class RUMFeature {
upload: upload,
configuration: configuration,
commonDependencies: commonDependencies,
vitalCPUReader: VitalCPUReader(telemetry: telemetry),
vitalMemoryReader: VitalMemoryReader(),
vitalRefreshRateReader: VitalRefreshRateReader(),
onSessionStart: configuration.onSessionStart
onSessionStart: configuration.onSessionStart,
telemetry: telemetry
)
}

Expand All @@ -160,10 +154,8 @@ internal final class RUMFeature {
upload: FeatureUpload,
configuration: FeaturesConfiguration.RUM,
commonDependencies: FeaturesCommonDependencies,
vitalCPUReader: SamplingBasedVitalReader,
vitalMemoryReader: SamplingBasedVitalReader,
vitalRefreshRateReader: ContinuousVitalReader,
onSessionStart: RUMSessionListener?
onSessionStart: RUMSessionListener?,
telemetry: Telemetry?
) {
// Configuration
self.configuration = configuration
Expand All @@ -183,10 +175,8 @@ internal final class RUMFeature {
self.storage = storage
self.upload = upload

self.vitalCPUReader = vitalCPUReader
self.vitalMemoryReader = vitalMemoryReader
self.vitalRefreshRateReader = vitalRefreshRateReader
self.onSessionStart = onSessionStart
self.telemetry = telemetry
}

internal func deinitialize() {
Expand Down
23 changes: 16 additions & 7 deletions Sources/Datadog/RUM/RUMMonitor/Scopes/RUMScopeDependencies.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ internal typealias RUMSessionListener = (String, Bool) -> Void

/// Dependency container for injecting components to `RUMScopes` hierarchy.
internal struct RUMScopeDependencies {
struct VitalsReaders {
let frequency: TimeInterval
let cpu: SamplingBasedVitalReader
let memory: SamplingBasedVitalReader
let refreshRate: ContinuousVitalReader
}

let rumApplicationID: String
let sessionSampler: Sampler
/// The start time of the application, indicated as SDK init. Measured in device time (without NTP correction).
Expand Down Expand Up @@ -37,10 +44,7 @@ internal struct RUMScopeDependencies {
/// Produces `RUMViewUpdatesThrottlerType` for each started RUM view scope.
let viewUpdatesThrottlerFactory: () -> RUMViewUpdatesThrottlerType

let vitalCPUReader: SamplingBasedVitalReader
let vitalMemoryReader: SamplingBasedVitalReader
let vitalRefreshRateReader: ContinuousVitalReader

let vitalsReaders: VitalsReaders?
let onSessionStart: RUMSessionListener?
}

Expand Down Expand Up @@ -74,9 +78,14 @@ internal extension RUMScopeDependencies {
crashContextIntegration: RUMWithCrashContextIntegration(),
ciTest: CITestIntegration.active?.rumCITest,
viewUpdatesThrottlerFactory: { RUMViewUpdatesThrottler() },
vitalCPUReader: rumFeature.vitalCPUReader,
vitalMemoryReader: rumFeature.vitalMemoryReader,
vitalRefreshRateReader: rumFeature.vitalRefreshRateReader,
vitalsReaders: rumFeature.configuration.vitalsFrequency.map {
.init(
frequency: $0,
cpu: VitalCPUReader(telemetry: rumFeature.telemetry),
memory: VitalMemoryReader(),
refreshRate: VitalRefreshRateReader()
)
},
onSessionStart: rumFeature.onSessionStart
)
}
Expand Down
36 changes: 19 additions & 17 deletions Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewScope.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ internal class RUMViewScope: RUMScope, RUMContextProvider {
/// It can be toggled from inside `RUMResourceScope`/`RUMUserActionScope` callbacks, as they are called from processing `RUMCommand`s inside `process()`.
private var needsViewUpdate = false

private let vitalInfoSampler: VitalInfoSampler
private let vitalInfoSampler: VitalInfoSampler?

/// Samples view update events, so we can minimize the number of events in payload.
private let viewUpdatesThrottler: RUMViewUpdatesThrottlerType
Expand All @@ -98,12 +98,14 @@ internal class RUMViewScope: RUMScope, RUMContextProvider {
self.viewName = name
self.viewStartTime = startTime
self.dateCorrection = dependencies.dateCorrector.currentCorrection

self.vitalInfoSampler = VitalInfoSampler(
cpuReader: dependencies.vitalCPUReader,
memoryReader: dependencies.vitalMemoryReader,
refreshRateReader: dependencies.vitalRefreshRateReader
)
self.vitalInfoSampler = dependencies.vitalsReaders.map {
.init(
cpuReader: $0.cpu,
memoryReader: $0.memory,
refreshRateReader: $0.refreshRate,
frequency: $0.frequency
)
}
self.viewUpdatesThrottler = dependencies.viewUpdatesThrottlerFactory()
}

Expand Down Expand Up @@ -375,10 +377,10 @@ internal class RUMViewScope: RUMScope, RUMContextProvider {
let isActive = isActiveView || !resourceScopes.isEmpty
// RUMM-2079 `time_spent` can't be lower than 1ns
let timeSpent = max(1e-9, command.time.timeIntervalSince(viewStartTime))
let cpuInfo = vitalInfoSampler.cpu
let memoryInfo = vitalInfoSampler.memory
let refreshRateInfo = vitalInfoSampler.refreshRate
let isSlowRendered = refreshRateInfo.meanValue.flatMap { $0 < Constants.slowRenderingThresholdFPS }
let cpuInfo = vitalInfoSampler?.cpu
let memoryInfo = vitalInfoSampler?.memory
let refreshRateInfo = vitalInfoSampler?.refreshRate
let isSlowRendered = refreshRateInfo?.meanValue.flatMap { $0 < Constants.slowRenderingThresholdFPS }

let eventData = RUMViewEvent(
dd: .init(
Expand All @@ -403,8 +405,8 @@ internal class RUMViewScope: RUMScope, RUMContextProvider {
version: dependencies.applicationVersion,
view: .init(
action: .init(count: actionsCount.toInt64),
cpuTicksCount: cpuInfo.greatestDiff,
cpuTicksPerSecond: cpuInfo.greatestDiff?.divideIfNotZero(by: Double(timeSpent)),
cpuTicksCount: cpuInfo?.greatestDiff,
cpuTicksPerSecond: cpuInfo?.greatestDiff?.divideIfNotZero(by: Double(timeSpent)),
crash: nil,
cumulativeLayoutShift: nil,
customTimings: customTimings.reduce(into: [:]) { acc, element in
Expand All @@ -427,12 +429,12 @@ internal class RUMViewScope: RUMScope, RUMContextProvider {
loadingTime: nil,
loadingType: nil,
longTask: .init(count: longTasksCount),
memoryAverage: memoryInfo.meanValue,
memoryMax: memoryInfo.maxValue,
memoryAverage: memoryInfo?.meanValue,
memoryMax: memoryInfo?.maxValue,
name: viewName,
referrer: nil,
refreshRateAverage: refreshRateInfo.meanValue,
refreshRateMin: refreshRateInfo.minValue,
refreshRateAverage: refreshRateInfo?.meanValue,
refreshRateMin: refreshRateInfo?.minValue,
resource: .init(count: resourcesCount.toInt64),
timeSpent: timeSpent.toInt64Nanoseconds,
url: viewPath
Expand Down
4 changes: 1 addition & 3 deletions Sources/Datadog/RUM/RUMVitals/VitalInfoSampler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ internal final class VitalInfoSampler {
static let normalizedRefreshRate = 60.0
}

private static let frequency: TimeInterval = 1.0

let cpuReader: SamplingBasedVitalReader
private let cpuPublisher = VitalPublisher(initialValue: VitalInfo())

Expand Down Expand Up @@ -54,7 +52,7 @@ internal final class VitalInfoSampler {
cpuReader: SamplingBasedVitalReader,
memoryReader: SamplingBasedVitalReader,
refreshRateReader: ContinuousVitalReader,
frequency: TimeInterval = VitalInfoSampler.frequency,
frequency: TimeInterval,
maximumRefreshRate: Double = Double(UIScreen.main.maximumFramesPerSecond)
) {
self.cpuReader = cpuReader
Expand Down
22 changes: 22 additions & 0 deletions Sources/DatadogObjc/DatadogConfiguration+objc.swift
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,23 @@ public enum DDUploadFrequency: Int {
}
}

@objc
public enum DDVitalsFrequency: Int {
case frequent
case average
case rare
case never

internal var swiftType: Datadog.Configuration.VitalsFrequency {
switch self {
case .frequent: return .frequent
case .average: return .average
case .rare: return .rare
case .never: return .never
}
}
}

@objc
public protocol DDDataEncryption: AnyObject {
/// Encrypts given `Data` with user-chosen encryption.
Expand Down Expand Up @@ -379,6 +396,11 @@ public class DDConfigurationBuilder: NSObject {
}
}

@objc
public func set(mobileVitalsFrequency: DDVitalsFrequency) {
_ = sdkBuilder.set(mobileVitalsFrequency: mobileVitalsFrequency.swiftType)
}

@objc
public func set(batchSize: DDBatchSize) {
_ = sdkBuilder.set(batchSize: batchSize.swiftType)
Expand Down
38 changes: 38 additions & 0 deletions Tests/DatadogTests/Datadog/Core/FeaturesConfigurationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,44 @@ class FeaturesConfigurationTests: XCTestCase {
XCTAssertNil(longTaskConfigured.rum!.instrumentation!.uiKitRUMUserActionsPredicate)
}

func testMobileVitalsFrequency() throws {
var custom = try FeaturesConfiguration(
configuration: .mockWith(
rumEnabled: true,
mobileVitalsFrequency: .average
),
appContext: .mockAny()
)
XCTAssertEqual(custom.rum?.vitalsFrequency, 0.5)

custom = try FeaturesConfiguration(
configuration: .mockWith(
rumEnabled: true,
mobileVitalsFrequency: .frequent
),
appContext: .mockAny()
)
XCTAssertEqual(custom.rum?.vitalsFrequency, 0.1)

custom = try FeaturesConfiguration(
configuration: .mockWith(
rumEnabled: true,
mobileVitalsFrequency: .rare
),
appContext: .mockAny()
)
XCTAssertEqual(custom.rum?.vitalsFrequency, 1)

custom = try FeaturesConfiguration(
configuration: .mockWith(
rumEnabled: true,
mobileVitalsFrequency: .never
),
appContext: .mockAny()
)
XCTAssertNil(custom.rum?.vitalsFrequency)
}

// MARK: - Crash Reporting Configuration Tests

func testWhenCrashReportingIsDisabled() throws {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ class DatadogConfigurationBuilderTests: XCTestCase {
XCTAssertNil(configuration.rumErrorEventMapper)
XCTAssertNil(configuration.rumLongTaskEventMapper)
XCTAssertNil(configuration.rumResourceAttributesProvider)
XCTAssertEqual(configuration.mobileVitalsFrequency, .rare)
XCTAssertEqual(configuration.batchSize, .medium)
XCTAssertEqual(configuration.uploadFrequency, .average)
XCTAssertEqual(configuration.additionalConfiguration.count, 0)
Expand Down Expand Up @@ -101,6 +102,7 @@ class DatadogConfigurationBuilderTests: XCTestCase {
.setRUMActionEventMapper { _ in mockRUMActionEvent }
.setRUMLongTaskEventMapper { _ in mockRUMLongTaskEvent }
.setRUMResourceAttributesProvider { _, _, _, _ in ["foo": "bar"] }
.set(mobileVitalsFrequency: .frequent)
.set(batchSize: .small)
.set(uploadFrequency: .frequent)
.set(additionalConfiguration: ["foo": 42, "bar": "something"])
Expand Down Expand Up @@ -162,6 +164,7 @@ class DatadogConfigurationBuilderTests: XCTestCase {
XCTAssertEqual(configuration.rumLongTaskEventMapper?(.mockRandom()), mockRUMLongTaskEvent)
XCTAssertEqual(configuration.rumResourceAttributesProvider?(.mockAny(), nil, nil, nil) as? [String: String], ["foo": "bar"])
XCTAssertFalse(configuration.rumBackgroundEventTrackingEnabled)
XCTAssertEqual(configuration.mobileVitalsFrequency, .frequent)
XCTAssertEqual(configuration.batchSize, .small)
XCTAssertEqual(configuration.uploadFrequency, .frequent)
XCTAssertEqual(configuration.additionalConfiguration["foo"] as? Int, 42)
Expand Down
Loading

0 comments on commit cdc9526

Please sign in to comment.