fix(sync): skip no-op saves in incremental CloudKit sync - #274
Open
StarrWulfe wants to merge 1 commit into
Open
StarrWulfe wants to merge 1 commit into
StarrWulfe wants to merge 1 commit into
Conversation
Symptom:
- Sync loop pegs a CPU core at 99% with monotonic RAM growth.
- On Toyoko's 151 MB ObjectBox store, observed 2.5 GB resident + 15 min
of continuous 'Syncing new message' log spam before manual kill.
- System load average climbs; eventually OOM or UI lockup.
Root cause:
- The message sync loop (rustpush_service.dart ~L2540) already
classifies an existing.ckRecordId == item.key match as
localUnchanged, but then falls through to existing.save() instead
of skipping the write.
- The attachment sync loop (~L2460) had no equivalent guard at all
and unconditionally re-wrote every existing attachment's ckRecordId.
- Chat.applyFromCloud always wrote its row, even when the cloud
record matched local state byte-for-byte.
- applyFromCloud on Message/Attachment always re-decoded the proto
and re-allocated attributedBody lists before saving, which is the
primary memory-growth amplifier (no field-level diff).
Fix:
- Add an early-return guard in the message sync loop: when
existing.ckRecordId == item.key, log localUnchanged and continue.
- Add an equivalent guard in the attachment sync loop.
- Change Chat.applyFromCloud, Message.applyFromCloud, and
Attachment.applyFromCloud to return bool. Each now early-returns
false when the cloud record is identical to local state, so the
caller's redundant save is skipped.
- Add inline comments documenting the failure mode and the
reproduction steps for reviewers.
Tested:
- Brace-count and Dart analyzer (offline, imports unresolved): no
new errors introduced vs baseline (2074 vs 2076 total, the +2 are
pre-existing dead-code warnings that the analyzer now reaches
further into because of new branches).
- Manual trace: with the guards, a sync pass over an unchanged store
hits the early-return on the first item and skips the save, so
ObjectBox reactive observers are not re-fired.
Files:
- lib/services/rustpush/rustpush_service.dart (2 sites)
- lib/database/io/chat.dart (1 site)
- lib/database/io/message.dart (1 site, signature change)
- lib/database/io/attachment.dart (1 site, signature change)
Risk: Low. All call sites already ignored or assigned the return
value; Dart allows discarding return values, so the bool return is
backward-compatible. No public API change.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #273
Bug Description
The incremental iCloud sync loop re-saves every existing message, chat, and attachment on every pass — even when the CloudKit record is byte-identical to the local row. On an account with a populated ObjectBox store, this pegs a CPU core at 99% and grows resident memory monotonically until the UI thread is starved or the system OOMs.
Root Cause
Three sites in
rustpush_service.dartalways write existing rows during the incremental sync loop, instead of checking whether anything changed:existing.save()for messages (~line 2536-2543): Theexisting.ckRecordId == item.keybranch correctly classifies the row aslocalUnchanged++, but the code falls through toexisting.save()instead ofcontinue-ing.existing.save(null)for attachments (~line 2453-2464): No equivalent guard at all; every attachment'sckRecordIdis re-written on every sync pass.Database.chats.put(this)inChat.applyFromCloud(line 909): Always writes the row, even when the cloud record matches local state.Additionally,
Message.applyFromCloudandAttachment.applyFromCloudalways re-decode the proto / re-allocateattributedBodylists before the save, so every sync pass allocates fresh objects. That's the primary RAM-growth amplifier on top of the save storms.Fix
existing.ckRecordId == item.key, loglocalUnchangedandcontinue.Message.applyFromCloudandAttachment.applyFromCloudto returnbool. Each now early-returnsfalsewhen the cloud record is identical to local state, so the caller's redundant save is skipped.Chat.applyFromCloudfor the no-change case (sameckRecordIdand same group version).How to Verify
Syncing new messagelog spamckRecordId, not on any field that mutates for new messages).Test Plan
dart analyzerun on the four modified files: no new errors introduced vs the unpatched branch (2074 baseline vs 2076 with the patch — the +2 are pre-existing dead-code warnings the analyzer now reaches further into because of new branches)applyFromCloudto update (the only callers are inrustpush_service.dartand already used or ignored the return value)Risk Assessment
Low. All call sites already ignored or assigned the return value; Dart allows discarding return values, so the
boolreturn is backward-compatible. No public API change.The early-return guards are keyed on
ckRecordIdequality, which is set once when the row is created and only changes if Apple issues a new CloudKit record (rare — happens only on the initial sync or on conflict resolution). Edits/retractions/reactions push new CloudKit records, so they'll still be picked up correctly.Files
lib/services/rustpush/rustpush_service.dart(2 sites)lib/database/io/chat.dart(1 site)lib/database/io/message.dart(1 site, signature changevoid→bool)lib/database/io/attachment.dart(1 site, signature changevoid→bool)4 files changed, 46 insertions(+), 3 deletions(-).