Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions Sources/Nodes/Core/AbstractContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ open class _BaseContext: Context { // swiftlint:disable:this type_name
if isActive {
assertionFailure("Lifecycle Violation: Expected `AbstractContext` to deactivate before it is deallocated.")
}
LeakDetector.detect(self) { $0.workerController }
#endif
}
}
Expand Down Expand Up @@ -205,6 +206,14 @@ open class AbstractPresentableContext<CancellableType: Cancellable, PresentableT
self.presentable = presentable
super.init(workers: workers)
}

#if DEBUG

deinit {
LeakDetector.detect(self, delay: 5) { $0.presentable as AnyObject }
}

#endif
}

// swiftlint:enable file_types_order period_spacing
2 changes: 2 additions & 0 deletions Sources/Nodes/Core/AbstractFlow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,8 @@ open class AbstractFlow<ContextInterfaceType, ViewControllerType>: Flow {
if _isStarted {
assertionFailure("Lifecycle Violation: Expected `AbstractFlow` to end before it is deallocated.")
}
LeakDetector.detect(self) { $0._context }
LeakDetector.detect(flowController)
#endif
}
}
Expand Down
45 changes: 39 additions & 6 deletions Sources/Nodes/Utility/LeakDetector.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,15 @@ public enum LeakDetector {
return (info.kp_proc.p_flag & P_TRACED) != 0
}

/// Detects whether the given `object` deallocates from memory as expected.
/// Detects whether the given `object` instance deallocates from memory as expected.
///
/// - Parameters:
/// - object: The instance with which to detect the expected deallocation.
/// - delay: The time interval in seconds to wait before leak detection occurs.
public static func detect(_ object: AnyObject, delay: TimeInterval = 1) {
public static func detect(
_ object: AnyObject,
delay: TimeInterval = 1
) {
struct WeakBox: @unchecked Sendable {
weak var object: AnyObject?
}
Expand All @@ -51,8 +54,8 @@ public enum LeakDetector {
else { return }
let message: String = "Expected object to deallocate: \(object)"
DispatchQueue.main.async {
if let callStack: String = callStackSymbols?.joined(separator: "\n") {
print(callStack)
if let callStackSymbols: [String] {
print(callStackSymbols.joined(separator: "\n"))
}
guard isDebuggedProcessBeingTraced
else { return assertionFailure(message) }
Expand All @@ -62,6 +65,24 @@ public enum LeakDetector {
}
}

/// Detects whether the instance returned by the given `object` closure deallocates from memory as expected.
///
/// - Parameters:
/// - root: The root instance to be passed to the `object` closure.
/// - delay: The time interval in seconds to wait before leak detection occurs.
/// - object: The closure returning the instance with which to detect the expected deallocation.
internal static func detect<Root>(
_ root: Root,
delay: TimeInterval = 1,
_ object: @MainActor (Root) -> AnyObject
) {
typealias Object = (Root) -> AnyObject
withoutActuallyEscaping(object) { object in
let object: Object = unsafeBitCast(object, to: Object.self)
detect(object(root), delay: delay)
}
}

// swiftlint:disable:next discouraged_optional_collection
private static func callStackSymbols() -> [String]? {
let environment: [String: String] = ProcessInfo.processInfo.environment
Expand All @@ -72,8 +93,20 @@ public enum LeakDetector {

#else

// swiftlint:disable:next unused_parameter
public static func detect(_ object: AnyObject, delay: TimeInterval = 1) {}
// swiftlint:disable unused_parameter

public static func detect(
_ object: AnyObject,
delay: TimeInterval = 1
) {}

public static func detect<Root>(
_ root: Root,
delay: TimeInterval = 1,
_ object: @MainActor (Root) -> AnyObject
) {}

// swiftlint:enable unused_parameter

#endif
}