mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-10-19 22:17:29 +02:00
135 lines
4.6 KiB
Dart
135 lines
4.6 KiB
Dart
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<String, int> _measure(DownloadQueueState initial) {
|
|
final container = ProviderContainer(
|
|
overrides: [downloadQueueProvider.overrideWith(() => _Queue(initial))],
|
|
);
|
|
var queueChanges = 0;
|
|
var membershipChanges = 0;
|
|
final selected = <String, DownloadItem?>{};
|
|
final subscriptions = <ProviderSubscription<Object?>>[];
|
|
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<DownloadItem?>(
|
|
downloadQueueLookupProvider.select((lookup) => lookup.byItemId[id]),
|
|
(_, next) {
|
|
queueChanges++;
|
|
selected[id] = next;
|
|
},
|
|
),
|
|
);
|
|
}
|
|
for (final id in trackIds) {
|
|
subscriptions.add(
|
|
container.listen<bool>(
|
|
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 = <Map<String, dynamic>>[];
|
|
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 = <String, List<Map<String, int>>>{
|
|
'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});
|
|
});
|
|
}
|
|
}
|