Verhaltensgleich aus search_modal.dart herausgeloest (gleiche Deckel 1500/60/500, gleiche Schleife; Medien-Filter jetzt Parameter). Tests laufen ueber die echte SDK-DB-Schicht mit synthetischen Daten (test/support/synthetic_matrix_data.dart). Co-Authored-By: Claude Opus 5.5 <[email protected]>
201 lines
5.7 KiB
Dart
201 lines
5.7 KiB
Dart
import 'dart:math';
|
||
|
||
import 'package:matrix/matrix.dart';
|
||
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
||
|
||
/// Synthetische Matrix-Daten für Tests und `benchmark/` – komplett offline:
|
||
/// kein Homeserver, keine echten Konten, keine Secrets.
|
||
///
|
||
/// Die Daten gehen über die ECHTE SDK-Datenbankschicht (`MatrixSdkDatabase`,
|
||
/// `storeRoomUpdate`/`storeEventUpdate` in Sync-artigen Transaktionen) – also
|
||
/// denselben Weg wie beim echten Sync. Dadurch messen/prüfen Tests und
|
||
/// Benchmarks die Speicherstruktur, die die App wirklich auf der Platte hat.
|
||
class SyntheticMatrixData {
|
||
SyntheticMatrixData._();
|
||
|
||
static const homeserverDomain = 'bench.invalid';
|
||
static const _senders = 5;
|
||
|
||
/// Wörter für die Nachrichtentexte (bewusst mit Umlauten – die Suche
|
||
/// arbeitet mit `toLowerCase`).
|
||
static const _words = [
|
||
'hallo',
|
||
'morgen',
|
||
'treffen',
|
||
'kaffee',
|
||
'bahnhof',
|
||
'urlaub',
|
||
'foto',
|
||
'danke',
|
||
'später',
|
||
'gleich',
|
||
'abend',
|
||
'essen',
|
||
'kino',
|
||
'wetter',
|
||
'regen',
|
||
'sonne',
|
||
'arbeit',
|
||
'pause',
|
||
'auto',
|
||
'zug',
|
||
'buch',
|
||
'musik',
|
||
'spiel',
|
||
'garten',
|
||
'hund',
|
||
'katze',
|
||
'einkaufen',
|
||
'termin',
|
||
'grüße',
|
||
'übermorgen',
|
||
];
|
||
|
||
static String roomId(int r) => '!room$r:$homeserverDomain';
|
||
static String eventId(int r, int i) => '\$r${r}e$i';
|
||
static String sender(int i) => '@user${i % _senders}:$homeserverDomain';
|
||
|
||
/// Öffnet die SDK-Datenbank auf [db] und baut einen Offline-Client darauf.
|
||
static Future<Client> openClient(
|
||
Database db, {
|
||
String name = 'PyramidBench',
|
||
}) async {
|
||
final sdkDb = await MatrixSdkDatabase.init('pyramid', database: db);
|
||
return Client(name, database: sdkDb);
|
||
}
|
||
|
||
/// Schreibt [rooms] Räume mit je [eventsPerRoom] Nachrichten in die DB.
|
||
///
|
||
/// Wie beim echten Sync landen die Events in Transaktionen à [batchSize]
|
||
/// (≈ eine Sync-Antwort) und in zeitlicher Reihenfolge (älteste zuerst).
|
||
/// Jede 10. Nachricht ist ein Bild (`m.image`). Ist [needleEvery] gesetzt,
|
||
/// enthält jede n-te Nachricht zusätzlich das Wort [needle] – so kennen
|
||
/// Tests die exakte Trefferzahl der Suche.
|
||
static Future<void> populate(
|
||
Client client, {
|
||
required int rooms,
|
||
required int eventsPerRoom,
|
||
int batchSize = 50,
|
||
String needle = 'nadelwort',
|
||
int? needleEvery,
|
||
int seed = 42,
|
||
}) async {
|
||
final db = client.database;
|
||
final rnd = Random(seed);
|
||
final baseTs = DateTime.utc(2026, 1, 1).millisecondsSinceEpoch;
|
||
|
||
for (var r = 0; r < rooms; r++) {
|
||
final id = roomId(r);
|
||
await db.transaction(() async {
|
||
await db.storeRoomUpdate(
|
||
id,
|
||
JoinedRoomUpdate(
|
||
summary: RoomSummary.fromJson({
|
||
'm.joined_member_count': _senders,
|
||
'm.invited_member_count': 0,
|
||
}),
|
||
),
|
||
null,
|
||
client,
|
||
);
|
||
for (final state in _roomState(r)) {
|
||
await db.storeEventUpdate(id, state, EventUpdateType.state, client);
|
||
}
|
||
});
|
||
|
||
for (var start = 0; start < eventsPerRoom; start += batchSize) {
|
||
final end = min(start + batchSize, eventsPerRoom);
|
||
await db.transaction(() async {
|
||
for (var i = start; i < end; i++) {
|
||
final hasNeedle = needleEvery != null && i % needleEvery == 0;
|
||
await db.storeEventUpdate(
|
||
id,
|
||
_message(r, i, baseTs, rnd, hasNeedle ? needle : null),
|
||
EventUpdateType.timeline,
|
||
client,
|
||
);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Lädt die Raumliste aus der DB in `client.rooms` – das, was beim
|
||
/// App-Start `client.init()` macht, ohne Netzwerk/Login.
|
||
static Future<List<Room>> loadRooms(Client client) async {
|
||
final rooms = await client.database.getRoomList(client);
|
||
client.rooms
|
||
..clear()
|
||
..addAll(rooms);
|
||
return rooms;
|
||
}
|
||
|
||
static List<MatrixEvent> _roomState(int r) {
|
||
final ts = DateTime.utc(2025, 12, 1).millisecondsSinceEpoch;
|
||
MatrixEvent state(
|
||
String type,
|
||
String stateKey,
|
||
Map<String, Object?> content,
|
||
int n,
|
||
) => MatrixEvent(
|
||
type: type,
|
||
eventId: '\$state${r}_$n',
|
||
senderId: sender(0),
|
||
originServerTs: DateTime.fromMillisecondsSinceEpoch(ts + n),
|
||
stateKey: stateKey,
|
||
content: content,
|
||
roomId: roomId(r),
|
||
);
|
||
return [
|
||
state(EventTypes.RoomCreate, '', {'creator': sender(0)}, 0),
|
||
state(EventTypes.RoomName, '', {'name': 'Raum $r'}, 1),
|
||
for (var u = 0; u < _senders; u++)
|
||
state(EventTypes.RoomMember, sender(u), {
|
||
'membership': 'join',
|
||
'displayname': 'Nutzer $u',
|
||
}, 2 + u),
|
||
];
|
||
}
|
||
|
||
static MatrixEvent _message(
|
||
int r,
|
||
int i,
|
||
int baseTs,
|
||
Random rnd,
|
||
String? needle,
|
||
) {
|
||
final isImage = i % 10 == 9;
|
||
final Map<String, Object?> content;
|
||
if (isImage) {
|
||
content = {
|
||
'msgtype': 'm.image',
|
||
'body': 'IMG_${r}_$i.jpg',
|
||
'url': 'mxc://$homeserverDomain/img$r$i',
|
||
'info': {
|
||
'w': 1920,
|
||
'h': 1080,
|
||
'mimetype': 'image/jpeg',
|
||
'size': 250000,
|
||
},
|
||
};
|
||
} else {
|
||
final words = List.generate(
|
||
4 + rnd.nextInt(20),
|
||
(_) => _words[rnd.nextInt(_words.length)],
|
||
);
|
||
if (needle != null) words.insert(rnd.nextInt(words.length), needle);
|
||
content = {'msgtype': 'm.text', 'body': words.join(' ')};
|
||
}
|
||
return MatrixEvent(
|
||
type: EventTypes.Message,
|
||
eventId: eventId(r, i),
|
||
senderId: sender(i),
|
||
originServerTs: DateTime.fromMillisecondsSinceEpoch(
|
||
baseTs + r * 1000 + i * 60000,
|
||
),
|
||
content: content,
|
||
roomId: roomId(r),
|
||
);
|
||
}
|
||
}
|