Memory usage unstoppably increasing when updating SwiftData Object from Timer

For a kind of podcast player I need to periodically update a swiftData object to keep track of the listening progress. (Happy to hear if there are better ways) I need to do this in many places in my app so I wanted to extract the modelContext into a Singleton so I can write a global function that starts the timer. In doing so I stumbled upon a problem: The memory used by my app is steadily increasing and the device is turning hot.

@Observable
class Helper {
    static let shared = Helper()
    var modelContext: ModelContext?
}
@main
struct SingletontestApp: App {
    let modelContainer: ModelContainer
    init() {
        do {
            modelContainer = try ModelContainer(
                for: Item.self, Item.self
            )
        } catch {
            fatalError("Could not initialize ModelContainer")
        }
        Helper.shared.modelContext = modelContainer.mainContext
    }   
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(modelContainer)
    }
}
struct ContentView: View {
    @Query private var items: [Item]
    
    var body: some View {
        NavigationSplitView {
            List {
                ForEach(items) { item in
                    Text(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))
                }
            }
            .toolbar {
                ToolbarItem {
                    Button(action: addItem) {
                        Label("Add Item", systemImage: "plus")
                    }
                }
                ToolbarItem {
                    Button(action: updateItemPeriodically) {
                        Label("Change random", systemImage: "dice")
                    }
                }
            }
        } detail: {
            Text("Select an item")
        }
    }
    
    func addItem() {
        withAnimation {
            let newItem = Item(timestamp: Date())
            Helper.shared.modelContext!.insert(newItem)
        }
    }
    
    @MainActor
    func updateItemPeriodically() { // Doesn't matter if run as global or local func
        let descriptor = FetchDescriptor<Item>(sortBy: [SortDescriptor(\.timestamp)])
        let results = (try? Helper.shared.modelContext?.fetch(descriptor)) ?? []
        let element = results.randomElement()
        
        
        let timer = Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { timer in // Smaller time intervals worsen the problem
            element?.timestamp = Date.now
            
        }
        
    }
}

Calling save() manually or automatically in the timer does not have any effect. I am not sure about my general way of keeping track of listening process so if you think there is a better way, feel free to correct me.

Thanks for your help

Answered by DTS Engineer in 788689022

SwiftData's default backing store (BackingData) is based on Core Data, which caches the fetched objects to achieve better performance. If Helper.shared.modelContext?.fetch returns more objects over time, the cache will grow, which may explain why your app’s memory consumption increases.

ModelContext doesn't provide an API to reset the cache. What I can suggest is that, instead of using the main context, try to create a new context (init(_:)) every time your timer is triggered. Since the context is released when your code exits the timer handler, the cache won’t grow.

The device getting hot is an indication that your code does too much. I am unclear why you need to fetch all the objects and set the time stamp for only a random one. If you can share your concrete use case and what you are trying to achieve, folks may be able to give you a better suggestion.

SwiftData's default backing store (BackingData) is based on Core Data, which caches the fetched objects to achieve better performance. If Helper.shared.modelContext?.fetch returns more objects over time, the cache will grow, which may explain why your app’s memory consumption increases.

ModelContext doesn't provide an API to reset the cache. What I can suggest is that, instead of using the main context, try to create a new context (init(_:)) every time your timer is triggered. Since the context is released when your code exits the timer handler, the cache won’t grow.

The device getting hot is an indication that your code does too much. I am unclear why you need to fetch all the objects and set the time stamp for only a random one. If you can share your concrete use case and what you are trying to achieve, folks may be able to give you a better suggestion.

So apparently something is wrong in this function. A timer makes the problem more obvious but seems to not have a direct effect. I want to change a property of a swiftdata object without using @query. In this case insert does not add a new item but updates the existing one, exactly the behavior I am looking for.

func doManually() {
        let descriptor = FetchDescriptor<Item>()
        let results = (try? Helper.shared.modelContext?.fetch(FetchDescriptor<Item>())) ?? []
        let element = results.first!
        element.timestamp = Date.now
        Helper.shared.modelContext?.insert(element)
}

If a) doManually() still increases the memory consumption of your app every time it is called, and b) the memory doesn't ever go down, which eventually triggers a low memory warning in your app, you might file a feedback report against SwiftData. Note that a) may be an expected behavior because of the caching mechanism in Core Data, and b), if true, is the real problem that SwiftData doesn't provide a way to clear the cache.

If b) indeed happens, you can consider creating a new model context for the task (and have it released when the task is done), as I said in my previous reply. The cache should be cleared when the context is released.

Memory usage unstoppably increasing when updating SwiftData Object from Timer
 
 
Q