From 5eded9e33ca59cce2b986a347f3aa0996e82bcb3 Mon Sep 17 00:00:00 2001 From: Strycher Date: Mon, 20 Jul 2026 04:21:45 -0400 Subject: [PATCH] fix(#306): guard every legacy-key removal, not just the channel-settings one The previous commit fixed loadSmazEnabled and killed the post-connect stall (startup-load channelSettings: 37792ms -> 0ms, measured twice). The contact pull still choked, in multi-second bursts, each landing immediately after an "Added new contact" line. No frame handler exceeded the 100ms threshold, because the work is not in a handler: the contact handler fires _loadMessagesForContact unawaited, so it runs after the handler returns and escapes that timing. message_store.loadMessages had the same unconditional prefs.remove(oldKey), and it runs ONCE PER CONTACT during a pull. On Windows every prefs mutation rewrites the entire file, so a 234-contact pull meant 234 full multi-MB writes for legacy keys that do not exist. A sweep found the identical pattern in six more stores: contact_store, unread_store, channel_store, community_store, contact_group_store and channel_order_store. Those run once per load rather than per item, so they are far cheaper, but it is the same bug and they are fixed the same way. In every case the removal now happens only when a legacy key actually exists, paired with the write that migrates it, so the cleanup still occurs on a real migration. flutter analyze clean, dart format clean, 494 tests pass. --- lib/storage/channel_order_store.dart | 4 +++- lib/storage/channel_store.dart | 4 +++- lib/storage/community_store.dart | 4 +++- lib/storage/contact_group_store.dart | 4 +++- lib/storage/contact_store.dart | 4 +++- lib/storage/message_store.dart | 7 ++++++- lib/storage/unread_store.dart | 4 +++- 7 files changed, 24 insertions(+), 7 deletions(-) diff --git a/lib/storage/channel_order_store.dart b/lib/storage/channel_order_store.dart index 88d3f7a..4e268b2 100644 --- a/lib/storage/channel_order_store.dart +++ b/lib/storage/channel_order_store.dart @@ -29,13 +29,15 @@ class ChannelOrderStore { String? jsonString = prefs.getString(keyFor); if (jsonString == null || jsonString.isEmpty) { // Attempt migration from legacy unscoped key on first load + // Only remove the legacy key when it actually exists: every prefs + // mutation rewrites the whole file on Windows. final legacyJsonString = prefs.getString(_keyPrefix); - prefs.remove(_keyPrefix); if (legacyJsonString != null && legacyJsonString.isNotEmpty) { appLogger.info( 'Migrating channel order from legacy key $_keyPrefix to scoped key $keyFor', ); await prefs.setString(keyFor, legacyJsonString); + await prefs.remove(_keyPrefix); jsonString = legacyJsonString; } } diff --git a/lib/storage/channel_store.dart b/lib/storage/channel_store.dart index 4f40482..b52b5b7 100644 --- a/lib/storage/channel_store.dart +++ b/lib/storage/channel_store.dart @@ -22,13 +22,15 @@ class ChannelStore { String? jsonString = prefs.getString(keyFor); if (jsonString == null || jsonString.isEmpty) { // Attempt migration from legacy unscoped key on first load + // Only remove the legacy key when it actually exists: every prefs + // mutation rewrites the whole file on Windows. final legacyJsonString = prefs.getString(_keyPrefix); - prefs.remove(_keyPrefix); if (legacyJsonString != null && legacyJsonString.isNotEmpty) { appLogger.info( 'Migrating channel messages from legacy key $_keyPrefix to scoped key $keyFor', ); await prefs.setString(keyFor, legacyJsonString); + await prefs.remove(_keyPrefix); jsonString = legacyJsonString; } } diff --git a/lib/storage/community_store.dart b/lib/storage/community_store.dart index c69d0b8..a8010a1 100644 --- a/lib/storage/community_store.dart +++ b/lib/storage/community_store.dart @@ -28,13 +28,15 @@ class CommunityStore { String? jsonString = prefs.getString(keyFor); if (jsonString == null || jsonString.isEmpty) { // Attempt migration from legacy unscoped key on first load + // Only remove the legacy key when it actually exists: every prefs + // mutation rewrites the whole file on Windows. final legacyJsonString = prefs.getString(_keyPrefix); - prefs.remove(_keyPrefix); if (legacyJsonString != null && legacyJsonString.isNotEmpty) { appLogger.info( 'Migrating communities from legacy key $_keyPrefix to scoped key $keyFor', ); await prefs.setString(keyFor, legacyJsonString); + await prefs.remove(_keyPrefix); jsonString = legacyJsonString; } } diff --git a/lib/storage/contact_group_store.dart b/lib/storage/contact_group_store.dart index ce6a0c6..0813274 100644 --- a/lib/storage/contact_group_store.dart +++ b/lib/storage/contact_group_store.dart @@ -21,13 +21,15 @@ class ContactGroupStore { String? jsonString = prefs.getString(keyFor); if (jsonString == null || jsonString.isEmpty) { // Attempt migration from legacy unscoped key on first load + // Only remove the legacy key when it actually exists: every prefs + // mutation rewrites the whole file on Windows. final legacyJsonString = prefs.getString(_keyPrefix); - prefs.remove(_keyPrefix); if (legacyJsonString != null && legacyJsonString.isNotEmpty) { appLogger.info( 'Migrating channel messages from legacy key $_keyPrefix to scoped key $keyFor', ); await prefs.setString(keyFor, legacyJsonString); + await prefs.remove(_keyPrefix); jsonString = legacyJsonString; } } diff --git a/lib/storage/contact_store.dart b/lib/storage/contact_store.dart index 5d1805d..9085d4e 100644 --- a/lib/storage/contact_store.dart +++ b/lib/storage/contact_store.dart @@ -23,13 +23,15 @@ class ContactStore { String? jsonString = prefs.getString(keyFor); if (jsonString == null || jsonString.isEmpty) { // Attempt migration from legacy unscoped key on first load + // Only remove the legacy key when it actually exists: every prefs + // mutation rewrites the whole file on Windows. final legacyJsonString = prefs.getString(_keyPrefix); - prefs.remove(_keyPrefix); if (legacyJsonString != null && legacyJsonString.isNotEmpty) { appLogger.info( 'Migrating contacts from legacy key $_keyPrefix to scoped key $keyFor', ); await prefs.setString(keyFor, legacyJsonString); + await prefs.remove(_keyPrefix); jsonString = legacyJsonString; } } diff --git a/lib/storage/message_store.dart b/lib/storage/message_store.dart index ccaf9ea..6045342 100644 --- a/lib/storage/message_store.dart +++ b/lib/storage/message_store.dart @@ -40,13 +40,18 @@ class MessageStore { String? jsonString = prefs.getString(key); if (jsonString == null || jsonString.isEmpty) { // Attempt migration from legacy unscoped key on first load + // Only touch storage when a legacy key actually exists. This ran + // unconditionally, and loadMessages is called once per contact during a + // contact pull. On Windows shared_preferences rewrites the WHOLE prefs + // file on every mutation, so each no-op remove cost a full multi-MB + // write - hundreds of them back to back on a large address book. final legacyJsonString = prefs.getString(oldKey); - prefs.remove(oldKey); if (legacyJsonString != null && legacyJsonString.isNotEmpty) { appLogger.info( 'Migrating messages from legacy key $oldKey to scoped key $key', ); await prefs.setString(key, legacyJsonString); + await prefs.remove(oldKey); jsonString = legacyJsonString; } } diff --git a/lib/storage/unread_store.dart b/lib/storage/unread_store.dart index 3b615b1..cc1f377 100644 --- a/lib/storage/unread_store.dart +++ b/lib/storage/unread_store.dart @@ -35,13 +35,15 @@ class UnreadStore { String? jsonString = prefs.getString(keyFor); if (jsonString == null || jsonString.isEmpty) { // Attempt migration from legacy unscoped key on first load + // Only remove the legacy key when it actually exists: every prefs + // mutation rewrites the whole file on Windows. final legacyJsonString = prefs.getString(_keyPrefix); - prefs.remove(_keyPrefix); if (legacyJsonString != null && legacyJsonString.isNotEmpty) { appLogger.info( 'Migrating channel messages from legacy key $_keyPrefix to scoped key $keyFor', ); await prefs.setString(keyFor, legacyJsonString); + await prefs.remove(_keyPrefix); jsonString = legacyJsonString; } }