From cd9c78d2efce7de1509cba68607dea5737c5bd5a Mon Sep 17 00:00:00 2001 From: Ganesh Jangir Date: Mon, 15 Jul 2024 15:30:01 +0200 Subject: [PATCH] RUM-5248 feat: setup ContextReceiver which can observe to single property in context message --- CHANGELOG.md | 1 + Datadog/Datadog.xcodeproj/project.pbxproj | 30 +++++++++++ .../Core/Context/MemoryWarningPublisher.swift | 51 +++++++++++++++++++ DatadogCore/Sources/Core/DatadogCore.swift | 21 +++++++- DatadogCore/Sources/Core/MessageBus.swift | 11 ++-- .../Sources/Context/DatadogContext.swift | 6 ++- .../Sources/Context/LaunchTime.swift | 2 +- .../Sources/Context/MemoryWarning.swift | 36 +++++++++++++ .../Sources/MessageBus/ContextReceiver.swift | 24 +++++++++ .../MessageBus/ContextReceptionManager.swift | 51 +++++++++++++++++++ .../MessageBus/FeatureMessageReceiver.swift | 28 ++++++++-- DatadogRUM/Sources/Feature/RUMFeature.swift | 3 +- .../MemoryWarningReporter.swift | 26 ++++++++++ .../Tests/ContextMessageReceiverTests.swift | 38 ++++++++++++++ .../Mocks/CoreMocks/PassthroughCoreMock.swift | 31 +++++++++-- .../CoreMocks/SingleFeatureCoreMock.swift | 6 ++- 16 files changed, 346 insertions(+), 19 deletions(-) create mode 100644 DatadogCore/Sources/Core/Context/MemoryWarningPublisher.swift create mode 100644 DatadogInternal/Sources/Context/MemoryWarning.swift create mode 100644 DatadogInternal/Sources/MessageBus/ContextReceiver.swift create mode 100644 DatadogInternal/Sources/MessageBus/ContextReceptionManager.swift create mode 100644 DatadogRUM/Sources/Instrumentation/MemoryWarningReporter.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index d24b8927d2..aef6a1fe42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ - [FEATURE] Enable DatadogCore, DatadogLogs and DatadogTrace to compile on watchOS platform. See [#1918][] (Thanks [@jfiser-paylocity][]) [#1946][] - [IMPROVEMENT] Ability to clear feature data storage using `clearAllData` API. See [#1940][] +- [IMPROVEMENT] Send memory warning as RUM error. # 2.14.1 / 09-07-2024 diff --git a/Datadog/Datadog.xcodeproj/project.pbxproj b/Datadog/Datadog.xcodeproj/project.pbxproj index 3c84945753..3f5c198b6f 100644 --- a/Datadog/Datadog.xcodeproj/project.pbxproj +++ b/Datadog/Datadog.xcodeproj/project.pbxproj @@ -47,6 +47,12 @@ 3C41693C29FBF4D50042B9D2 /* DatadogWebViewTracking.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3CE119FE29F7BE0100202522 /* DatadogWebViewTracking.framework */; }; 3C43A3882C188974000BFB21 /* WatchdogTerminationMonitorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C43A3862C188970000BFB21 /* WatchdogTerminationMonitorTests.swift */; }; 3C43A3892C188975000BFB21 /* WatchdogTerminationMonitorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C43A3862C188970000BFB21 /* WatchdogTerminationMonitorTests.swift */; }; + 3C5CD8C22C3EBA1700B12303 /* MemoryWarningPublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C5CD8C12C3EBA1700B12303 /* MemoryWarningPublisher.swift */; }; + 3C5CD8C32C3EBA1700B12303 /* MemoryWarningPublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C5CD8C12C3EBA1700B12303 /* MemoryWarningPublisher.swift */; }; + 3C5CD8C72C3EC61F00B12303 /* MemoryWarning.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C5CD8C42C3EC61500B12303 /* MemoryWarning.swift */; }; + 3C5CD8C82C3EC62000B12303 /* MemoryWarning.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C5CD8C42C3EC61500B12303 /* MemoryWarning.swift */; }; + 3C5CD8CD2C3ECB9400B12303 /* MemoryWarningReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C5CD8CA2C3ECB4800B12303 /* MemoryWarningReporter.swift */; }; + 3C5CD8CE2C3ECB9400B12303 /* MemoryWarningReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C5CD8CA2C3ECB4800B12303 /* MemoryWarningReporter.swift */; }; 3C5D63692B55512B00FEB4BA /* OTelTraceState+Datadog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C5D63682B55512B00FEB4BA /* OTelTraceState+Datadog.swift */; }; 3C5D636A2B55512B00FEB4BA /* OTelTraceState+Datadog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C5D63682B55512B00FEB4BA /* OTelTraceState+Datadog.swift */; }; 3C5D636C2B55513500FEB4BA /* OTelTraceState+DatadogTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C5D636B2B55513500FEB4BA /* OTelTraceState+DatadogTests.swift */; }; @@ -103,6 +109,10 @@ 3CCECDB32BC68A0A0013C125 /* SpanIDTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CCECDB12BC68A0A0013C125 /* SpanIDTests.swift */; }; 3CDA3F7E2BCD866D005D2C13 /* DatadogSDKTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 3CDA3F7D2BCD866D005D2C13 /* DatadogSDKTesting */; }; 3CDA3F802BCD8687005D2C13 /* DatadogSDKTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 3CDA3F7F2BCD8687005D2C13 /* DatadogSDKTesting */; }; + 3CDD60DE2C45574500371331 /* ContextReceptionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CDD60DD2C45574500371331 /* ContextReceptionManager.swift */; }; + 3CDD60DF2C45574500371331 /* ContextReceptionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CDD60DD2C45574500371331 /* ContextReceptionManager.swift */; }; + 3CDD60E12C4557BE00371331 /* ContextReceiver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CDD60E02C4557BE00371331 /* ContextReceiver.swift */; }; + 3CDD60E22C4557BE00371331 /* ContextReceiver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CDD60E02C4557BE00371331 /* ContextReceiver.swift */; }; 3CE11A1129F7BE0900202522 /* DatadogWebViewTracking.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3CE119FE29F7BE0100202522 /* DatadogWebViewTracking.framework */; }; 3CE11A1229F7BE0900202522 /* DatadogWebViewTracking.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3CE119FE29F7BE0100202522 /* DatadogWebViewTracking.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 3CEC57732C16FD0B0042B5F2 /* WatchdogTerminationMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CEC57702C16FD000042B5F2 /* WatchdogTerminationMocks.swift */; }; @@ -2107,6 +2117,9 @@ 3C33E4062BEE35A7003B2988 /* RUMContextMocks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RUMContextMocks.swift; sourceTree = ""; }; 3C3EF2AF2C1AEBAB009E9E57 /* LaunchReport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchReport.swift; sourceTree = ""; }; 3C43A3862C188970000BFB21 /* WatchdogTerminationMonitorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchdogTerminationMonitorTests.swift; sourceTree = ""; }; + 3C5CD8C12C3EBA1700B12303 /* MemoryWarningPublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryWarningPublisher.swift; sourceTree = ""; }; + 3C5CD8C42C3EC61500B12303 /* MemoryWarning.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryWarning.swift; sourceTree = ""; }; + 3C5CD8CA2C3ECB4800B12303 /* MemoryWarningReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryWarningReporter.swift; sourceTree = ""; }; 3C5D63682B55512B00FEB4BA /* OTelTraceState+Datadog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OTelTraceState+Datadog.swift"; sourceTree = ""; }; 3C5D636B2B55513500FEB4BA /* OTelTraceState+DatadogTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OTelTraceState+DatadogTests.swift"; sourceTree = ""; }; 3C6C7FE02B459AAA006F5CBC /* OTelSpan.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTelSpan.swift; sourceTree = ""; }; @@ -2132,6 +2145,8 @@ 3CCCA5C62ABAF5230029D7BD /* DDURLSessionInstrumentationConfigurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDURLSessionInstrumentationConfigurationTests.swift; sourceTree = ""; }; 3CCECDAE2BC688120013C125 /* SpanIDGeneratorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpanIDGeneratorTests.swift; sourceTree = ""; }; 3CCECDB12BC68A0A0013C125 /* SpanIDTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpanIDTests.swift; sourceTree = ""; }; + 3CDD60DD2C45574500371331 /* ContextReceptionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextReceptionManager.swift; sourceTree = ""; }; + 3CDD60E02C4557BE00371331 /* ContextReceiver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextReceiver.swift; sourceTree = ""; }; 3CE119FE29F7BE0100202522 /* DatadogWebViewTracking.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = DatadogWebViewTracking.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3CE11A0529F7BE0300202522 /* DatadogWebViewTrackingTests iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "DatadogWebViewTrackingTests iOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 3CEC57702C16FD000042B5F2 /* WatchdogTerminationMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchdogTerminationMocks.swift; sourceTree = ""; }; @@ -4811,6 +4826,7 @@ 616CCE11250A181C009FED46 /* Instrumentation */ = { isa = PBXGroup; children = ( + 3C5CD8CA2C3ECB4800B12303 /* MemoryWarningReporter.swift */, 616CCE12250A1868009FED46 /* RUMCommandSubscriber.swift */, 616CCE15250A467E009FED46 /* RUMInstrumentation.swift */, 61F3CDA1251118DD00C816E5 /* Views */, @@ -5827,6 +5843,7 @@ D23039BE298D5235001A1FA3 /* LaunchTime.swift */, D2F8235229915E12003C7E99 /* DatadogSite.swift */, 6174D6122BFDF16C00EC7469 /* BundleType.swift */, + 3C5CD8C42C3EC61500B12303 /* MemoryWarning.swift */, ); path = Context; sourceTree = ""; @@ -5837,6 +5854,8 @@ D2216EBF2A94DE2800ADAEC8 /* FeatureBaggage.swift */, D23039C1298D5235001A1FA3 /* FeatureMessageReceiver.swift */, D23039C2298D5235001A1FA3 /* FeatureMessage.swift */, + 3CDD60DD2C45574500371331 /* ContextReceptionManager.swift */, + 3CDD60E02C4557BE00371331 /* ContextReceiver.swift */, ); path = MessageBus; sourceTree = ""; @@ -6439,6 +6458,7 @@ D2553828288F0B2300727FAD /* LowPowerModePublisher.swift */, D29294DF291D5ECD00F8EFF9 /* ApplicationVersionPublisher.swift */, D2FB1253292E0E92005B13F8 /* TrackingConsentPublisher.swift */, + 3C5CD8C12C3EBA1700B12303 /* MemoryWarningPublisher.swift */, ); path = Context; sourceTree = ""; @@ -8036,6 +8056,7 @@ D2C7E3AE28FEBDA10023B2CC /* LaunchTimePublisher.swift in Sources */, 61133BD12423979B00786299 /* FilesOrchestrator.swift in Sources */, D20605A3287464F40047275C /* ContextValuePublisher.swift in Sources */, + 3C5CD8C22C3EBA1700B12303 /* MemoryWarningPublisher.swift in Sources */, 61DA8CAF28620C760074A606 /* Cryptography.swift in Sources */, E1D5AEA724B4D45B007F194B /* Versioning.swift in Sources */, 61133BD82423979B00786299 /* URLSessionClient.swift in Sources */, @@ -8579,6 +8600,7 @@ D23039F9298D5236001A1FA3 /* CoreLogger.swift in Sources */, D2160CA229C0DE5700FAA9A5 /* NetworkInstrumentationFeature.swift in Sources */, D2EBEE1F29BA160F00B15732 /* HTTPHeadersReader.swift in Sources */, + 3C5CD8C72C3EC61F00B12303 /* MemoryWarning.swift in Sources */, E2AA55E72C32C6D9002FEF28 /* ApplicationNotifications.swift in Sources */, D263BCAF29DAFFEB00FA0E21 /* PerformancePresetOverride.swift in Sources */, D23039E7298D5236001A1FA3 /* NetworkConnectionInfo.swift in Sources */, @@ -8617,6 +8639,7 @@ D23039FA298D5236001A1FA3 /* Telemetry.swift in Sources */, D23039FC298D5236001A1FA3 /* DataFormat.swift in Sources */, D2160CED29C0E0E600FAA9A5 /* DatadogURLSessionHandler.swift in Sources */, + 3CDD60DE2C45574500371331 /* ContextReceptionManager.swift in Sources */, D2160C9E29C0DE5700FAA9A5 /* TracingHeaderType.swift in Sources */, D23039F5298D5236001A1FA3 /* AnyEncodable.swift in Sources */, D2303A00298D5236001A1FA3 /* DatadogExtended.swift in Sources */, @@ -8635,6 +8658,7 @@ 3C9B27252B9F174700569C07 /* SpanID.swift in Sources */, D2216EC02A94DE2900ADAEC8 /* FeatureBaggage.swift in Sources */, D23039F1298D5236001A1FA3 /* AnyDecodable.swift in Sources */, + 3CDD60E12C4557BE00371331 /* ContextReceiver.swift in Sources */, 6167E6E22B81207200C3CA2D /* DDCrashReport.swift in Sources */, D2160CC529C0DED100FAA9A5 /* URLSessionTaskInterception.swift in Sources */, 6167E6FD2B81EC0400C3CA2D /* BacktraceReporter.swift in Sources */, @@ -8751,6 +8775,7 @@ D23F8E8229DDCD28001CFAE8 /* RUMSessionScope.swift in Sources */, D23F8E8329DDCD28001CFAE8 /* RUMUser.swift in Sources */, D23F8E8429DDCD28001CFAE8 /* UIKitRUMUserActionsPredicate.swift in Sources */, + 3C5CD8CE2C3ECB9400B12303 /* MemoryWarningReporter.swift in Sources */, D23F8E8529DDCD28001CFAE8 /* SwiftUIExtensions.swift in Sources */, 3CFF4F952C09E63C006F191D /* WatchdogTerminationChecker.swift in Sources */, D23F8E8629DDCD28001CFAE8 /* RUMDataModelsMapping.swift in Sources */, @@ -9080,6 +9105,7 @@ D29A9F5C29DD85BB005C54A4 /* RUMSessionScope.swift in Sources */, D29A9F6629DD85BB005C54A4 /* RUMUser.swift in Sources */, D29A9F8229DD85BB005C54A4 /* UIKitRUMUserActionsPredicate.swift in Sources */, + 3C5CD8CD2C3ECB9400B12303 /* MemoryWarningReporter.swift in Sources */, D29A9F8E29DD8665005C54A4 /* SwiftUIExtensions.swift in Sources */, 3CFF4F942C09E63C006F191D /* WatchdogTerminationChecker.swift in Sources */, D29A9F7829DD85BB005C54A4 /* RUMDataModelsMapping.swift in Sources */, @@ -9301,6 +9327,7 @@ D2CB6E9727C50EAE00A62B57 /* DataUploadStatus.swift in Sources */, D255382A288F0B2400727FAD /* LowPowerModePublisher.swift in Sources */, D2CB6E9927C50EAE00A62B57 /* DataUploadWorker.swift in Sources */, + 3C5CD8C32C3EBA1700B12303 /* MemoryWarningPublisher.swift in Sources */, D2CB6E9A27C50EAE00A62B57 /* KronosTimeStorage.swift in Sources */, D2CB6E9B27C50EAE00A62B57 /* FilesOrchestrator.swift in Sources */, D2553827288F0B1A00727FAD /* BatteryStatusPublisher.swift in Sources */, @@ -9547,6 +9574,7 @@ D2DA2358298D57AA00C6C7E6 /* CoreLogger.swift in Sources */, D2160CA329C0DE5700FAA9A5 /* NetworkInstrumentationFeature.swift in Sources */, D2EBEE2D29BA161100B15732 /* HTTPHeadersReader.swift in Sources */, + 3C5CD8C82C3EC62000B12303 /* MemoryWarning.swift in Sources */, E2AA55E82C32C6D9002FEF28 /* ApplicationNotifications.swift in Sources */, D263BCB029DAFFEB00FA0E21 /* PerformancePresetOverride.swift in Sources */, D2DA2359298D57AA00C6C7E6 /* NetworkConnectionInfo.swift in Sources */, @@ -9585,6 +9613,7 @@ D2DA2367298D57AA00C6C7E6 /* Telemetry.swift in Sources */, D2DA2368298D57AA00C6C7E6 /* DataFormat.swift in Sources */, D2160CEE29C0E0E600FAA9A5 /* DatadogURLSessionHandler.swift in Sources */, + 3CDD60DF2C45574500371331 /* ContextReceptionManager.swift in Sources */, D2160C9F29C0DE5700FAA9A5 /* TracingHeaderType.swift in Sources */, D2DA2369298D57AA00C6C7E6 /* AnyEncodable.swift in Sources */, D2DA236A298D57AA00C6C7E6 /* DatadogExtended.swift in Sources */, @@ -9603,6 +9632,7 @@ 3C9B27262B9F174700569C07 /* SpanID.swift in Sources */, D2216EC12A94DE2900ADAEC8 /* FeatureBaggage.swift in Sources */, D2DA2370298D57AA00C6C7E6 /* AnyDecodable.swift in Sources */, + 3CDD60E22C4557BE00371331 /* ContextReceiver.swift in Sources */, 6167E6E32B81207200C3CA2D /* DDCrashReport.swift in Sources */, D2160CC629C0DED100FAA9A5 /* URLSessionTaskInterception.swift in Sources */, 6167E6FE2B81EC0400C3CA2D /* BacktraceReporter.swift in Sources */, diff --git a/DatadogCore/Sources/Core/Context/MemoryWarningPublisher.swift b/DatadogCore/Sources/Core/Context/MemoryWarningPublisher.swift new file mode 100644 index 0000000000..a028a07590 --- /dev/null +++ b/DatadogCore/Sources/Core/Context/MemoryWarningPublisher.swift @@ -0,0 +1,51 @@ +/* + * 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 +import DatadogInternal +import UIKit + +/// Tracks the memory warnings history and publishes it to the subscribers. +internal final class MemoryWarningPublisher: ContextValuePublisher { + private let queue: DispatchQueue + var initialValue: MemoryWarningsHistory + private var receiver: ContextValueReceiver? + private let notificationCenter: NotificationCenter + private var history: MemoryWarningsHistory + + private static let defaultQueue = DispatchQueue( + label: "com.datadoghq.memory-warning-publisher", + target: .global(qos: .utility) + ) + + init( + notificationCenter: NotificationCenter = .default, + queue: DispatchQueue = MemoryWarningPublisher.defaultQueue, + initialValue: MemoryWarningsHistory = .init() + ) { + self.notificationCenter = notificationCenter + self.queue = queue + self.initialValue = initialValue + self.history = initialValue + } + + func publish(to receiver: @escaping ContextValueReceiver) { + queue.async { self.receiver = receiver } + notificationCenter.addObserver(self, selector: #selector(didReceiveMemoryWarning), name: UIApplication.didReceiveMemoryWarningNotification, object: nil) + } + + @objc + func didReceiveMemoryWarning() { + queue.async { + self.history.append(warning: .init()) + self.receiver?(self.history) + } + } + + func cancel() { + notificationCenter.removeObserver(self) + } +} diff --git a/DatadogCore/Sources/Core/DatadogCore.swift b/DatadogCore/Sources/Core/DatadogCore.swift index bbae88954f..b04dbb140f 100644 --- a/DatadogCore/Sources/Core/DatadogCore.swift +++ b/DatadogCore/Sources/Core/DatadogCore.swift @@ -51,6 +51,8 @@ internal final class DatadogCore { /// The message-bus instance. let bus = MessageBus() + let contextReceptionManager: ContextReceptionManager + /// Registry for Features. @ReadWriteLock private(set) var stores: [String: (storage: FeatureStorage, upload: FeatureUpload)] = [:] @@ -93,7 +95,8 @@ internal final class DatadogCore { applicationVersion: String, maxBatchesPerUpload: Int, backgroundTasksEnabled: Bool, - isRunFromExtension: Bool = false + isRunFromExtension: Bool = false, + contextReceptionManager: ContextReceptionManager = .init() ) { self.directory = directory self.dateProvider = dateProvider @@ -106,6 +109,7 @@ internal final class DatadogCore { self.isRunFromExtension = isRunFromExtension self.applicationVersionPublisher = ApplicationVersionPublisher(version: applicationVersion) self.consentPublisher = TrackingConsentPublisher(consent: initialConsent) + self.contextReceptionManager = contextReceptionManager self.contextProvider.subscribe(\.userInfo, to: userInfoPublisher) self.contextProvider.subscribe(\.version, to: applicationVersionPublisher) self.contextProvider.subscribe(\.trackingConsent, to: consentPublisher) @@ -181,7 +185,17 @@ internal final class DatadogCore { private func add(messageReceiver: FeatureMessageReceiver, forKey key: String) { bus.connect(messageReceiver, forKey: key) contextProvider.read { context in - self.bus.queue.async { messageReceiver.receive(message: .context(context), from: self) } + self.bus.queue.async { + let message: FeatureMessage = .context(context) + switch messageReceiver { + case let contextReceiver as ContextReceiver: + if self.contextReceptionManager.canReceive(contextReceiver, message: message) { + contextReceiver.receive(message: message, from: self) + } + default: + messageReceiver.receive(message: message, from: self) + } + } } } @@ -452,6 +466,9 @@ extension DatadogContextProvider { self.subscribe(\.applicationStateHistory, to: applicationStatePublisher) } #endif + + let memoryWarningPublisher = MemoryWarningPublisher() + self.subscribe(\.memoryWarningHistory, to: memoryWarningPublisher) } } diff --git a/DatadogCore/Sources/Core/MessageBus.swift b/DatadogCore/Sources/Core/MessageBus.swift index cd446ccbee..926654e8d0 100644 --- a/DatadogCore/Sources/Core/MessageBus.swift +++ b/DatadogCore/Sources/Core/MessageBus.swift @@ -72,7 +72,7 @@ internal final class MessageBus { } /// Removes the given key and its associated receiver from the bus. - /// + /// /// - Parameter key: The key to remove along with its associated receiver. func removeReceiver(forKey key: String) { queue.async { self.bus.removeValue(forKey: key) } @@ -99,8 +99,13 @@ internal final class MessageBus { return } - let receivers = self.bus.values.filter { - $0.receive(message: message, from: core) + let receivers = self.bus.values.filter { receiver in + switch receiver { + case let contextReceiver as ContextReceiver: + return contextReceiver.receive(message: message, from: core) + default: + return receiver.receive(message: message, from: core) + } } if receivers.isEmpty { diff --git a/DatadogInternal/Sources/Context/DatadogContext.swift b/DatadogInternal/Sources/Context/DatadogContext.swift index daabc0323d..623ab19b70 100644 --- a/DatadogInternal/Sources/Context/DatadogContext.swift +++ b/DatadogInternal/Sources/Context/DatadogContext.swift @@ -85,6 +85,8 @@ public struct DatadogContext { /// Provides the history of app foreground / background states. public var applicationStateHistory: AppStateHistory + public var memoryWarningHistory: MemoryWarningsHistory + // MARK: - Device Specific /// Network information. @@ -140,7 +142,8 @@ public struct DatadogContext { carrierInfo: CarrierInfo? = nil, batteryStatus: BatteryStatus? = nil, isLowPowerModeEnabled: Bool = false, - baggages: [String: FeatureBaggage] = [:] + baggages: [String: FeatureBaggage] = [:], + memoryWarningHistory: MemoryWarningsHistory = .init() ) { self.site = site self.clientToken = clientToken @@ -169,6 +172,7 @@ public struct DatadogContext { self.batteryStatus = batteryStatus self.isLowPowerModeEnabled = isLowPowerModeEnabled self.baggages = baggages + self.memoryWarningHistory = memoryWarningHistory } // swiftlint:enable function_default_parameter_at_end } diff --git a/DatadogInternal/Sources/Context/LaunchTime.swift b/DatadogInternal/Sources/Context/LaunchTime.swift index 069441fd73..bf0382c4d0 100644 --- a/DatadogInternal/Sources/Context/LaunchTime.swift +++ b/DatadogInternal/Sources/Context/LaunchTime.swift @@ -7,7 +7,7 @@ import Foundation /// Provides the application launch time. -public struct LaunchTime: Codable, Equatable, PassthroughAnyCodable { +public struct LaunchTime: Codable, Equatable, PassthroughAnyCodable, Hashable { /// The app process launch duration (in seconds) measured as the time from process start time /// to receiving `UIApplication.didBecomeActiveNotification` notification. /// diff --git a/DatadogInternal/Sources/Context/MemoryWarning.swift b/DatadogInternal/Sources/Context/MemoryWarning.swift new file mode 100644 index 0000000000..dee62e2d05 --- /dev/null +++ b/DatadogInternal/Sources/Context/MemoryWarning.swift @@ -0,0 +1,36 @@ +/* + * 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 +import UIKit + +/// Represents a type containing list of memory warnings +public struct MemoryWarningsHistory: Hashable { + /// List of memory warnings + public private(set) var warnings: [MemoryWarning] + + public init(warnings: [MemoryWarning] = []) { + self.warnings = warnings + } + + /// Appends a new memory warning to the history. + /// - Parameter warning: The memory warning to append. + public mutating func append(warning: MemoryWarning) { + self.warnings.append(warning) + } +} + +/// Represents a memory warning +public struct MemoryWarning: Hashable { + /// The date when the memory warning was received. + public var date: Date + + /// Creates a new memory warning. + /// - Parameter date: The date when the memory warning was received. + public init(date: Date = .init()) { + self.date = date + } +} diff --git a/DatadogInternal/Sources/MessageBus/ContextReceiver.swift b/DatadogInternal/Sources/MessageBus/ContextReceiver.swift new file mode 100644 index 0000000000..6436217cff --- /dev/null +++ b/DatadogInternal/Sources/MessageBus/ContextReceiver.swift @@ -0,0 +1,24 @@ +/* + * 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 receiver of context property messages which can selectively receive them based +/// on the provided `path` provided by the receiver. +public protocol ContextReceiver: FeatureMessageReceiver { + /// The key path to the property in `DatadogContext` which this receiver is interested in. + var path: PartialKeyPath { get } + + /// Receiving a context message will loop though receivers and stop on the first that is able to + /// consume the given message. + /// + /// - Parameters: + /// - message: The message. + /// - core: An instance of the core from which the message is transmitted. + /// - Returns: `true` if the message was processed by one of the receiver; `false` if it was ignored. + @discardableResult + func receive(message: FeatureMessage, from core: DatadogCoreProtocol) -> Bool +} diff --git a/DatadogInternal/Sources/MessageBus/ContextReceptionManager.swift b/DatadogInternal/Sources/MessageBus/ContextReceptionManager.swift new file mode 100644 index 0000000000..10575f340b --- /dev/null +++ b/DatadogInternal/Sources/MessageBus/ContextReceptionManager.swift @@ -0,0 +1,51 @@ +/* + * 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 manager which determines if a receiver can receive a context message. +/// It keeps track of the old context message to compare it with the new one. +public class ContextReceptionManager { + @ReadWriteLock + var old: DatadogContext? + + /// Creates a new `ContextReceptionManager`. + /// - Parameter old: The old context message. + public init(old: DatadogContext? = nil) { + self.old = old + } + + /// Determines if the receiver can receive the message. + /// - Parameters: + /// - receiver: The receiver to check if it can receive the message. + /// - message: The message to check if it can be received. + /// - Returns: `true` if the receiver can receive the message; `false` otherwise. + public func canReceive(_ receiver: ContextReceiver, message: FeatureMessage) -> Bool { + switch message { + case .context(let context): + defer { + old = context + } + let oldValue = old?[keyPath: receiver.path] + let newValue = context[keyPath: receiver.path] + return !equals(oldValue, newValue) + default: + return false + } + } + + func equals(_ left: Any?, _ right: Any?) -> Bool { + guard let left = left as? AnyHashable else { + return false + } + + guard let right = right as? AnyHashable else { + return false + } + + return left == right + } +} diff --git a/DatadogInternal/Sources/MessageBus/FeatureMessageReceiver.swift b/DatadogInternal/Sources/MessageBus/FeatureMessageReceiver.swift index 89558283bd..79e485ea01 100644 --- a/DatadogInternal/Sources/MessageBus/FeatureMessageReceiver.swift +++ b/DatadogInternal/Sources/MessageBus/FeatureMessageReceiver.swift @@ -43,17 +43,26 @@ public struct NOPFeatureMessageReceiver: FeatureMessageReceiver { /// A receiver that combines multiple receivers. It will loop though receivers and stop on the first that is able to /// consume the given message. -public struct CombinedFeatureMessageReceiver: FeatureMessageReceiver { +public class CombinedFeatureMessageReceiver: FeatureMessageReceiver { let receivers: [FeatureMessageReceiver] + let contextReceptionManager: ContextReceptionManager /// Creates an instance initialized with the given receivers. - public init(_ receivers: FeatureMessageReceiver...) { + public init( + _ receivers: FeatureMessageReceiver..., + contextReceptionManager: ContextReceptionManager = .init() + ) { self.receivers = Array(receivers) + self.contextReceptionManager = contextReceptionManager } /// Creates an instance initialized with the given receivers. - public init(_ receivers: [FeatureMessageReceiver]) { + public init( + _ receivers: [FeatureMessageReceiver], + contextReceptionManager: ContextReceptionManager = .init() + ) { self.receivers = receivers + self.contextReceptionManager = contextReceptionManager } /// Receiving a message will loop though receivers and stop on the first that is able to @@ -64,6 +73,17 @@ public struct CombinedFeatureMessageReceiver: FeatureMessageReceiver { /// - core: An instance of the core from which the message is transmitted. /// - Returns: `true` if the message was processed by one of the receiver; `false` if it was ignored. public func receive(message: FeatureMessage, from core: DatadogCoreProtocol) -> Bool { - receivers.contains(where: { $0.receive(message: message, from: core) }) + receivers.contains { receiver in + switch receiver { + case let contextReceiver as ContextReceiver: + if contextReceptionManager.canReceive(contextReceiver, message: message) { + return contextReceiver.receive(message: message, from: core) + } else { + return false + } + default: + return receiver.receive(message: message, from: core) + } + } } } diff --git a/DatadogRUM/Sources/Feature/RUMFeature.swift b/DatadogRUM/Sources/Feature/RUMFeature.swift index 1f4fab6f45..b927a7ac9d 100644 --- a/DatadogRUM/Sources/Feature/RUMFeature.swift +++ b/DatadogRUM/Sources/Feature/RUMFeature.swift @@ -161,7 +161,8 @@ internal final class RUMFeature: DatadogRemoteFeature { } }(), eventsMapper: eventsMapper - ) + ), + MemoryWarningReporter() ] if let watchdogTermination = watchdogTermination { diff --git a/DatadogRUM/Sources/Instrumentation/MemoryWarningReporter.swift b/DatadogRUM/Sources/Instrumentation/MemoryWarningReporter.swift new file mode 100644 index 0000000000..d457e73411 --- /dev/null +++ b/DatadogRUM/Sources/Instrumentation/MemoryWarningReporter.swift @@ -0,0 +1,26 @@ +/* + * 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 DatadogInternal +import UIKit + +/// Receives memory warnings and reports them as RUM errors. +internal struct MemoryWarningReporter: ContextReceiver { + var path: PartialKeyPath = \.memoryWarningHistory + + init() { + } + + /// Receives memory warning and reports it as RUM error. + /// - Parameters: + /// - message: Context message. + /// - core: Datadog core. + /// - Returns: `false` to keep other receivers receiving the message. + func receive(message: DatadogInternal.FeatureMessage, from core: any DatadogInternal.DatadogCoreProtocol) -> Bool { + return false + } +} diff --git a/DatadogTrace/Tests/ContextMessageReceiverTests.swift b/DatadogTrace/Tests/ContextMessageReceiverTests.swift index d8cb9aeb3b..3be3160473 100644 --- a/DatadogTrace/Tests/ContextMessageReceiverTests.swift +++ b/DatadogTrace/Tests/ContextMessageReceiverTests.swift @@ -27,4 +27,42 @@ class ContextMessageReceiverTests: XCTestCase { // Then XCTAssertEqual(receiver.context.applicationStateHistory?.currentSnapshot.state, .active) } + + func testItReceivesLaunchTime() throws { + // given + let launchTimeExpectation = expectation(description: "Launch time received") + + let receiver = LaunchTimeReceiver { + launchTimeExpectation.fulfill() + } + let core = PassthroughCoreMock( + context: .mockWith(), + messageReceiver: receiver + ) + + // When + // this must not call `launchTimeExpectation.fulfill()` + core.context.applicationStateHistory.append(.init(state: .active, date: Date())) + + // this must call `launchTimeExpectation.fulfill()` + core.context.launchTime = .init(launchTime: .mockAny(), launchDate: .mockAny(), isActivePrewarm: .mockAny()) + + // Then + wait(for: [launchTimeExpectation], timeout: 0.5) + } +} + +class LaunchTimeReceiver: ContextPropertyReceiver { + var path: PartialKeyPath = \.launchTime + + let didReceive: () -> Void + + init(didReceive: @escaping () -> Void) { + self.didReceive = didReceive + } + + func receive(message: DatadogInternal.FeatureMessage, from core: any DatadogInternal.DatadogCoreProtocol) -> Bool { + didReceive() + return false + } } diff --git a/TestUtilities/Mocks/CoreMocks/PassthroughCoreMock.swift b/TestUtilities/Mocks/CoreMocks/PassthroughCoreMock.swift index fff2964a5d..ab50e8a488 100644 --- a/TestUtilities/Mocks/CoreMocks/PassthroughCoreMock.swift +++ b/TestUtilities/Mocks/CoreMocks/PassthroughCoreMock.swift @@ -51,6 +51,8 @@ open class PassthroughCoreMock: DatadogCoreProtocol, FeatureScope { /// is executed with `bypassConsent` parameter set to `true`. public var bypassConsentExpectation: XCTestExpectation? + let contextPropertyManager: ContextPropertyManager + /// Creates a Passthrough core mock. /// /// - Parameters: @@ -65,15 +67,25 @@ open class PassthroughCoreMock: DatadogCoreProtocol, FeatureScope { dataStore: DataStore = NOPDataStore(), expectation: XCTestExpectation? = nil, bypassConsentExpectation: XCTestExpectation? = nil, - messageReceiver: FeatureMessageReceiver = NOPFeatureMessageReceiver() + messageReceiver: FeatureMessageReceiver = NOPFeatureMessageReceiver(), + contextPropertyManager: ContextPropertyManager = .init() ) { self.context = context self.dataStore = dataStore self.expectation = expectation self.bypassConsentExpectation = bypassConsentExpectation self.messageReceiver = messageReceiver - - messageReceiver.receive(message: .context(context), from: self) + self.contextPropertyManager = contextPropertyManager + + let message: FeatureMessage = .context(context) + switch messageReceiver { + case let contextReceiver as ContextPropertyReceiver: + if self.contextPropertyManager.canReceive(contextReceiver, message: message) { + contextReceiver.receive(message: message, from: self) + } + default: + messageReceiver.receive(message: message, from: self) + } PassthroughCoreMock.referenceCount += 1 } @@ -97,8 +109,17 @@ open class PassthroughCoreMock: DatadogCoreProtocol, FeatureScope { } public func send(message: FeatureMessage, else fallback: () -> Void) { - if !messageReceiver.receive(message: message, from: self) { - fallback() + switch messageReceiver { + case let contextReceiver as ContextPropertyReceiver: + if self.contextPropertyManager.canReceive(contextReceiver, message: message) { + if !contextReceiver.receive(message: message, from: self) { + fallback() + } + } + default: + if !messageReceiver.receive(message: message, from: self) { + fallback() + } } } diff --git a/TestUtilities/Mocks/CoreMocks/SingleFeatureCoreMock.swift b/TestUtilities/Mocks/CoreMocks/SingleFeatureCoreMock.swift index d75274cbf4..1d9f3b9fca 100644 --- a/TestUtilities/Mocks/CoreMocks/SingleFeatureCoreMock.swift +++ b/TestUtilities/Mocks/CoreMocks/SingleFeatureCoreMock.swift @@ -72,7 +72,8 @@ public final class SingleFeatureCoreMock: PassthroughCoreMock where Fea dataStore: DataStore = NOPDataStore(), expectation: XCTestExpectation? = nil, bypassConsentExpectation: XCTestExpectation? = nil, - messageReceiver: FeatureMessageReceiver = NOPFeatureMessageReceiver() + messageReceiver: FeatureMessageReceiver = NOPFeatureMessageReceiver(), + contextPropertyManager: ContextPropertyManager = .init() ) { self.feature = nil @@ -81,7 +82,8 @@ public final class SingleFeatureCoreMock: PassthroughCoreMock where Fea dataStore: dataStore, expectation: expectation, bypassConsentExpectation: bypassConsentExpectation, - messageReceiver: messageReceiver + messageReceiver: messageReceiver, + contextPropertyManager: contextPropertyManager ) }