From b4ea7a223b84d83a5345d5ef6f091436b675ace3 Mon Sep 17 00:00:00 2001 From: Alexander Cohen Date: Sat, 11 May 2024 17:27:57 -0400 Subject: [PATCH 01/14] Create ChatGTP.md I asked chatgtp about the code. --- ChatGTP.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 ChatGTP.md diff --git a/ChatGTP.md b/ChatGTP.md new file mode 100644 index 0000000..c5df4ce --- /dev/null +++ b/ChatGTP.md @@ -0,0 +1,24 @@ +This Swift code defines a class `Footprint` to manage app memory by tracking its usage and providing notifications when the memory state changes. This utility can be particularly useful for iOS, macOS, tvOS, and watchOS developers to adjust their applications' behavior based on available memory resources. Here's an overview of how it works and some key components: + +### Class Overview +- **`Footprint` Class**: A singleton class that provides mechanisms to track and manage memory usage across the lifecycle of an application. +- **Memory States**: Defines various memory states (`normal`, `warning`, `urgent`, `critical`, `terminal`) to describe how close an app is to being terminated due to memory constraints. + +### Core Features +- **Memory Management**: It checks the actual memory usage (`used`), the available memory (`remaining`), and the total memory limit (`limit`). These values help determine the current memory state of the application. +- **State Change Notifications**: Sends notifications when there is a change in memory state or pressure, which can be utilized to make adjustments in the app's behavior (like reducing cache sizes or other memory-intensive operations). + +### Technical Details +- **Fetching Memory Info**: Utilizes system calls (`task_info`) to fetch memory-related data (`task_vm_info_data_t`). +- **Handling Simulator Differences**: Includes specific conditions for the iOS simulator where memory behaviors are simulated differently from actual devices. +- **Concurrency and Timers**: Uses `DispatchSourceTimer` and `DispatchSourceMemoryPressure` to periodically check and respond to memory conditions. +- **SwiftUI Integration**: Provides SwiftUI extensions for easy integration, allowing views to react to changes in memory conditions directly. + +### Practical Applications +The practical use of `Footprint` might include dynamically managing resources like image caches or complex data structures based on the current memory state. For example, reducing cache limits when the state changes to `warning` or `critical` to prevent the app from being terminated. + +### Considerations +- **Thread Safety**: It uses `NSLock` to manage thread safety, ensuring that changes to memory states are handled without race conditions. +- **Notification Mechanism**: Utilizes `NotificationCenter` to broadcast changes, allowing multiple components of an app to respond to memory state changes efficiently. + +This implementation is robust for applications that need fine-grained control over their memory usage, especially in environments with tight memory resources like mobile devices or wearables. From cf1db025321fde9575757c7402d0a0a3018f1245 Mon Sep 17 00:00:00 2001 From: Alexander Cohen Date: Sun, 12 May 2024 19:13:29 -0400 Subject: [PATCH 02/14] Added a memory provider --- Sources/Footprint/DefaultMemoryProvider.swift | 41 +++++++++++ Sources/Footprint/Footprint.swift | 68 +++++++++---------- 2 files changed, 74 insertions(+), 35 deletions(-) create mode 100644 Sources/Footprint/DefaultMemoryProvider.swift diff --git a/Sources/Footprint/DefaultMemoryProvider.swift b/Sources/Footprint/DefaultMemoryProvider.swift new file mode 100644 index 0000000..04ac84e --- /dev/null +++ b/Sources/Footprint/DefaultMemoryProvider.swift @@ -0,0 +1,41 @@ +/// +/// DefaultMemoryProvider.swift +/// Footprint +/// +/// Copyright (c) 2024 Alexander Cohen. All rights reserved. +/// + +import Foundation + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, visionOS 1.0, *) +extension Footprint { + class DefaultMemoryProvider : MemoryProvider { + + func provide(_ pressure: Footprint.Memory.State = .normal) -> Footprint.Memory { + + var info = task_vm_info_data_t() + var infoCount = TASK_VM_INFO_COUNT + + let kerr = withUnsafeMutablePointer(to: &info) { + $0.withMemoryRebound(to: integer_t.self, capacity: 1) { + task_info(mach_task_self_, thread_flavor_t(TASK_VM_INFO), $0, &infoCount) + } + } + let used = kerr == KERN_SUCCESS ? Int(info.phys_footprint) : 0 + let compressed = kerr == KERN_SUCCESS ? Int(info.compressed) : 0 +#if targetEnvironment(simulator) + // In the simulator `limit_bytes_remaining` returns -1 + // which means we can't calculate limits. + // Due to this, we just set it to 4GB. + let limit = Int(4e+9) + let remaining = max(limit - used, 0) +#else + let remaining = kerr == KERN_SUCCESS ? Int(info.limit_bytes_remaining) : 0 +#endif + return Footprint.Memory(used: used, remaining: remaining, compressed: compressed, pressure: pressure) + } + + private let TASK_BASIC_INFO_COUNT = mach_msg_type_number_t(MemoryLayout.size / MemoryLayout.size) + private let TASK_VM_INFO_COUNT = mach_msg_type_number_t(MemoryLayout.size / MemoryLayout.size) + } +} diff --git a/Sources/Footprint/Footprint.swift b/Sources/Footprint/Footprint.swift index 01dc120..62df423 100644 --- a/Sources/Footprint/Footprint.swift +++ b/Sources/Footprint/Footprint.swift @@ -77,35 +77,22 @@ final public class Footprint : @unchecked Sendable { /// The time at which this snapshot was taken in monotonic milliseconds of uptime. public let timestamp: UInt64 - init(memoryPressure: State = .normal) { - var info = task_vm_info_data_t() - var infoCount = TASK_VM_INFO_COUNT + /// Initialize for the `Memory` structure. + init(used: Int, remaining: Int, compressed: Int = 0, pressure: State = .normal) { - let kerr = withUnsafeMutablePointer(to: &info) { - $0.withMemoryRebound(to: integer_t.self, capacity: 1) { - task_info(mach_task_self_, thread_flavor_t(TASK_VM_INFO), $0, &infoCount) - } - } - used = kerr == KERN_SUCCESS ? Int(info.phys_footprint) : 0 - compressed = kerr == KERN_SUCCESS ? Int(info.compressed) : 0 -#if targetEnvironment(simulator) - // In the simulator `limit_bytes_remaining` returns -1 - // which means we can't calculate limits. - // Due to this, we just set it to 4GB. - limit = Int(4e+9) - remaining = max(limit - used, 0) -#else - remaining = kerr == KERN_SUCCESS ? Int(info.limit_bytes_remaining) : 0 - limit = used + remaining -#endif + self.used = used + self.remaining = remaining + self.limit = used + remaining + self.compressed = compressed + self.pressure = pressure + + let usedRatio = Double(used)/Double(limit) + self.state = usedRatio < 0.25 ? .normal : + usedRatio < 0.50 ? .warning : + usedRatio < 0.75 ? .urgent : + usedRatio < 0.90 ? .critical : .terminal - usedRatio = Double(used)/Double(limit) - state = usedRatio < 0.25 ? .normal : - usedRatio < 0.50 ? .warning : - usedRatio < 0.75 ? .urgent : - usedRatio < 0.90 ? .critical : .terminal - pressure = memoryPressure - timestamp = { + self.timestamp = { let time = mach_absolute_time() var timebaseInfo = mach_timebase_info_data_t() guard mach_timebase_info(&timebaseInfo) == KERN_SUCCESS else { @@ -117,9 +104,6 @@ final public class Footprint : @unchecked Sendable { } private let compressed: Int - private let usedRatio: Double - private let TASK_BASIC_INFO_COUNT = mach_msg_type_number_t(MemoryLayout.size / MemoryLayout.size) - private let TASK_VM_INFO_COUNT = mach_msg_type_number_t(MemoryLayout.size / MemoryLayout.size) } /// The footprint instance that is used throughout the lifetime of your app. @@ -168,7 +152,7 @@ final public class Footprint : @unchecked Sendable { /// /// - returns: A `Bool` indicating if allocating `bytes` will likely work. public func canAllocate(bytes: UInt) -> Bool { - return bytes < Footprint.Memory().remaining + return bytes < provideMemory().remaining } /// The currently tracked memory state. @@ -185,8 +169,10 @@ final public class Footprint : @unchecked Sendable { return _memory.pressure } - private init() { - _memory = Memory() + private init(_ provider: MemoryProvider = DefaultMemoryProvider()) { + + _provider = provider + _memory = _provider.provide(.normal) let timerSource = DispatchSource.makeTimerSource(queue: _queue) timerSource.schedule(deadline: .now(), repeating: .milliseconds(500), leeway: .milliseconds(500)) @@ -215,7 +201,7 @@ final public class Footprint : @unchecked Sendable { } private func heartbeat() { - let memory = Memory(memoryPressure: currentPressureFromSource()) + let memory = provideMemory() storeAndSendObservers(for: memory) #if targetEnvironment(simulator) // In the simulator there are no memory terminations, @@ -227,6 +213,10 @@ final public class Footprint : @unchecked Sendable { #endif } + private func provideMemory() -> Memory { + _provider.provide(currentPressureFromSource()) + } + private func currentPressureFromSource() -> Memory.State { guard let source = _memoryPressureSource else { return .normal @@ -297,10 +287,18 @@ final public class Footprint : @unchecked Sendable { private var _timerSource: DispatchSourceTimer? = nil private let _heartbeatInterval = 500 // milliseconds private var _memoryLock: NSLock = NSLock() - private var _memory: Memory = Memory() + private let _provider: MemoryProvider + private var _memory: Memory private var _memoryPressureSource: DispatchSourceMemoryPressure? = nil } +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, visionOS 1.0, *) +public extension Footprint { + protocol MemoryProvider { + func provide(_ pressure: Footprint.Memory.State) -> Footprint.Memory + } +} + #if canImport(SwiftUI) import SwiftUI From 16b0c29dd0c828be174df0b87bb2273661b70e57 Mon Sep 17 00:00:00 2001 From: Alex Cohen Date: Mon, 3 Jun 2024 15:05:31 -0400 Subject: [PATCH 03/14] Added pod and updated some types --- Footprint.podspec | 17 ++++++ Sources/Footprint/DefaultMemoryProvider.swift | 17 ++++-- Sources/Footprint/Footprint.swift | 61 ++++++++----------- 3 files changed, 55 insertions(+), 40 deletions(-) create mode 100644 Footprint.podspec diff --git a/Footprint.podspec b/Footprint.podspec new file mode 100644 index 0000000..ea3ff32 --- /dev/null +++ b/Footprint.podspec @@ -0,0 +1,17 @@ +Pod::Spec.new do |spec| + spec.name = "Footprint" + spec.version = "1.0.2" + spec.summary = "Footprint is a Swift library that facilitates dynamic memory management." + spec.description = "Footprint is a Swift library that facilitates dynamic memory management in iOS apps" + spec.homepage = "https://github.com/naftaly/Footprint" + spec.license = "MIT" + spec.author = { "Alex Cohen" => "naftaly@me.com" } + spec.ios.deployment_target = "13.0" + spec.osx.deployment_target = "10.15" + spec.watchos.deployment_target = "6.0" + spec.tvos.deployment_target = "13.0" + spec.visionos.deployment_target = "1.0" + spec.source = { :git => "https://github.com/naftaly/Footprint.git", :tag => "v#{spec.version}" } + spec.source_files = "Sources", "Sources/**/*.{h,m,swift}" + spec.swift_versions = "5.0" +end diff --git a/Sources/Footprint/DefaultMemoryProvider.swift b/Sources/Footprint/DefaultMemoryProvider.swift index 04ac84e..a216b67 100644 --- a/Sources/Footprint/DefaultMemoryProvider.swift +++ b/Sources/Footprint/DefaultMemoryProvider.swift @@ -21,18 +21,23 @@ extension Footprint { task_info(mach_task_self_, thread_flavor_t(TASK_VM_INFO), $0, &infoCount) } } - let used = kerr == KERN_SUCCESS ? Int(info.phys_footprint) : 0 - let compressed = kerr == KERN_SUCCESS ? Int(info.compressed) : 0 + let used: Int64 = kerr == KERN_SUCCESS ? Int64(info.phys_footprint) : 0 + let compressed: Int64 = kerr == KERN_SUCCESS ? Int64(info.compressed) : 0 #if targetEnvironment(simulator) // In the simulator `limit_bytes_remaining` returns -1 // which means we can't calculate limits. // Due to this, we just set it to 4GB. - let limit = Int(4e+9) - let remaining = max(limit - used, 0) + let limit: Int64 = 4_000_000_000 + let remaining: Int64 = max(limit - used, 0) #else - let remaining = kerr == KERN_SUCCESS ? Int(info.limit_bytes_remaining) : 0 + let remaining: Int64 = kerr == KERN_SUCCESS ? Int64(info.limit_bytes_remaining) : 0 #endif - return Footprint.Memory(used: used, remaining: remaining, compressed: compressed, pressure: pressure) + return Footprint.Memory( + used: used, + remaining: remaining, + compressed: compressed, + pressure: pressure + ) } private let TASK_BASIC_INFO_COUNT = mach_msg_type_number_t(MemoryLayout.size / MemoryLayout.size) diff --git a/Sources/Footprint/Footprint.swift b/Sources/Footprint/Footprint.swift index 62df423..814058a 100644 --- a/Sources/Footprint/Footprint.swift +++ b/Sources/Footprint/Footprint.swift @@ -59,14 +59,14 @@ final public class Footprint : @unchecked Sendable { } /// The amount of app used memory. Equivalent to `task_vm_info_data_t.phys_footprint`. - public let used: Int + public let used: Int64 /// The amount of memory remaining to the app. Equivalent to `task_vm_info_data_t.limit_bytes_remaining` /// or `os_proc_available_memory`. - public let remaining: Int + public let remaining: Int64 /// The high watermark of memory bytes your app can use before being terminated. - public let limit: Int + public let limit: Int64 /// The state describing where your app sits within the scope of its memory limit. public let state: State @@ -78,7 +78,7 @@ final public class Footprint : @unchecked Sendable { public let timestamp: UInt64 /// Initialize for the `Memory` structure. - init(used: Int, remaining: Int, compressed: Int = 0, pressure: State = .normal) { + init(used: Int64, remaining: Int64, compressed: Int64 = 0, pressure: State = .normal) { self.used = used self.remaining = remaining @@ -103,7 +103,7 @@ final public class Footprint : @unchecked Sendable { }() } - private let compressed: Int + private let compressed: Int64 } /// The footprint instance that is used throughout the lifetime of your app. @@ -151,7 +151,7 @@ final public class Footprint : @unchecked Sendable { /// - Parameter bytes: The number of bytes you are interested in allocating. /// /// - returns: A `Bool` indicating if allocating `bytes` will likely work. - public func canAllocate(bytes: UInt) -> Bool { + public func canAllocate(bytes: UInt64) -> Bool { return bytes < provideMemory().remaining } @@ -174,30 +174,27 @@ final public class Footprint : @unchecked Sendable { _provider = provider _memory = _provider.provide(.normal) - let timerSource = DispatchSource.makeTimerSource(queue: _queue) - timerSource.schedule(deadline: .now(), repeating: .milliseconds(500), leeway: .milliseconds(500)) - timerSource.setEventHandler { [weak self] in + _timerSource = DispatchSource.makeTimerSource(queue: _queue) + _memoryPressureSource = DispatchSource.makeMemoryPressureSource(eventMask: [.all], queue: _queue) + + _timerSource.schedule(deadline: .now(), repeating: .milliseconds(500), leeway: .milliseconds(500)) + _timerSource.setEventHandler { [weak self] in self?.heartbeat() } - timerSource.activate() - _timerSource = timerSource - - let memorySource = DispatchSource.makeMemoryPressureSource(eventMask: [.all], queue: _queue) - memorySource.setEventHandler { [weak self] in + _memoryPressureSource.setEventHandler { [weak self] in self?.heartbeat() } - memorySource.activate() - _memoryPressureSource = memorySource + + _timerSource.activate() + _memoryPressureSource.activate() } deinit { - _timerSource?.suspend() - _timerSource?.cancel() - _timerSource = nil + _timerSource.suspend() + _timerSource.cancel() - _memoryPressureSource?.suspend() - _memoryPressureSource?.cancel() - _memoryPressureSource = nil + _memoryPressureSource.suspend() + _memoryPressureSource.cancel() } private func heartbeat() { @@ -218,13 +215,10 @@ final public class Footprint : @unchecked Sendable { } private func currentPressureFromSource() -> Memory.State { - guard let source = _memoryPressureSource else { - return .normal - } - if source.data.contains(.critical) { + if _memoryPressureSource.data.contains(.critical) { return .critical } - if source.data.contains(.warning) { + if _memoryPressureSource.data.contains(.warning) { return .warning } return .normal @@ -284,19 +278,18 @@ final public class Footprint : @unchecked Sendable { } private let _queue = DispatchQueue(label: "com.bedroomcode.footprint.heartbeat.queue", qos: .utility, target: DispatchQueue.global(qos: .utility)) - private var _timerSource: DispatchSourceTimer? = nil + private let _timerSource: DispatchSourceTimer private let _heartbeatInterval = 500 // milliseconds - private var _memoryLock: NSLock = NSLock() private let _provider: MemoryProvider + private let _memoryPressureSource: DispatchSourceMemoryPressure + + private let _memoryLock: NSLock = NSLock() private var _memory: Memory - private var _memoryPressureSource: DispatchSourceMemoryPressure? = nil } @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, visionOS 1.0, *) -public extension Footprint { - protocol MemoryProvider { - func provide(_ pressure: Footprint.Memory.State) -> Footprint.Memory - } +public protocol MemoryProvider { + func provide(_ pressure: Footprint.Memory.State) -> Footprint.Memory } #if canImport(SwiftUI) From 5e85435a5d611ba0aa7b448eed7fa42d8af7a8fe Mon Sep 17 00:00:00 2001 From: Alexander Cohen Date: Mon, 3 Jun 2024 15:54:02 -0400 Subject: [PATCH 04/14] Update Footprint.podspec --- Footprint.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Footprint.podspec b/Footprint.podspec index ea3ff32..5094ccf 100644 --- a/Footprint.podspec +++ b/Footprint.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = "Footprint" - spec.version = "1.0.2" + spec.version = "1.0.3" spec.summary = "Footprint is a Swift library that facilitates dynamic memory management." spec.description = "Footprint is a Swift library that facilitates dynamic memory management in iOS apps" spec.homepage = "https://github.com/naftaly/Footprint" From 407a635ae489ef026ffc64b56e7ec18591cc3693 Mon Sep 17 00:00:00 2001 From: Brad Fol Date: Tue, 5 Nov 2024 12:40:57 -0800 Subject: [PATCH 05/14] Minor doc comment fixes --- Sources/Footprint/Footprint.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/Footprint/Footprint.swift b/Sources/Footprint/Footprint.swift index 814058a..58d2792 100644 --- a/Sources/Footprint/Footprint.swift +++ b/Sources/Footprint/Footprint.swift @@ -115,21 +115,21 @@ final public class Footprint : @unchecked Sendable { /// Notification name sent when the Footprint.Memory.state and/or /// Footprint.Memory.pressure changes. /// - /// The notification userInfo dict will contain they `.oldMemoryKey`, - /// .newMemoryKey` and `.changesKey` keys. + /// The notification userInfo dict will contain the `.oldMemoryKey`, + /// `.newMemoryKey` and `.changesKey` keys. public static let memoryDidChangeNotification: NSNotification.Name = NSNotification.Name("FootprintMemoryDidChangeNotification") /// Key for the previous value of the memory state in the the /// `.stateDidChangeNotification` userInfo object. - /// Type is `Footprint.Memory`. + /// Value type is `Footprint.Memory`. public static let oldMemoryKey: String = "oldMemory" - /// Key for the new value of the memory statein the the `.stateDidChangeNotification` - /// userInfo object. Type is `Footprint.Memory`. + /// Key for the new value of the memory state in the the `.stateDidChangeNotification` + /// userInfo object. Value type is `Footprint.Memory`. public static let newMemoryKey: String = "newMemory" /// Key for the changes of the memory in the the `.stateDidChangeNotification` - /// userInfo object. Type is `Set` + /// userInfo object. Value type is `Set` public static let changesKey: String = "changes" /// Types of changes possible From 5cbdf44ccc7581b9f90010d21da26810f5681d8d Mon Sep 17 00:00:00 2001 From: Alexander Cohen Date: Tue, 5 Nov 2024 16:12:02 -0500 Subject: [PATCH 06/14] import --- Sources/Footprint/DefaultMemoryProvider.swift | 18 +- Sources/Footprint/Footprint.swift | 215 +++++++++++------- 2 files changed, 142 insertions(+), 91 deletions(-) diff --git a/Sources/Footprint/DefaultMemoryProvider.swift b/Sources/Footprint/DefaultMemoryProvider.swift index a216b67..0cc8d46 100644 --- a/Sources/Footprint/DefaultMemoryProvider.swift +++ b/Sources/Footprint/DefaultMemoryProvider.swift @@ -9,13 +9,13 @@ import Foundation @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, visionOS 1.0, *) extension Footprint { - class DefaultMemoryProvider : MemoryProvider { - + class DefaultMemoryProvider: MemoryProvider { + func provide(_ pressure: Footprint.Memory.State = .normal) -> Footprint.Memory { - + var info = task_vm_info_data_t() var infoCount = TASK_VM_INFO_COUNT - + let kerr = withUnsafeMutablePointer(to: &info) { $0.withMemoryRebound(to: integer_t.self, capacity: 1) { task_info(mach_task_self_, thread_flavor_t(TASK_VM_INFO), $0, &infoCount) @@ -23,15 +23,15 @@ extension Footprint { } let used: Int64 = kerr == KERN_SUCCESS ? Int64(info.phys_footprint) : 0 let compressed: Int64 = kerr == KERN_SUCCESS ? Int64(info.compressed) : 0 -#if targetEnvironment(simulator) + #if targetEnvironment(simulator) // In the simulator `limit_bytes_remaining` returns -1 // which means we can't calculate limits. // Due to this, we just set it to 4GB. let limit: Int64 = 4_000_000_000 let remaining: Int64 = max(limit - used, 0) -#else + #else let remaining: Int64 = kerr == KERN_SUCCESS ? Int64(info.limit_bytes_remaining) : 0 -#endif + #endif return Footprint.Memory( used: used, remaining: remaining, @@ -39,8 +39,8 @@ extension Footprint { pressure: pressure ) } - - private let TASK_BASIC_INFO_COUNT = mach_msg_type_number_t(MemoryLayout.size / MemoryLayout.size) + + private let TASK_BASIC_INFO_COUNT = mach_msg_type_number_t(MemoryLayout.size / MemoryLayout.size) private let TASK_VM_INFO_COUNT = mach_msg_type_number_t(MemoryLayout.size / MemoryLayout.size) } } diff --git a/Sources/Footprint/Footprint.swift b/Sources/Footprint/Footprint.swift index 58d2792..0404c1d 100644 --- a/Sources/Footprint/Footprint.swift +++ b/Sources/Footprint/Footprint.swift @@ -28,70 +28,82 @@ import Foundation /// /// A simple use example is with caches. You could change the maximum cost /// of said cache based on the `.State`. Say, `.normal` has a 100% multiplier, -///`.warning` is 80%, `.critical` is 50% and so on. This leads to your +/// `.warning` is 80%, `.critical` is 50% and so on. This leads to your /// caches being purged based on the users behavior and the memory footprint /// used by your app has a much lower upper bound and much smaller drops. @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, visionOS 1.0, *) -final public class Footprint : @unchecked Sendable { +public final class Footprint: @unchecked Sendable { /// A structure that represents the different values required for easier memory /// handling throughout your apps lifetime. public struct Memory { - + /// State describes how close to app termination your app is based on memory. - public enum State: Int { + public enum State: Comparable, CaseIterable { + /// Everything is good, no need to worry. case normal - + /// You're still doing ok, but start reducing memory usage. case warning - + /// Reduce your memory footprint now. case urgent - + /// Time is of the essence, memory usage is very high, reduce your footprint. case critical - + /// Termination is imminent. If you make it here, you haven't changed your /// memory usage behavior. /// Please revisit memory best practices and profile your app. case terminal + + /// Init from String value + public init?(_ value: String) { + for c in Self.allCases { + if "\(c)" == value { + self = c + return + } + } + return nil + } } - + /// The amount of app used memory. Equivalent to `task_vm_info_data_t.phys_footprint`. public let used: Int64 - + /// The amount of memory remaining to the app. Equivalent to `task_vm_info_data_t.limit_bytes_remaining` /// or `os_proc_available_memory`. public let remaining: Int64 - + /// The high watermark of memory bytes your app can use before being terminated. public let limit: Int64 - + /// The state describing where your app sits within the scope of its memory limit. public let state: State - + /// The state of memory pressure (aka. how close the app is to being Jetsamed/Jetisoned). public let pressure: State - + /// The time at which this snapshot was taken in monotonic milliseconds of uptime. public let timestamp: UInt64 - + /// Initialize for the `Memory` structure. init(used: Int64, remaining: Int64, compressed: Int64 = 0, pressure: State = .normal) { - + self.used = used self.remaining = remaining self.limit = used + remaining self.compressed = compressed self.pressure = pressure - - let usedRatio = Double(used)/Double(limit) + + let usedRatio = Double(used) / Double(limit) self.state = usedRatio < 0.25 ? .normal : usedRatio < 0.50 ? .warning : usedRatio < 0.75 ? .urgent : usedRatio < 0.90 ? .critical : .terminal - + self.timestamp = { let time = mach_absolute_time() var timebaseInfo = mach_timebase_info_data_t() @@ -102,49 +114,50 @@ final public class Footprint : @unchecked Sendable { return timeInNanoseconds / 1_000_000 }() } - + private let compressed: Int64 } - + /// The footprint instance that is used throughout the lifetime of your app. /// /// Although the first call to this method can be made an any point, /// it is best to call this API as soon as possible at startup. public static let shared = Footprint() - + /// Notification name sent when the Footprint.Memory.state and/or /// Footprint.Memory.pressure changes. /// - /// The notification userInfo dict will contain the `.oldMemoryKey`, - /// `.newMemoryKey` and `.changesKey` keys. + /// The notification userInfo dict will contain they `.oldMemoryKey`, + /// .newMemoryKey` and `.changesKey` keys. public static let memoryDidChangeNotification: NSNotification.Name = NSNotification.Name("FootprintMemoryDidChangeNotification") - + /// Key for the previous value of the memory state in the the /// `.stateDidChangeNotification` userInfo object. - /// Value type is `Footprint.Memory`. + /// Type is `Footprint.Memory`. public static let oldMemoryKey: String = "oldMemory" - - /// Key for the new value of the memory state in the the `.stateDidChangeNotification` - /// userInfo object. Value type is `Footprint.Memory`. + + /// Key for the new value of the memory statein the the `.stateDidChangeNotification` + /// userInfo object. Type is `Footprint.Memory`. public static let newMemoryKey: String = "newMemory" - + /// Key for the changes of the memory in the the `.stateDidChangeNotification` - /// userInfo object. Value type is `Set` + /// userInfo object. Type is `Set` public static let changesKey: String = "changes" - + /// Types of changes possible public enum ChangeType: Comparable { case state case pressure + case footprint } - + /// Returns a copy of the current memory structure. public var memory: Memory { _memoryLock.lock() defer { _memoryLock.unlock() } return _memory } - + /// Based on the current memory footprint, tells you if you should be able to allocate /// a certain amount of memory. /// @@ -152,7 +165,7 @@ final public class Footprint : @unchecked Sendable { /// /// - returns: A `Bool` indicating if allocating `bytes` will likely work. public func canAllocate(bytes: UInt64) -> Bool { - return bytes < provideMemory().remaining + bytes < provideMemory().remaining } /// The currently tracked memory state. @@ -161,22 +174,22 @@ final public class Footprint : @unchecked Sendable { defer { _memoryLock.unlock() } return _memory.state } - + /// The currently tracked memory pressure. public var pressure: Memory.State { _memoryLock.lock() defer { _memoryLock.unlock() } return _memory.pressure } - + private init(_ provider: MemoryProvider = DefaultMemoryProvider()) { - + _provider = provider _memory = _provider.provide(.normal) - + _timerSource = DispatchSource.makeTimerSource(queue: _queue) _memoryPressureSource = DispatchSource.makeMemoryPressureSource(eventMask: [.all], queue: _queue) - + _timerSource.schedule(deadline: .now(), repeating: .milliseconds(500), leeway: .milliseconds(500)) _timerSource.setEventHandler { [weak self] in self?.heartbeat() @@ -184,105 +197,140 @@ final public class Footprint : @unchecked Sendable { _memoryPressureSource.setEventHandler { [weak self] in self?.heartbeat() } - + _timerSource.activate() _memoryPressureSource.activate() } - + deinit { _timerSource.suspend() _timerSource.cancel() - + _memoryPressureSource.suspend() _memoryPressureSource.cancel() } - + private func heartbeat() { let memory = provideMemory() storeAndSendObservers(for: memory) -#if targetEnvironment(simulator) + #if targetEnvironment(simulator) // In the simulator there are no memory terminations, // so we fake one. if memory.state == .terminal { - print("Footprint: exiting due to the memory limit") - _exit(EXIT_FAILURE) + // Anything in this env var will enable this + if ProcessInfo.processInfo.environment["SIM_FOOTPRINT_OOM_TERM_ENABLED"] != nil { + print("Footprint: exiting due to the memory limit") + kill(getpid(), SIGTERM) + _exit(EXIT_FAILURE) + } } -#endif + #endif } - + private func provideMemory() -> Memory { _provider.provide(currentPressureFromSource()) } - + private func currentPressureFromSource() -> Memory.State { if _memoryPressureSource.data.contains(.critical) { return .critical - } - if _memoryPressureSource.data.contains(.warning) { + } else if _memoryPressureSource.data.contains(.warning) { return .warning } return .normal } - + + internal func observe(_ action: @escaping (Memory) -> Void) { + _memoryLock.lock() + defer { _memoryLock.unlock() } + _observers.append(action) + let mem = _memory + + DispatchQueue.global().async { + action(mem) + } + } + private func update(with memory: Memory) -> (Memory, Set)? { - + _memoryLock.lock() defer { _memoryLock.unlock() } - + // Verify that state changed... var changeSet: Set = [] - + if _memory.state != memory.state { changeSet.insert(.state) + changeSet.insert(.footprint) } if _memory.pressure != memory.pressure { changeSet.insert(.pressure) + changeSet.insert(.footprint) + } + // memory used changes only on ~1MB intevals + // that's enough precision + if _memory.used - memory.used > 1000 { + changeSet.insert(.footprint) } guard !changeSet.isEmpty else { return nil } - + // ... and enough time has passed to send out // notifications again. Approximately the heartbeat interval. guard memory.timestamp - _memory.timestamp >= _heartbeatInterval else { print("Footprint.state changed but not enough time (\(memory.timestamp - _memory.timestamp)) has changed to deploy it.") return nil } - + print("Footprint changed after \(memory.timestamp - _memory.timestamp)") let oldMemory = _memory _memory = memory - + return (oldMemory, changeSet) } - + private func storeAndSendObservers(for memory: Memory) { - + guard let (oldMemory, changeSet) = update(with: memory) else { return } - + // send all observers outside of the lock on the main queue. // main queue is important since most of us will want to // make changes that might touch the UI. - print("Footprint changes \(changeSet)") - print("Footprint.state \(memory.state)") - print("Footprint.pressure \(memory.pressure)") - DispatchQueue.main.async { - NotificationCenter.default.post(name: Footprint.memoryDidChangeNotification, object: nil, userInfo: [ - Footprint.newMemoryKey: memory, - Footprint.oldMemoryKey: oldMemory, - Footprint.changesKey: changeSet - ]) + if changeSet.contains(.pressure) || changeSet.contains(.state) { + + print("Footprint changes \(changeSet)") + print("Footprint.state \(memory.state)") + print("Footprint.pressure \(memory.pressure)") + DispatchQueue.main.async { + NotificationCenter.default.post(name: Footprint.memoryDidChangeNotification, object: nil, userInfo: [ + Footprint.newMemoryKey: memory, + Footprint.oldMemoryKey: oldMemory, + Footprint.changesKey: changeSet, + ]) + } + } + + // send footprint observers + if changeSet.contains(.footprint) { + // copy behind the lock + // deploy outside the lock + _memoryLock.lock() + let observers = _observers + _memoryLock.unlock() + observers.forEach { $0(memory) } } } - + private let _queue = DispatchQueue(label: "com.bedroomcode.footprint.heartbeat.queue", qos: .utility, target: DispatchQueue.global(qos: .utility)) private let _timerSource: DispatchSourceTimer private let _heartbeatInterval = 500 // milliseconds private let _provider: MemoryProvider private let _memoryPressureSource: DispatchSourceMemoryPressure - + + private var _observers: [(Memory) -> Void] = [] private let _memoryLock: NSLock = NSLock() private var _memory: Memory } @@ -295,9 +343,9 @@ public protocol MemoryProvider { #if canImport(SwiftUI) import SwiftUI -@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, visionOS 1.0, *) extension View { - + /// A SwiftUI extension providing a convenient way to observe changes in the memory /// state of the app through the `onFootprintMemoryDidChange` modifier. /// @@ -305,7 +353,7 @@ extension View { /// /// The `onFootprintMemoryDidChange` extension allows you to respond /// to changes in the app's memory state and pressure by providing a closure that is executed - /// whenever the memory state transitions. You can also use specific modifiers for + /// whenever the memory state transitions. You can also use specific modifiers for /// state (`onFootprintMemoryStateDidChange`) or /// pressure (`onFootprintMemoryPressureDidChange`). /// @@ -322,36 +370,39 @@ extension View { return onReceive(NotificationCenter.default.publisher(for: Footprint.memoryDidChangeNotification)) { note in if let changes = note.userInfo?[Footprint.changesKey] as? Set, let memory = note.userInfo?[Footprint.newMemoryKey] as? Footprint.Memory, - let prevMemory = note.userInfo?[Footprint.oldMemoryKey] as? Footprint.Memory { + let prevMemory = note.userInfo?[Footprint.oldMemoryKey] as? Footprint.Memory + { action(memory, prevMemory, changes) } } } - + @inlinable public func onFootprintMemoryStateDidChange(perform action: @escaping (_ state: Footprint.Memory.State, _ previousState: Footprint.Memory.State) -> Void) -> some View { _ = Footprint.shared // make sure it's running return onReceive(NotificationCenter.default.publisher(for: Footprint.memoryDidChangeNotification)) { note in if let changes = note.userInfo?[Footprint.changesKey] as? Set, changes.contains(.state), let memory = note.userInfo?[Footprint.newMemoryKey] as? Footprint.Memory, - let prevMemory = note.userInfo?[Footprint.oldMemoryKey] as? Footprint.Memory { + let prevMemory = note.userInfo?[Footprint.oldMemoryKey] as? Footprint.Memory + { action(memory.state, prevMemory.state) } } } - + @inlinable public func onFootprintMemoryPressureDidChange(perform action: @escaping (_ pressure: Footprint.Memory.State, _ previousPressure: Footprint.Memory.State) -> Void) -> some View { _ = Footprint.shared // make sure it's running return onReceive(NotificationCenter.default.publisher(for: Footprint.memoryDidChangeNotification)) { note in if let changes = note.userInfo?[Footprint.changesKey] as? Set, changes.contains(.pressure), let memory = note.userInfo?[Footprint.newMemoryKey] as? Footprint.Memory, - let prevMemory = note.userInfo?[Footprint.oldMemoryKey] as? Footprint.Memory { + let prevMemory = note.userInfo?[Footprint.oldMemoryKey] as? Footprint.Memory + { action(memory.pressure, prevMemory.pressure) } } } - + } #endif From 951293e86bd5e633189278f9ecbabc09f9565ead Mon Sep 17 00:00:00 2001 From: Alexander Cohen Date: Tue, 5 Nov 2024 16:15:10 -0500 Subject: [PATCH 07/14] added previous changes --- Sources/Footprint/Footprint.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/Footprint/Footprint.swift b/Sources/Footprint/Footprint.swift index 0404c1d..404e31c 100644 --- a/Sources/Footprint/Footprint.swift +++ b/Sources/Footprint/Footprint.swift @@ -127,21 +127,21 @@ public final class Footprint: @unchecked Sendable { /// Notification name sent when the Footprint.Memory.state and/or /// Footprint.Memory.pressure changes. /// - /// The notification userInfo dict will contain they `.oldMemoryKey`, - /// .newMemoryKey` and `.changesKey` keys. + /// The notification userInfo dict will contain the `.oldMemoryKey`, + /// `.newMemoryKey` and `.changesKey` keys. public static let memoryDidChangeNotification: NSNotification.Name = NSNotification.Name("FootprintMemoryDidChangeNotification") /// Key for the previous value of the memory state in the the /// `.stateDidChangeNotification` userInfo object. - /// Type is `Footprint.Memory`. + /// Value type is `Footprint.Memory`. public static let oldMemoryKey: String = "oldMemory" - /// Key for the new value of the memory statein the the `.stateDidChangeNotification` - /// userInfo object. Type is `Footprint.Memory`. + /// Key for the new value of the memory state in the the `.stateDidChangeNotification` + /// userInfo object. Value type is `Footprint.Memory`. public static let newMemoryKey: String = "newMemory" /// Key for the changes of the memory in the the `.stateDidChangeNotification` - /// userInfo object. Type is `Set` + /// userInfo object. Value type is `Set` public static let changesKey: String = "changes" /// Types of changes possible From 45d3cf328bb25d8e84210c624e7aca4cb4918466 Mon Sep 17 00:00:00 2001 From: Alexander Cohen Date: Thu, 7 Nov 2024 22:07:48 -0500 Subject: [PATCH 08/14] Cleaned up locks and fixed a footprint change bug --- Sources/Footprint/Footprint.swift | 36 ++++++++++------------- Tests/FootprintTests/FootprintTests.swift | 3 ++ 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/Sources/Footprint/Footprint.swift b/Sources/Footprint/Footprint.swift index 404e31c..7847576 100644 --- a/Sources/Footprint/Footprint.swift +++ b/Sources/Footprint/Footprint.swift @@ -39,7 +39,7 @@ public final class Footprint: @unchecked Sendable { public struct Memory { /// State describes how close to app termination your app is based on memory. - public enum State: Comparable, CaseIterable { + public enum State: Int, Comparable, CaseIterable { /// Everything is good, no need to worry. case normal @@ -57,7 +57,10 @@ public final class Footprint: @unchecked Sendable { /// memory usage behavior. /// Please revisit memory best practices and profile your app. case terminal - + + public static func < (lhs: Self, rhs: Self) -> Bool { + lhs.rawValue < rhs.rawValue + } /// Init from String value public init?(_ value: String) { for c in Self.allCases { @@ -153,9 +156,7 @@ public final class Footprint: @unchecked Sendable { /// Returns a copy of the current memory structure. public var memory: Memory { - _memoryLock.lock() - defer { _memoryLock.unlock() } - return _memory + _memoryLock.withLock { _memory } } /// Based on the current memory footprint, tells you if you should be able to allocate @@ -170,16 +171,12 @@ public final class Footprint: @unchecked Sendable { /// The currently tracked memory state. public var state: Memory.State { - _memoryLock.lock() - defer { _memoryLock.unlock() } - return _memory.state + _memoryLock.withLock { _memory.state } } /// The currently tracked memory pressure. public var pressure: Memory.State { - _memoryLock.lock() - defer { _memoryLock.unlock() } - return _memory.pressure + _memoryLock.withLock { _memory.pressure } } private init(_ provider: MemoryProvider = DefaultMemoryProvider()) { @@ -240,12 +237,11 @@ public final class Footprint: @unchecked Sendable { return .normal } - internal func observe(_ action: @escaping (Memory) -> Void) { - _memoryLock.lock() - defer { _memoryLock.unlock() } - _observers.append(action) - let mem = _memory - + public func observe(_ action: @escaping (Memory) -> Void) { + let mem = _memoryLock.withLock { + _observers.append(action) + return _memory + } DispatchQueue.global().async { action(mem) } @@ -269,7 +265,7 @@ public final class Footprint: @unchecked Sendable { } // memory used changes only on ~1MB intevals // that's enough precision - if _memory.used - memory.used > 1000 { + if abs(_memory.used - memory.used) > 1000000 { changeSet.insert(.footprint) } guard !changeSet.isEmpty else { @@ -317,9 +313,7 @@ public final class Footprint: @unchecked Sendable { if changeSet.contains(.footprint) { // copy behind the lock // deploy outside the lock - _memoryLock.lock() - let observers = _observers - _memoryLock.unlock() + let observers = _memoryLock.withLock { _observers } observers.forEach { $0(memory) } } } diff --git a/Tests/FootprintTests/FootprintTests.swift b/Tests/FootprintTests/FootprintTests.swift index 5d26756..102572e 100644 --- a/Tests/FootprintTests/FootprintTests.swift +++ b/Tests/FootprintTests/FootprintTests.swift @@ -9,4 +9,7 @@ class FootprintTests: XCTestCase { XCTAssertGreaterThan(mem.limit, 0) } + func testName() { + XCTAssertEqual("\(Footprint.Memory.State.normal)", "normal") + } } From a6da3b02f7f7596e1748e0c0cbfbce2881851c7a Mon Sep 17 00:00:00 2001 From: Alexander Cohen Date: Wed, 5 Feb 2025 13:56:40 -0500 Subject: [PATCH 09/14] Update .gitignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 4f20167..7036078 100644 --- a/.gitignore +++ b/.gitignore @@ -44,7 +44,7 @@ playground.xcworkspace # # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata # hence it is not needed unless you have added a package configuration file to your project -# .swiftpm +.swiftpm .build/ From d68aaa43b2da44609a48347d9602accdd9b342bf Mon Sep 17 00:00:00 2001 From: Alexander Cohen Date: Wed, 5 Feb 2025 13:57:16 -0500 Subject: [PATCH 10/14] Delete contents.xcworkspacedata --- .../xcode/package.xcworkspace/contents.xcworkspacedata | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata diff --git a/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 919434a..0000000 --- a/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - From 0d12b576ff3953d17dcc9840bdef66a56e273a4a Mon Sep 17 00:00:00 2001 From: Alexander Cohen Date: Sat, 12 Apr 2025 16:57:46 -0400 Subject: [PATCH 11/14] Update Footprint.swift --- Sources/Footprint/Footprint.swift | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Sources/Footprint/Footprint.swift b/Sources/Footprint/Footprint.swift index 7847576..1afc752 100644 --- a/Sources/Footprint/Footprint.swift +++ b/Sources/Footprint/Footprint.swift @@ -275,11 +275,9 @@ public final class Footprint: @unchecked Sendable { // ... and enough time has passed to send out // notifications again. Approximately the heartbeat interval. guard memory.timestamp - _memory.timestamp >= _heartbeatInterval else { - print("Footprint.state changed but not enough time (\(memory.timestamp - _memory.timestamp)) has changed to deploy it.") return nil } - print("Footprint changed after \(memory.timestamp - _memory.timestamp)") let oldMemory = _memory _memory = memory @@ -297,9 +295,6 @@ public final class Footprint: @unchecked Sendable { // make changes that might touch the UI. if changeSet.contains(.pressure) || changeSet.contains(.state) { - print("Footprint changes \(changeSet)") - print("Footprint.state \(memory.state)") - print("Footprint.pressure \(memory.pressure)") DispatchQueue.main.async { NotificationCenter.default.post(name: Footprint.memoryDidChangeNotification, object: nil, userInfo: [ Footprint.newMemoryKey: memory, From 7f9010e91642e6e3e012ebd6b014e0d60dad3d11 Mon Sep 17 00:00:00 2001 From: Alexander Cohen Date: Mon, 26 May 2025 13:08:12 -0400 Subject: [PATCH 12/14] Added an AsyncStream to receive footprint changes --- Sources/Footprint/DefaultMemoryProvider.swift | 2 +- Sources/Footprint/Footprint.swift | 36 +++++++++++++------ 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/Sources/Footprint/DefaultMemoryProvider.swift b/Sources/Footprint/DefaultMemoryProvider.swift index 0cc8d46..1585d2e 100644 --- a/Sources/Footprint/DefaultMemoryProvider.swift +++ b/Sources/Footprint/DefaultMemoryProvider.swift @@ -27,7 +27,7 @@ extension Footprint { // In the simulator `limit_bytes_remaining` returns -1 // which means we can't calculate limits. // Due to this, we just set it to 4GB. - let limit: Int64 = 4_000_000_000 + let limit: Int64 = 6_000_000_000 let remaining: Int64 = max(limit - used, 0) #else let remaining: Int64 = kerr == KERN_SUCCESS ? Int64(info.limit_bytes_remaining) : 0 diff --git a/Sources/Footprint/Footprint.swift b/Sources/Footprint/Footprint.swift index 1afc752..324f93c 100644 --- a/Sources/Footprint/Footprint.swift +++ b/Sources/Footprint/Footprint.swift @@ -6,6 +6,7 @@ /// import Foundation +import os /// The footprint manages snapshots of app memory limits and state, /// and notifies your app when these change. @@ -82,7 +83,7 @@ public final class Footprint: @unchecked Sendable { /// The high watermark of memory bytes your app can use before being terminated. public let limit: Int64 - + /// The state describing where your app sits within the scope of its memory limit. public let state: State @@ -93,7 +94,12 @@ public final class Footprint: @unchecked Sendable { public let timestamp: UInt64 /// Initialize for the `Memory` structure. - init(used: Int64, remaining: Int64, compressed: Int64 = 0, pressure: State = .normal) { + init( + used: Int64, + remaining: Int64, + compressed: Int64 = 0, + pressure: State = .normal + ) { self.used = used self.remaining = remaining @@ -108,13 +114,8 @@ public final class Footprint: @unchecked Sendable { usedRatio < 0.90 ? .critical : .terminal self.timestamp = { - let time = mach_absolute_time() - var timebaseInfo = mach_timebase_info_data_t() - guard mach_timebase_info(&timebaseInfo) == KERN_SUCCESS else { - return 0 - } - let timeInNanoseconds = time * UInt64(timebaseInfo.numer) / UInt64(timebaseInfo.denom) - return timeInNanoseconds / 1_000_000 + let timeInNanoseconds = clock_gettime_nsec_np(CLOCK_UPTIME_RAW) + return timeInNanoseconds / NSEC_PER_MSEC }() } @@ -158,7 +159,14 @@ public final class Footprint: @unchecked Sendable { public var memory: Memory { _memoryLock.withLock { _memory } } - + + /// Returns an AsyncStream that pushes a _Memory_ as it changes. + public var memoryStream: AsyncStream { + AsyncStream { continuation in + _memoryStreamContinuations.append(continuation) + } + } + /// Based on the current memory footprint, tells you if you should be able to allocate /// a certain amount of memory. /// @@ -205,6 +213,8 @@ public final class Footprint: @unchecked Sendable { _memoryPressureSource.suspend() _memoryPressureSource.cancel() + + _memoryStreamContinuations.forEach { $0.finish() } } private func heartbeat() { @@ -308,8 +318,11 @@ public final class Footprint: @unchecked Sendable { if changeSet.contains(.footprint) { // copy behind the lock // deploy outside the lock - let observers = _memoryLock.withLock { _observers } + let (observers, continuations) = _memoryLock.withLock { + (_observers, _memoryStreamContinuations) + } observers.forEach { $0(memory) } + continuations.forEach { $0.yield(memory) } } } @@ -322,6 +335,7 @@ public final class Footprint: @unchecked Sendable { private var _observers: [(Memory) -> Void] = [] private let _memoryLock: NSLock = NSLock() private var _memory: Memory + private var _memoryStreamContinuations: [AsyncStream.Continuation] = [] } @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, visionOS 1.0, *) From b499ba40feb93ceee2839f8fc40dfce3bbab9bb2 Mon Sep 17 00:00:00 2001 From: Alexander Cohen Date: Mon, 26 May 2025 13:15:19 -0400 Subject: [PATCH 13/14] Update Footprint.podspec --- Footprint.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Footprint.podspec b/Footprint.podspec index 5094ccf..80133ba 100644 --- a/Footprint.podspec +++ b/Footprint.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = "Footprint" - spec.version = "1.0.3" + spec.version = "1.0.6" spec.summary = "Footprint is a Swift library that facilitates dynamic memory management." spec.description = "Footprint is a Swift library that facilitates dynamic memory management in iOS apps" spec.homepage = "https://github.com/naftaly/Footprint" From 61f8d57d2022c474e1eda17e2f80b651a3b2229a Mon Sep 17 00:00:00 2001 From: Alexander Cohen Date: Fri, 30 May 2025 17:10:11 -0400 Subject: [PATCH 14/14] Updated readme --- ChatGTP.md | 24 ------ README.md | 229 +++++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 198 insertions(+), 55 deletions(-) delete mode 100644 ChatGTP.md diff --git a/ChatGTP.md b/ChatGTP.md deleted file mode 100644 index c5df4ce..0000000 --- a/ChatGTP.md +++ /dev/null @@ -1,24 +0,0 @@ -This Swift code defines a class `Footprint` to manage app memory by tracking its usage and providing notifications when the memory state changes. This utility can be particularly useful for iOS, macOS, tvOS, and watchOS developers to adjust their applications' behavior based on available memory resources. Here's an overview of how it works and some key components: - -### Class Overview -- **`Footprint` Class**: A singleton class that provides mechanisms to track and manage memory usage across the lifecycle of an application. -- **Memory States**: Defines various memory states (`normal`, `warning`, `urgent`, `critical`, `terminal`) to describe how close an app is to being terminated due to memory constraints. - -### Core Features -- **Memory Management**: It checks the actual memory usage (`used`), the available memory (`remaining`), and the total memory limit (`limit`). These values help determine the current memory state of the application. -- **State Change Notifications**: Sends notifications when there is a change in memory state or pressure, which can be utilized to make adjustments in the app's behavior (like reducing cache sizes or other memory-intensive operations). - -### Technical Details -- **Fetching Memory Info**: Utilizes system calls (`task_info`) to fetch memory-related data (`task_vm_info_data_t`). -- **Handling Simulator Differences**: Includes specific conditions for the iOS simulator where memory behaviors are simulated differently from actual devices. -- **Concurrency and Timers**: Uses `DispatchSourceTimer` and `DispatchSourceMemoryPressure` to periodically check and respond to memory conditions. -- **SwiftUI Integration**: Provides SwiftUI extensions for easy integration, allowing views to react to changes in memory conditions directly. - -### Practical Applications -The practical use of `Footprint` might include dynamically managing resources like image caches or complex data structures based on the current memory state. For example, reducing cache limits when the state changes to `warning` or `critical` to prevent the app from being terminated. - -### Considerations -- **Thread Safety**: It uses `NSLock` to manage thread safety, ensuring that changes to memory states are handled without race conditions. -- **Notification Mechanism**: Utilizes `NotificationCenter` to broadcast changes, allowing multiple components of an app to respond to memory state changes efficiently. - -This implementation is robust for applications that need fine-grained control over their memory usage, especially in environments with tight memory resources like mobile devices or wearables. diff --git a/README.md b/README.md index fa62dfd..f51d154 100644 --- a/README.md +++ b/README.md @@ -6,83 +6,250 @@ ## Overview -Footprint is a Swift library that helps manage and monitor memory usage in your app. It provides a flexible approach to handling memory levels, allowing you to adapt your app's behavior based on the available memory and potential termination risks. +Footprint is a Swift library that provides proactive memory management for your Apple platform apps. Instead of waiting for memory warnings that come too late, Footprint gives you real-time insights into your app's memory usage and proximity to termination, allowing you to adapt your app's behavior dynamically. -### Key Features +### The Problem -- **Memory State Management:** Footprint categorizes memory states into normal, warning, critical, and terminal, providing insights into your app's proximity to termination due to memory constraints. +Traditional memory management on Apple platforms relies on memory warnings that often arrive too late, especially for larger apps. While `os_proc_available_memory` tells you how much memory remains, you still lack the complete picture of your memory boundaries and usage patterns. -- **Dynamic Memory Handling:** Change your app's behavior dynamically based on the current memory state. For instance, adjust cache sizes or optimize resource usage to enhance performance. +### The Solution -- **SwiftUI Integration:** Easily observe and respond to changes in the app's memory state within SwiftUI views using the `onFootprintMemoryStateDidChange` modifier. +Footprint bridges this gap by providing: +- **Complete memory visibility**: Track used, remaining, and total memory limits +- **Proactive state management**: Five distinct memory states from normal to terminal +- **Behavioral adaptation**: Change your app's behavior before hitting critical memory limits +- **Multiple observation patterns**: NotificationCenter, async streams, and SwiftUI modifiers + +## Key Features + +- **Five Memory States**: Navigate through normal, warning, urgent, critical, and terminal states based on memory usage ratios +- **Dual Tracking**: Monitor both memory footprint and system memory pressure +- **Real-time Monitoring**: 500ms heartbeat with smart change detection +- **SwiftUI Integration**: Convenient view modifiers for reactive UI updates +- **Async Support**: Modern async/await patterns with AsyncStream +- **Cross-platform**: Works on iOS, macOS, tvOS, watchOS, and visionOS ## Installation -Add the Footprint library to your project: +Add Footprint to your project using Swift Package Manager: -1. In Xcode, with your app project open, navigate to File > Add Packages. -2. When prompted, add the Firebase Apple platforms SDK repository: +1. In Xcode, navigate to File > Add Package Dependencies +2. Enter the repository URL: ``` https://github.com/naftaly/Footprint ``` ## Usage -### Initialization +### Basic Setup -Initialize Footprint as early as possible in your app's lifecycle: +Initialize Footprint early in your app's lifecycle. The shared instance automatically begins monitoring: ```swift +// Start monitoring (typically in your App or AppDelegate) let _ = Footprint.shared ``` ### Memory State Observation -Respond to changes in memory state using the provided notification: +#### Using NotificationCenter + +```swift +NotificationCenter.default.addObserver( + forName: Footprint.memoryDidChangeNotification, + object: nil, + queue: nil +) { notification in + guard let newMemory = notification.userInfo?[Footprint.newMemoryKey] as? Footprint.Memory, + let oldMemory = notification.userInfo?[Footprint.oldMemoryKey] as? Footprint.Memory, + let changes = notification.userInfo?[Footprint.changesKey] as? Set + else { return } + + if changes.contains(.state) { + print("Memory state changed from \(oldMemory.state) to \(newMemory.state)") + adaptBehavior(for: newMemory.state) + } +} +``` + +#### Using Closures ```swift -NotificationCenter.default.addObserver(forName: Footprint.stateDidChangeNotification, object: nil, queue: nil) { notification in - if let newState = notification.userInfo?[Footprint.newMemoryStateKey] as? Footprint.Memory.State, - let oldState = notification.userInfo?[Footprint.oldMemoryStateKey] as? Footprint.Memory.State { - print("Memory state changed from \(oldState) to \(newState)") - // Perform actions based on the memory state change +Footprint.shared.observe { memory in + print("Current memory state: \(memory.state)") + print("Used: \(ByteCountFormatter.string(fromByteCount: memory.used, countStyle: .memory))") + print("Remaining: \(ByteCountFormatter.string(fromByteCount: memory.remaining, countStyle: .memory))") +} +``` + +#### Using Async Streams + +```swift +Task { + for await memory in Footprint.shared.memoryStream { + await handleMemoryChange(memory) } } ``` ### SwiftUI Integration -Use the SwiftUI extension to observe changes in memory state within your views: +#### Comprehensive Memory Changes + +```swift +Text("Memory Status: \(memoryState)") + .onFootprintMemoryDidChange { newMemory, oldMemory, changes in + if changes.contains(.state) { + updateCachePolicy(for: newMemory.state) + } + if changes.contains(.pressure) { + handleMemoryPressure(newMemory.pressure) + } + } +``` + +#### State-Specific Changes ```swift -Text("Hello, World!") +MyView() .onFootprintMemoryStateDidChange { newState, oldState in - print("Memory state changed from \(oldState) to \(newState)") - // Perform actions based on the memory state change + switch newState { + case .normal: + enableFullFeatures() + case .warning: + reduceCacheSize(by: 0.2) + case .urgent: + reduceCacheSize(by: 0.5) + case .critical: + clearNonEssentialCaches() + case .terminal: + emergencyMemoryCleanup() + } + } +``` + +#### Pressure-Specific Changes + +```swift +ContentView() + .onFootprintMemoryPressureDidChange { newPressure, oldPressure in + handleSystemMemoryPressure(newPressure) } ``` -### Memory Information Retrieval +### Memory Information -Retrieve current memory information: +Access current memory state and information: ```swift -let currentMemory = footprint.memory -print("Used Memory: \(currentMemory.used) bytes") -print("Remaining Memory: \(currentMemory.remaining) bytes") -print("Memory Limit: \(currentMemory.limit) bytes") -print("Memory State: \(currentMemory.state)") +let memory = Footprint.shared.memory + +print("Used: \(memory.used) bytes") +print("Remaining: \(memory.remaining) bytes") +print("Limit: \(memory.limit) bytes") +print("State: \(memory.state)") +print("Pressure: \(memory.pressure)") +print("Timestamp: \(memory.timestamp)") ``` -### Memory Allocation Check +### Memory Allocation Planning -Check if a certain amount of memory can be allocated: +Check if memory allocation is likely to succeed: ```swift -let canAllocate = footprint.canAllocate(bytes: 1024) -print("Can allocate 1KB: \(canAllocate)") +let sizeNeeded: UInt64 = 50_000_000 // 50MB +if Footprint.shared.canAllocate(bytes: sizeNeeded) { + // Proceed with allocation + performMemoryIntensiveOperation() +} else { + // Consider alternatives or cleanup + cleanupBeforeAllocation() +} ``` +## Memory States Explained + +Footprint categorizes memory usage into five states based on the ratio of used memory to total limit: + +- **Normal** (< 25%): Full functionality, optimal performance +- **Warning** (25-50%): Begin reducing memory usage, optimize caches +- **Urgent** (50-75%): Significant memory reduction needed +- **Critical** (75-90%): Aggressive cleanup required +- **Terminal** (> 90%): Imminent termination risk, emergency measures + +## Practical Examples + +### Adaptive Cache Management + +```swift +class ImageCache { + private var maxCost: Int = 100_000_000 // 100MB default + + init() { + Footprint.shared.observe { [weak self] memory in + self?.adjustCacheSize(for: memory.state) + } + } + + private func adjustCacheSize(for state: Footprint.Memory.State) { + let multiplier: Double = switch state { + case .normal: 1.0 + case .warning: 0.8 + case .urgent: 0.5 + case .critical: 0.2 + case .terminal: 0.0 + } + + cache.totalCostLimit = Int(Double(maxCost) * multiplier) + } +} +``` + +### Conditional Feature Loading + +```swift +func loadOptionalFeatures() { + let currentState = Footprint.shared.state + + guard currentState < .urgent else { + // Skip non-essential features in high memory usage + return + } + + enableAdvancedAnimations() + preloadAdditionalContent() +} +``` + +## Development and Testing + +### Simulator Support + +Footprint includes simulator-specific handling since memory limits work differently. You can enable simulated termination for testing: + +```bash +# Enable simulated out-of-memory termination in simulator +export SIM_FOOTPRINT_OOM_TERM_ENABLED=1 +``` + +### Custom Memory Providers + +For testing or custom scenarios, implement the `MemoryProvider` protocol: + +```swift +class MockMemoryProvider: MemoryProvider { + func provide(_ pressure: Footprint.Memory.State) -> Footprint.Memory { + // Return custom memory values for testing + } +} +``` + +## Requirements + +- iOS 13.0+, macOS 10.15+, tvOS 13.0+, watchOS 6.0+, visionOS 1.0+ +- Swift 5.0+ +- Xcode 11.0+ + ## License Footprint is available under the MIT license. See the [LICENSE](LICENSE) file for more info.