-
Notifications
You must be signed in to change notification settings - Fork 133
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
RUM-3461 Write fatal App Hang to RUM data store, read upon restart
- Loading branch information
Showing
28 changed files
with
898 additions
and
182 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
/* | ||
* 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 | ||
|
||
internal extension FeatureScope { | ||
/// Data store endpoint suited for RUM data. | ||
var rumDataStore: RUMDataStore { | ||
RUMDataStore(featureScope: self) | ||
} | ||
|
||
/// RUM data store endpoint within SDK context. | ||
func rumDataStoreContext(_ block: @escaping (DatadogContext, RUMDataStore) -> Void) { | ||
dataStoreContext { context, dataStore in | ||
block(context, rumDataStore) | ||
} | ||
} | ||
} | ||
|
||
/// RUM interface for data store. | ||
/// | ||
/// It stores values in JSON format and implements convenience for type-safe key referencing and data serialization. | ||
/// Serialization errors are logged to telemetry. | ||
internal struct RUMDataStore { | ||
internal enum Key: String { | ||
/// References pending App Hang information. | ||
/// If found during app start it is considered a fatal hang in previous process. | ||
case fatalAppHangKey = "fatal-app-hang" | ||
} | ||
|
||
/// Encodes values in RUM data store. | ||
private static let encoder = JSONEncoder() | ||
/// Decodes values in RUM data store. | ||
private static let decoder = JSONDecoder() | ||
|
||
/// RUM feature scope. | ||
let featureScope: FeatureScope | ||
|
||
func setValue<V: Codable>(_ value: V, forKey key: Key, version: DataStoreKeyVersion = dataStoreDefaultKeyVersion) { | ||
do { | ||
let data = try RUMDataStore.encoder.encode(value) | ||
featureScope.dataStore.setValue(data, forKey: key.rawValue, version: version) | ||
} catch let error { | ||
DD.logger.error("Failed to encode \(type(of: value)) in RUM Data Store") | ||
featureScope.telemetry.error("Failed to encode \(type(of: value)) in RUM Data Store", error: error) | ||
} | ||
} | ||
|
||
func value<V: Codable>(forKey key: Key, version: DataStoreKeyVersion = dataStoreDefaultKeyVersion, callback: @escaping (V?) -> Void) { | ||
featureScope.dataStore.value(forKey: key.rawValue) { result in | ||
guard let data = result.data(expectedVersion: version) else { | ||
// One of following: | ||
// - no value | ||
// - value but in wrong version → skip | ||
// - error in reading the value (already logged in telemetry by `store`) | ||
callback(nil) | ||
return | ||
} | ||
do { | ||
let value = try RUMDataStore.decoder.decode(V.self, from: data) | ||
callback(value) | ||
} catch let error { | ||
DD.logger.error("Failed to decode \(V.self) from RUM Data Store") | ||
featureScope.telemetry.error("Failed to decode \(V.self) from RUM Data Store", error: error) | ||
callback(nil) | ||
} | ||
} | ||
} | ||
|
||
func removeValue(forKey key: Key) { | ||
featureScope.dataStore.removeValue(forKey: key.rawValue) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
99 changes: 99 additions & 0 deletions
99
DatadogRUM/Sources/Instrumentation/AppHangs/AppHangsMonitor.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
/* | ||
* 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 | ||
|
||
internal final class AppHangsMonitor { | ||
enum Constants { | ||
/// The standardized `error.message` for RUM errors describing an app hang. | ||
static let appHangErrorMessage = "App Hang" | ||
/// The standardized `error.type` for RUM errors describing an app hang. | ||
static let appHangErrorType = "AppHang" | ||
/// The standardized `error.stack` when backtrace generation was not available. | ||
static let appHangStackNotAvailableErrorMessage = "Stack trace was not generated because `DatadogCrashReporting` had not been enabled." | ||
/// The standardized `error.stack` when backtrace generation failed due to an internal error. | ||
static let appHangStackGenerationFailedErrorMessage = "Failed to generate stack trace. This is a known issue and we work on it." | ||
} | ||
|
||
/// Watchdog thread that monitors the main queue for App Hangs. | ||
private let watchdogThread: AppHangsObservingThread | ||
/// Handles non-fatal App Hangs. | ||
internal let nonFatalHangsHandler: NonFatalAppHangsHandler | ||
/// Handles non-fatal App Hangs. | ||
internal let fatalHangsHandler: FatalAppHangsHandler | ||
|
||
convenience init( | ||
featureScope: FeatureScope, | ||
appHangThreshold: TimeInterval, | ||
observedQueue: DispatchQueue, | ||
backtraceReporter: BacktraceReporting, | ||
fatalErrorContext: FatalErrorContextNotifier, | ||
dateProvider: DateProvider, | ||
processID: UUID | ||
) { | ||
self.init( | ||
featureScope: featureScope, | ||
watchdogThread: AppHangsWatchdogThread( | ||
appHangThreshold: appHangThreshold, | ||
queue: observedQueue, | ||
dateProvider: dateProvider, | ||
backtraceReporter: backtraceReporter, | ||
telemetry: featureScope.telemetry | ||
), | ||
fatalErrorContext: fatalErrorContext, | ||
processID: processID | ||
) | ||
} | ||
|
||
init( | ||
featureScope: FeatureScope, | ||
watchdogThread: AppHangsObservingThread, | ||
fatalErrorContext: FatalErrorContextNotifier, | ||
processID: UUID | ||
) { | ||
self.watchdogThread = watchdogThread | ||
self.nonFatalHangsHandler = NonFatalAppHangsHandler() | ||
self.fatalHangsHandler = FatalAppHangsHandler( | ||
featureScope: featureScope, | ||
fatalErrorContext: fatalErrorContext, | ||
processID: processID | ||
) | ||
} | ||
|
||
func start() { | ||
fatalHangsHandler.reportFatalAppHangIfFound() | ||
watchdogThread.onHangStarted = { [weak self] hang in | ||
self?.fatalHangsHandler.startHang(hang: hang) | ||
} | ||
watchdogThread.onHangCancelled = { [weak self] _ in | ||
self?.fatalHangsHandler.cancelHang() | ||
} | ||
watchdogThread.onHangEnded = { [weak self] hang, duration in | ||
self?.fatalHangsHandler.endHang() | ||
self?.nonFatalHangsHandler.endHang(appHang: hang, duration: duration) | ||
} | ||
watchdogThread.start() | ||
} | ||
|
||
func stop() { | ||
watchdogThread.stop() | ||
watchdogThread.onHangStarted = nil | ||
watchdogThread.onHangCancelled = nil | ||
watchdogThread.onHangEnded = nil | ||
} | ||
} | ||
|
||
extension AppHangsMonitor { | ||
/// Awaits the processing of pending app hang. | ||
/// | ||
/// Note: This method is synchronous and will block the caller thread, in worst case up for `appHangThreshold`. | ||
func flush() { | ||
let semaphore = DispatchSemaphore(value: 0) | ||
watchdogThread.onBeforeSleep = { semaphore.signal() } | ||
semaphore.wait() | ||
} | ||
} |
119 changes: 0 additions & 119 deletions
119
DatadogRUM/Sources/Instrumentation/AppHangs/AppHangsObserver.swift
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.