Add reference counting for accounts

Closes #97
This commit is contained in:
Shadowfacts 2020-05-11 21:59:46 -04:00
parent 0a89dd3041
commit 82ad3b9fc4
Signed by untrusted user: shadowfacts
GPG Key ID: 94A5AB95422746E5
8 changed files with 80 additions and 19 deletions

View File

@ -84,8 +84,16 @@ class MastodonController {
run(request) { response in run(request) { response in
guard case let .success(account, _) = response else { fatalError() } guard case let .success(account, _) = response else { fatalError() }
self.account = account self.account = account
self.persistentContainer.addOrUpdate(account: account) self.persistentContainer.backgroundContext.perform {
completion?(account) if let accountMO = self.persistentContainer.account(for: account.id, in: self.persistentContainer.backgroundContext) {
accountMO.updateFrom(apiAccount: account, container: self.persistentContainer)
} else {
// the first time the user's account is added to the store,
// increment its reference count so that it's never removed
self.persistentContainer.addOrUpdate(account: account, incrementReferenceCount: true)
}
completion?(account)
}
} }
} }
} }

View File

@ -32,6 +32,7 @@ public final class AccountMO: NSManagedObject, AccountProtocol {
@NSManaged public var locked: Bool @NSManaged public var locked: Bool
@NSManaged public var movedCD: Bool @NSManaged public var movedCD: Bool
@NSManaged public var note: String @NSManaged public var note: String
@NSManaged public var referenceCount: Int
@NSManaged public var statusesCount: Int @NSManaged public var statusesCount: Int
@NSManaged public var url: URL @NSManaged public var url: URL
@NSManaged public var username: String @NSManaged public var username: String
@ -46,12 +47,30 @@ public final class AccountMO: NSManagedObject, AccountProtocol {
public var bot: Bool? { botCD } public var bot: Bool? { botCD }
public var moved: Bool? { movedCD } public var moved: Bool? { movedCD }
func incrementReferenceCount() {
referenceCount += 1
}
func decrementReferenceCount() {
referenceCount -= 1
if referenceCount <= 0 {
managedObjectContext!.delete(self)
}
}
public override func prepareForDeletion() {
super.prepareForDeletion()
movedTo?.decrementReferenceCount()
}
} }
extension AccountMO { extension AccountMO {
convenience init(apiAccount account: Pachyderm.Account, container: MastodonCachePersistentStore, context: NSManagedObjectContext) { convenience init(apiAccount account: Pachyderm.Account, container: MastodonCachePersistentStore, context: NSManagedObjectContext) {
self.init(context: context) self.init(context: context)
self.updateFrom(apiAccount: account, container: container) self.updateFrom(apiAccount: account, container: container)
movedTo?.incrementReferenceCount()
} }
func updateFrom(apiAccount account: Pachyderm.Account, container: MastodonCachePersistentStore) { func updateFrom(apiAccount account: Pachyderm.Account, container: MastodonCachePersistentStore) {

View File

@ -105,18 +105,25 @@ class MastodonCachePersistentStore: NSPersistentContainer {
} }
@discardableResult @discardableResult
private func upsert(account: Account) -> AccountMO { private func upsert(account: Account, incrementReferenceCount: Bool) -> AccountMO {
if let accountMO = self.account(for: account.id, in: self.backgroundContext) { if let accountMO = self.account(for: account.id, in: self.backgroundContext) {
accountMO.updateFrom(apiAccount: account, container: self) accountMO.updateFrom(apiAccount: account, container: self)
if incrementReferenceCount {
accountMO.incrementReferenceCount()
}
return accountMO return accountMO
} else { } else {
return AccountMO(apiAccount: account, container: self, context: self.backgroundContext) let accountMO = AccountMO(apiAccount: account, container: self, context: self.backgroundContext)
if incrementReferenceCount {
accountMO.incrementReferenceCount()
}
return accountMO
} }
} }
func addOrUpdate(account: Account, completion: ((AccountMO) -> Void)? = nil) { func addOrUpdate(account: Account, incrementReferenceCount: Bool, completion: ((AccountMO) -> Void)? = nil) {
backgroundContext.perform { backgroundContext.perform {
let accountMO = self.upsert(account: account) let accountMO = self.upsert(account: account, incrementReferenceCount: incrementReferenceCount)
if self.backgroundContext.hasChanges { if self.backgroundContext.hasChanges {
try! self.backgroundContext.save() try! self.backgroundContext.save()
} }
@ -127,7 +134,7 @@ class MastodonCachePersistentStore: NSPersistentContainer {
func addAll(accounts: [Account], completion: (() -> Void)? = nil) { func addAll(accounts: [Account], completion: (() -> Void)? = nil) {
backgroundContext.perform { backgroundContext.perform {
accounts.forEach { self.upsert(account: $0) } accounts.forEach { self.upsert(account: $0, incrementReferenceCount: true) }
if self.backgroundContext.hasChanges { if self.backgroundContext.hasChanges {
try! self.backgroundContext.save() try! self.backgroundContext.save()
} }
@ -139,9 +146,11 @@ class MastodonCachePersistentStore: NSPersistentContainer {
func addAll(notifications: [Pachyderm.Notification], completion: (() -> Void)? = nil) { func addAll(notifications: [Pachyderm.Notification], completion: (() -> Void)? = nil) {
backgroundContext.perform { backgroundContext.perform {
let statuses = notifications.compactMap { $0.status } let statuses = notifications.compactMap { $0.status }
let accounts = notifications.map { $0.account } // filter out mentions, otherwise we would double increment the reference count of those accounts
// since the status has the same account as the notification
let accounts = notifications.filter { $0.kind != .mention }.map { $0.account }
statuses.forEach { self.upsert(status: $0, incrementReferenceCount: true) } statuses.forEach { self.upsert(status: $0, incrementReferenceCount: true) }
accounts.forEach { self.upsert(account: $0) } accounts.forEach { self.upsert(account: $0, incrementReferenceCount: true) }
if self.backgroundContext.hasChanges { if self.backgroundContext.hasChanges {
try! self.backgroundContext.save() try! self.backgroundContext.save()
} }
@ -157,7 +166,7 @@ class MastodonCachePersistentStore: NSPersistentContainer {
var updatedStatuses = [String]() var updatedStatuses = [String]()
block(self.backgroundContext, { (accounts) in block(self.backgroundContext, { (accounts) in
accounts.forEach { self.upsert(account: $0) } accounts.forEach { self.upsert(account: $0, incrementReferenceCount: true) }
updatedAccounts.append(contentsOf: accounts.map { $0.id }) updatedAccounts.append(contentsOf: accounts.map { $0.id })
}, { (statuses) in }, { (statuses) in
statuses.forEach { self.upsert(status: $0, incrementReferenceCount: true) } statuses.forEach { self.upsert(status: $0, incrementReferenceCount: true) }

View File

@ -82,6 +82,7 @@ public final class StatusMO: NSManagedObject, StatusProtocol {
public override func prepareForDeletion() { public override func prepareForDeletion() {
super.prepareForDeletion() super.prepareForDeletion()
reblog?.decrementReferenceCount() reblog?.decrementReferenceCount()
account.decrementReferenceCount()
} }
} }
@ -92,6 +93,7 @@ extension StatusMO {
self.updateFrom(apiStatus: status, container: container) self.updateFrom(apiStatus: status, container: container)
reblog?.incrementReferenceCount() reblog?.incrementReferenceCount()
account.incrementReferenceCount()
} }
func updateFrom(apiStatus status: Pachyderm.Status, container: MastodonCachePersistentStore) { func updateFrom(apiStatus status: Pachyderm.Status, container: MastodonCachePersistentStore) {

View File

@ -15,6 +15,7 @@
<attribute name="locked" attributeType="Boolean" usesScalarValueType="YES"/> <attribute name="locked" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="movedCD" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/> <attribute name="movedCD" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="note" attributeType="String"/> <attribute name="note" attributeType="String"/>
<attribute name="referenceCount" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="statusesCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/> <attribute name="statusesCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="url" attributeType="URI"/> <attribute name="url" attributeType="URI"/>
<attribute name="username" attributeType="String"/> <attribute name="username" attributeType="String"/>
@ -49,7 +50,6 @@
<attribute name="uri" attributeType="String"/> <attribute name="uri" attributeType="String"/>
<attribute name="url" optional="YES" attributeType="URI"/> <attribute name="url" optional="YES" attributeType="URI"/>
<attribute name="visibilityString" attributeType="String"/> <attribute name="visibilityString" attributeType="String"/>
<relationship name="account" maxCount="1" deletionRule="Nullify" destinationEntity="Account"/> <relationship name="account" maxCount="1" deletionRule="Nullify" destinationEntity="Account"/>
<relationship name="reblog" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status"/> <relationship name="reblog" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status"/>
<uniquenessConstraints> <uniquenessConstraints>
@ -59,7 +59,7 @@
</uniquenessConstraints> </uniquenessConstraints>
</entity> </entity>
<elements> <elements>
<element name="Account" positionX="169.21875" positionY="78.9609375" width="128" height="313"/> <element name="Account" positionX="169.21875" positionY="78.9609375" width="128" height="328"/>
<element name="Status" positionX="-63" positionY="-18" width="128" height="418"/> <element name="Status" positionX="-63" positionY="-18" width="128" height="418"/>
</elements> </elements>
</model> </model>

View File

@ -61,6 +61,15 @@ class ProfileTableViewController: EnhancedTableViewController {
fatalError("init(coder:) has not been implemeneted") fatalError("init(coder:) has not been implemeneted")
} }
deinit {
if let id = accountID {
let container = mastodonController.persistentContainer
container.backgroundContext.perform {
container.account(for: id, in: container.backgroundContext)?.decrementReferenceCount()
}
}
}
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
@ -90,7 +99,7 @@ class ProfileTableViewController: EnhancedTableViewController {
} }
return return
} }
self.mastodonController.persistentContainer.addOrUpdate(account: account) { (_) in self.mastodonController.persistentContainer.addOrUpdate(account: account, incrementReferenceCount: true) { (_) in
DispatchQueue.main.async { DispatchQueue.main.async {
self.updateAccountUI() self.updateAccountUI()
self.tableView.reloadData() self.tableView.reloadData()

View File

@ -134,11 +134,10 @@ class SearchResultsViewController: EnhancedTableViewController {
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>() var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
self.mastodonController.persistentContainer.performBatchUpdates({ (context, addAccounts, addStatuses) in self.mastodonController.persistentContainer.performBatchUpdates({ (context, addAccounts, addStatuses) in
// todo: reference count accounts oldSnapshot.itemIdentifiers(inSection: .accounts).forEach { (item) in
// oldSnapshot.itemIdentifiers(inSection: .accounts).forEach { (item) in guard case let .account(id) = item else { return }
// guard case let .account(id) = item else { return } self.mastodonController.persistentContainer.account(for: id, in: context)?.decrementReferenceCount()
// self.mastodonController.persistentContainer.account(for: id, in: context)?.decrementReferenceCount() }
// }
oldSnapshot.itemIdentifiers(inSection: .statuses).forEach { (item) in oldSnapshot.itemIdentifiers(inSection: .statuses).forEach { (item) in
guard case let .status(id, _) = item else { return } guard case let .status(id, _) = item else { return }

View File

@ -58,6 +58,17 @@ class StatusActionAccountListTableViewController: EnhancedTableViewController {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
deinit {
if let accountIDs = self.accountIDs {
let container = self.mastodonController.persistentContainer
container.backgroundContext.perform {
for id in accountIDs {
container.account(for: id, in: container.backgroundContext)?.decrementReferenceCount()
}
}
}
}
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
@ -71,7 +82,11 @@ class StatusActionAccountListTableViewController: EnhancedTableViewController {
tableView.tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: CGFloat.leastNormalMagnitude)) tableView.tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: CGFloat.leastNormalMagnitude))
if accountIDs == nil { if let accountIDs = accountIDs {
accountIDs.forEach { (id) in
self.mastodonController.persistentContainer.account(for: id)?.incrementReferenceCount()
}
} else {
// account IDs haven't been set, so perform a request to load them // account IDs haven't been set, so perform a request to load them
guard let status = mastodonController.persistentContainer.status(for: statusID) else { guard let status = mastodonController.persistentContainer.status(for: statusID) else {
fatalError("Missing cached status \(statusID)") fatalError("Missing cached status \(statusID)")