Memory leak for ContiguousArrayStorage<NSPersistentStore> #225

Open
opened 2025-12-29 15:26:57 +01:00 by adam · 3 comments
Owner

Originally created by @DmitrijMaz on GitHub (Jul 27, 2018).

Hi,
Prerequisites:

CoreStore (5.1.1)

Sample NSManagedObject subclass. Others use the same principle for importing/updating
Every NSManagedObject subclass have uniqueConstraints setup (usually its id)

infix operator <<->: AssignmentPrecedence

/// Assignment operator that checks values for equality before assigning
///
/// - Parameters:
///   - to: Destination for assignment. Will only be assigned to `from` if they are not equal
///   - from: Source for assignment.
func <<-> <T>(to: inout T, from: T) where T: Equatable  {
    if to == from {
        return
    }
    to = from
}

/// Assignment operator that checks values for equality and unwraps before assigning
///
/// - Parameters:
///   - to: Destination for assignment. Will only be assigned to `from` if they are not equal, and `from` is not `nil`
///   - from: Source for assignment.
func <<-> <T>(to: inout T, from: T?) where T: Equatable  {
    guard let from = from else { return }
    to <<-> from
}
public final class Transfer: NSManagedObject {

    static let dateFormatter: DateFormatter = makeDateFormatter()

    @objc
    public enum Probability: Int16, Equatable & Decodable {
        case confirmed = 1
        case rumor = 2
        case doubtful = 3
    }

    @objc
    public enum Mode: Int16, Equatable & Decodable {
        case contractExtension = 1
        case transfer = 2
        case loan = 3
        case returns = 4
    }

    // MARK: Public
    @NSManaged private(set) var id: UniqueIDType
    @NSManaged private(set) var date: NSDate
    @NSManaged private(set) var daySection: NSNumber?

    // MARK: Relationship
    @NSManaged private(set) var fromTeam: Team?
    @NSManaged private(set) var toTeam: Team?
    @NSManaged private(set) var player: Player?

    // MARK: Private
    @NSManaged private(set) var sa_probability: Probability.RawValue
    @NSManaged private(set) var sa_type: Mode.RawValue
}

public extension Transfer {
    var icon: Images.Transfer {
        switch (probability, type) {
        case (.confirmed, .transfer): return .confirmed
        case (_, .transfer): return .rumor
        case (_, .loan): return .loan
        case (_, .contractExtension): return .contractExtension
        case (_, .returns): return .upDown
        }
    }

    var probability: Probability {
        return Probability(rawValue: sa_probability)!
    }

    var type: Mode {
        return Mode(rawValue: sa_type)!
    }

    func date(withFormat format: String) -> String {
        Transfer.dateFormatter.dateFormat = format
        return Transfer.dateFormatter.string(from: date as Date)
    }
}

extension Transfer: ImportableUniqueObject {
    public typealias UniqueIDType = Int64
    public static var uniqueIDKeyPath: String { return #keyPath(Transfer.id) }

    public struct ImportSource: Decodable {
        let id: UniqueIDType
        let fromTeam: Team.ImportSource
        let toTeam: Team.ImportSource
        let player: Player.ImportSource
        let transferProbabilityType: Probability
        let transferType: Mode
        let transferDate: Date
    }

    public static func uniqueID(from source: ImportSource, in transaction: BaseDataTransaction) throws -> UniqueIDType? {
        return source.id
    }

    public func update(from source: ImportSource, in transaction: BaseDataTransaction) throws {
        id <<-> source.id
        sa_probability <<-> source.transferProbabilityType.rawValue
        sa_type <<-> source.transferType.rawValue
        date <<-> source.transferDate as NSDate
        daySection <<-> Int64(date(withFormat: "yyyyMMdd")).flatMap(NSNumber.init)

        fromTeam = try transaction.importUniqueObject(Into<Team>(), source: source.fromTeam)
        toTeam = try transaction.importUniqueObject(Into<Team>(), source: source.toTeam)
        player = try transaction.importUniqueObject(Into<Player>(), source: source.player)
    }
}

fileprivate extension Transfer {
    static func makeDateFormatter() -> DateFormatter {
        let formatter = DateFormatter()
        formatter.timeZone = TimeZone(abbreviation: "UTC")
        return formatter
    }
}

Core store is setup using the following code:

public func initializeStorage(completion: @escaping (Result<(), StorageService.Errors>) -> Void) throws -> Progress? {
        guard dataStack == .none else {
            throw StorageService.Errors.alreadyInitialized
        }

        dataStack = DataStack(
            xcodeModelName: "<app_name>"
        )
        let storage = getStorage()
        App.logger?[.db].log(
            level: .debug,
            formatLog("Initialize Start", body: "\(storage)")
        )

        defer {
            CoreStore.defaultStack = dataStack
        }

        let storeCompletion: (Result<(), CoreStoreError>) -> Void = { (result) in
            switch result {
            case .success:
                App.logger?[.db].log(
                    level: .debug,
                    self.formatLog("Initialize End", body: "Data stack\n\(CoreStore.defaultStack)")
                )
                completion(.success(()))

            case .failure(let error):
                completion(
                    .failure(.initializationError(error))
                )
            }
        }
        switch storage {
        case let localStorage as SQLiteStore:
            return dataStack.addStorage(localStorage) { storeCompletion($0.asResult()) }
            
        case let inMemory as InMemoryStore:
            dataStack.addStorage(inMemory) { storeCompletion($0.asResult()) }
            return nil
        default:
            fatalError("Unknown storage \(storage)")
        }
    }

I have 5 screens that use ListMonitor to display the same object, just with a diffrent Where clauses

