import CodexBarCoreimport Dispatchimport Foundation @MainActorstruct MemoryPressureCacheTrimSummary: Equatable { var menuCardHeights = 0 var menuWidths = 0 var mergedSwitcherSelections = 0 var recycledMenuCardViews = 0 var openAIWebDebugLines = 0 var total: Int { self.menuCardHeights + self.menuWidths + self.mergedSwitcherSelections + self.recycledMenuCardViews + self.openAIWebDebugLines } var metadata: [String: String] { [ "menuCardHeights": "\(self.menuCardHeights)", "menuWidths": "\(self.menuWidths)", "mergedSwitcherSelections": "\(self.mergedSwitcherSelections)", "recycledMenuCardViews": "\(self.recycledMenuCardViews)", "openAIWebDebugLines": "\(self.openAIWebDebugLines)", "total": "\(self.total)", ] } mutating func merge(_ other: MemoryPressureCacheTrimSummary) { self.menuCardHeights += other.menuCardHeights self.menuWidths += other.menuWidths self.mergedSwitcherSelections += other.mergedSwitcherSelections self.recycledMenuCardViews += other.recycledMenuCardViews self.openAIWebDebugLines += other.openAIWebDebugLines }} @MainActorfinal class MemoryPressureMonitor { typealias CacheTrimHandler = @MainActor () -> MemoryPressureCacheTrimSummary private let logger = CodexBarLog.logger(LogCategories.memoryPressure) private let releaseFreeMallocPages: @Sendable () -> Void private let trimAppCaches: CacheTrimHandler private var source: DispatchSourceMemoryPressure? init( trimAppCaches: @escaping CacheTrimHandler = { MemoryPressureCacheTrimSummary() }, releaseFreeMallocPages: @escaping @Sendable () -> Void = { MemoryPressureRelief.releaseFreeMallocPages() }) { self.trimAppCaches = trimAppCaches self.releaseFreeMallocPages = releaseFreeMallocPages } func start() { guard self.source == nil else { return } let source = DispatchSource.makeMemoryPressureSource( eventMask: [.warning, .critical], queue: .global(qos: .utility)) source.setEventHandler(handler: Self.makeEventHandler( source: source, handle: { [weak self] isWarning, isCritical in self?.handleMemoryPressure(isWarning: isWarning, isCritical: isCritical) })) self.source = source source.resume() } nonisolated static func makeEventHandler( source: DispatchSourceMemoryPressure, handle: @escaping @MainActor @Sendable (_ isWarning: Bool, _ isCritical: Bool) -> Void) -> @Sendable () -> Void { self.makeEventHandler( eventReader: { [weak source] in source?.data ?? [] }, handle: handle) } nonisolated static func makeEventHandler( eventReader: @escaping @Sendable () -> DispatchSource.MemoryPressureEvent, handle: @escaping @MainActor @Sendable (_ isWarning: Bool, _ isCritical: Bool) -> Void) -> @Sendable () -> Void { // DispatchSource invokes this on the utility queue. Keep the handler // nonisolated, then hop to MainActor for app-state cleanup. { @Sendable in let event = eventReader() let isWarning = event.contains(.warning) let isCritical = event.contains(.critical) Task { @MainActor in handle(isWarning, isCritical) } } } func stop() { self.source?.cancel() self.source = nil } deinit { self.source?.cancel() } #if DEBUG func handleMemoryPressureForTesting(isWarning: Bool, isCritical: Bool) { self.handleMemoryPressure(isWarning: isWarning, isCritical: isCritical) } #endif private func handleMemoryPressure(isWarning: Bool, isCritical: Bool) { let level = if isCritical { "critical" } else if isWarning { "warning" } else { "normal" } self.logger.warning("System memory pressure", metadata: ["level": level]) #if DEBUG let cachedWebViewsBefore = OpenAIDashboardFetcher.cachedWebViewCountForTesting() #endif OpenAIDashboardFetcher.evictIdleCachedWebViews() #if DEBUG let cachedWebViewsAfter = OpenAIDashboardFetcher.cachedWebViewCountForTesting() self.logger.info( "Memory pressure OpenAI webview cache", metadata: [ "before": "\(cachedWebViewsBefore)", "after": "\(cachedWebViewsAfter)", "evicted": "\(max(0, cachedWebViewsBefore - cachedWebViewsAfter))", ]) #endif let trimSummary = self.trimAppCaches() if trimSummary.total > 0 { self.logger.info("Trimmed app caches for memory pressure", metadata: trimSummary.metadata) } let releaseFreeMallocPages = self.releaseFreeMallocPages Task.detached(priority: .utility) { releaseFreeMallocPages() } }}
import CodexBarCore
import Dispatch
import Foundation
@MainActor
struct MemoryPressureCacheTrimSummary: Equatable {
var menuCardHeights = 0
var menuWidths = 0
var mergedSwitcherSelections = 0
var recycledMenuCardViews = 0
var openAIWebDebugLines = 0
var total: Int {
self.menuCardHeights +
self.menuWidths +
self.mergedSwitcherSelections +
self.recycledMenuCardViews +
self.openAIWebDebugLines
}
var metadata: [String: String] {
[
"menuCardHeights": "\(self.menuCardHeights)",
"menuWidths": "\(self.menuWidths)",
"mergedSwitcherSelections": "\(self.mergedSwitcherSelections)",
"recycledMenuCardViews": "\(self.recycledMenuCardViews)",
"openAIWebDebugLines": "\(self.openAIWebDebugLines)",
"total": "\(self.total)",
]
}
mutating func merge(_ other: MemoryPressureCacheTrimSummary) {
self.menuCardHeights += other.menuCardHeights
self.menuWidths += other.menuWidths
self.mergedSwitcherSelections += other.mergedSwitcherSelections
self.recycledMenuCardViews += other.recycledMenuCardViews
self.openAIWebDebugLines += other.openAIWebDebugLines
}
}
@MainActor
final class MemoryPressureMonitor {
typealias CacheTrimHandler = @MainActor () -> MemoryPressureCacheTrimSummary
private let logger = CodexBarLog.logger(LogCategories.memoryPressure)
private let releaseFreeMallocPages: @Sendable () -> Void
private let trimAppCaches: CacheTrimHandler
private var source: DispatchSourceMemoryPressure?
init(
trimAppCaches: @escaping CacheTrimHandler = { MemoryPressureCacheTrimSummary() },
releaseFreeMallocPages: @escaping @Sendable () -> Void = {
MemoryPressureRelief.releaseFreeMallocPages()
})
{
self.trimAppCaches = trimAppCaches
self.releaseFreeMallocPages = releaseFreeMallocPages
}
func start() {
guard self.source == nil else { return }
let source = DispatchSource.makeMemoryPressureSource(
eventMask: [.warning, .critical],
queue: .global(qos: .utility))
source.setEventHandler(handler: Self.makeEventHandler(
source: source,
handle: { [weak self] isWarning, isCritical in
self?.handleMemoryPressure(isWarning: isWarning, isCritical: isCritical)
}))
self.source = source
source.resume()
}
nonisolated static func makeEventHandler(
source: DispatchSourceMemoryPressure,
handle: @escaping @MainActor @Sendable (_ isWarning: Bool, _ isCritical: Bool) -> Void)
-> @Sendable () -> Void
{
self.makeEventHandler(
eventReader: { [weak source] in source?.data ?? [] },
handle: handle)
}
nonisolated static func makeEventHandler(
eventReader: @escaping @Sendable () -> DispatchSource.MemoryPressureEvent,
handle: @escaping @MainActor @Sendable (_ isWarning: Bool, _ isCritical: Bool) -> Void)
-> @Sendable () -> Void
{
// DispatchSource invokes this on the utility queue. Keep the handler
// nonisolated, then hop to MainActor for app-state cleanup.
{ @Sendable in
let event = eventReader()
let isWarning = event.contains(.warning)
let isCritical = event.contains(.critical)
Task { @MainActor in
handle(isWarning, isCritical)
}
}
}
func stop() {
self.source?.cancel()
self.source = nil
}
deinit {
self.source?.cancel()
}
#if DEBUG
func handleMemoryPressureForTesting(isWarning: Bool, isCritical: Bool) {
self.handleMemoryPressure(isWarning: isWarning, isCritical: isCritical)
}
#endif
private func handleMemoryPressure(isWarning: Bool, isCritical: Bool) {
let level = if isCritical {
"critical"
} else if isWarning {
"warning"
} else {
"normal"
}
self.logger.warning("System memory pressure", metadata: ["level": level])
#if DEBUG
let cachedWebViewsBefore = OpenAIDashboardFetcher.cachedWebViewCountForTesting()
#endif
OpenAIDashboardFetcher.evictIdleCachedWebViews()
#if DEBUG
let cachedWebViewsAfter = OpenAIDashboardFetcher.cachedWebViewCountForTesting()
self.logger.info(
"Memory pressure OpenAI webview cache",
metadata: [
"before": "\(cachedWebViewsBefore)",
"after": "\(cachedWebViewsAfter)",
"evicted": "\(max(0, cachedWebViewsBefore - cachedWebViewsAfter))",
])
#endif
let trimSummary = self.trimAppCaches()
if trimSummary.total > 0 {
self.logger.info("Trimmed app caches for memory pressure", metadata: trimSummary.metadata)
}
let releaseFreeMallocPages = self.releaseFreeMallocPages
Task.detached(priority: .utility) {
releaseFreeMallocPages()
}
}
}