realm / realm-swift

Realm is a mobile database: a replacement for Core Data & SQLite
https://realm.io
Apache License 2.0
16.18k stars 2.14k forks source link

Is it possible to fully deallocate the Memory used by Realm #8641

Open Brian-Masse opened 5 days ago

Brian-Masse commented 5 days ago

How frequently does the bug occur?

Always

Description

When you initialize a flexible sync Realm in Swift, it takes up about 20MB of memory on the simulator, according to the debugger. When you .logout() of the app, invalidate the realm, and remove all references to the Realm however, you get none of that memory back. Is it possible to uninitialize RealmSwift in a way to reclaim some of that memory?

Digging around in instruments and the memory graph tree, it doesn't look like any one reference is causing the memory consumption, but rather its an aggregate of thousands of small, unmanaged references created by RealmSwift somewhere in the process of opening a realm. But after spending a few days playing around with the API I couldn't find anything that effectively cleans them.

Motivation

I am making a WidgetExtension for my app, and need to fetch data in real time, without the user necessarily entering in the app where I could manually refresh the widget timeline. The app uses MongoDB and realm sync to store its data, so I figured I could initialize a flexible realm in the widget when I needed to fetch data and later invalidate it. Apple however, imposes a 30MB limit on the memory of widgets, so when I'm done pulling the data from the realm, I need to invalidate it and reclaim its memory so I can continue to render the widget.

Stacktrace & log output

No response

Can you reproduce the bug?

Always

Reproduction Steps

In any project with RealmSwift, this container replicates the issue. Initially (from the debugger), the app loads in with around ~25MB of memory. Pressing the 'load Realm' button authenticates some user to a RLMApp and opens a flexible realm, taking up an additional ~20MB of memory. Pressing the 'Clean Realm' Button signs the user out, invalidates the realm, nullifies all references to the objects, and calls a few cleanup functions, yet after that call the memory impact does not change.

class RealmManager: ObservableObject {

    static let shared: RealmManager = RealmManager()

    @Published var app: RealmSwift.App? = nil
    @Published var realm: Realm? = nil

    //MARK: Load    

    @MainActor
    func loadRealm() async {
        self.app = RealmSwift.App(id: "APP-ID")
        let credentials = Credentials.anonymous()
        let user = try! await app!.login(credentials: credentials)

        let configuration = user.flexibleSyncConfiguration()

        let realm = try! await Realm(configuration: configuration,
                                     downloadBeforeOpen: .never )

        self.realm = realm
    }

    @MainActor
    private func removeSubscriptions() async {
        let subs =  self.realm!.subscriptions

        try! await subs.update { subs.removeAll() }
    }

    //MARK: Clean

    @MainActor
    func cleanRealm() async {

        await removeSubscriptions()

        _ = try! Realm.deleteFiles(for: Realm.Configuration.defaultConfiguration)

        try! await self.app!.currentUser?.remove()

        self.realm!.invalidate()

        self.realm = nil
        self.app = nil

        App.resetAppCache()
    }
}

struct ContentView: View {

    @ObservedObject var realmManager = RealmManager.shared

    var body: some View {

        Text("REALM: \(RealmManager.shared.realm != nil)" )
        Text("APP: \(RealmManager.shared.app != nil)" )

        /// when first clicked the memory spikes to around 45MB, but does not come down when 'clean' is run
        Button {
            Task { await RealmManager.shared.loadRealm() }
        } label: {
            Text("load")
        }

        Button {
            Task { await RealmManager.shared.cleanRealm() }
        } label: {
            Text("clean")
        }

    }
}

Version

all

What Atlas Services are you using?

Atlas Device Sync

Are you using encryption?

No

Platform OS and version(s)

MacOS - 14.5

Build environment

ProductName: macOS ProductVersion: 14.5 BuildVersion: 23F79

/Applications/Xcode.app/Contents/Developer Xcode 15.4 Build version 15F31d

/bin/bash GNU bash, version 3.2.57(1)-release (arm64-apple-darwin23)

/usr/bin/git git version 2.39.3 (Apple Git-146)

sync-by-unito[bot] commented 5 days ago

➤ PM Bot commented:

Jira ticket: RCOCOA-2403

tgoyne commented 5 days ago

In a memory-constrained environment you need to set objectTypes on your Realm.Configuration. A large portion of the increase in memory usage is that automatically discovering the Object subclasses forces an eager load of all of the lazily-loaded linked libraries.

We create a subclass of each of your model types at runtime, and those cannot be freed. There's some incidental globals which we could free but don't, but they're a pretty small amount of memory.

Brian-Masse commented 5 days ago

How do I specify the objectTypes for a flexible Sync Realm. I see theyre get only, so I need to set them when I initialize a Realm.Configuration, however that won't work with my realm. I see there is a SyncConfiguration parameter when initializing a configuration, but that takes in a SyncConfiguration, which user.flexibleSyncConfiguration() does not produce, and its initializer seems to be deprecated. Any help would be appreciated :)

/// It looks like this initializer is depricated
let syncConfig = RealmSwift.SyncConfiguration.defaultConfiguration(user);

...

/// this returns an instance of a Realm.Configuration, and has no params to edit objectTypes
let syncConfig = user.flexibleSyncConfiguration() 

...

/// this takes in a SyncConfigurationn, not a Realm.Configuration
let config = Realm.Configuration(syncConfiguration: (syncConfig as SyncConfiguration), objectTypes: [...])
tgoyne commented 5 days ago

var config = user.flexibleSyncConfiguration(); config.objectTypes = [...]