CoreStore icon indicating copy to clipboard operation
CoreStore copied to clipboard

Memory leak for ContiguousArrayStorage<NSPersistentStore>

Open DmitrijMaz opened this issue 7 years ago • 3 comments

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!

DmitrijMaz avatar Jul 27 '18 16:07 DmitrijMaz

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 avatar Jul 28 '18 17:07 JohnEstropia

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

ghost avatar Jul 28 '18 18:07 ghost

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 avatar Jul 30 '18 07:07 DmitrijMaz