diff --git a/Datadog/Datadog.xcodeproj/project.pbxproj b/Datadog/Datadog.xcodeproj/project.pbxproj index 10ab2384b5..bd04bdab3d 100644 --- a/Datadog/Datadog.xcodeproj/project.pbxproj +++ b/Datadog/Datadog.xcodeproj/project.pbxproj @@ -665,6 +665,7 @@ D255382A288F0B2400727FAD /* LowPowerModePublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2553828288F0B2300727FAD /* LowPowerModePublisher.swift */; }; D255382C288F161500727FAD /* BatteryStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = D255382B288F161500727FAD /* BatteryStatus.swift */; }; D255382D288F161500727FAD /* BatteryStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = D255382B288F161500727FAD /* BatteryStatus.swift */; }; + D2612F48290197C700509B7D /* LaunchTimePublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2C7E3AD28FEBDA10023B2CC /* LaunchTimePublisher.swift */; }; D26C49AF2886DC7B00802B2D /* ApplicationStatePublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D26C49AE2886DC7B00802B2D /* ApplicationStatePublisherTests.swift */; }; D26C49B02886DC7B00802B2D /* ApplicationStatePublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D26C49AE2886DC7B00802B2D /* ApplicationStatePublisherTests.swift */; }; D26C49B22886E10E00802B2D /* AppStateHistoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D26C49B12886E10E00802B2D /* AppStateHistoryTests.swift */; }; @@ -697,8 +698,6 @@ D2A1EE27287C35DE00D28DFB /* ContextValueReaderMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A1EE25287C35DE00D28DFB /* ContextValueReaderMock.swift */; }; D2A1EE29287D914900D28DFB /* LaunchTime.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A1EE28287D914900D28DFB /* LaunchTime.swift */; }; D2A1EE2A287D914900D28DFB /* LaunchTime.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A1EE28287D914900D28DFB /* LaunchTime.swift */; }; - D2A1EE2C287D91D900D28DFB /* LaunchTimeReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A1EE2B287D91D900D28DFB /* LaunchTimeReader.swift */; }; - D2A1EE2D287D91D900D28DFB /* LaunchTimeReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A1EE2B287D91D900D28DFB /* LaunchTimeReader.swift */; }; D2A1EE2F287DA4F200D28DFB /* UserInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A1EE2E287DA4F200D28DFB /* UserInfo.swift */; }; D2A1EE30287DA4F200D28DFB /* UserInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A1EE2E287DA4F200D28DFB /* UserInfo.swift */; }; D2A1EE32287DA51900D28DFB /* UserInfoPublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A1EE31287DA51900D28DFB /* UserInfoPublisher.swift */; }; @@ -709,8 +708,8 @@ D2A1EE39287EEB7600D28DFB /* NetworkConnectionInfoPublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A1EE37287EBE4200D28DFB /* NetworkConnectionInfoPublisherTests.swift */; }; D2A1EE3B287EECC000D28DFB /* CarrierInfoPublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A1EE3A287EECA800D28DFB /* CarrierInfoPublisherTests.swift */; }; D2A1EE3C287EECC200D28DFB /* CarrierInfoPublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A1EE3A287EECA800D28DFB /* CarrierInfoPublisherTests.swift */; }; - D2A1EE3E2885D7EC00D28DFB /* LaunchTimeReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A1EE3D2885D7EC00D28DFB /* LaunchTimeReaderTests.swift */; }; - D2A1EE3F2885D7EC00D28DFB /* LaunchTimeReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A1EE3D2885D7EC00D28DFB /* LaunchTimeReaderTests.swift */; }; + D2A1EE3E2885D7EC00D28DFB /* LaunchTimePublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A1EE3D2885D7EC00D28DFB /* LaunchTimePublisherTests.swift */; }; + D2A1EE3F2885D7EC00D28DFB /* LaunchTimePublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A1EE3D2885D7EC00D28DFB /* LaunchTimePublisherTests.swift */; }; D2A1EE442886B8B400D28DFB /* UserInfoPublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A1EE432886B8B400D28DFB /* UserInfoPublisherTests.swift */; }; D2A1EE452886B8B400D28DFB /* UserInfoPublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A1EE432886B8B400D28DFB /* UserInfoPublisherTests.swift */; }; D2B3F0442823EE8400C2B5EE /* DataBlockTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2B3F0432823EE8300C2B5EE /* DataBlockTests.swift */; }; @@ -724,6 +723,7 @@ D2B3F052282E827700C2B5EE /* DDHTTPHeadersWriter+apiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D2B3F051282E826A00C2B5EE /* DDHTTPHeadersWriter+apiTests.m */; }; D2B3F053282E827B00C2B5EE /* DDHTTPHeadersWriter+apiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D2B3F051282E826A00C2B5EE /* DDHTTPHeadersWriter+apiTests.m */; }; D2C7E3AB28F97DCF0023B2CC /* BatteryStatusPublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2C7E3AA28F97DCF0023B2CC /* BatteryStatusPublisherTests.swift */; }; + D2C7E3AE28FEBDA10023B2CC /* LaunchTimePublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2C7E3AD28FEBDA10023B2CC /* LaunchTimePublisher.swift */; }; D2CB6E0C27C50EAE00A62B57 /* Datadog.h in Headers */ = {isa = PBXBuildFile; fileRef = 61133B85242393DE00786299 /* Datadog.h */; settings = {ATTRIBUTES = (Public, ); }; }; D2CB6E0D27C50EAE00A62B57 /* ObjcAppLaunchHandler.h in Headers */ = {isa = PBXBuildFile; fileRef = 6179FFD1254ADB1100556A0B /* ObjcAppLaunchHandler.h */; settings = {ATTRIBUTES = (Public, ); }; }; D2CB6E0E27C50EAE00A62B57 /* ObjcExceptionHandler.h in Headers */ = {isa = PBXBuildFile; fileRef = 9E68FB54244707FD0013A8AA /* ObjcExceptionHandler.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -1914,13 +1914,12 @@ D2A1EE22287740B500D28DFB /* ApplicationStatePublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationStatePublisher.swift; sourceTree = ""; }; D2A1EE25287C35DE00D28DFB /* ContextValueReaderMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextValueReaderMock.swift; sourceTree = ""; }; D2A1EE28287D914900D28DFB /* LaunchTime.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchTime.swift; sourceTree = ""; }; - D2A1EE2B287D91D900D28DFB /* LaunchTimeReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchTimeReader.swift; sourceTree = ""; }; D2A1EE2E287DA4F200D28DFB /* UserInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserInfo.swift; sourceTree = ""; }; D2A1EE31287DA51900D28DFB /* UserInfoPublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserInfoPublisher.swift; sourceTree = ""; }; D2A1EE34287EB8DB00D28DFB /* ServerOffsetPublisherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerOffsetPublisherTests.swift; sourceTree = ""; }; D2A1EE37287EBE4200D28DFB /* NetworkConnectionInfoPublisherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkConnectionInfoPublisherTests.swift; sourceTree = ""; }; D2A1EE3A287EECA800D28DFB /* CarrierInfoPublisherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarrierInfoPublisherTests.swift; sourceTree = ""; }; - D2A1EE3D2885D7EC00D28DFB /* LaunchTimeReaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchTimeReaderTests.swift; sourceTree = ""; }; + D2A1EE3D2885D7EC00D28DFB /* LaunchTimePublisherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchTimePublisherTests.swift; sourceTree = ""; }; D2A1EE432886B8B400D28DFB /* UserInfoPublisherTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserInfoPublisherTests.swift; sourceTree = ""; }; D2B3F0432823EE8300C2B5EE /* DataBlockTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBlockTests.swift; sourceTree = ""; }; D2B3F04628292D6E00C2B5EE /* DataMigratorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataMigratorTests.swift; sourceTree = ""; }; @@ -1928,6 +1927,7 @@ D2B3F04C282A85FD00C2B5EE /* DatadogCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatadogCore.swift; sourceTree = ""; }; D2B3F051282E826A00C2B5EE /* DDHTTPHeadersWriter+apiTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "DDHTTPHeadersWriter+apiTests.m"; sourceTree = ""; }; D2C7E3AA28F97DCF0023B2CC /* BatteryStatusPublisherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryStatusPublisherTests.swift; sourceTree = ""; }; + D2C7E3AD28FEBDA10023B2CC /* LaunchTimePublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchTimePublisher.swift; sourceTree = ""; }; D2CB6ED127C50EAE00A62B57 /* Datadog.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Datadog.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D2CB6F8F27C520D400A62B57 /* DatadogTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DatadogTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; D2CB6FB027C5217A00A62B57 /* DatadogObjc.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = DatadogObjc.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -4419,7 +4419,7 @@ D20605A82874C1CD0047275C /* NetworkConnectionInfoPublisher.swift */, D20605B12874E1660047275C /* CarrierInfoPublisher.swift */, D2A1EE31287DA51900D28DFB /* UserInfoPublisher.swift */, - D2A1EE2B287D91D900D28DFB /* LaunchTimeReader.swift */, + D2C7E3AD28FEBDA10023B2CC /* LaunchTimePublisher.swift */, D2A1EE22287740B500D28DFB /* ApplicationStatePublisher.swift */, D2553825288F0B1A00727FAD /* BatteryStatusPublisher.swift */, D2553828288F0B2300727FAD /* LowPowerModePublisher.swift */, @@ -4435,7 +4435,7 @@ D2A1EE3A287EECA800D28DFB /* CarrierInfoPublisherTests.swift */, D2A1EE37287EBE4200D28DFB /* NetworkConnectionInfoPublisherTests.swift */, D2A1EE432886B8B400D28DFB /* UserInfoPublisherTests.swift */, - D2A1EE3D2885D7EC00D28DFB /* LaunchTimeReaderTests.swift */, + D2A1EE3D2885D7EC00D28DFB /* LaunchTimePublisherTests.swift */, D26C49AE2886DC7B00802B2D /* ApplicationStatePublisherTests.swift */, D2C7E3AA28F97DCF0023B2CC /* BatteryStatusPublisherTests.swift */, D21C26D028A64599005DD405 /* MessageBusTests.swift */, @@ -5412,7 +5412,6 @@ 61133BE02423979B00786299 /* Datadog.swift in Sources */, 61E945E32869BF3D00A946C4 /* CoreLogger.swift in Sources */, 61133BCB2423979B00786299 /* CarrierInfoProvider.swift in Sources */, - D2A1EE2C287D91D900D28DFB /* LaunchTimeReader.swift in Sources */, 61C5A89024509AA700DA608C /* TracingFeature.swift in Sources */, 61E5333624B84B43003D6C4E /* RUMMonitor.swift in Sources */, 9EB4B862274E79D50041CD03 /* WKUserContentController+Datadog.swift in Sources */, @@ -5442,6 +5441,7 @@ 61133BE82423979B00786299 /* LogFileOutput.swift in Sources */, 61133BD72423979B00786299 /* DataUploadWorker.swift in Sources */, 61D3E0D4277B23F1008BE766 /* KronosTimeStorage.swift in Sources */, + D2C7E3AE28FEBDA10023B2CC /* LaunchTimePublisher.swift in Sources */, 61133BD12423979B00786299 /* FilesOrchestrator.swift in Sources */, D20605A3287464F40047275C /* ContextValuePublisher.swift in Sources */, 61133BCD2423979B00786299 /* NetworkConnectionInfoProvider.swift in Sources */, @@ -5700,7 +5700,7 @@ 61B5E42726DFB145000B0A5F /* DDDatadog+apiTests.m in Sources */, 6121627C247D220500AC5D67 /* TracingWithLoggingIntegrationTests.swift in Sources */, 61FF282124B8981D000B3D9B /* RUMEventBuilderTests.swift in Sources */, - D2A1EE3E2885D7EC00D28DFB /* LaunchTimeReaderTests.swift in Sources */, + D2A1EE3E2885D7EC00D28DFB /* LaunchTimePublisherTests.swift in Sources */, 61FD9FD22853562B00214BD9 /* RUMDeviceInfoTests.swift in Sources */, 61B5E42926DFB60A000B0A5F /* DDConfiguration+apiTests.m in Sources */, D232CAD52832D762001B262C /* DatadogCoreMock.swift in Sources */, @@ -5959,6 +5959,7 @@ D2CB6E1327C50EAE00A62B57 /* SwiftUIViewModifier.swift in Sources */, D2CB6E1427C50EAE00A62B57 /* SwiftUIExtensions.swift in Sources */, D2CB6E1527C50EAE00A62B57 /* ServerDateCorrector.swift in Sources */, + D2612F48290197C700509B7D /* LaunchTimePublisher.swift in Sources */, D2CB6E1627C50EAE00A62B57 /* RUMWithCrashContextIntegration.swift in Sources */, D2CB6E1727C50EAE00A62B57 /* OTSpan.swift in Sources */, D2CB6E1827C50EAE00A62B57 /* RUMViewIdentity.swift in Sources */, @@ -6087,7 +6088,6 @@ D2CB6E7E27C50EAE00A62B57 /* RUMMonitor.swift in Sources */, D26C49C0288982DA00802B2D /* FeatureUpload.swift in Sources */, D2CB6E8027C50EAE00A62B57 /* WKUserContentController+Datadog.swift in Sources */, - D2A1EE2D287D91D900D28DFB /* LaunchTimeReader.swift in Sources */, D2CB6E8127C50EAE00A62B57 /* DataUploader.swift in Sources */, D2CB6E8227C50EAE00A62B57 /* DeleteAllDataMigrator.swift in Sources */, D2CB6E8327C50EAE00A62B57 /* RUMUserActionScope.swift in Sources */, @@ -6376,7 +6376,7 @@ D2CB6F7F27C520D400A62B57 /* DDDatadog+apiTests.m in Sources */, D2CB6F8027C520D400A62B57 /* TracingWithLoggingIntegrationTests.swift in Sources */, D2CB6F8227C520D400A62B57 /* RUMEventBuilderTests.swift in Sources */, - D2A1EE3F2885D7EC00D28DFB /* LaunchTimeReaderTests.swift in Sources */, + D2A1EE3F2885D7EC00D28DFB /* LaunchTimePublisherTests.swift in Sources */, 61FD9FD12853562A00214BD9 /* RUMDeviceInfoTests.swift in Sources */, D2CB6F8327C520D400A62B57 /* DDConfiguration+apiTests.m in Sources */, D232CAD62832D762001B262C /* DatadogCoreMock.swift in Sources */, diff --git a/Sources/Datadog/DatadogCore/Context/LaunchTimePublisher.swift b/Sources/Datadog/DatadogCore/Context/LaunchTimePublisher.swift new file mode 100644 index 0000000000..2e3531ce2a --- /dev/null +++ b/Sources/Datadog/DatadogCore/Context/LaunchTimePublisher.swift @@ -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-2020 Datadog, Inc. + */ + +import Foundation + +#if SPM_BUILD +import _Datadog_Private +#endif + +internal struct LaunchTimePublisher: ContextValuePublisher { + private typealias AppLaunchHandler = __dd_private_AppLaunchHandler + + let initialValue: LaunchTime? + + init() { + initialValue = LaunchTime( + launchTime: AppLaunchHandler.shared.launchTime?.doubleValue, + launchDate: AppLaunchHandler.shared.launchDate, + isActivePrewarm: AppLaunchHandler.shared.isActivePrewarm + ) + } + + func publish(to receiver: @escaping ContextValueReceiver) { + let launchDate = AppLaunchHandler.shared.launchDate + let isActivePrewarm = AppLaunchHandler.shared.isActivePrewarm + + AppLaunchHandler.shared.setApplicationDidBecomeActiveCallback { launchTime in + let value = LaunchTime( + launchTime: launchTime, + launchDate: launchDate, + isActivePrewarm: isActivePrewarm + ) + receiver(value) + } + } + + func cancel() { + AppLaunchHandler.shared.setApplicationDidBecomeActiveCallback { _ in } + } +} diff --git a/Sources/Datadog/DatadogCore/Context/LaunchTimeReader.swift b/Sources/Datadog/DatadogCore/Context/LaunchTimeReader.swift deleted file mode 100644 index 18999855a1..0000000000 --- a/Sources/Datadog/DatadogCore/Context/LaunchTimeReader.swift +++ /dev/null @@ -1,37 +0,0 @@ -/* - * 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 - -#if SPM_BUILD -import _Datadog_Private -#endif - -internal struct LaunchTimeReader: ContextValueReader { - /// Lock object to sync launch time read. - let lock: Any - - init() { - lock = NSObject() - } - - func read(to receiver: inout LaunchTime) { - receiver = __dd_private_objc_sync_LaunchTime(lock) - } -} - -private func __dd_private_objc_sync_LaunchTime(_ lock: Any) -> LaunchTime { - // Even if __dd_private_AppLaunchTime() is using a lock behind the - // scenes, TSAN will report a data race if there are no - // synchronizations at this level. - objc_sync_enter(lock) - let time = LaunchTime( - launchTime: __dd_private_AppLaunchTime(), - isActivePrewarm: __dd_private_isActivePrewarm() - ) - objc_sync_exit(lock) - return time -} diff --git a/Sources/Datadog/DatadogCore/DatadogCore.swift b/Sources/Datadog/DatadogCore/DatadogCore.swift index a8a0553179..688576c550 100644 --- a/Sources/Datadog/DatadogCore/DatadogCore.swift +++ b/Sources/Datadog/DatadogCore/DatadogCore.swift @@ -449,7 +449,7 @@ extension DatadogContextProvider { self.init(context: context) subscribe(\.serverTimeOffset, to: ServerOffsetPublisher(provider: serverDateProvider)) - assign(reader: LaunchTimeReader(), to: \.launchTime) + subscribe(\.launchTime, to: LaunchTimePublisher()) if #available(iOS 12, tvOS 12, *) { subscribe(\.networkConnectionInfo, to: NWPathMonitorPublisher()) diff --git a/Sources/Datadog/DatadogInternal/Context/DatadogContext.swift b/Sources/Datadog/DatadogInternal/Context/DatadogContext.swift index 3c6a9884dc..23f6198d8d 100644 --- a/Sources/Datadog/DatadogInternal/Context/DatadogContext.swift +++ b/Sources/Datadog/DatadogInternal/Context/DatadogContext.swift @@ -61,8 +61,8 @@ public struct DatadogContext { /// Application launch time. /// - /// Can be `zero` if the launch could not yet been evaluated. - var launchTime: LaunchTime = .zero + /// Can be `nil` if the launch could not yet been evaluated. + var launchTime: LaunchTime? /// Provides the history of app foreground / background states. var applicationStateHistory: AppStateHistory diff --git a/Sources/Datadog/DatadogInternal/Context/LaunchTime.swift b/Sources/Datadog/DatadogInternal/Context/LaunchTime.swift index 58ce5a9c7b..82dfc811f1 100644 --- a/Sources/Datadog/DatadogInternal/Context/LaunchTime.swift +++ b/Sources/Datadog/DatadogInternal/Context/LaunchTime.swift @@ -11,17 +11,12 @@ import Foundation /// The app process launch duration (in seconds) measured as the time from process start time /// to receiving `UIApplication.didBecomeActiveNotification` notification. /// - /// If the `UIApplication.didBecomeActiveNotification` has not yet been received by the - /// time this value is provided, it will represent the time interval between now and the process start time. - /* public */ let launchTime: TimeInterval + /// If the `UIApplication.didBecomeActiveNotification` has not yet been received the value will be `nil`. + /* public */ let launchTime: TimeInterval? + + /// The date when the application process started. + /* public */ let launchDate: Date /// Returns `true` if the application is pre-warmed. /* public */ let isActivePrewarm: Bool } - -extension LaunchTime { - /// Returns a zero launch time with inactive pre-warm. - /* public */ static var zero: LaunchTime { - .init(launchTime: 0, isActivePrewarm: false) - } -} diff --git a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewScope.swift b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewScope.swift index f18304351e..240df78113 100644 --- a/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewScope.swift +++ b/Sources/Datadog/RUM/RUMMonitor/Scopes/RUMViewScope.swift @@ -314,13 +314,23 @@ internal class RUMViewScope: RUMScope, RUMContextProvider { var attributes = self.attributes var loadingTime: Int64? = nil - if context.launchTime.isActivePrewarm { + if context.launchTime?.isActivePrewarm == true { // Set `active_pre_warm` attribute to true in case - // of pre-warmed app + // of pre-warmed app. attributes[Constants.activePrewarm] = true - } else { + } else if let launchTime = context.launchTime?.launchTime { // Report Application Launch Time only if not pre-warmed - loadingTime = context.launchTime.launchTime.toInt64Nanoseconds + loadingTime = launchTime.toInt64Nanoseconds + } else if let launchDate = context.launchTime?.launchDate { + // The launchTime can be `nil` if the application is not yet + // active (UIApplicationDidBecomeActiveNotification). That is + // the case when instrumenting a SwiftUI application that start + // a RUM view on `SwiftUI.View/onAppear`. + // + // In that case, we consider the time between the application + // launch and the first view start as the application loading + // time. + loadingTime = viewStartTime.timeIntervalSince(launchDate).toInt64Nanoseconds } let actionEvent = RUMActionEvent( diff --git a/Sources/_Datadog_Private/ObjcAppLaunchHandler.m b/Sources/_Datadog_Private/ObjcAppLaunchHandler.m index 97e16351d4..e7083ae387 100644 --- a/Sources/_Datadog_Private/ObjcAppLaunchHandler.m +++ b/Sources/_Datadog_Private/ObjcAppLaunchHandler.m @@ -4,97 +4,127 @@ * Copyright 2019-2020 Datadog, Inc. */ -#import "ObjcAppLaunchHandler.h" -#import #import #import +#import + +#import "ObjcAppLaunchHandler.h" -// `AppLaunchHandler` aims to track some times as part of the sequence described in Apple's "About the App Launch Sequence" -// https://developer.apple.com/documentation/uikit/app_and_environment/responding_to_the_launch_of_your_app/about_the_app_launch_sequence - -// A Read-Write lock to allow concurrent reads of TimeToApplicationDidBecomeActive, unless the initial (and only) write is locking it. -static pthread_rwlock_t rwLock; -// The framework load time in seconds relative to the absolute reference date of Jan 1 2001 00:00:00 GMT. -static NSTimeInterval FrameworkLoadTime = 0.0; -// The time interval between the application starts and it's responsive and accepts touch events. -static NSTimeInterval TimeToApplicationDidBecomeActive = 0.0; -// System sets environment variable ActivePrewarm to 1 when app is pre-warmed. -static BOOL isActivePrewarm = NO; // A very long application launch time is most-likely the result of a pre-warmed process. // We consider 30s as a threshold for pre-warm detection. -static NSTimeInterval ValidAppLaunchTimeThreshold = 30; - -NS_INLINE NSTimeInterval QueryProcessStartTimeWithFallback(NSTimeInterval fallbackTime) { - NSTimeInterval processStartTime; - // Query the current process' start time: - // https://www.freebsd.org/cgi/man.cgi?sysctl(3) - // https://github.com/darwin-on-arm/xnu/blob/707bfdc4e9a46e3612e53994fffc64542d3f7e72/bsd/sys/sysctl.h#L681 - // https://github.com/darwin-on-arm/xnu/blob/707bfdc4e9a46e3612e53994fffc64542d3f7e72/bsd/sys/proc.h#L97 - - struct kinfo_proc kip; - size_t kipSize = sizeof(kip); - int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()}; - int res = sysctl(mib, 4, &kip, &kipSize, NULL, 0); - - if (res == 0) { - // The process' start time is provided relative to 1 Jan 1970 - struct timeval startTime = kip.kp_proc.p_starttime; - processStartTime = startTime.tv_sec + startTime.tv_usec / USEC_PER_SEC; - // Convert to time since 1 Jan 2001 to align with CFAbsoluteTimeGetCurrent() - processStartTime -= kCFAbsoluteTimeIntervalSince1970; - } else { - // Fallback to less accurate delta with DD's framework load time - processStartTime = fallbackTime; - } - return processStartTime; -} - -NS_INLINE NSTimeInterval ComputeProcessTimeFromStart() { - NSTimeInterval now = CFAbsoluteTimeGetCurrent(); - NSTimeInterval processStartTime = QueryProcessStartTimeWithFallback(FrameworkLoadTime); - return now - processStartTime; +#define COLD_START_TIME_THRESHOLD 30 + +/// Get the process start time from kernel. +/// +/// The time interval is related to the 1 January 2001 00:00:00 GMT reference date. +/// +/// - Parameter timeInterval: Pointer to time interval to hold the process start time interval. +int processStartTimeIntervalSinceReferenceDate(NSTimeInterval *timeInterval); + +/// `AppLaunchHandler` aims to track some times as part of the sequence +/// described in Apple's "About the App Launch Sequence" +/// +/// ref. https://developer.apple.com/documentation/uikit/app_and_environment/responding_to_the_launch_of_your_app/about_the_app_launch_sequence +@implementation __dd_private_AppLaunchHandler { + NSTimeInterval _processStartTime; + NSTimeInterval _timeToApplicationDidBecomeActive; + BOOL _isActivePrewarm; + UIApplicationDidBecomeActiveCallback _applicationDidBecomeActiveCallback; } -@interface AppLaunchHandler : NSObject -@end - -@implementation AppLaunchHandler +/// Shared instance of the Application Launch Handler. +static __dd_private_AppLaunchHandler *_shared; + (void)load { // This is called at the `_Datadog_Private` load time, keep the work minimal - FrameworkLoadTime = CFAbsoluteTimeGetCurrent(); - - isActivePrewarm = [NSProcessInfo.processInfo.environment[@"ActivePrewarm"] isEqualToString:@"1"]; + _shared = [[self alloc] initWithProcessInfo:NSProcessInfo.processInfo + loadTime:CFAbsoluteTimeGetCurrent()]; NSNotificationCenter * __weak center = NSNotificationCenter.defaultCenter; - id __block token = [center - addObserverForName:UIApplicationDidBecomeActiveNotification - object:nil - queue:NSOperationQueue.mainQueue - usingBlock:^(NSNotification *_){ + id __block token = [center addObserverForName:UIApplicationDidBecomeActiveNotification + object:nil + queue:NSOperationQueue.mainQueue + usingBlock:^(NSNotification *_){ - pthread_rwlock_init(&rwLock, NULL); - pthread_rwlock_wrlock(&rwLock); - TimeToApplicationDidBecomeActive = ComputeProcessTimeFromStart(); - pthread_rwlock_unlock(&rwLock); + @synchronized(_shared) { + NSTimeInterval time = CFAbsoluteTimeGetCurrent() - _shared->_processStartTime; + _shared->_timeToApplicationDidBecomeActive = time; + _shared->_applicationDidBecomeActiveCallback(time); + } [center removeObserver:token]; token = nil; }]; } -@end ++ (__dd_private_AppLaunchHandler *)shared { + return _shared; +} + +- (instancetype)initWithProcessInfo:(NSProcessInfo *)processInfo loadTime:(NSTimeInterval)loadTime { + NSTimeInterval startTime; + if (processStartTimeIntervalSinceReferenceDate(&startTime) != 0) { + // fallback on the loading time + startTime = loadTime; + } + + // The ActivePrewarm variable indicates whether the app was launched via pre-warming. + BOOL isActivePrewarm = [processInfo.environment[@"ActivePrewarm"] isEqualToString:@"1"]; + return [self initWithStartTime:startTime isActivePrewarm:isActivePrewarm]; +} -CFTimeInterval __dd_private_AppLaunchTime() { - pthread_rwlock_rdlock(&rwLock); - CFTimeInterval time = TimeToApplicationDidBecomeActive; - pthread_rwlock_unlock(&rwLock); - if (time == 0) time = ComputeProcessTimeFromStart(); - return time; +- (instancetype)initWithStartTime:(NSTimeInterval)startTime isActivePrewarm:(BOOL)isActivePrewarm { + self = [super init]; + if (!self) return nil; + _processStartTime = startTime; + _isActivePrewarm = isActivePrewarm; + _applicationDidBecomeActiveCallback = ^(NSTimeInterval _) {}; + return self; } -BOOL __dd_private_isActivePrewarm() { - if (isActivePrewarm) return isActivePrewarm; - CFTimeInterval time = __dd_private_AppLaunchTime(); - return time > ValidAppLaunchTimeThreshold; +- (NSDate *)launchDate { + @synchronized(self) { + return [NSDate dateWithTimeIntervalSinceReferenceDate:_processStartTime]; + } +} + +- (NSNumber *)launchTime { + @synchronized(self) { + return _timeToApplicationDidBecomeActive > 0 ? + @(_timeToApplicationDidBecomeActive) : nil; + } +} + +- (BOOL)isActivePrewarm { + @synchronized(self) { + if (_isActivePrewarm) return _isActivePrewarm; + return _timeToApplicationDidBecomeActive > COLD_START_TIME_THRESHOLD; + } +} + +- (void)setApplicationDidBecomeActiveCallback:(UIApplicationDidBecomeActiveCallback)callback { + @synchronized(self) { + _applicationDidBecomeActiveCallback = callback; + } +} + +@end + +int processStartTimeIntervalSinceReferenceDate(NSTimeInterval *timeInterval) { + // Query the current process' start time: + // https://www.freebsd.org/cgi/man.cgi?sysctl(3) + // https://github.com/darwin-on-arm/xnu/blob/707bfdc4e9a46e3612e53994fffc64542d3f7e72/bsd/sys/sysctl.h#L681 + // https://github.com/darwin-on-arm/xnu/blob/707bfdc4e9a46e3612e53994fffc64542d3f7e72/bsd/sys/proc.h#L97 + struct kinfo_proc kip; + size_t kipSize = sizeof(kip); + int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()}; + int res = sysctl(mib, 4, &kip, &kipSize, NULL, 0); + if (res != 0) return res; + + // The process' start time is provided relative to 1 Jan 1970 + struct timeval startTime = kip.kp_proc.p_starttime; + NSTimeInterval processStartTime = startTime.tv_sec + startTime.tv_usec / USEC_PER_SEC; + // Convert to time since 1 Jan 2001 to align with CFAbsoluteTimeGetCurrent() + *timeInterval = processStartTime - kCFAbsoluteTimeIntervalSince1970; + return res; } diff --git a/Sources/_Datadog_Private/include/ObjcAppLaunchHandler.h b/Sources/_Datadog_Private/include/ObjcAppLaunchHandler.h index 1233fead24..a290ec6aad 100644 --- a/Sources/_Datadog_Private/include/ObjcAppLaunchHandler.h +++ b/Sources/_Datadog_Private/include/ObjcAppLaunchHandler.h @@ -4,16 +4,45 @@ * Copyright 2019-2020 Datadog, Inc. */ -#import +#import -/// Returns the time interval between startup of the application process and the -/// `UIApplicationDidBecomeActiveNotification`. +NS_ASSUME_NONNULL_BEGIN + +/// `AppLaunchHandler` aims to track some times as part of the sequence +/// described in Apple's "About the App Launch Sequence" /// -/// If the `UIApplicationDidBecomeActiveNotification` has not been reached yet, -/// it returns time interval between startup of the application process and now. -CFTimeInterval __dd_private_AppLaunchTime(void); +/// ref. https://developer.apple.com/documentation/uikit/app_and_environment/responding_to_the_launch_of_your_app/about_the_app_launch_sequence +@interface __dd_private_AppLaunchHandler : NSObject + +typedef void (^UIApplicationDidBecomeActiveCallback) (NSTimeInterval); + +/// Sole instance of the Application Launch Handler. +@property (class, readonly) __dd_private_AppLaunchHandler *shared; + +/// Returns the Application process launch date. +@property (atomic, readonly) NSDate* launchDate; + +/// Returns the time interval in seconds between startup of the application process and the +/// `UIApplicationDidBecomeActiveNotification`. Or `nil` If the +/// `UIApplicationDidBecomeActiveNotification` has not been reached yet. +@property (atomic, readonly, nullable) NSNumber* launchTime; /// Returns `true` when the application is pre-warmed. /// /// System sets environment variable `ActivePrewarm` to 1 when app is pre-warmed. -BOOL __dd_private_isActivePrewarm(void); +@property (atomic, readonly) BOOL isActivePrewarm; + +/// Sets the callback to be invoked when the application becomes active. +/// +/// The closure get the updated handler as argument. You will not get any +/// notification if the application became active before setting the callback +/// +/// - Parameter callback: The callback closure. +- (void)setApplicationDidBecomeActiveCallback:(UIApplicationDidBecomeActiveCallback)callback; + +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)new NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Tests/DatadogBenchmarkTests/BenchmarkMocks.swift b/Tests/DatadogBenchmarkTests/BenchmarkMocks.swift index 35a61fb625..6ffc1db58a 100644 --- a/Tests/DatadogBenchmarkTests/BenchmarkMocks.swift +++ b/Tests/DatadogBenchmarkTests/BenchmarkMocks.swift @@ -46,7 +46,7 @@ extension DatadogContext: AnyMockable { sdkInitDate: .mockRandomInThePast(), device: DeviceInfo(), userInfo: nil, - launchTime: .zero, + launchTime: nil, applicationStateHistory: .active(since: Date()), networkConnectionInfo: .unknown, carrierInfo: nil, diff --git a/Tests/DatadogTests/Datadog/DatadogCore/LaunchTimePublisherTests.swift b/Tests/DatadogTests/Datadog/DatadogCore/LaunchTimePublisherTests.swift new file mode 100644 index 0000000000..e75788c4c8 --- /dev/null +++ b/Tests/DatadogTests/Datadog/DatadogCore/LaunchTimePublisherTests.swift @@ -0,0 +1,65 @@ +/* + * 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 LaunchTimePublisherTests: XCTestCase { + override func tearDown() { + super.tearDown() + setenv("ActivePrewarm", "", 1) + } + + func testGivenStartedApplication_itHasLaunchDate() throws { + // Given + let publisher = LaunchTimePublisher() + + // When + let launchTime = publisher.initialValue + + // Then + XCTAssertNotNil(launchTime?.launchDate) + } + + func testThreadSafety() { + let handler = __dd_private_AppLaunchHandler.shared + + // swiftlint:disable opening_brace + callConcurrently( + closures: [ + { _ = handler.launchTime }, + { _ = handler.launchDate }, + { _ = handler.isActivePrewarm }, + { handler.setApplicationDidBecomeActiveCallback { _ in } } + ], + iterations: 1_000 + ) + // swiftlint:enable opening_brace + } + + func testIsActivePrewarm_returnsTrue() { + // Given + setenv("ActivePrewarm", "1", 1) + NSClassFromString("__dd_private_AppLaunchHandler")?.load() + + // When + let publisher = LaunchTimePublisher() + + // Then + XCTAssertTrue(publisher.initialValue?.isActivePrewarm ?? false) + } + + func testIsActivePrewarm_returnsFalse() { + // Given + NSClassFromString("__dd_private_AppLaunchHandler")?.load() + + // When + let publisher = LaunchTimePublisher() + + // Then + XCTAssertFalse(publisher.initialValue?.isActivePrewarm ?? true) + } +} diff --git a/Tests/DatadogTests/Datadog/DatadogCore/LaunchTimeReaderTests.swift b/Tests/DatadogTests/Datadog/DatadogCore/LaunchTimeReaderTests.swift deleted file mode 100644 index 6cc0d0d2e5..0000000000 --- a/Tests/DatadogTests/Datadog/DatadogCore/LaunchTimeReaderTests.swift +++ /dev/null @@ -1,78 +0,0 @@ -/* - * 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 LaunchTimeReaderTests: XCTestCase { - override func tearDown() { - super.tearDown() - setenv("ActivePrewarm", "", 1) - } - - func testGivenStartedApplication_whenRequestingLaunchTimeAtAnyTime_itReturnsTheSameValue() throws { - // Given - let reader = LaunchTimeReader() - - // When - var values: [TimeInterval] = [] - try (0..<10).forEach { _ in - Thread.sleep(forTimeInterval: 0.01) - var launchTime = LaunchTime(launchTime: .mockRandom(), isActivePrewarm: false) - reader.read(to: &launchTime) - try values.append(XCTUnwrap(launchTime.launchTime)) - } - - // Then - let uniqueValues = Set(values) - XCTAssertEqual(uniqueValues.count, 1, "All collected `launchTime` values should be the same.") - XCTAssertGreaterThan(values[0], 0) - } - - func testThreadSafety() { - let reader = LaunchTimeReader() - - // swiftlint:disable opening_brace - callConcurrently( - closures: [ - { - var launchTime: LaunchTime = .mockAny() - reader.read(to: &launchTime) - } - ], - iterations: 1_000 - ) - // swiftlint:enable opening_brace - } - - func testIsActivePrewarm_returnsTrue() { - // Given - let reader = LaunchTimeReader() - - // When - setenv("ActivePrewarm", "1", 1) - NSClassFromString("AppLaunchHandler")?.load() - - var launchTime = LaunchTime(launchTime: 0, isActivePrewarm: false) - reader.read(to: &launchTime) - - // Then - XCTAssertTrue(launchTime.isActivePrewarm) - } - - func testIsActivePrewarm_returnsFalse() { - // Given - let reader = LaunchTimeReader() - - // When - NSClassFromString("AppLaunchHandler")?.load() - var launchTime = LaunchTime(launchTime: 0, isActivePrewarm: true) - reader.read(to: &launchTime) - - // Then - XCTAssertFalse(launchTime.isActivePrewarm) - } -} diff --git a/Tests/DatadogTests/Datadog/Mocks/DatadogInternal/DatadogContextMock.swift b/Tests/DatadogTests/Datadog/Mocks/DatadogInternal/DatadogContextMock.swift index 547d564bb8..8f6583c7ba 100644 --- a/Tests/DatadogTests/Datadog/Mocks/DatadogInternal/DatadogContextMock.swift +++ b/Tests/DatadogTests/Datadog/Mocks/DatadogInternal/DatadogContextMock.swift @@ -64,6 +64,7 @@ extension LaunchTime: AnyMockable { static func mockAny() -> LaunchTime { .init( launchTime: .mockAny(), + launchDate: .mockAny(), isActivePrewarm: .mockAny() ) } diff --git a/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMViewScopeTests.swift b/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMViewScopeTests.swift index 8db1204c29..29e9c6fa48 100644 --- a/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMViewScopeTests.swift +++ b/Tests/DatadogTests/Datadog/RUM/RUMMonitor/Scopes/RUMViewScopeTests.swift @@ -88,7 +88,11 @@ class RUMViewScopeTests: XCTestCase { // Given let currentTime: Date = .mockDecember15th2019At10AMUTC() var context = self.context - context.launchTime = .init(launchTime: 2, isActivePrewarm: false) + context.launchTime = .init( + launchTime: 2, + launchDate: .distantPast, + isActivePrewarm: false + ) let scope = RUMViewScope( isInitialView: true, @@ -134,10 +138,7 @@ class RUMViewScopeTests: XCTestCase { // Given let currentTime: Date = .mockDecember15th2019At10AMUTC() let source = String.mockAnySource() - let custonContext: DatadogContext = .mockWith(source: source) - - var context = self.context - context.launchTime = .init(launchTime: 2, isActivePrewarm: false) + let customContext: DatadogContext = .mockWith(source: source) let scope = RUMViewScope( isInitialView: true, @@ -155,7 +156,7 @@ class RUMViewScopeTests: XCTestCase { // When _ = scope.process( command: RUMCommandMock(time: currentTime), - context: custonContext, + context: customContext, writer: writer ) @@ -164,10 +165,44 @@ class RUMViewScopeTests: XCTestCase { XCTAssertEqual(event.source, .init(rawValue: source)) } + func testWhenNoLoadingTime_itSendsApplicationStartAction_basedOnLoadingDate() throws { + // Given + var context = self.context + let date = Date() + context.launchTime = .init( + launchTime: nil, + launchDate: date.addingTimeInterval(-2), + isActivePrewarm: false + ) + + let scope: RUMViewScope = .mockWith( + isInitialView: true, + parent: parent, + dependencies: .mockAny(), + identity: mockView, + startTime: date + ) + + // When + _ = scope.process( + command: RUMCommandMock(), + context: context, + writer: writer + ) + + // Then + let event = try XCTUnwrap(writer.events(ofType: RUMActionEvent.self).first) + XCTAssertEqual(event.action.loadingTime, 2_000_000_000) // 2e+9 ns + } + func testWhenActivePrewarm_itSendsApplicationStartAction_withoutLoadingTime() throws { // Given var context = self.context - context.launchTime = .init(launchTime: 2, isActivePrewarm: true) + context.launchTime = .init( + launchTime: 2, + launchDate: .distantPast, + isActivePrewarm: true + ) let scope: RUMViewScope = .mockWith( isInitialView: true,