From fc137f4f11c48b8da2bbfd3e845b8a4627610ee7 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 18 Jul 2026 16:43:53 +0000 Subject: [PATCH] Add memory-headroom breadcrumbs across the ingest/stems pipeline The device still jetsams with no stems log lines in the console, so stop guessing which stage eats the budget: log os_proc_available_memory headroom at analysis begin/end, separation task start, model ensure, ORT session load, every 10th inference window, and separation end. One run's console output now names the eater and its size. --- .../Sources/Ingest/MemoryFootprint.swift | 14 ++++++++++++++ .../Sources/Ingest/PreparationQueue+Stems.swift | 2 ++ .../Sources/Ingest/PreparationQueue.swift | 2 ++ .../Sources/Ingest/StemSeparator.swift | 6 ++++++ 4 files changed, 24 insertions(+) create mode 100644 Packages/ContinuityKit/Sources/Ingest/MemoryFootprint.swift diff --git a/Packages/ContinuityKit/Sources/Ingest/MemoryFootprint.swift b/Packages/ContinuityKit/Sources/Ingest/MemoryFootprint.swift new file mode 100644 index 0000000..c8c405c --- /dev/null +++ b/Packages/ContinuityKit/Sources/Ingest/MemoryFootprint.swift @@ -0,0 +1,14 @@ +import Foundation +import os + +/// Jetsam-debugging breadcrumbs. `os_proc_available_memory` reports how much of the process's +/// memory allowance remains before the OS kills it — logging it at each pipeline stage turns a +/// silent jetsam into a console trace that names the eater. Cheap enough to leave in release. +enum MemoryFootprint { + private static let logger = Logger(subsystem: "com.continuity.app", category: "mem") + + static func breadcrumb(_ label: String) { + let remainingMB = Int(os_proc_available_memory() / 1_048_576) + logger.info("mem[\(label, privacy: .public)] \(remainingMB, privacy: .public) MB headroom") + } +} diff --git a/Packages/ContinuityKit/Sources/Ingest/PreparationQueue+Stems.swift b/Packages/ContinuityKit/Sources/Ingest/PreparationQueue+Stems.swift index f096c16..35c1cd8 100644 --- a/Packages/ContinuityKit/Sources/Ingest/PreparationQueue+Stems.swift +++ b/Packages/ContinuityKit/Sources/Ingest/PreparationQueue+Stems.swift @@ -90,7 +90,9 @@ extension PreparationQueue { return } do { + MemoryFootprint.breadcrumb("separation task begin") let modelURL = try await StemModelStore.ensureModel() + MemoryFootprint.breadcrumb("model ensured") let separator = OnnxStemSeparator(modelURL: modelURL) _ = try separator.separate(inputURL: inputURL, vocalsOut: vocalsOut, accompanimentOut: accompanimentOut) await MainActor.run { diff --git a/Packages/ContinuityKit/Sources/Ingest/PreparationQueue.swift b/Packages/ContinuityKit/Sources/Ingest/PreparationQueue.swift index da52ff0..662c745 100644 --- a/Packages/ContinuityKit/Sources/Ingest/PreparationQueue.swift +++ b/Packages/ContinuityKit/Sources/Ingest/PreparationQueue.swift @@ -274,9 +274,11 @@ public final class PreparationQueue { if needsReanalysis, track.modelContext != nil, let relativePath = track.localRelativePath { let url = AudioCache.url(forRelativePath: relativePath) await ingestLimiter.acquire() + MemoryFootprint.breadcrumb("analysis begin") let analysis = try? await Task.detached(priority: .utility) { try TrackAnalyzer.analyze(fileURL: url) }.value + MemoryFootprint.breadcrumb("analysis end") await ingestLimiter.release() if let analysis, track.modelContext != nil { track.bpm = analysis.bpm > 0 ? analysis.bpm : nil diff --git a/Packages/ContinuityKit/Sources/Ingest/StemSeparator.swift b/Packages/ContinuityKit/Sources/Ingest/StemSeparator.swift index 001f8ca..a46a9bd 100644 --- a/Packages/ContinuityKit/Sources/Ingest/StemSeparator.swift +++ b/Packages/ContinuityKit/Sources/Ingest/StemSeparator.swift @@ -108,6 +108,7 @@ final class OnnxStemSeparator: StemSeparating { defer { sessionLock.unlock() } if let cached = cachedSession, cached.path == modelURL.path { return cached.session } do { + MemoryFootprint.breadcrumb("ort session load begin") let env = try ORTEnv(loggingLevel: .warning) let options = try ORTSessionOptions() // Cap thread fan-out: default parallel arenas on a phone balloon RSS toward the @@ -121,6 +122,7 @@ final class OnnxStemSeparator: StemSeparating { try options.addConfigEntry(withKey: "session.disable_prepacking", value: "1") let session = try ORTSession(env: env, modelPath: modelURL.path, sessionOptions: options) cachedSession = (modelURL.path, session) + MemoryFootprint.breadcrumb("ort session ready") return session } catch { throw StemSeparationError.inference("session: \(error)") @@ -164,6 +166,7 @@ final class OnnxStemSeparator: StemSeparating { try? FileManager.default.removeItem(at: accompanimentOut) throw error } + MemoryFootprint.breadcrumb("separation done") return StemPaths(vocals: vocalsOut, accompaniment: accompanimentOut) } @@ -184,6 +187,7 @@ final class OnnxStemSeparator: StemSeparating { var input = [Float](repeating: 0, count: channels * segment) var start = 0 + var windowCount = 0 while true { // Decode ahead through the end of this window (or EOF / the length cap). while !atEOF && decodedEnd < start + segment { @@ -215,6 +219,8 @@ final class OnnxStemSeparator: StemSeparating { input[segment + i] = 0 } + if windowCount % 10 == 0 { MemoryFootprint.breadcrumb("window \(windowCount)") } + windowCount += 1 let vocals = try runWindow(input) guard vocals.count >= channels * segment else { throw StemSeparationError.inference("short vocals output: \(vocals.count)")