Matthias Nott
yesterday e6eb093cc63ba020799844905439f1cabfddcd3c
lib/services/message_store.dart
....@@ -88,19 +88,32 @@
8888 }
8989 }
9090
91
+ /// Session id, without decoding the message it belongs to.
92
+ ///
93
+ /// The index needs ONE short field per line. jsonDecode gave it by parsing
94
+ /// the whole record — including an inline base64 attachment that can be tens
95
+ /// of megabytes — so building the index cost the size of the entire history.
96
+ /// With enough of it, startup exceeded the iOS watchdog and the app could
97
+ /// never open again: 9.5 seconds of CPU, killed at 0x8BADF00D, on a store
98
+ /// that had grown a few megabytes at a time for weeks.
99
+ ///
100
+ /// A line is written by us and always carries "sessionId":"…", so reading it
101
+ /// is a scan, not a parse.
102
+ static final RegExp _sessionIdPattern = RegExp(r'"sessionId"\s*:\s*"([^"]+)"');
103
+
104
+ static String? _sessionIdOf(String line) =>
105
+ _sessionIdPattern.firstMatch(line)?.group(1);
106
+
91107 static Future<void> _rebuildIndex(File logFile) async {
92108 _index.clear();
93109 final lines = logFile.readAsLinesSync();
94110 for (var i = 0; i < lines.length; i++) {
95111 final line = lines[i].trim();
96112 if (line.isEmpty) continue;
97
- try {
98
- final map = jsonDecode(line) as Map<String, dynamic>;
99
- final sessionId = map['sessionId'] as String?;
100
- if (sessionId != null) {
101
- _index.putIfAbsent(sessionId, () => []).add(i);
102
- }
103
- } catch (_) {}
113
+ final sessionId = _sessionIdOf(line);
114
+ if (sessionId != null) {
115
+ _index.putIfAbsent(sessionId, () => []).add(i);
116
+ }
104117 }
105118 }
106119
....@@ -189,6 +202,9 @@
189202
190203 // ------------------------------------------------------------- compact --
191204
205
+ /// Above this, compact regardless of how few lines there are.
206
+ static const int _compactAboveBytes = 8 * 1024 * 1024;
207
+
192208 /// Rewrite the log keeping at most [keepPerSession] messages per session.
193209 /// Called once on startup after initialize(). NOT called during normal use.
194210 static Future<void> compact({int keepPerSession = 200}) async {
....@@ -198,7 +214,12 @@
198214 if (!logFile.existsSync()) return;
199215
200216 final allLines = logFile.readAsLinesSync();
201
- if (allLines.length < 500) return; // nothing worth compacting
217
+ // Compact on SIZE as well as line count. A store is bounded at 200
218
+ // messages per session, which bounds nothing when a message carries an
219
+ // inline attachment: 200 screenshots is tens of megabytes, and the count
220
+ // never trips the threshold. Bytes are what startup has to read.
221
+ final bytes = logFile.lengthSync();
222
+ if (allLines.length < 500 && bytes < _compactAboveBytes) return;
202223
203224 // Build a set of line numbers to keep: last keepPerSession per session.
204225 final keepLines = <int>{};
....@@ -224,14 +245,13 @@
224245 final line = allLines[i].trim();
225246 if (line.isEmpty) continue;
226247 buffer.write('$line\n');
227
- // Extract sessionId for new index.
228
- try {
229
- final map = jsonDecode(line) as Map<String, dynamic>;
230
- final sid = map['sessionId'] as String?;
231
- if (sid != null) {
232
- newIndex.putIfAbsent(sid, () => []).add(newLine);
233
- }
234
- } catch (_) {}
248
+ // Same scan as the index build, for the same reason: the only field
249
+ // needed here is short, and decoding the record to reach it makes
250
+ // compaction cost the size of the history it exists to bound.
251
+ final sid = _sessionIdOf(line);
252
+ if (sid != null) {
253
+ newIndex.putIfAbsent(sid, () => []).add(newLine);
254
+ }
235255 newLine++;
236256 }
237257