chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user