From a9578b35fa9d7a4c9367395d65f547cc7cf526af Mon Sep 17 00:00:00 2001 From: zarzet <42882290+zarzet@users.noreply.github.com> Date: Thu, 8 Oct 2026 16:28:33 +0700 Subject: [PATCH] test(downloads): measure selectors after queue removals --- integration_test/queue_index_read_test.dart | 134 ++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 integration_test/queue_index_read_test.dart diff --git a/integration_test/queue_index_read_test.dart b/integration_test/queue_index_read_test.dart new file mode 100644 index 00000000..21d1eb39 --- /dev/null +++ b/integration_test/queue_index_read_test.dart @@ -0,0 +1,134 @@ +import 'dart:io'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:spotiflac_android/models/download_item.dart'; +import 'package:spotiflac_android/providers/download_queue_provider.dart'; + +import '../test/support/queue_removal_benchmark.dart'; + +class _Queue extends DownloadQueueNotifier { + _Queue(this._initial); + final DownloadQueueState _initial; + // Exercise the real sparse progress path without restore, downloads or I/O. + @override + DownloadQueueState build() => _initial; +} + +Map _measure(DownloadQueueState initial) { + final container = ProviderContainer( + overrides: [downloadQueueProvider.overrideWith(() => _Queue(initial))], + ); + var queueChanges = 0; + var membershipChanges = 0; + final selected = {}; + final subscriptions = >[]; + final activeIds = [for (var i = 0; i < 3; i++) initial.items[100 + i].id]; + final visibleIds = [for (var i = 0; i < 40; i++) initial.items[100 + i].id]; + final trackIds = [ + for (var i = 0; i < 50; i++) + initial.items[(i * 131) % initial.items.length].track.id, + for (var i = 0; i < 50; i++) 'absent-$i', + ]; + for (final id in visibleIds) { + subscriptions.add( + container.listen( + downloadQueueLookupProvider.select((lookup) => lookup.byItemId[id]), + (_, next) { + queueChanges++; + selected[id] = next; + }, + ), + ); + } + for (final id in trackIds) { + subscriptions.add( + container.listen( + downloadQueueLookupProvider.select( + (lookup) => lookup.byTrackId.containsKey(id), + ), + (_, _) => membershipChanges++, + ), + ); + } + try { + final queue = container.read(downloadQueueProvider.notifier); + final watch = Stopwatch()..start(); + for (var tick = 0; tick < 500; tick++) { + queue.updateItemStatus( + activeIds[tick % 3], + DownloadStatus.downloading, + progress: (tick % 101) / 101, + bytesReceived: tick * 104857, + bytesTotal: 100000000, + speedMBps: 1.2, + ); + // Flush derived provider invalidation each tick as live row reads do. + container.read(downloadQueueLookupProvider); + } + watch.stop(); + expect(queueChanges, 500); + expect(membershipChanges, 0); + final finalState = container.read(downloadQueueProvider); + for (final id in activeIds) { + expect(selected[id], same(finalState.lookup.byItemId[id])); + } + expect(initial.items[100].progress, 0); + return { + 'microseconds': watch.elapsedMicroseconds, + 'updates': 500, + 'queue_changes': queueChanges, + 'membership_changes': membershipChanges, + 'selectors': 140, + }; + } finally { + for (final subscription in subscriptions) { + subscription.close(); + } + container.dispose(); + } +} + +void main() { + final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + final cases = >[]; + tearDownAll(() { + binding.reportData ??= {}; + binding.reportData!['queue_index_read'] = { + 'platform': Platform.operatingSystem, + 'completed_cases': cases.length, + 'note': + 'Five alternating pairs of 500 real notifier sparse progress updates with 40 queue item and 100 track-membership Riverpod selectors, matching production row selectors. Compare 63 distributed removals with an equivalent fully rebuilt index. CPU/provider propagation only, no rendering or FPS. iOS debug correctness only.', + 'cases': cases, + }; + }); + for (final count in [1000, 10000]) { + testWidgets('selectors and sparse progress after removals from $count', ( + tester, + ) async { + var retained = await queueRemovalFixture(count); + for (var i = 0; i < 63; i++) { + final index = (i * 137) % retained.items.length; + retained = retained.withoutItem(retained.items[index].id); + } + final rebuilt = retained.copyWith(items: retained.items); + expect(retained.lookup.byItemId, rebuilt.lookup.byItemId); + expect(retained.lookup.byTrackId, rebuilt.lookup.byTrackId); + _measure(rebuilt); + _measure(retained); + final samples = >>{ + 'rebuilt': [], + 'retained': [], + }; + for (var pair = 0; pair < 5; pair++) { + for (final useRetained in pair.isEven ? [false, true] : [true, false]) { + samples[useRetained ? 'retained' : 'rebuilt']!.add( + _measure(useRetained ? retained : rebuilt), + ); + } + } + cases.add({'original_count': count, 'samples': samples}); + }); + } +}