Summary
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.
Reproduction
- Install OpenBubbles 1.15.x on Linux (Flatpak) or any desktop target.
- Sign in with an Apple ID that has at least ~6 months of iMessage history. (151 MB ObjectBox store observed on Toyoko; should reproduce at any nontrivial size.)
- Launch the app and let it sit for 5-15 minutes without interacting.
- Observe:
- One CPU core pinned at 99-194% in
top/btop
- Resident memory grows from ~600 MB to >2.5 GB over ~15 minutes
journalctl --user shows a continuous stream of flutter: ... [INFO] [BlueBubblesApp] Syncing new message lines
- System load average climbs steadily
- Killing the process is the only way to stop it; on next launch the loop restarts.
Root cause
Three sites in rustpush_service.dart always write existing rows during the incremental sync loop, instead of checking whether anything changed:
existing.save() for messages (~line 2541): The existing.ckRecordId == item.key branch correctly classifies the row as localUnchanged++, but the code falls through to existing.save() instead of continue-ing.
existing.save(null) for attachments (~line 2460): No equivalent guard at all; every attachment's ckRecordId is re-written on every sync pass.
Database.chats.put(this) in Chat.applyFromCloud: Always writes the row, even when the cloud record matches local state.
Additionally, Message.applyFromCloud and Attachment.applyFromCloud always re-decode the proto / re-allocate attributedBody lists before the save, so every sync pass allocates fresh objects. That's the primary RAM-growth amplifier on top of the save storms.
Fix
PR forthcoming. The minimal fix is:
- Add
continue to the message sync loop's localUnchanged branch (so the save is skipped).
- Add an equivalent guard to the attachment sync loop.
- Change
Chat.applyFromCloud, Message.applyFromCloud, and Attachment.applyFromCloud to return bool. Each early-returns false when the cloud record is identical to local state, so the caller's redundant save is skipped.
Verification
- Manual: with the fix, a sync pass over an unchanged store hits the early-return on the first item and returns immediately. RAM stays flat, CPU stays idle, no log spam.
- Static:
dart analyze shows no new errors vs the unpatched branch (2074 baseline vs 2076 with the patch — the +2 are pre-existing dead-code warnings the analyzer reaches further into).
- Dynamic: needs a Flutter toolchain to build the app and validate end-to-end. Patching this without Flutter installed is fine for review; the maintainers' CI will exercise the full build.
Environment
- OpenBubbles 1.15.0.0 (Flatpak, stable channel)
- Linux 7.2.5-3-omarchy (Arch-derived)
- ObjectBox store: 151 MB (~6 months of iMessage history, 8 chats, mixed group + 1:1)
- Hardware: Framework 16 laptop, AMD Radeon RX 7600, 60 GB RAM
Summary
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.
Reproduction
top/btopjournalctl --usershows a continuous stream offlutter: ... [INFO] [BlueBubblesApp] Syncing new messagelinesRoot 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 2541): 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 2460): No equivalent guard at all; every attachment'sckRecordIdis re-written on every sync pass.Database.chats.put(this)inChat.applyFromCloud: 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
PR forthcoming. The minimal fix is:
continueto the message sync loop'slocalUnchangedbranch (so the save is skipped).Chat.applyFromCloud,Message.applyFromCloud, andAttachment.applyFromCloudto returnbool. Each early-returnsfalsewhen the cloud record is identical to local state, so the caller's redundant save is skipped.Verification
dart analyzeshows no new errors vs the unpatched branch (2074 baseline vs 2076 with the patch — the +2 are pre-existing dead-code warnings the analyzer reaches further into).Environment