Memory leak for ContiguousArrayStorage<NSPersistentStore>
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:

Memory instrument trace:

Any help would be much appreciated. Contact me if more detailed information is needed.
Thanks!
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?
Its created using local option storage with .recreateStoreOnMismatch Everything is in sqlite store
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