Files
steipete--codexbar/Sources/CodexBar/MemoryPressureMonitor.swift
T
2026-07-13 12:22:33 +08:00

150 lines
5.1 KiB
Swift

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()
}
}
}