perf(library): stream normalized scan timestamp snapshots

This commit is contained in:
zarzet
2026-09-06 02:34:49 +07:00
parent 3368cf8c5d
commit d95d070565
3 changed files with 71 additions and 27 deletions
+4 -2
View File
@@ -759,7 +759,7 @@ class LocalLibraryNotifier extends Notifier<LocalLibraryState> {
final useSnapshotBridge =
Platform.isAndroid && existingFiles.isNotEmpty;
final snapshotPath = useSnapshotBridge
? await _db.writeFileModTimesSnapshot(sourceId: activeSourceId)
? await _db.writeFileModTimesSnapshot(existingFiles)
: null;
Map<String, dynamic> result;
@@ -1263,7 +1263,9 @@ class LocalLibraryNotifier extends Notifier<LocalLibraryState> {
required Map<String, int> existingFiles,
}) async {
final legacyPaths = existingFiles.entries
.where((entry) => entry.value <= 0)
// Negative timestamps deliberately force a metadata-version rescan;
// only a missing timestamp (zero) may be filled from the filesystem.
.where((entry) => entry.value == 0)
.map((entry) => entry.key)
.toList();
if (legacyPaths.isEmpty) {
+28 -25
View File
@@ -2064,14 +2064,11 @@ class LibraryDatabase {
return result;
}
Future<String> writeFileModTimesSnapshot({String? sourceId}) async {
final db = await database;
final rows = await db.rawQuery(
'SELECT file_path, COALESCE(file_mod_time, 0) AS file_mod_time, '
'audio_metadata_scan_version FROM library '
'${sourceId == null ? '' : 'WHERE source_id = ?'}',
sourceId == null ? const [] : [sourceId],
);
/// Uses the already normalized, backfilled timestamps from the scan caller.
/// Bounded writes avoid a second query and a library-sized StringBuffer.
Future<String> writeFileModTimesSnapshot(
Map<String, int> fileModTimes,
) async {
final tempDir = await getTemporaryDirectory();
final file = File(
join(
@@ -2079,24 +2076,30 @@ class LibraryDatabase {
'library_file_mod_times_${DateTime.now().microsecondsSinceEpoch}.tsv',
),
);
final buffer = StringBuffer();
for (final row in rows) {
final path = row['file_path'] as String?;
if (path == null || path.isEmpty) continue;
final modTime = (row['file_mod_time'] as num?)?.toInt() ?? 0;
final scanVersion =
(row['audio_metadata_scan_version'] as num?)?.toInt() ?? 0;
buffer
..write(
libraryIncrementalSnapshotModTime(
storedModTime: modTime,
storedScanVersion: scanVersion,
),
)
..write('\t')
..writeln(path);
try {
final output = await file.open(mode: FileMode.write);
try {
final buffer = StringBuffer();
for (final entry in fileModTimes.entries) {
if (entry.key.isEmpty) continue;
buffer
..write(entry.value)
..write('\t')
..writeln(entry.key);
if (buffer.length >= 64 * 1024) {
await output.writeString(buffer.toString());
buffer.clear();
}
}
if (buffer.isNotEmpty) await output.writeString(buffer.toString());
await output.flush();
} finally {
await output.close();
}
} catch (_) {
if (await file.exists()) await file.delete();
rethrow;
}
await file.writeAsString(buffer.toString(), flush: true);
return file.path;
}
+39
View File
@@ -0,0 +1,39 @@
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:spotiflac_android/services/library_database.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
test(
'scan snapshot preserves normalized sentinel and backfilled timestamps across chunks',
() async {
final directory = await Directory.systemTemp.createTemp(
'scan-snapshot-test-',
);
const channel = MethodChannel('plugins.flutter.io/path_provider');
final messenger =
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger;
messenger.setMockMethodCallHandler(channel, (_) async => directory.path);
addTearDown(() async {
messenger.setMockMethodCallHandler(channel, null);
await directory.delete(recursive: true);
});
final timestamps = <String, int>{
for (var i = 0; i < 4000; i++) '/music/歌曲 café $i.flac': i,
'/music/legacy.flac': -1,
'/music/backfilled.flac': 1720000000000,
};
final path = await LibraryDatabase.instance.writeFileModTimesSnapshot(
timestamps,
);
final lines = await File(path).readAsLines();
expect(
lines,
timestamps.entries
.map((entry) => '${entry.value}\t${entry.key}')
.toList(),
);
},
);
}