Fix: "Retrieving nodes" significantly slower after reconnect extracted from #1424 (#1477)

* Fix: "Retrieving nodes" significantly slower after reconnect (#1424)

The node database retrieval was calling context.save() for every single
NodeInfo packet received (250 saves for 250 nodes). This caused severe
performance degradation on reconnect when CoreData had accumulated state.

Root Cause:
- nodeInfoPacket() called context.save() immediately for each node
- With 250 nodes, this meant 250 individual CoreData save operations
- On first connection, CoreData is fresh and fast
- On reconnect, CoreData has accumulated change tracking, undo management,
  and memory pressure, making each save progressively slower
- This resulted in 10+ second retrieval times vs 1-2 seconds initially

Solution:
- Added deferSave parameter to nodeInfoPacket() function
- During database retrieval (.retrievingDatabase state), defer all saves
- Perform a single batch save when database retrieval completes
  (when NONCE_ONLY_DB configCompleteID is received)
- This reduces 250 saves to 1 save

Performance Impact:
- Eliminates N individual saves during node database sync
- Reduces database retrieval time back to 1-2 seconds on reconnect
- Matches first-connection performance consistently

Fixes #1424

* Revert *MessageListUnified files

---------

Co-authored-by: Martin Bogomolni <martinbogo@gmail.com>
Co-authored-by: Jake-B <jake-b@users.noreply.github.com>
This commit is contained in:
Garth Vander Houwen 2025-10-20 11:38:18 -07:00 committed by GitHub
parent 9e0a1ffea9
commit 16e56e7f07
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 24 additions and 6 deletions

View file

@ -106,8 +106,11 @@ extension AccessoryManager {
return
}
// Check if we're in database retrieval mode to defer saves for performance
let isRetrievingDatabase = if case .retrievingDatabase = self.state { true } else { false }
// TODO: nodeInfoPacket's channel: parameter is not used
if let nodeInfo = nodeInfoPacket(nodeInfo: nodeInfo, channel: 0, context: context) {
if let nodeInfo = nodeInfoPacket(nodeInfo: nodeInfo, channel: 0, context: context, deferSave: isRetrievingDatabase) {
if let activeDevice = activeConnection?.device, activeDevice.num == nodeInfo.num {
if let user = nodeInfo.user {
updateDevice(deviceId: activeDevice.id, key: \.shortName, value: user.shortName ?? "?")

View file

@ -665,6 +665,17 @@ class AccessoryManager: ObservableObject, MqttClientProxyManagerDelegate {
self.firstDatabaseNodeInfoContinuation = nil
}
// Perform a single batch save after database retrieval completes
// This significantly improves performance on reconnect
do {
try context.save()
Logger.data.info("💾 [Database] Batch saved all node info after database retrieval")
} catch {
context.rollback()
let nsError = error as NSError
Logger.data.error("💥 [Database] Error saving batch node info: \(nsError, privacy: .public)")
}
default:
Logger.transport.error("[Accessory] Unknown nonce completed: \(configCompleteID)")
}

View file

@ -264,7 +264,7 @@ func deviceMetadataPacket (metadata: DeviceMetadata, fromNum: Int64, sessionPass
}
}
func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObjectContext) -> NodeInfoEntity? {
func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObjectContext, deferSave: Bool = false) -> NodeInfoEntity? {
let logString = String.localizedStringWithFormat("[NodeInfo] received for: %@".localized, String(nodeInfo.num))
Logger.mesh.info("📟 \(logString, privacy: .public)")
@ -375,8 +375,10 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje
newNode.myInfo = fetchedMyInfo[0]
}
do {
try context.save()
Logger.data.info("💾 Saved a new Node Info For: \(String(nodeInfo.num), privacy: .public)")
if !deferSave {
try context.save()
Logger.data.info("💾 Saved a new Node Info For: \(String(nodeInfo.num), privacy: .public)")
}
return newNode
} catch {
context.rollback()
@ -500,8 +502,10 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje
fetchedNode[0].myInfo = fetchedMyInfo[0]
}
do {
try context.save()
Logger.data.info("💾 [NodeInfo] saved for \(nodeInfo.num.toHex(), privacy: .public)")
if !deferSave {
try context.save()
Logger.data.info("💾 [NodeInfo] saved for \(nodeInfo.num.toHex(), privacy: .public)")
}
return fetchedNode[0]
} catch {
context.rollback()