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-824 Add API for customizing batch size and upload frequency #358

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
4 changes: 4 additions & 0 deletions Datadog/Example/AppConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ struct ExampleAppConfiguration: AppConfiguration {
environment: "tests"
)
.set(serviceName: serviceName)
.set(batchSize: .small)
.set(uploadFrequency: .frequent)

// If the app was launched with test scenarion ENV, apply the scenario configuration
if let testScenario = testScenario {
Expand Down Expand Up @@ -75,6 +77,8 @@ struct UITestsAppConfiguration: AppConfiguration {
environment: "integration"
)
.set(serviceName: "ui-tests-service-name")
.set(batchSize: .small)
.set(uploadFrequency: .frequent)

let serverMockConfiguration = Environment.serverMockConfiguration()

Expand Down
6 changes: 5 additions & 1 deletion Sources/Datadog/Core/FeaturesConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,11 @@ extension FeaturesConfiguration {
applicationBundleIdentifier: appContext.bundleIdentifier ?? "unknown",
serviceName: configuration.serviceName ?? appContext.bundleIdentifier ?? "ios",
environment: try ifValid(environment: configuration.environment),
performance: .best(for: appContext.bundleType)
performance: PerformancePreset(
batchSize: configuration.batchSize,
uploadFrequency: configuration.uploadFrequency,
bundleType: appContext.bundleType
)
)

if configuration.loggingEnabled {
Expand Down
116 changes: 70 additions & 46 deletions Sources/Datadog/Core/PerformancePreset.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,56 +68,80 @@ internal struct PerformancePreset: Equatable, StoragePerformancePreset, UploadPe
let minUploadDelay: TimeInterval
let maxUploadDelay: TimeInterval
let uploadDelayChangeRate: Double
}

// MARK: - Predefined presets

/// Default performance preset.
static let `default` = lowRuntimeImpact

/// Performance preset optimized for low runtime impact.
/// Minimalizes number of data requests send to the server.
static let lowRuntimeImpact = PerformancePreset(
// persistence
maxFileSize: 4 * 1_024 * 1_024, // 4MB
maxDirectorySize: 512 * 1_024 * 1_024, // 512 MB
maxFileAgeForWrite: 4.75,
minFileAgeForRead: 4.75 + 0.5, // `maxFileAgeForWrite` + 0.5s margin
maxFileAgeForRead: 18 * 60 * 60, // 18h
maxObjectsInFile: 500,
maxObjectSize: 256 * 1_024, // 256KB
internal extension PerformancePreset {
init(
batchSize: Datadog.Configuration.BatchSize,
uploadFrequency: Datadog.Configuration.UploadFrequency,
bundleType: BundleType
) {
let meanFileAgeInSeconds: TimeInterval = {
switch (bundleType, batchSize) {
case (.iOSApp, .small): return 5
Copy link
Member

Choose a reason for hiding this comment

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

Out of curiosity, what's the difference between iOSApp and iOSAppExtension ?

Copy link
Contributor

Choose a reason for hiding this comment

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

this is due to GH issue number 52
(i'm not giving the actual refererence to it on purpose, no need to alert it i guess)

Copy link
Member Author

Choose a reason for hiding this comment

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

iOS App Extensions are the way to add functionality beyond the app so they can interact with other apps or the system (e.g. home screen widgets).

We differentiate PerformancePreset in app extension, because its process is usually designed to work only for the very short amount of time, when the user interacts with it, e.g. chooses the Slack channel and message to upload photo directly from the iOS photo library through Slack Extension (w/o launching the Slack app).

Such PerformancePreset for app extension stores and uploads data much mor frequently + its initial upload is scheduled almost right away when the app extension starts. This is to make sure the SDK has enough chance to upload all data before the extension is closed (which usually happens in few seconds after it was started).

case (.iOSApp, .medium): return 15
case (.iOSApp, .large): return 60
case (.iOSAppExtension, .small): return 1
case (.iOSAppExtension, .medium): return 3
case (.iOSAppExtension, .large): return 12
}
}()

// upload
initialUploadDelay: 5, // postpone to not impact app launch time
defaultUploadDelay: 5,
minUploadDelay: 1,
maxUploadDelay: 20,
uploadDelayChangeRate: 0.1
)
let minUploadDelayInSeconds: TimeInterval = {
switch (bundleType, uploadFrequency) {
case (.iOSApp, .frequent): return 1
case (.iOSApp, .average): return 5
case (.iOSApp, .rare): return 10
case (.iOSAppExtension, .frequent): return 0.5
case (.iOSAppExtension, .average): return 1
case (.iOSAppExtension, .rare): return 5
}
}()

/// Performance preset optimized for instant data delivery.
/// Minimalizes the time between receiving data form the user and delivering it to the server.
static let instantDataDelivery = PerformancePreset(
// persistence
maxFileSize: `default`.maxFileSize,
maxDirectorySize: `default`.maxDirectorySize,
maxFileAgeForWrite: 2.75,
minFileAgeForRead: 2.75 + 0.5, // `maxFileAgeForWrite` + 0.5s margin
maxFileAgeForRead: `default`.maxFileAgeForRead,
maxObjectsInFile: `default`.maxObjectsInFile,
maxObjectSize: `default`.maxObjectSize,
let uploadDelayFactors: (initial: Double, default: Double, min: Double, max: Double, changeRate: Double) = {
switch bundleType {
case .iOSApp:
return (
initial: 5,
default: 5,
min: 1,
max: 10,
changeRate: 0.1
)
case .iOSAppExtension:
return (
initial: 0.5, // ensures the the first upload is checked quickly after starting the short-lived app extension
default: 3,
min: 1,
max: 5,
changeRate: 0.5 // if batches are found, reduces interval significantly for more uploads in short-lived app extension
)
}
}()

// upload
initialUploadDelay: 0.5, // send quick to have a chance for upload in short-lived app extensions
defaultUploadDelay: 3,
minUploadDelay: 1,
maxUploadDelay: 5,
uploadDelayChangeRate: 0.5 // reduce significantly for more uploads in short-lived app extensions
)
self.init(
meanFileAge: meanFileAgeInSeconds,
minUploadDelay: minUploadDelayInSeconds,
uploadDelayFactors: uploadDelayFactors
)
}

static func best(for bundleType: BundleType) -> PerformancePreset {
switch bundleType {
case .iOSApp: return `default`
case .iOSAppExtension: return instantDataDelivery
}
init(
meanFileAge: TimeInterval,
minUploadDelay: TimeInterval,
uploadDelayFactors: (initial: Double, default: Double, min: Double, max: Double, changeRate: Double)
) {
self.maxFileSize = 4 * 1_024 * 1_024 // 4MB
self.maxDirectorySize = 512 * 1_024 * 1_024 // 512 MB
self.maxFileAgeForWrite = meanFileAge * 0.95 // 5% below the mean age
self.minFileAgeForRead = meanFileAge * 1.05 // 5% above the mean age
self.maxFileAgeForRead = 18 * 60 * 60 // 18h
self.maxObjectsInFile = 500
self.maxObjectSize = 256 * 1_024 // 256KB
self.initialUploadDelay = minUploadDelay * uploadDelayFactors.initial
self.defaultUploadDelay = minUploadDelay * uploadDelayFactors.default
self.minUploadDelay = minUploadDelay * uploadDelayFactors.min
self.maxUploadDelay = minUploadDelay * uploadDelayFactors.max
self.uploadDelayChangeRate = uploadDelayFactors.changeRate
}
}
43 changes: 42 additions & 1 deletion Sources/Datadog/DatadogConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,27 @@ extension Datadog {

/// Datadog SDK configuration.
public struct Configuration {
/// Defines the Datadog SDK policy when batching data together before uploading it to Datadog servers.
/// Smaller batches mean smaller but more network requests, whereas larger batches mean fewer but larger network requests.
public enum BatchSize {
/// Prefer small sized data batches.
case small
/// Prefer medium sized data batches.
case medium
/// Prefer large sized data batches.
case large
}

/// Defines the frequency at which Datadog SDK will try to upload data batches.
public enum UploadFrequency {
/// Try to upload batched data frequently.
case frequent
/// Try to upload batched data with a medium frequency.
case average
/// Try to upload batched data rarely.
case rare
}

public enum DatadogEndpoint {
/// US based servers.
/// Sends data to [app.datadoghq.com](https://app.datadoghq.com/).
Expand Down Expand Up @@ -152,6 +173,8 @@ extension Datadog {
private(set) var rumSessionsSamplingRate: Float
private(set) var rumUIKitViewsPredicate: UIKitRUMViewsPredicate?
private(set) var rumUIKitActionsTrackingEnabled: Bool
private(set) var batchSize: BatchSize
private(set) var uploadFrequency: UploadFrequency

/// Creates the builder for configuring the SDK to work with RUM, Logging and Tracing features.
/// - Parameter rumApplicationID: RUM Application ID obtained on Datadog website.
Expand Down Expand Up @@ -209,7 +232,9 @@ extension Datadog {
firstPartyHosts: nil,
rumSessionsSamplingRate: 100.0,
rumUIKitViewsPredicate: nil,
rumUIKitActionsTrackingEnabled: false
rumUIKitActionsTrackingEnabled: false,
batchSize: .medium,
uploadFrequency: .average
)
}

Expand Down Expand Up @@ -425,6 +450,22 @@ extension Datadog {
return self
}

/// Sets the preferred size of batched data uploaded to Datadog servers.
/// This value impacts the size and number of requests performed by the SDK.
/// - Parameter batchSize: `.medium` by default.
public func set(batchSize: BatchSize) -> Builder {
configuration.batchSize = batchSize
return self
}

/// Sets the preferred frequency of uploading data to Datadog servers.
/// This value impacts the frequency of performing network requests by the SDK.
/// - Parameter uploadFrequency: `.average` by default.
public func set(uploadFrequency: UploadFrequency) -> Builder {
configuration.uploadFrequency = uploadFrequency
return self
}

/// Builds `Datadog.Configuration` object.
public func build() -> Configuration {
return configuration
Expand Down
38 changes: 38 additions & 0 deletions Sources/DatadogObjc/DatadogConfiguration+objc.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,36 @@ public class DDTracesEndpoint: NSObject {
public static func custom(url: String) -> DDTracesEndpoint { .init(sdkEndpoint: .custom(url: url)) }
}

@objc
public enum DDBatchSize: Int {
case small
case medium
case large

internal var swiftType: Datadog.Configuration.BatchSize {
switch self {
case .small: return .small
case .medium: return .medium
case .large: return .large
}
}
}

@objc
public enum DDUploadFrequency: Int {
case frequent
case average
case rare

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

@objcMembers
public class DDConfiguration: NSObject {
internal let sdkConfiguration: Datadog.Configuration
Expand Down Expand Up @@ -157,6 +187,14 @@ public class DDConfigurationBuilder: NSObject {
_ = sdkBuilder.trackUIKitActions(true)
}

public func set(batchSize: DDBatchSize) {
_ = sdkBuilder.set(batchSize: batchSize.swiftType)
}

public func set(uploadFrequency: DDUploadFrequency) {
_ = sdkBuilder.set(uploadFrequency: uploadFrequency.swiftType)
}

public func build() -> DDConfiguration {
return DDConfiguration(sdkConfiguration: sdkBuilder.build())
}
Expand Down
6 changes: 5 additions & 1 deletion Tests/DatadogBenchmarkTests/BenchmarkMocks.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,15 @@ private struct DateCorrectorMock: DateCorrectorType {
}
}

extension PerformancePreset {
static let benchmarksPreset = PerformancePreset(batchSize: .small, uploadFrequency: .frequent, bundleType: .iOSApp)
}

extension FeaturesCommonDependencies {
static func mockAny() -> Self {
return .init(
consentProvider: ConsentProvider(initialConsent: .granted),
performance: .default,
performance: .benchmarksPreset,
httpClient: HTTPClient(),
mobileDevice: .current,
dateProvider: SystemDateProvider(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ class LoggingStorageBenchmarkTests: XCTestCase {
}

// Wait enough time for `reader` to accept the youngest batch file
Thread.sleep(forTimeInterval: PerformancePreset.default.minFileAgeForRead + 0.1)
Thread.sleep(forTimeInterval: PerformancePreset.benchmarksPreset.minFileAgeForRead + 0.1)

measureMetrics([.wallClockTime], automaticallyStartMeasuring: false) {
self.startMeasuring()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ class RUMStorageBenchmarkTests: XCTestCase {
}

// Wait enough time for `reader` to accept the youngest batch file
Thread.sleep(forTimeInterval: PerformancePreset.default.minFileAgeForRead + 0.1)
Thread.sleep(forTimeInterval: PerformancePreset.benchmarksPreset.minFileAgeForRead + 0.1)

measureMetrics([.wallClockTime], automaticallyStartMeasuring: false) {
self.startMeasuring()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ class TracingStorageBenchmarkTests: XCTestCase {
}

// Wait enough time for `reader` to accept the youngest batch file
Thread.sleep(forTimeInterval: PerformancePreset.default.minFileAgeForRead + 0.1)
Thread.sleep(forTimeInterval: PerformancePreset.benchmarksPreset.minFileAgeForRead + 0.1)

measureMetrics([.wallClockTime], automaticallyStartMeasuring: false) {
self.startMeasuring()
Expand Down
25 changes: 15 additions & 10 deletions Tests/DatadogTests/Datadog/Core/FeaturesConfigurationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -104,16 +104,21 @@ class FeaturesConfigurationTests: XCTestCase {
verify(invalidEnvironmentName: String(repeating: "a", count: 197))
}

func testPerformance() throws {
let iOSAppConfiguration = try FeaturesConfiguration(
configuration: .mockAny(), appContext: .mockWith(bundleType: .iOSApp)
)
XCTAssertEqual(iOSAppConfiguration.common.performance, .lowRuntimeImpact)

let iOSAppExtensionConfiguration = try FeaturesConfiguration(
configuration: .mockAny(), appContext: .mockWith(bundleType: .iOSAppExtension)
)
XCTAssertEqual(iOSAppExtensionConfiguration.common.performance, .instantDataDelivery)
func testPerformancePreset() throws {
try BatchSize.allCases
.combined(with: UploadFrequency.allCases)
.combined(with: BundleType.allCases)
.map { ($0.0, $0.1, $1) }
.forEach { batchSize, uploadFrequency, bundleType in
let actualPerformancePreset = try FeaturesConfiguration(
configuration: .mockWith(batchSize: batchSize,uploadFrequency: uploadFrequency),
appContext: .mockWith(bundleType: bundleType)
).common.performance

let expectedPerformancePreset = PerformancePreset(batchSize: batchSize, uploadFrequency: uploadFrequency, bundleType: bundleType)

XCTAssertEqual(actualPerformancePreset, expectedPerformancePreset)
}
}

func testEndpoint() throws {
Expand Down
Loading