fix(keyboard): stabilize Flow startup under memory pressure

Delay competing Rime work, add extension memory telemetry and stress coverage, and refresh the 1.8 release assets and metadata for build 79.
This commit is contained in:
Rocky
2026-08-18 10:24:02 +08:00
parent aa1059135f
commit 0f9280bd00
27 changed files with 809 additions and 105 deletions
@@ -0,0 +1,171 @@
// KeyboardExtensionMemoryTelemetry.swift
// OSGKeyboard · Shared
//
// Observes keyboard-extension memory without changing runtime behavior.
// The host process never starts this monitor, so shared typing code can emit
// extension-only milestones without duplicating host telemetry.
import Foundation
import Darwin
public enum KeyboardExtensionMemoryBudget {
/// Start preserving evidence before the extension reaches its safe ceiling.
public static let warningMB: Double = 36
/// Internal release target. Apple's keyboard-extension limit is not public.
public static let safePeakMB: Double = 40
/// Leave headroom below the observed ~60 MiB device jetsam boundary.
public static let criticalMB: Double = 48
public enum Level: String, Sendable, Equatable {
case normal
case warning
case high
case critical
case unavailable
}
public static func level(forPhysFootprintMB footprintMB: Double) -> Level {
guard footprintMB >= 0 else { return .unavailable }
if footprintMB >= criticalMB { return .critical }
if footprintMB >= safePeakMB { return .high }
if footprintMB >= warningMB { return .warning }
return .normal
}
}
@MainActor
public enum KeyboardExtensionMemoryTelemetry {
private static let peakLogStepMB: Double = 4
private static let bootSampleInterval = Duration.milliseconds(50)
private static let bootSampleDuration: TimeInterval = 4
private static var isActive = false
private static var processID: Int32 = 0
private static var context = "surface=- language=-"
private static var baselineFootprintMB: Double = -1
private static var peakFootprintMB: Double = -1
private static var lastLoggedPeakMB: Double = -1
private static var highestLevel = KeyboardExtensionMemoryBudget.Level.normal
private static var startedAt: TimeInterval = 0
private static var samplingTask: Task<Void, Never>?
public static func begin(context initialContext: String) {
samplingTask?.cancel()
samplingTask = nil
isActive = true
processID = getpid()
context = initialContext
startedAt = ProcessInfo.processInfo.systemUptime
let snapshot = OSGDiag.memorySnapshot()
baselineFootprintMB = snapshot.physFootprintMB
peakFootprintMB = snapshot.physFootprintMB
lastLoggedPeakMB = snapshot.physFootprintMB
highestLevel = .normal
emit(stage: "process.begin", snapshot: snapshot, alwaysLog: true)
}
public static func updateContext(_ newContext: String) {
guard isActive else { return }
context = newContext
}
public static func record(_ stage: String, details: String? = nil) {
guard isActive else { return }
let eventContext = details.map { "\(context) \($0)" } ?? context
emit(
stage: stage,
snapshot: OSGDiag.memorySnapshot(),
eventContext: eventContext,
alwaysLog: true
)
}
/// Samples short-lived startup spikes that milestone-only logging can miss.
/// Poll samples log only on a new budget band or each additional 4 MiB peak.
public static func startBootSampling() {
guard isActive else { return }
samplingTask?.cancel()
let deadline = ProcessInfo.processInfo.systemUptime + bootSampleDuration
samplingTask = Task { @MainActor in
while !Task.isCancelled, ProcessInfo.processInfo.systemUptime < deadline {
try? await Task.sleep(for: bootSampleInterval)
guard !Task.isCancelled else { return }
emit(
stage: "boot.sample",
snapshot: OSGDiag.memorySnapshot(),
eventContext: context,
alwaysLog: false
)
}
samplingTask = nil
record("boot.sample.complete")
}
}
private static func emit(
stage: String,
snapshot: OSGDiag.MemorySnapshot,
eventContext: String? = nil,
alwaysLog: Bool
) {
let footprint = snapshot.physFootprintMB
if footprint >= 0 {
peakFootprintMB = max(peakFootprintMB, footprint)
}
let level = KeyboardExtensionMemoryBudget.level(forPhysFootprintMB: footprint)
let crossedLevel = levelRank(level) > levelRank(highestLevel)
if crossedLevel {
highestLevel = level
}
let peakAdvanced = peakFootprintMB >= 0
&& (lastLoggedPeakMB < 0 || peakFootprintMB - lastLoggedPeakMB >= peakLogStepMB)
guard alwaysLog || crossedLevel || peakAdvanced else { return }
if peakFootprintMB >= 0 {
lastLoggedPeakMB = peakFootprintMB
}
let elapsedMS = max(
0,
Int((ProcessInfo.processInfo.systemUptime - startedAt) * 1_000)
)
let delta = baselineFootprintMB >= 0 && peakFootprintMB >= 0
? peakFootprintMB - baselineFootprintMB
: -1
OSGDiag.log(
String(
format: "extMemory pid=%d stage=%@ elapsed=%dms level=%@ crossed=%d "
+ "rss=%.1fMB foot=%.1fMB peak=%.1fMB delta=%.1fMB "
+ "safe=%dMB critical=%dMB context={%@}",
processID,
stage,
elapsedMS,
level.rawValue,
crossedLevel ? 1 : 0,
snapshot.rssMB,
footprint,
peakFootprintMB,
delta,
Int(KeyboardExtensionMemoryBudget.safePeakMB),
Int(KeyboardExtensionMemoryBudget.criticalMB),
eventContext ?? context
),
category: "memory"
)
}
private static func levelRank(_ level: KeyboardExtensionMemoryBudget.Level) -> Int {
switch level {
case .unavailable:
return -1
case .normal:
return 0
case .warning:
return 1
case .high:
return 2
case .critical:
return 3
}
}
}
+36 -2
View File
@@ -9,12 +9,22 @@ import Foundation
import Darwin
public enum OSGDiag {
public struct MemorySnapshot: Sendable, Equatable {
public let rssMB: Double
public let physFootprintMB: Double
public init(rssMB: Double, physFootprintMB: Double) {
self.rssMB = rssMB
self.physFootprintMB = physFootprintMB
}
}
/// Prefix every line for easy Console search: `OSGDiag`
public static func log(_ message: String, category: String = "diag") {
let line = "[OSGDiag/\(category)] \(message)"
NSLog("%@", line)
switch category {
case "keyboardExt", "boot":
case "keyboardExt", "boot", "memory":
OSGLog.keyboardExt.info("\(line, privacy: .public)")
case "flow", "asr":
OSGLog.flow.info("\(line, privacy: .public)")
@@ -40,7 +50,31 @@ public enum OSGDiag {
return Double(info.resident_size) / 1_048_576.0
}
/// Physical footprint in MiB. iOS jetsam tracks this more closely than RSS.
public static func physFootprintMB() -> Double {
var info = task_vm_info_data_t()
var count = mach_msg_type_number_t(
MemoryLayout<task_vm_info_data_t>.size / MemoryLayout<natural_t>.size
)
let kr = withUnsafeMutablePointer(to: &info) { ptr in
ptr.withMemoryRebound(to: integer_t.self, capacity: Int(count)) { rebound in
task_info(mach_task_self_, task_flavor_t(TASK_VM_INFO), rebound, &count)
}
}
guard kr == KERN_SUCCESS else { return -1 }
return Double(info.phys_footprint) / 1_048_576.0
}
public static func memorySnapshot() -> MemorySnapshot {
MemorySnapshot(rssMB: memoryMB(), physFootprintMB: physFootprintMB())
}
public static func memoryTag() -> String {
String(format: "rss=%.1fMB", memoryMB())
let snapshot = memorySnapshot()
return String(
format: "rss=%.1fMB foot=%.1fMB",
snapshot.rssMB,
snapshot.physFootprintMB
)
}
}