mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-10-19 14:07:26 +02:00
perf(network): prepare large SMB listings off the UI thread
This commit is contained in:
1 parent
0af197b82a
commit
105f1b0946
4 files changed
+246
-17
No files matched your search
@@ -0,0 +1,54 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:spotiflac_android/services/network_storage_service.dart';
|
||||
|
||||
import '../test/support/library_collections_benchmark.dart';
|
||||
import '../test/support/smb_listing_benchmark.dart';
|
||||
|
||||
void main() {
|
||||
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
final report = <String, dynamic>{
|
||||
'platform': Platform.operatingSystem,
|
||||
'note':
|
||||
'SMB result adapter/sort only, no live server or network throughput/FPS claim.',
|
||||
};
|
||||
var completed = 0;
|
||||
tearDownAll(() {
|
||||
report['completed_cases'] = completed;
|
||||
binding.reportData ??= {};
|
||||
binding.reportData!['smb_listing'] = report;
|
||||
});
|
||||
for (final count in [32, 256, 1024, 4096, 10000, 50000]) {
|
||||
testWidgets('prepare $count SMB entries', (tester) async {
|
||||
final input = await compute(smbListingFixture, count);
|
||||
final expected = smbListingRows(legacySmbListing(input));
|
||||
final samples = <String, List<Map<String, int>>>{
|
||||
for (final kind in ['legacy', 'current']) kind: [],
|
||||
};
|
||||
legacySmbListing(input);
|
||||
await NetworkStorageService.prepareSmbEntries(input.files, input.path);
|
||||
for (var pair = 0; pair < 5; pair++) {
|
||||
for (final kind
|
||||
in pair.isEven ? samples.keys : samples.keys.toList().reversed) {
|
||||
final result = await measureCollectionOperation<List<NetworkEntry>>(
|
||||
() => switch (kind) {
|
||||
'legacy' => legacySmbListing(input),
|
||||
_ => NetworkStorageService.prepareSmbEntries(
|
||||
input.files,
|
||||
input.path,
|
||||
),
|
||||
},
|
||||
);
|
||||
expect(smbListingRows(result.value), expected);
|
||||
expect(input.files.length, count);
|
||||
samples[kind]!.add(result.timing);
|
||||
}
|
||||
}
|
||||
report['entries_$count'] = samples;
|
||||
completed++;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:dart_smb2/dart_smb2.dart' show Smb2DirEntry;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@@ -16,6 +17,23 @@ import 'package:xml/xml.dart';
|
||||
|
||||
part 'network_storage_upload.dart';
|
||||
|
||||
typedef _SmbListingInput = ({
|
||||
List<({String name, bool directory, int size})> files,
|
||||
String path,
|
||||
});
|
||||
|
||||
List<NetworkEntry> _prepareSmbListingInBackground(_SmbListingInput input) =>
|
||||
NetworkStorageService._sorted([
|
||||
for (final file in input.files)
|
||||
if (file.name != '.' && file.name != '..')
|
||||
NetworkEntry(
|
||||
'${input.path}${file.name}${file.directory ? '/' : ''}',
|
||||
file.name,
|
||||
directory: file.directory,
|
||||
size: file.size,
|
||||
),
|
||||
]);
|
||||
|
||||
List<NetworkEntry> _parseNetworkListingInBackground(
|
||||
Map<String, dynamic> request,
|
||||
) {
|
||||
@@ -447,24 +465,16 @@ class NetworkStorageService {
|
||||
]);
|
||||
}
|
||||
final smb = await _smb(c, target);
|
||||
final List<Smb2DirEntry> files;
|
||||
try {
|
||||
final files = await smb.list().timeout(timeout);
|
||||
return _sorted([
|
||||
for (final file in files)
|
||||
if (file.name != '.' && file.name != '..')
|
||||
NetworkEntry(
|
||||
'$path${file.name}${file.stat.isDirectory ? '/' : ''}',
|
||||
file.name,
|
||||
directory: file.stat.isDirectory,
|
||||
size: file.stat.size,
|
||||
),
|
||||
]);
|
||||
files = await smb.list().timeout(timeout);
|
||||
} finally {
|
||||
// A failed disconnect must not hide the original share/login error.
|
||||
try {
|
||||
await smb.close();
|
||||
} catch (_) {}
|
||||
}
|
||||
return prepareSmbEntries(files, path);
|
||||
}
|
||||
final client = _client();
|
||||
try {
|
||||
@@ -506,12 +516,43 @@ class NetworkStorageService {
|
||||
}
|
||||
}
|
||||
|
||||
static List<NetworkEntry> _sorted(List<NetworkEntry> entries) =>
|
||||
entries..sort(
|
||||
(a, b) => a.directory != b.directory
|
||||
? (a.directory ? -1 : 1)
|
||||
: a.name.toLowerCase().compareTo(b.name.toLowerCase()),
|
||||
);
|
||||
/// Large SMB listings arrive from native I/O as plain stat objects. Transfer
|
||||
/// only fields used by the browser, avoiding copies of unused timestamps.
|
||||
static Future<List<NetworkEntry>> prepareSmbEntries(
|
||||
List<Smb2DirEntry> files,
|
||||
String path,
|
||||
) async {
|
||||
final input = (
|
||||
path: path,
|
||||
files: [
|
||||
for (final file in files)
|
||||
(
|
||||
name: file.name,
|
||||
directory: file.stat.isDirectory,
|
||||
size: file.stat.size,
|
||||
),
|
||||
],
|
||||
);
|
||||
// Small lists are cheaper inline; large sorts must leave the UI isolate.
|
||||
if (files.length < 4096) return _prepareSmbListingInBackground(input);
|
||||
return compute(_prepareSmbListingInBackground, input);
|
||||
}
|
||||
|
||||
static List<NetworkEntry> _sorted(List<NetworkEntry> entries) {
|
||||
final keyed =
|
||||
[
|
||||
for (final entry in entries)
|
||||
(entry: entry, key: entry.name.toLowerCase()),
|
||||
]..sort(
|
||||
(a, b) => a.entry.directory != b.entry.directory
|
||||
? (a.entry.directory ? -1 : 1)
|
||||
: a.key.compareTo(b.key),
|
||||
);
|
||||
for (var index = 0; index < entries.length; index++) {
|
||||
entries[index] = keyed[index].entry;
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
static List<NetworkEntry> parseListing(
|
||||
NetworkConnection c,
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import 'package:dart_smb2/dart_smb2.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:spotiflac_android/services/network_storage_service.dart';
|
||||
|
||||
import 'support/smb_listing_benchmark.dart';
|
||||
|
||||
void main() {
|
||||
for (final count in [0, 1, 32, 256, 4095, 4096, 10000]) {
|
||||
test(
|
||||
'SMB adapter retains original order and values for $count entries',
|
||||
() async {
|
||||
final input = smbListingFixture(count);
|
||||
final original = [...input.files];
|
||||
final expected = smbListingRows(legacySmbListing(input));
|
||||
final result = await NetworkStorageService.prepareSmbEntries(
|
||||
input.files,
|
||||
input.path,
|
||||
);
|
||||
expect(smbListingRows(result), expected);
|
||||
expect(input.files, original);
|
||||
expect(
|
||||
result.any((entry) => entry.name == '.' || entry.name == '..'),
|
||||
isFalse,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
test(
|
||||
'case ties, links, literal paths and large sizes retain their semantics',
|
||||
() async {
|
||||
final date = DateTime.utc(2026);
|
||||
final input = (
|
||||
path: '100% + #1/日本語/',
|
||||
files: [
|
||||
for (final (name, type, size) in [
|
||||
('Same', Smb2FileType.file, 9007199254740991),
|
||||
('SAME', Smb2FileType.directory, 1),
|
||||
('same', Smb2FileType.file, 2),
|
||||
('same', Smb2FileType.link, 3),
|
||||
('İ.dot', Smb2FileType.file, 4),
|
||||
('i.dot', Smb2FileType.file, 5),
|
||||
('literal%20 #+.mp3', Smb2FileType.file, 6),
|
||||
('..', Smb2FileType.directory, 0),
|
||||
('.', Smb2FileType.directory, 0),
|
||||
])
|
||||
Smb2DirEntry(
|
||||
name: name,
|
||||
stat: Smb2Stat(
|
||||
type: type,
|
||||
size: size,
|
||||
modified: date,
|
||||
created: date,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
final result = await NetworkStorageService.prepareSmbEntries(
|
||||
input.files,
|
||||
input.path,
|
||||
);
|
||||
expect(smbListingRows(result), smbListingRows(legacySmbListing(input)));
|
||||
expect(result.first.path, '100% + #1/日本語/SAME/');
|
||||
expect(result.where((entry) => entry.directory), hasLength(1));
|
||||
expect(result.singleWhere((entry) => entry.size == 3).directory, isFalse);
|
||||
expect(result.any((entry) => entry.size == 9007199254740991), isTrue);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:dart_smb2/dart_smb2.dart';
|
||||
import 'package:spotiflac_android/services/network_storage_service.dart';
|
||||
|
||||
typedef SmbListingInput = ({List<Smb2DirEntry> files, String path});
|
||||
|
||||
// Frozen adapter and comparator from NetworkStorageService.list/_sorted.
|
||||
List<NetworkEntry> legacySmbListing(SmbListingInput input) =>
|
||||
[
|
||||
for (final file in input.files)
|
||||
if (file.name != '.' && file.name != '..')
|
||||
NetworkEntry(
|
||||
'${input.path}${file.name}${file.stat.isDirectory ? '/' : ''}',
|
||||
file.name,
|
||||
directory: file.stat.isDirectory,
|
||||
size: file.stat.size,
|
||||
),
|
||||
]..sort(
|
||||
(a, b) => a.directory != b.directory
|
||||
? (a.directory ? -1 : 1)
|
||||
: a.name.toLowerCase().compareTo(b.name.toLowerCase()),
|
||||
);
|
||||
|
||||
SmbListingInput smbListingFixture(int count) {
|
||||
const names = [
|
||||
'Track',
|
||||
'track',
|
||||
'Ångström',
|
||||
'İstanbul',
|
||||
'音楽 🎵',
|
||||
'Κόσμος',
|
||||
'ß',
|
||||
];
|
||||
final random = Random(8127);
|
||||
final files = List.generate(count, (index) {
|
||||
final directory = index % 7 == 0;
|
||||
return Smb2DirEntry(
|
||||
name: switch (index) {
|
||||
0 => '.',
|
||||
1 => '..',
|
||||
_ =>
|
||||
'${names[index % names.length]} — ${index % 400 == 3 ? 'SAME' : index.toString()} Album with spaces #1${directory ? '' : '.flac'}',
|
||||
},
|
||||
stat: Smb2Stat(
|
||||
type: directory ? Smb2FileType.directory : Smb2FileType.file,
|
||||
size: 1000000000 + index,
|
||||
modified: DateTime.fromMillisecondsSinceEpoch(
|
||||
1700000000000 + index,
|
||||
isUtc: true,
|
||||
),
|
||||
created: DateTime.fromMillisecondsSinceEpoch(
|
||||
1600000000000 + index,
|
||||
isUtc: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
})..shuffle(random);
|
||||
return (files: files, path: 'Folder #1/');
|
||||
}
|
||||
|
||||
List<Object?> smbListingRows(List<NetworkEntry> entries) => [
|
||||
for (final entry in entries)
|
||||
[entry.path, entry.name, entry.directory, entry.size],
|
||||
];
|
||||
Reference in new issue
Block a user