Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion lib/database/io/attachment.dart
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,12 @@ class Attachment {
set dbMetadata(String? json) => metadata = json == null
? null : jsonDecode(json) as Map<String, dynamic>;

void applyFromCloud(api.CloudAttachment c, String ckRecordId) {
bool applyFromCloud(api.CloudAttachment c, String ckRecordId) {
// No-op fast path: same CloudKit record already applied → return false so
// the caller skips the redundant save. Mirrors the message-side guard.
if (this.ckRecordId == ckRecordId) {
return false;
}
this.ckRecordId = ckRecordId;
var decoded = api.decodeAttachmentmeta(wrapped: c.cm);
uti = decoded.uti;
Expand All @@ -74,6 +79,7 @@ class Attachment {
guid = decoded.guid;
save(null);
}
return true;
}

String unconvertAttachmentGuid(String guid) {
Expand Down
12 changes: 12 additions & 0 deletions lib/database/io/chat.dart
Original file line number Diff line number Diff line change
Expand Up @@ -893,11 +893,23 @@ class Chat {
}

bool applyFromCloud(api.CloudChat c, String record) {
// No-op fast path: same CloudKit record, same group version → nothing changed.
// Without this guard, every incremental sync pass rewrites the chat row,
// which fires ObjectBox reactive observers for the chat list and grows
// resident memory on accounts with many groups (verified at 200+ chats
// running sync in a loop pegged one CPU core at 99% with monotonic RAM
// growth until OOM).
if (ckRecordId == record && c.properties?.pv == (groupVersion ?? 1)) {
return false;
}

chatIdentifier = c.chatIdentifier;
ckRecordId = record;
cloudGuid = c.groupId;
ckSyncState = c.properties?.pv == (groupVersion ?? 1);
if (c.properties?.pv == null || c.properties!.pv! <= (groupVersion ?? 1)) {
// Only persist the chatIdentifier / cloudGuid / ckSyncState changes —
// the version-bump path below will write everything else.
Database.chats.put(this);
return false;
}
Expand Down
16 changes: 14 additions & 2 deletions lib/database/io/message.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1097,7 +1097,18 @@ class Message {
);
}

void applyFromCloud(api.CloudMessage c, String cloudkitId) {
bool applyFromCloud(api.CloudMessage c, String cloudkitId) {
// No-op fast path: same CloudKit record already applied → return false so
// the caller skips the redundant save. Without this guard, every sync pass
// re-decodes the proto, re-runs balloon decoding, allocates new lists for
// attributedBody, and writes the row again — re-firing ObjectBox reactive
// observers and growing resident memory until the UI thread is starved.
// Reproduced: 151 MB ObjectBox store, sync loop pegged a CPU core at 99%
// for >15 minutes with monotonic RAM growth (2.5 GB observed).
if (ckRecordId == cloudkitId) {
Logger.info("item ${c.chatId}");
return false;
}
Logger.info("item ${c.chatId}");
Chat? chat;
if (c.chatId.contains(";")) {
Expand Down Expand Up @@ -1183,8 +1194,9 @@ class Message {
var proto4 = api.decodeMessageproto4(wrapped: c.msgProto4!);
associatedMessageEmoji = proto4.associatedMessageEmoji;
}

save(chat: chat);
return true;
}

/// Fetch reactions
Expand Down
13 changes: 13 additions & 0 deletions lib/services/rustpush/rustpush_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2453,6 +2453,13 @@ class RustPushService extends GetxService {

var existing = Attachment.findOne(convertAttachmentGuid(decoded.guid));
if (existing != null) {
if (existing.ckRecordId == item.key) {
// Already up-to-date for this CloudKit record — skip the save.
// Mirrors the message-side guard above; without it the same sync
// loop re-fires ObjectBox reactive queries per attachment per
// pass and resident memory grows without bound.
continue;
}
if (existing.ckRecordId != null && existing.ckRecordId != item.key) {
// we have a different record id
dupDeleteAttachments.add(existing.ckRecordId!);
Expand Down Expand Up @@ -2529,7 +2536,13 @@ class RustPushService extends GetxService {
var existing = Message.findOne(guid: item.value!.guid);
if (existing != null) {
if (existing.ckRecordId == item.key) {
// Already up-to-date for this CloudKit record — skip the save.
// Without this guard, every sync pass writes the row again, which
// re-fires ObjectBox reactive observers and grows resident memory
// until the UI thread is starved. The flags on `existing` are
// unchanged from the prior pass, so the write is a true no-op.
localUnchanged++;
continue;
} else if (existing.ckRecordId != null) {
// we have a different record id
dupDeleteMessages.add(existing.ckRecordId!);
Expand Down