        let from = From<Transfer>()
        let sectionBy = SectionBy<Transfer>(\.daySection)
        let fetchClauses = fetchClauseProvider(from)
        App.storage.monitorSectionedList(
            createAsynchronously: { [weak self] (listMonitor) in
                self?.listMonitor = listMonitor
                handler(listMonitor)
            },
            from,
            sectionBy,
            fetchClauses
        )

The data is loaded from an API and imported using the code i've provided.

The results are the following:
screen shot 2018-07-27 at 19 11 41

Memory instrument trace:
screen shot 2018-07-27 at 19 19 03

Any help would be much appreciated. Contact me if more detailed information is needed.

Thanks!

Originally created by @DmitrijMaz on GitHub (Jul 27, 2018). Hi, Prerequisites: **CoreStore (5.1.1)** Sample `NSManagedObject` subclass. Others use the same principle for importing/updating Every `NSManagedObject` subclass have `uniqueConstraints` setup (usually its `id`) ```swift infix operator <<->: AssignmentPrecedence /// Assignment operator that checks values for equality before assigning /// /// - Parameters: /// - to: Destination for assignment. Will only be assigned to `from` if they are not equal /// - from: Source for assignment. func <<-> <T>(to: inout T, from: T) where T: Equatable { if to == from { return } to = from } /// Assignment operator that checks values for equality and unwraps before assigning /// /// - Parameters: /// - to: Destination for assignment. Will only be assigned to `from` if they are not equal, and `from` is not `nil` /// - from: Source for assignment. func <<-> <T>(to: inout T, from: T?) where T: Equatable { guard let from = from else { return } to <<-> from } ``` ```swift public final class Transfer: NSManagedObject { static let dateFormatter: DateFormatter = makeDateFormatter() @objc public enum Probability: Int16, Equatable & Decodable { case confirmed = 1 case rumor = 2 case doubtful = 3 } @objc public enum Mode: Int16, Equatable & Decodable { case contractExtension = 1 case transfer = 2 case loan = 3 case returns = 4 } // MARK: Public @NSManaged private(set) var id: UniqueIDType @NSManaged private(set) var date: NSDate @NSManaged private(set) var daySection: NSNumber? // MARK: Relationship @NSManaged private(set) var fromTeam: Team? @NSManaged private(set) var toTeam: Team? @NSManaged private(set) var player: Player? // MARK: Private @NSManaged private(set) var sa_probability: Probability.RawValue @NSManaged private(set) var sa_type: Mode.RawValue } public extension Transfer { var icon: Images.Transfer { switch (probability, type) { case (.confirmed, .transfer): return .confirmed case (_, .transfer): return .rumor case (_, .loan): return .loan case (_, .contractExtension): return .contractExtension case (_, .returns): return .upDown } } var probability: Probability { return Probability(rawValue: sa_probability)! } var type: Mode { return Mode(rawValue: sa_type)! } func date(withFormat format: String) -> String { Transfer.dateFormatter.dateFormat = format return Transfer.dateFormatter.string(from: date as Date) } } extension Transfer: ImportableUniqueObject { public typealias UniqueIDType = Int64 public static var uniqueIDKeyPath: String { return #keyPath(Transfer.id) } public struct ImportSource: Decodable { let id: UniqueIDType let fromTeam: Team.ImportSource let toTeam: Team.ImportSource let player: Player.ImportSource let transferProbabilityType: Probability let transferType: Mode let transferDate: Date } public static func uniqueID(from source: ImportSource, in transaction: BaseDataTransaction) throws -> UniqueIDType? { return source.id } public func update(from source: ImportSource, in transaction: BaseDataTransaction) throws { id <<-> source.id sa_probability <<-> source.transferProbabilityType.rawValue sa_type <<-> source.transferType.rawValue date <<-> source.transferDate as NSDate daySection <<-> Int64(date(withFormat: "yyyyMMdd")).flatMap(NSNumber.init) fromTeam = try transaction.importUniqueObject(Into<Team>(), source: source.fromTeam) toTeam = try transaction.importUniqueObject(Into<Team>(), source: source.toTeam) player = try transaction.importUniqueObject(Into<Player>(), source: source.player) } } fileprivate extension Transfer { static func makeDateFormatter() -> DateFormatter { let formatter = DateFormatter() formatter.timeZone = TimeZone(abbreviation: "UTC") return formatter } } ``` Core store is setup using the following code: ```swift public func initializeStorage(completion: @escaping (Result<(), StorageService.Errors>) -> Void) throws -> Progress? { guard dataStack == .none else { throw StorageService.Errors.alreadyInitialized } dataStack = DataStack( xcodeModelName: "<app_name>" ) let storage = getStorage() App.logger?[.db].log( level: .debug, formatLog("Initialize Start", body: "\(storage)") ) defer { CoreStore.defaultStack = dataStack } let storeCompletion: (Result<(), CoreStoreError>) -> Void = { (result) in switch result { case .success: App.logger?[.db].log( level: .debug, self.formatLog("Initialize End", body: "Data stack\n\(CoreStore.defaultStack)") ) completion(.success(())) case .failure(let error): completion( .failure(.initializationError(error)) ) } } switch storage { case let localStorage as SQLiteStore: return dataStack.addStorage(localStorage) { storeCompletion($0.asResult()) } case let inMemory as InMemoryStore: dataStack.addStorage(inMemory) { storeCompletion($0.asResult()) } return nil default: fatalError("Unknown storage \(storage)") } } ``` I have 5 screens that use `ListMonitor` to display the same object, just with a diffrent `Where` clauses ```swift let from = From<Transfer>() let sectionBy = SectionBy<Transfer>(\.daySection) let fetchClauses = fetchClauseProvider(from) App.storage.monitorSectionedList( createAsynchronously: { [weak self] (listMonitor) in self?.listMonitor = listMonitor handler(listMonitor) }, from, sectionBy, fetchClauses ) ``` The data is loaded from an API and imported using the code i've provided. The results are the following: ![screen shot 2018-07-27 at 19 11 41](https://user-images.githubusercontent.com/6554750/43332757-c4ea3294-91d1-11e8-9c1f-7c648603d4ad.png) Memory instrument trace: ![screen shot 2018-07-27 at 19 19 03](https://user-images.githubusercontent.com/6554750/43332833-febb6006-91d1-11e8-8e5b-4b7a8a6cdf61.png) Any help would be much appreciated. Contact me if more detailed information is needed. Thanks!
Author
Owner

@JohnEstropia commented on GitHub (Jul 28, 2018):

Hi, thanks for the detailed info. May I ask how getStorage() creates your SQLiteStore?

Also, are Transfer objects managed in the SQLiteStore or the InMemoryStore?

@JohnEstropia commented on GitHub (Jul 28, 2018): Hi, thanks for the detailed info. May I ask how `getStorage()` creates your `SQLiteStore`? Also, are `Transfer` objects managed in the `SQLiteStore` or the `InMemoryStore`?
Author
Owner

@ghost commented on GitHub (Jul 28, 2018):

Its created using local option storage with .recreateStoreOnMismatch
Everything is in sqlite store

@ghost commented on GitHub (Jul 28, 2018): Its created using local option storage with .recreateStoreOnMismatch Everything is in sqlite store
Author
Owner

@DmitrijMaz commented on GitHub (Jul 30, 2018):

private func getStorage() -> StorageInterface {
        switch mode {
        case .default:
            return SQLiteStore(
                fileName: "<app_name>",
                localStorageOptions: .recreateStoreOnModelMismatch
            )
        case .testing:
            return InMemoryStore()
        }
    }

I was doing all this in default mode

@DmitrijMaz commented on GitHub (Jul 30, 2018): ```swift private func getStorage() -> StorageInterface { switch mode { case .default: return SQLiteStore( fileName: "<app_name>", localStorageOptions: .recreateStoreOnModelMismatch ) case .testing: return InMemoryStore() } } ``` I was doing all this in `default` mode
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/CoreStore#225