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

RUMM-1272 Implement a VitalObserver #493

Merged
merged 2 commits into from
May 20, 2021
Merged
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
8 changes: 8 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,14 @@ The workspace for SDK development and integration (tests, benchmarks, example ap

`DatadogTests` (unit tests), `DatadogIntegrationTests` (integration tests), and `DatadogBenchmarkTests` (benchmarks) source files

#### Lint

We're using swiftlint to ensure our codebase follows Swift standard syntax. You can run the lint with our custom rules with the following command line:

```shell
$ ./tools/lint/run-linter.sh
```

#### Dependency manager tests

Isolated example apps using `cocoapods`, `carthage` and `spm` to ensure SDK is well integrated with all supported dependency managers.
32 changes: 32 additions & 0 deletions Datadog/Datadog.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,10 @@
9EEA4871258B76A100EBDA9D /* Global+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EEA4870258B76A100EBDA9D /* Global+objc.swift */; };
9EF963E82537556300235F98 /* DDURLSessionDelegateAsSuperclassTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EF963E72537556300235F98 /* DDURLSessionDelegateAsSuperclassTests.swift */; };
9EFD112C24B32D29003A1A2B /* FirstPartyURLsFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EFD112B24B32D29003A1A2B /* FirstPartyURLsFilter.swift */; };
B3FC3C0926526F0000DEED9E /* VitalInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3FC3C0626526EFF00DEED9E /* VitalInfo.swift */; };
B3FC3C0A26526F0000DEED9E /* VitalObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3FC3C0726526F0000DEED9E /* VitalObserver.swift */; };
B3FC3C332652AE1400DEED9E /* VitalListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3FC3C322652AE1400DEED9E /* VitalListener.swift */; };
B3FC3C3C2653A97700DEED9E /* VitalObserverTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3FC3C3B2653A97700DEED9E /* VitalObserverTest.swift */; };
E132727B24B333C700952F8B /* TracingBenchmarkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E132727A24B333C700952F8B /* TracingBenchmarkTests.swift */; };
E132727D24B35B5F00952F8B /* TracingStorageBenchmarkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E132727C24B35B5F00952F8B /* TracingStorageBenchmarkTests.swift */; };
E13A880C257922EC004FB174 /* EnvironmentSpanIntegration.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13A880B257922EC004FB174 /* EnvironmentSpanIntegration.swift */; };
Expand Down Expand Up @@ -982,6 +986,10 @@
9EF49F17244770AD004F2CA0 /* DatadogIntegrationTests.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = DatadogIntegrationTests.xcconfig; sourceTree = "<group>"; };
9EF963E72537556300235F98 /* DDURLSessionDelegateAsSuperclassTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDURLSessionDelegateAsSuperclassTests.swift; sourceTree = "<group>"; };
9EFD112B24B32D29003A1A2B /* FirstPartyURLsFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirstPartyURLsFilter.swift; sourceTree = "<group>"; };
B3FC3C0626526EFF00DEED9E /* VitalInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VitalInfo.swift; sourceTree = "<group>"; };
B3FC3C0726526F0000DEED9E /* VitalObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VitalObserver.swift; sourceTree = "<group>"; };
B3FC3C322652AE1400DEED9E /* VitalListener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VitalListener.swift; sourceTree = "<group>"; };
B3FC3C3B2653A97700DEED9E /* VitalObserverTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VitalObserverTest.swift; sourceTree = "<group>"; };
E132727A24B333C700952F8B /* TracingBenchmarkTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracingBenchmarkTests.swift; sourceTree = "<group>"; };
E132727C24B35B5F00952F8B /* TracingStorageBenchmarkTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracingStorageBenchmarkTests.swift; sourceTree = "<group>"; };
E13A880B257922EC004FB174 /* EnvironmentSpanIntegration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentSpanIntegration.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2525,6 +2533,7 @@
61E5332A24B75C3B003D6C4E /* RUM */ = {
isa = PBXGroup;
children = (
B3FC3C0426526EE900DEED9E /* RUMVitals */,
61E5332B24B75C51003D6C4E /* RUMFeature.swift */,
61E5333224B7A504003D6C4E /* DataModels */,
61C3E63124BF143C008053F2 /* RUMMonitor */,
Expand All @@ -2542,6 +2551,7 @@
61E5332D24B75DC7003D6C4E /* RUM */ = {
isa = PBXGroup;
children = (
B3FC3C1226526F4100DEED9E /* RUMVitals */,
61E5332E24B75DE2003D6C4E /* RUMFeatureTests.swift */,
618715FA24DC5EE700FC0F69 /* DataModels */,
617B953B24BF4D7300E6F443 /* RUMMonitor */,
Expand Down Expand Up @@ -2842,6 +2852,24 @@
path = DatadogIntegrationTests;
sourceTree = "<group>";
};
B3FC3C0426526EE900DEED9E /* RUMVitals */ = {
isa = PBXGroup;
children = (
B3FC3C0626526EFF00DEED9E /* VitalInfo.swift */,
B3FC3C0726526F0000DEED9E /* VitalObserver.swift */,
B3FC3C322652AE1400DEED9E /* VitalListener.swift */,
);
path = RUMVitals;
sourceTree = "<group>";
};
B3FC3C1226526F4100DEED9E /* RUMVitals */ = {
isa = PBXGroup;
children = (
B3FC3C3B2653A97700DEED9E /* VitalObserverTest.swift */,
);
path = RUMVitals;
sourceTree = "<group>";
};
/* End PBXGroup section */

/* Begin PBXHeadersBuildPhase section */
Expand Down Expand Up @@ -3351,6 +3379,7 @@
61C5A88E24509A1F00DA608C /* Tracer.swift in Sources */,
61E909EE24A24DD3005EA2DE /* OTFormat.swift in Sources */,
9E26E6B924C87693000B3270 /* RUMDataModels.swift in Sources */,
B3FC3C0A26526F0000DEED9E /* VitalObserver.swift in Sources */,
613E793B2577B6EE00DFCC17 /* DataReader.swift in Sources */,
614B0A5324EBFE5500A2A780 /* DDRUMMonitor.swift in Sources */,
61133BE32423979B00786299 /* UserInfoProvider.swift in Sources */,
Expand Down Expand Up @@ -3380,10 +3409,12 @@
61133BD72423979B00786299 /* DataUploadWorker.swift in Sources */,
61133BD12423979B00786299 /* FilesOrchestrator.swift in Sources */,
61133BCD2423979B00786299 /* NetworkConnectionInfoProvider.swift in Sources */,
B3FC3C332652AE1400DEED9E /* VitalListener.swift in Sources */,
61FF282424B8A1C3000B3D9B /* RUMEventFileOutput.swift in Sources */,
61B22E5A24F3E6B700DC26D2 /* RUMDebugging.swift in Sources */,
61C5A88B24509A0C00DA608C /* SpanOutput.swift in Sources */,
61133BE42423979B00786299 /* LogEncoder.swift in Sources */,
B3FC3C0926526F0000DEED9E /* VitalInfo.swift in Sources */,
613E81F025A740140084B751 /* RUMEventsMapper.swift in Sources */,
61D980BA24E28D0100E03345 /* RUMIntegrations.swift in Sources */,
61C5A88424509A0C00DA608C /* DDSpan.swift in Sources */,
Expand Down Expand Up @@ -3511,6 +3542,7 @@
61C1510D25AC8C1B00362D4B /* RUMViewIdentityTests.swift in Sources */,
613E820525A879AF0084B751 /* RUMDataModelMocks.swift in Sources */,
61133C492423990D00786299 /* DDLoggerBuilderTests.swift in Sources */,
B3FC3C3C2653A97700DEED9E /* VitalObserverTest.swift in Sources */,
61133C4B2423990D00786299 /* DDLoggerTests.swift in Sources */,
618715FC24DC5F0800FC0F69 /* RUMDataModelsMappingTests.swift in Sources */,
61C5A89624509BF600DA608C /* TracerTests.swift in Sources */,
Expand Down
21 changes: 21 additions & 0 deletions Sources/Datadog/RUM/RUMVitals/VitalInfo.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* 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-Present Datadog, Inc.
*/

import Foundation

internal struct VitalInfo {
/// Number of sample for this info
internal let sampleCount: Int

/// Minimum value across all samples
internal let minValue: Double

/// Maximum value across all samples
internal let maxValue: Double

/// Average value across all samples
internal let meanValue: Double
}
12 changes: 12 additions & 0 deletions Sources/Datadog/RUM/RUMVitals/VitalListener.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* 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-Present Datadog, Inc.
*/

import Foundation

/// A listener getting aggregated information from a VitalObserver
internal protocol VitalListener {
func onVitalInfo(info: VitalInfo)
}
43 changes: 43 additions & 0 deletions Sources/Datadog/RUM/RUMVitals/VitalObserver.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* 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-Present Datadog, Inc.
*/

import Foundation

/// Provides interface for observing Vital info from a producer
internal class VitalObserver: ValueObserver {
let listener: VitalListener

private var vitalInfo = VitalInfo(
sampleCount: 0,
minValue: Double.greatestFiniteMagnitude,
maxValue: -Double.greatestFiniteMagnitude,
meanValue: 0.0
)

init(listener: VitalListener) {
self.listener = listener
}

// MARK: - ValueObserver
final func onValueChanged(oldValue: Double, newValue: Double) {
let newSampleCount = vitalInfo.sampleCount + 1
// Assuming M(n) is the mean value of the first n samples
// M(n) = ∑ sample(n) / n
// n⨉M(n) = ∑ sample(n)
// M(n+1) = ∑ sample(n+1) / (n+1)
// = [ sample(n+1) + ∑ sample(n) ] / (n+1)
// = (sample(n+1) + n⨉M(n)) / (n+1)
let newMeanValue = (newValue + (Double(vitalInfo.sampleCount) * vitalInfo.meanValue)) / Double(newSampleCount)
let newVitalInfo = VitalInfo(
sampleCount: newSampleCount,
minValue: min(vitalInfo.minValue, newValue),
maxValue: max(vitalInfo.maxValue, newValue),
meanValue: newMeanValue
)
vitalInfo = newVitalInfo
listener.onVitalInfo(info: newVitalInfo)
}
}
8 changes: 8 additions & 0 deletions Tests/DatadogTests/Datadog/Mocks/RUMFeatureMocks.swift
Original file line number Diff line number Diff line change
Expand Up @@ -671,3 +671,11 @@ class UIKitRUMUserActionsHandlerMock: UIKitRUMUserActionsHandlerType {
onSendEvent?(application, event)
}
}

class VitalListenerMock: VitalListener {
var onVitalInfoUpdate: ((VitalInfo) -> Void)?

func onVitalInfo(info: VitalInfo) {
onVitalInfoUpdate?(info)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* 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-Present Datadog, Inc.
*/

import XCTest
@testable import Datadog

class VitalObserverTest: XCTestCase {
func testItUpdatesVitalInfoOnFirstValue() {
let randomOldValue = Double.random(in: -65_536.0...65_536.0)
let randomValue = Double.random(in: -65_536.0...65_536.0)
let notifyExpectation = expectation(description: "Notify vital info")
let mockListener = VitalListenerMock()
let testedObserver = VitalObserver(listener: mockListener)
mockListener.onVitalInfoUpdate = { vitalInfo in
XCTAssertEqual(vitalInfo.minValue, randomValue)
XCTAssertEqual(vitalInfo.maxValue, randomValue)
XCTAssertEqual(vitalInfo.meanValue, randomValue)
XCTAssertEqual(vitalInfo.sampleCount, 1)
notifyExpectation.fulfill()
}

// When
testedObserver.onValueChanged(oldValue: randomOldValue, newValue: randomValue)

// Then
wait(for: [notifyExpectation], timeout: 0.5, enforceOrder: true)
}

func testItUpdatesVitalInfoOnMultipleValue() {
let randomOldValue = Double.random(in: -65_536.0...65_536.0)
let randomValue1 = Double.random(in: -65_536.0...65_536.0)
let randomValue2 = Double.random(in: -65_536.0...65_536.0)
let randomValue3 = Double.random(in: -65_536.0...65_536.0)
let notifyExpectation1 = expectation(description: "Notify vital info 1")
let notifyExpectation2 = expectation(description: "Notify vital info 2")
let notifyExpectation3 = expectation(description: "Notify vital info 3")
let mockListener = VitalListenerMock()
let testedObserver = VitalObserver(listener: mockListener)

// When
mockListener.onVitalInfoUpdate = { vitalInfo in
XCTAssertEqual(vitalInfo.minValue, randomValue1)
XCTAssertEqual(vitalInfo.maxValue, randomValue1)
XCTAssertEqual(vitalInfo.meanValue, randomValue1)
XCTAssertEqual(vitalInfo.sampleCount, 1)
notifyExpectation1.fulfill()
}
testedObserver.onValueChanged(oldValue: randomOldValue, newValue: randomValue1)
mockListener.onVitalInfoUpdate = { vitalInfo in
XCTAssertEqual(vitalInfo.minValue, min(randomValue1, randomValue2))
XCTAssertEqual(vitalInfo.maxValue, max(randomValue1, randomValue2))
XCTAssertEqual(vitalInfo.meanValue, (randomValue1 + randomValue2) / 2.0)
XCTAssertEqual(vitalInfo.sampleCount, 2)
notifyExpectation2.fulfill()
}
testedObserver.onValueChanged(oldValue: randomValue1, newValue: randomValue2)
mockListener.onVitalInfoUpdate = { vitalInfo in
XCTAssertEqual(vitalInfo.minValue, min(randomValue1, min(randomValue2, randomValue3)))
XCTAssertEqual(vitalInfo.maxValue, max(randomValue1, max(randomValue2, randomValue3)))
XCTAssertEqual(vitalInfo.meanValue, (randomValue1 + randomValue2 + randomValue3) / 3.0)
XCTAssertEqual(vitalInfo.sampleCount, 3)
notifyExpectation3.fulfill()
}
testedObserver.onValueChanged(oldValue: randomValue2, newValue: randomValue3)

// Then
wait(for: [notifyExpectation1, notifyExpectation2, notifyExpectation3], timeout: 0.5, enforceOrder: true)
}
}