From 3f527c3032763db1f934570f57c81a31046c01d3 Mon Sep 17 00:00:00 2001 From: Ganesh Jangir Date: Wed, 17 Jul 2024 13:11:57 +0200 Subject: [PATCH] RUM-5248 feat: send memory warning as RUM error --- CHANGELOG.md | 1 + Datadog/Datadog.xcodeproj/project.pbxproj | 46 +++++++++++++++++ DatadogCore/Sources/Core/MessageBus.swift | 2 +- .../Sources/RUM/RUMDataModels+objc.swift | 5 +- .../Sources/DataModels/RUMDataModels.swift | 3 +- DatadogRUM/Sources/Feature/RUMFeature.swift | 9 +++- .../MemoryWarnings/MemoryWarning.swift | 30 +++++++++++ .../MemoryWarnings/MemoryWarningMonitor.swift | 50 +++++++++++++++++++ .../MemoryWarningReporter.swift | 50 +++++++++++++++++++ .../Instrumentation/RUMInstrumentation.swift | 9 +++- .../Sources/RUMMonitor/RUMCommand.swift | 20 ++++++++ .../MemoryWarnings/MemoryWarningMocks.swift | 41 +++++++++++++++ .../MemoryWarningMonitorTests.swift | 44 ++++++++++++++++ .../RUMInstrumentationTests.swift | 21 +++++--- 14 files changed, 319 insertions(+), 12 deletions(-) create mode 100644 DatadogRUM/Sources/Instrumentation/MemoryWarnings/MemoryWarning.swift create mode 100644 DatadogRUM/Sources/Instrumentation/MemoryWarnings/MemoryWarningMonitor.swift create mode 100644 DatadogRUM/Sources/Instrumentation/MemoryWarnings/MemoryWarningReporter.swift create mode 100644 DatadogRUM/Tests/Instrumentation/MemoryWarnings/MemoryWarningMocks.swift create mode 100644 DatadogRUM/Tests/Instrumentation/MemoryWarnings/MemoryWarningMonitorTests.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..5ce0bbcb54 100644 --- a/Datadog/Datadog.xcodeproj/project.pbxproj +++ b/Datadog/Datadog.xcodeproj/project.pbxproj @@ -47,6 +47,16 @@ 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 */; }; + 3C4CF9912C47BE07006DE1C0 /* MemoryWarningMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C5CD8C12C3EBA1700B12303 /* MemoryWarningMonitor.swift */; }; + 3C4CF9922C47BE07006DE1C0 /* MemoryWarningMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C5CD8C12C3EBA1700B12303 /* MemoryWarningMonitor.swift */; }; + 3C4CF9942C47CAE9006DE1C0 /* MemoryWarning.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C5CD8C42C3EC61500B12303 /* MemoryWarning.swift */; }; + 3C4CF9952C47CAEA006DE1C0 /* MemoryWarning.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C5CD8C42C3EC61500B12303 /* MemoryWarning.swift */; }; + 3C4CF9982C47CC91006DE1C0 /* MemoryWarningMonitorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C4CF9972C47CC8C006DE1C0 /* MemoryWarningMonitorTests.swift */; }; + 3C4CF9992C47CC92006DE1C0 /* MemoryWarningMonitorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C4CF9972C47CC8C006DE1C0 /* MemoryWarningMonitorTests.swift */; }; + 3C4CF99B2C47DAA5006DE1C0 /* MemoryWarningMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C4CF99A2C47DAA5006DE1C0 /* MemoryWarningMocks.swift */; }; + 3C4CF99C2C47DAA5006DE1C0 /* MemoryWarningMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C4CF99A2C47DAA5006DE1C0 /* MemoryWarningMocks.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 */; }; @@ -2107,6 +2117,11 @@ 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 = ""; }; + 3C4CF9972C47CC8C006DE1C0 /* MemoryWarningMonitorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryWarningMonitorTests.swift; sourceTree = ""; }; + 3C4CF99A2C47DAA5006DE1C0 /* MemoryWarningMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryWarningMocks.swift; sourceTree = ""; }; + 3C5CD8C12C3EBA1700B12303 /* MemoryWarningMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryWarningMonitor.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 = ""; }; @@ -3404,6 +3419,25 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 3C4CF9932C47BE10006DE1C0 /* MemoryWarnings */ = { + isa = PBXGroup; + children = ( + 3C5CD8C42C3EC61500B12303 /* MemoryWarning.swift */, + 3C5CD8C12C3EBA1700B12303 /* MemoryWarningMonitor.swift */, + 3C5CD8CA2C3ECB4800B12303 /* MemoryWarningReporter.swift */, + ); + path = MemoryWarnings; + sourceTree = ""; + }; + 3C4CF9962C47CC72006DE1C0 /* MemoryWarnings */ = { + isa = PBXGroup; + children = ( + 3C4CF9972C47CC8C006DE1C0 /* MemoryWarningMonitorTests.swift */, + 3C4CF99A2C47DAA5006DE1C0 /* MemoryWarningMocks.swift */, + ); + path = MemoryWarnings; + sourceTree = ""; + }; 3C68FCD12C05EE8E00723696 /* WatchdogTerminations */ = { isa = PBXGroup; children = ( @@ -4811,6 +4845,7 @@ 616CCE11250A181C009FED46 /* Instrumentation */ = { isa = PBXGroup; children = ( + 3C4CF9932C47BE10006DE1C0 /* MemoryWarnings */, 616CCE12250A1868009FED46 /* RUMCommandSubscriber.swift */, 616CCE15250A467E009FED46 /* RUMInstrumentation.swift */, 61F3CDA1251118DD00C816E5 /* Views */, @@ -5478,6 +5513,7 @@ 61F3CDA825121F8F00C816E5 /* Instrumentation */ = { isa = PBXGroup; children = ( + 3C4CF9962C47CC72006DE1C0 /* MemoryWarnings */, 61F3CDA925121FA100C816E5 /* Views */, 6141014C251A577D00E3C2D9 /* Actions */, 613F23EF252B1287006CD2D7 /* Resources */, @@ -8706,8 +8742,10 @@ 61C713AB2A3B790B00FA735A /* Monitor.swift in Sources */, D23F8E6429DDCD28001CFAE8 /* SwiftUIViewHandler.swift in Sources */, 3CFF4F922C09E630006F191D /* WatchdogTerminationAppStateManager.swift in Sources */, + 3C4CF9952C47CAEA006DE1C0 /* MemoryWarning.swift in Sources */, D23F8E6529DDCD28001CFAE8 /* RUMFeature.swift in Sources */, D23F8E6629DDCD28001CFAE8 /* RUMDebugging.swift in Sources */, + 3C4CF9912C47BE07006DE1C0 /* MemoryWarningMonitor.swift in Sources */, D23F8E6729DDCD28001CFAE8 /* RUMUUID.swift in Sources */, D23F8E6829DDCD28001CFAE8 /* UIKitExtensions.swift in Sources */, 61C713A82A3B78F900FA735A /* RUMMonitorProtocol+Convenience.swift in Sources */, @@ -8751,6 +8789,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 */, @@ -8778,6 +8817,7 @@ D23F8EA029DDCD38001CFAE8 /* RUMOffViewEventsHandlingRuleTests.swift in Sources */, 61C4534B2C0A0BBF00CC4C17 /* TelemetryInterceptorTests.swift in Sources */, D23F8EA229DDCD38001CFAE8 /* RUMSessionScopeTests.swift in Sources */, + 3C4CF9992C47CC92006DE1C0 /* MemoryWarningMonitorTests.swift in Sources */, D23F8EA329DDCD38001CFAE8 /* RUMUserActionScopeTests.swift in Sources */, 615B0F8C2BB33C2800E9ED6C /* AppHangsMonitorTests.swift in Sources */, 61C713B42A3C3A0B00FA735A /* RUMMonitorProtocol+InternalTests.swift in Sources */, @@ -8797,6 +8837,7 @@ 61C713B72A3C600400FA735A /* RUMMonitorProtocol+ConvenienceTests.swift in Sources */, D23F8EB129DDCD38001CFAE8 /* RUMViewScopeTests.swift in Sources */, D224431029E977A100274EC7 /* TelemetryReceiverTests.swift in Sources */, + 3C4CF99C2C47DAA5006DE1C0 /* MemoryWarningMocks.swift in Sources */, 3C43A3892C188975000BFB21 /* WatchdogTerminationMonitorTests.swift in Sources */, D23F8EB229DDCD38001CFAE8 /* ValuePublisherTests.swift in Sources */, 6174D61B2BFE449300EC7469 /* SessionEndedMetricTests.swift in Sources */, @@ -9035,8 +9076,10 @@ 61C713AA2A3B790B00FA735A /* Monitor.swift in Sources */, D29A9F8529DD85BB005C54A4 /* SwiftUIViewHandler.swift in Sources */, 3CFF4F912C09E630006F191D /* WatchdogTerminationAppStateManager.swift in Sources */, + 3C4CF9942C47CAE9006DE1C0 /* MemoryWarning.swift in Sources */, D29A9F7429DD85BB005C54A4 /* RUMFeature.swift in Sources */, D29A9F7729DD85BB005C54A4 /* RUMDebugging.swift in Sources */, + 3C4CF9922C47BE07006DE1C0 /* MemoryWarningMonitor.swift in Sources */, D29A9F6E29DD85BB005C54A4 /* RUMUUID.swift in Sources */, D29A9F8D29DD8665005C54A4 /* UIKitExtensions.swift in Sources */, 61C713A72A3B78F900FA735A /* RUMMonitorProtocol+Convenience.swift in Sources */, @@ -9080,6 +9123,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 */, @@ -9107,6 +9151,7 @@ D29A9FA629DDB483005C54A4 /* RUMOffViewEventsHandlingRuleTests.swift in Sources */, 61C4534A2C0A0BBF00CC4C17 /* TelemetryInterceptorTests.swift in Sources */, D29A9FBD29DDB483005C54A4 /* RUMSessionScopeTests.swift in Sources */, + 3C4CF9982C47CC91006DE1C0 /* MemoryWarningMonitorTests.swift in Sources */, D29A9FAB29DDB483005C54A4 /* RUMUserActionScopeTests.swift in Sources */, 615B0F8B2BB33C2800E9ED6C /* AppHangsMonitorTests.swift in Sources */, 61C713B32A3C3A0B00FA735A /* RUMMonitorProtocol+InternalTests.swift in Sources */, @@ -9126,6 +9171,7 @@ 61C713B62A3C600400FA735A /* RUMMonitorProtocol+ConvenienceTests.swift in Sources */, D29A9FB829DDB483005C54A4 /* RUMViewScopeTests.swift in Sources */, D224430F29E9779F00274EC7 /* TelemetryReceiverTests.swift in Sources */, + 3C4CF99B2C47DAA5006DE1C0 /* MemoryWarningMocks.swift in Sources */, 3C43A3882C188974000BFB21 /* WatchdogTerminationMonitorTests.swift in Sources */, D29A9F9D29DDB483005C54A4 /* ValuePublisherTests.swift in Sources */, 6174D61A2BFE449300EC7469 /* SessionEndedMetricTests.swift in Sources */, diff --git a/DatadogCore/Sources/Core/MessageBus.swift b/DatadogCore/Sources/Core/MessageBus.swift index cd446ccbee..c264236fb6 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) } diff --git a/DatadogObjc/Sources/RUM/RUMDataModels+objc.swift b/DatadogObjc/Sources/RUM/RUMDataModels+objc.swift index 433a90c8b6..e24fcee3dd 100644 --- a/DatadogObjc/Sources/RUM/RUMDataModels+objc.swift +++ b/DatadogObjc/Sources/RUM/RUMDataModels+objc.swift @@ -1732,6 +1732,7 @@ public enum DDRUMErrorEventErrorCategory: Int { case .appHang?: self = .appHang case .exception?: self = .exception case .watchdogTermination?: self = .watchdogTermination + case .memoryWarning?: self = .memoryWarning } } @@ -1742,6 +1743,7 @@ public enum DDRUMErrorEventErrorCategory: Int { case .appHang: return .appHang case .exception: return .exception case .watchdogTermination: return .watchdogTermination + case .memoryWarning: return .memoryWarning } } @@ -1750,6 +1752,7 @@ public enum DDRUMErrorEventErrorCategory: Int { case appHang case exception case watchdogTermination + case memoryWarning } @objc @@ -7707,4 +7710,4 @@ public class DDTelemetryConfigurationEventView: NSObject { // swiftlint:enable force_unwrapping -// Generated from https://github.com/DataDog/rum-events-format/tree/ae8c30a094339995e234fd55831ade0999bf0612 +// Generated from https://github.com/DataDog/rum-events-format/tree/c853be6db33125c8767ae563d2af47b92636c4e1 diff --git a/DatadogRUM/Sources/DataModels/RUMDataModels.swift b/DatadogRUM/Sources/DataModels/RUMDataModels.swift index bb1b5fe084..ce17a786c6 100644 --- a/DatadogRUM/Sources/DataModels/RUMDataModels.swift +++ b/DatadogRUM/Sources/DataModels/RUMDataModels.swift @@ -807,6 +807,7 @@ public struct RUMErrorEvent: RUMDataModel { case appHang = "App Hang" case exception = "Exception" case watchdogTermination = "Watchdog Termination" + case memoryWarning = "Memory Warning" } /// Properties for one of the error causes @@ -4334,4 +4335,4 @@ public struct RUMTelemetryOperatingSystem: Codable { } } -// Generated from https://github.com/DataDog/rum-events-format/tree/ae8c30a094339995e234fd55831ade0999bf0612 +// Generated from https://github.com/DataDog/rum-events-format/tree/c853be6db33125c8767ae563d2af47b92636c4e1 diff --git a/DatadogRUM/Sources/Feature/RUMFeature.swift b/DatadogRUM/Sources/Feature/RUMFeature.swift index 1f4fab6f45..f45231198b 100644 --- a/DatadogRUM/Sources/Feature/RUMFeature.swift +++ b/DatadogRUM/Sources/Feature/RUMFeature.swift @@ -108,6 +108,12 @@ internal final class RUMFeature: DatadogRemoteFeature { dateProvider: configuration.dateProvider ) + let memoryWarningReporter = MemoryWarningReporter() + let memoryWarningMonitor = MemoryWarningMonitor( + backtraceReporter: core.backtraceReporter, + memoryWarningReporter: memoryWarningReporter + ) + self.instrumentation = RUMInstrumentation( featureScope: featureScope, uiKitRUMViewsPredicate: configuration.uiKitViewsPredicate, @@ -119,7 +125,8 @@ internal final class RUMFeature: DatadogRemoteFeature { backtraceReporter: core.backtraceReporter, fatalErrorContext: dependencies.fatalErrorContext, processID: configuration.processID, - watchdogTermination: watchdogTermination + watchdogTermination: watchdogTermination, + memoryWarningMonitor: memoryWarningMonitor ) self.requestBuilder = RequestBuilder( customIntakeURL: configuration.customEndpoint, diff --git a/DatadogRUM/Sources/Instrumentation/MemoryWarnings/MemoryWarning.swift b/DatadogRUM/Sources/Instrumentation/MemoryWarnings/MemoryWarning.swift new file mode 100644 index 0000000000..e405f46d3b --- /dev/null +++ b/DatadogRUM/Sources/Instrumentation/MemoryWarnings/MemoryWarning.swift @@ -0,0 +1,30 @@ +/* + * 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 + +/// Represents a memory warning +internal struct MemoryWarning { + /// The date when the memory warning was received. + let date: Date + + /// The backtrace at the moment of memory warning. + let backtrace: BacktraceReport? + + /// Creates a new instance of `MemoryWarning + /// - Parameters: + /// - date: Date when the memory warning was received. + /// - backtrace: Backtrace at the moment of memory warning. + init( + date: Date, + backtrace: BacktraceReport? + ) { + self.date = date + self.backtrace = backtrace + } +} diff --git a/DatadogRUM/Sources/Instrumentation/MemoryWarnings/MemoryWarningMonitor.swift b/DatadogRUM/Sources/Instrumentation/MemoryWarnings/MemoryWarningMonitor.swift new file mode 100644 index 0000000000..06aa9ac492 --- /dev/null +++ b/DatadogRUM/Sources/Instrumentation/MemoryWarnings/MemoryWarningMonitor.swift @@ -0,0 +1,50 @@ +/* + * 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 MemoryWarningMonitor { + let notificationCenter: NotificationCenter + let backtraceReporter: BacktraceReporting? + let reporter: MemoryWarningReporting + + init( + backtraceReporter: BacktraceReporting?, + memoryWarningReporter: MemoryWarningReporting, + notificationCenter: NotificationCenter = .default + ) { + self.notificationCenter = notificationCenter + self.backtraceReporter = backtraceReporter + self.reporter = memoryWarningReporter + } + + /// Starts monitoring memory warnings by subscribing to `UIApplication.didReceiveMemoryWarningNotification`. + func start() { + notificationCenter.addObserver(self, selector: #selector(didReceiveMemoryWarning), name: UIApplication.didReceiveMemoryWarningNotification, object: nil) + } + + @objc + func didReceiveMemoryWarning() { + let date: Date = .init() + let backtrace: BacktraceReport? + do { + backtrace = try backtraceReporter?.generateBacktrace() + } catch { + backtrace = nil + } + let warning = MemoryWarning(date: date, backtrace: backtrace) + + reporter.report(warning: warning) + } + + /// Stops monitoring memory warnings. + func stop() { + notificationCenter.removeObserver(self) + } +} diff --git a/DatadogRUM/Sources/Instrumentation/MemoryWarnings/MemoryWarningReporter.swift b/DatadogRUM/Sources/Instrumentation/MemoryWarnings/MemoryWarningReporter.swift new file mode 100644 index 0000000000..ea925f04db --- /dev/null +++ b/DatadogRUM/Sources/Instrumentation/MemoryWarnings/MemoryWarningReporter.swift @@ -0,0 +1,50 @@ +/* + * 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 + +/// Defines operations used for reporting memory warnings. +internal protocol MemoryWarningReporting: RUMCommandPublisher { + /// Reports the given memory warning. + /// - Parameter warning: The memory warning to report. + func report(warning: MemoryWarning) +} + +/// Receives memory warnings and reports them as RUM errors. +internal class MemoryWarningReporter: MemoryWarningReporting { + enum Constants { + /// The standardized `error.message` for RUM errors describing a memory warning. + static let memoryWarningErrorMessage = "Memory Warning" + /// The standardized `error.type` for RUM errors describing a memory warning. + static let memoryWarningErrorType = "MemoryWarning" + /// The standardized `error.stack` when backtrace generation was not available. + static let memoryWarningStackNotAvailableErrorMessage = "Stack trace was not generated because `DatadogCrashReporting` had not been enabled." + } + + private(set) weak var subscriber: RUMCommandSubscriber? + + /// Reports the given memory warning as a RUM error. + /// - Parameter warning: The memory warning to report. + func report(warning: MemoryWarning) { + let command = RUMAddCurrentViewMemoryWarningCommand( + time: warning.date, + attributes: [:], + message: Constants.memoryWarningErrorMessage, + type: Constants.memoryWarningErrorType, + stack: warning.backtrace?.stack ?? Constants.memoryWarningStackNotAvailableErrorMessage, + threads: warning.backtrace?.threads, + binaryImages: warning.backtrace?.binaryImages, + isStackTraceTruncated: warning.backtrace?.wasTruncated + ) + subscriber?.process(command: command) + } + + func publish(to subscriber: any RUMCommandSubscriber) { + self.subscriber = subscriber + } +} diff --git a/DatadogRUM/Sources/Instrumentation/RUMInstrumentation.swift b/DatadogRUM/Sources/Instrumentation/RUMInstrumentation.swift index 8b86fb09c6..3988617e39 100644 --- a/DatadogRUM/Sources/Instrumentation/RUMInstrumentation.swift +++ b/DatadogRUM/Sources/Instrumentation/RUMInstrumentation.swift @@ -40,6 +40,8 @@ internal final class RUMInstrumentation: RUMCommandPublisher { /// Instruments Watchdog Terminations. let watchdogTermination: WatchdogTerminationMonitor? + let memoryWarningMonitor: MemoryWarningMonitor? + // MARK: - Initialization init( @@ -53,7 +55,8 @@ internal final class RUMInstrumentation: RUMCommandPublisher { backtraceReporter: BacktraceReporting, fatalErrorContext: FatalErrorContextNotifying, processID: UUID, - watchdogTermination: WatchdogTerminationMonitor? + watchdogTermination: WatchdogTerminationMonitor?, + memoryWarningMonitor: MemoryWarningMonitor ) { // Always create views handler (we can't know if it will be used by SwiftUI instrumentation) // and only swizzle `UIViewController` if UIKit instrumentation is configured: @@ -125,12 +128,14 @@ internal final class RUMInstrumentation: RUMCommandPublisher { self.longTasks = longTasks self.appHangs = appHangs self.watchdogTermination = watchdogTermination + self.memoryWarningMonitor = memoryWarningMonitor // Enable configured instrumentations: self.viewControllerSwizzler?.swizzle() self.uiApplicationSwizzler?.swizzle() self.longTasks?.start() self.appHangs?.start() + self.memoryWarningMonitor?.start() } deinit { @@ -140,6 +145,7 @@ internal final class RUMInstrumentation: RUMCommandPublisher { longTasks?.stop() appHangs?.stop() watchdogTermination?.stop() + memoryWarningMonitor?.stop() } func publish(to subscriber: RUMCommandSubscriber) { @@ -147,5 +153,6 @@ internal final class RUMInstrumentation: RUMCommandPublisher { actionsHandler?.publish(to: subscriber) longTasks?.publish(to: subscriber) appHangs?.nonFatalHangsHandler.publish(to: subscriber) + memoryWarningMonitor?.reporter.publish(to: subscriber) } } diff --git a/DatadogRUM/Sources/RUMMonitor/RUMCommand.swift b/DatadogRUM/Sources/RUMMonitor/RUMCommand.swift index 0be97fcd21..1cde361874 100644 --- a/DatadogRUM/Sources/RUMMonitor/RUMCommand.swift +++ b/DatadogRUM/Sources/RUMMonitor/RUMCommand.swift @@ -245,6 +245,26 @@ internal struct RUMAddCurrentViewAppHangCommand: RUMErrorCommand { let missedEventType: SessionEndedMetric.MissedEventType? = .error } +internal struct RUMAddCurrentViewMemoryWarningCommand: RUMErrorCommand { + var time: Date + var attributes: [AttributeKey: AttributeValue] + let canStartBackgroundView = false + let isUserInteraction = false + + let message: String + let type: String? + let stack: String? + let category: RUMErrorCategory = .memoryWarning + let isCrash: Bool? = false + let source: RUMInternalErrorSource = .source + let errorSourceType: RUMErrorEvent.Error.SourceType = .ios + let threads: [DDThread]? + let binaryImages: [BinaryImage]? + let isStackTraceTruncated: Bool? + + let missedEventType: SessionEndedMetric.MissedEventType? = .error +} + internal struct RUMAddViewTimingCommand: RUMCommand, RUMViewScopePropagatableAttributes { var time: Date var attributes: [AttributeKey: AttributeValue] diff --git a/DatadogRUM/Tests/Instrumentation/MemoryWarnings/MemoryWarningMocks.swift b/DatadogRUM/Tests/Instrumentation/MemoryWarnings/MemoryWarningMocks.swift new file mode 100644 index 0000000000..a72e7a8d4a --- /dev/null +++ b/DatadogRUM/Tests/Instrumentation/MemoryWarnings/MemoryWarningMocks.swift @@ -0,0 +1,41 @@ +/* + * 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 DatadogRUM +import TestUtilities + +final class MemoryWarningReporterMock: MemoryWarningReporting { + let didReport: (MemoryWarning) -> Void + + init(didReport: @escaping (MemoryWarning) -> Void) { + self.didReport = didReport + } + + func report(warning: DatadogRUM.MemoryWarning) { + didReport(warning) + } + + /// nop + func publish(to subscriber: any DatadogRUM.RUMCommandSubscriber) { + } +} + +extension MemoryWarningMonitor: RandomMockable { + public static func mockRandom() -> MemoryWarningMonitor { + return .init( + backtraceReporter: nil, + memoryWarningReporter: MemoryWarningReporterMock.mockRandom(), + notificationCenter: .default + ) + } +} + +extension MemoryWarningReporterMock: RandomMockable { + static func mockRandom() -> MemoryWarningReporterMock { + return .init { _ in } + } +} diff --git a/DatadogRUM/Tests/Instrumentation/MemoryWarnings/MemoryWarningMonitorTests.swift b/DatadogRUM/Tests/Instrumentation/MemoryWarnings/MemoryWarningMonitorTests.swift new file mode 100644 index 0000000000..dfdd682d55 --- /dev/null +++ b/DatadogRUM/Tests/Instrumentation/MemoryWarnings/MemoryWarningMonitorTests.swift @@ -0,0 +1,44 @@ +/* + * 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 DatadogRUM + +final class MemoryWarningMonitorTests: XCTestCase { + // swiftlint:disable implicitly_unwrapped_optional + var sut: MemoryWarningMonitor! + // swiftlint:enable implicitly_unwrapped_optional + let notificationCenter = NotificationCenter() + + func testStart_memoryWarningReported() throws { + let didReport = expectation(description: "Memory warning reported") + let memoryWarningMock = MemoryWarningReporterMock { _ in + didReport.fulfill() + } + sut = .init( + backtraceReporter: nil, + memoryWarningReporter: memoryWarningMock, + notificationCenter: notificationCenter + ) + sut.start() + notificationCenter.post(name: UIApplication.didReceiveMemoryWarningNotification, object: nil) + wait(for: [didReport], timeout: 0.5) + } + + func testStop_memoryWarningNotReported() { + let memoryWarningMock = MemoryWarningReporterMock { _ in + XCTFail("Memory warning should not be reported after `stop()`") + } + sut = .init( + backtraceReporter: nil, + memoryWarningReporter: memoryWarningMock, + notificationCenter: notificationCenter + ) + sut.start() + sut.stop() + notificationCenter.post(name: UIApplication.didReceiveMemoryWarningNotification, object: nil) + } +} diff --git a/DatadogRUM/Tests/Instrumentation/RUMInstrumentationTests.swift b/DatadogRUM/Tests/Instrumentation/RUMInstrumentationTests.swift index 3f404d6bab..ac9e4c65d1 100644 --- a/DatadogRUM/Tests/Instrumentation/RUMInstrumentationTests.swift +++ b/DatadogRUM/Tests/Instrumentation/RUMInstrumentationTests.swift @@ -25,7 +25,8 @@ class RUMInstrumentationTests: XCTestCase { backtraceReporter: BacktraceReporterMock(), fatalErrorContext: FatalErrorContextNotifierMock(), processID: .mockAny(), - watchdogTermination: .mockRandom() + watchdogTermination: .mockRandom(), + memoryWarningMonitor: .mockRandom() ) // Then @@ -51,7 +52,8 @@ class RUMInstrumentationTests: XCTestCase { backtraceReporter: BacktraceReporterMock(), fatalErrorContext: FatalErrorContextNotifierMock(), processID: .mockAny(), - watchdogTermination: .mockRandom() + watchdogTermination: .mockRandom(), + memoryWarningMonitor: .mockRandom() ) // Then @@ -74,7 +76,8 @@ class RUMInstrumentationTests: XCTestCase { backtraceReporter: BacktraceReporterMock(), fatalErrorContext: FatalErrorContextNotifierMock(), processID: .mockAny(), - watchdogTermination: .mockRandom() + watchdogTermination: .mockRandom(), + memoryWarningMonitor: .mockRandom() ) // Then @@ -100,7 +103,8 @@ class RUMInstrumentationTests: XCTestCase { backtraceReporter: BacktraceReporterMock(), fatalErrorContext: FatalErrorContextNotifierMock(), processID: .mockAny(), - watchdogTermination: .mockRandom() + watchdogTermination: .mockRandom(), + memoryWarningMonitor: .mockRandom() ) // Then @@ -122,7 +126,8 @@ class RUMInstrumentationTests: XCTestCase { backtraceReporter: BacktraceReporterMock(), fatalErrorContext: FatalErrorContextNotifierMock(), processID: .mockAny(), - watchdogTermination: .mockRandom() + watchdogTermination: .mockRandom(), + memoryWarningMonitor: .mockRandom() ) // Then @@ -144,7 +149,8 @@ class RUMInstrumentationTests: XCTestCase { backtraceReporter: BacktraceReporterMock(), fatalErrorContext: FatalErrorContextNotifierMock(), processID: .mockAny(), - watchdogTermination: .mockRandom() + watchdogTermination: .mockRandom(), + memoryWarningMonitor: .mockRandom() ) // Then @@ -166,7 +172,8 @@ class RUMInstrumentationTests: XCTestCase { backtraceReporter: BacktraceReporterMock(), fatalErrorContext: FatalErrorContextNotifierMock(), processID: .mockAny(), - watchdogTermination: .mockRandom() + watchdogTermination: .mockRandom(), + memoryWarningMonitor: .mockRandom() ) let subscriber = RUMCommandSubscriberMock()