From 4ee2f507031eec0bcfc1fb03ae6aeac37c100031 Mon Sep 17 00:00:00 2001 From: Strycher Date: Mon, 20 Jul 2026 04:04:36 -0400 Subject: [PATCH] fix(#306): stop rewriting the whole prefs file per channel on connect The connect freeze is a legacy-key migration that writes even when there is nothing to migrate. loadSmazEnabled called prefs.remove(oldKey) unconditionally whenever the scoped key was absent, which is the normal case for a channel that has never had the setting changed. On Windows shared_preferences re-serialises and rewrites the ENTIRE prefs file on any mutation, so each of those no-op removes cost a full multi-MB write. Repeated across every channel slot on every connect, that is tens of full-file rewrites back to back. Measured with the instrumentation from the previous commit, on the reporting device (6.9 MB prefs, ~42 channel slots): startup-load channelOrder 0ms startup-load contactCache 12ms startup-load channelSettings 37792ms <-- here startup-load cachedChannels 0ms startup-load channelMessages 21ms startup-load unreadState 0ms startup-load discoveredContacts 11ms No frame handler exceeded 100ms, confirming the stall was local storage work and not radio traffic. The remove now happens only when a legacy key actually exists, alongside the write that migrates it. contact_settings_store had the identical pattern per contact and is fixed the same way. This also explains the platform gap in the original report: Android's backend batches into native storage, so the same code costs almost nothing there, while every no-op remove on Windows is a full file rewrite. flutter analyze clean, dart format clean, 469 tests pass. --- lib/storage/channel_settings_store.dart | 7 ++++++- lib/storage/contact_settings_store.dart | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/storage/channel_settings_store.dart b/lib/storage/channel_settings_store.dart index e0b390f..e6af56b 100644 --- a/lib/storage/channel_settings_store.dart +++ b/lib/storage/channel_settings_store.dart @@ -25,13 +25,18 @@ class ChannelSettingsStore { bool? enabled = prefs.getBool(key); if (enabled == null) { // Attempt migration from legacy unscoped key on first load + // Only touch storage when a legacy key actually exists. This ran + // unconditionally, and on Windows shared_preferences re-serialises and + // rewrites the WHOLE prefs file on every mutation, so removing a key + // that was never there still cost a full multi-MB write. Repeated per + // channel/contact on connect, that blocked the UI isolate for ~38s. enabled = prefs.getBool(oldKey); - prefs.remove(oldKey); if (enabled != null) { appLogger.info( 'Migrating channel settings from legacy key $oldKey to scoped key $key', ); await prefs.setBool(key, enabled); + await prefs.remove(oldKey); } } return enabled ?? false; diff --git a/lib/storage/contact_settings_store.dart b/lib/storage/contact_settings_store.dart index 682f1af..109f5eb 100644 --- a/lib/storage/contact_settings_store.dart +++ b/lib/storage/contact_settings_store.dart @@ -25,13 +25,18 @@ class ContactSettingsStore { bool? enabled = prefs.getBool(key); if (enabled == null) { // Attempt migration from legacy unscoped key on first load + // Only touch storage when a legacy key actually exists. This ran + // unconditionally, and on Windows shared_preferences re-serialises and + // rewrites the WHOLE prefs file on every mutation, so removing a key + // that was never there still cost a full multi-MB write. Repeated per + // channel/contact on connect, that blocked the UI isolate for ~38s. enabled = prefs.getBool(oldKey); - prefs.remove(oldKey); if (enabled != null) { appLogger.info( 'Migrating contact settings from legacy key $oldKey to scoped key $key', ); await prefs.setBool(key, enabled); + await prefs.remove(oldKey); } } return prefs.getBool(key) ?? false;