perf(downloads): stream concurrent native queue work

This commit is contained in:
zarzet
2026-08-26 18:37:56 +07:00
parent e149ee5358
commit 0e3fca9967
9 changed files with 840 additions and 44 deletions
@@ -660,6 +660,14 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
final postProcessingEnabled =
settings.useExtensionProviders &&
extensionState.extensions.any((e) => e.enabled && e.hasPostProcessing);
final selectedDownloadExtension = extensionState.extensions
.where(
(extension) =>
extension.enabled &&
extension.hasDownloadProvider &&
extension.id.toLowerCase() == item.service.toLowerCase(),
)
.firstOrNull;
final normalizedTrackNumber =
(track.trackNumber != null && track.trackNumber! > 0)
? track.trackNumber!
@@ -752,6 +760,11 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
: '',
qualityVariantCollisionOnly: qualityVariantCollisionOnly,
songLinkRegion: settings.songLinkRegion,
networkConcurrencyLimit:
selectedDownloadExtension
?.downloadTransferPolicy
.maxConcurrentDownloads ??
3,
);
}
@@ -181,12 +181,6 @@ extension _DownloadQueueNativeWorker on DownloadQueueNotifier {
if (!Platform.isAndroid || !settings.nativeDownloadWorkerEnabled) {
return false;
}
if (settings.concurrentDownloads > 1) {
// The native worker downloads strictly sequentially, so
// prefer the Dart queue when the user enabled concurrent downloads.
_log.i('Concurrent downloads enabled; skipping native worker');
return false;
}
if (!settings.useExtensionProviders) {
return false;
}
@@ -563,26 +557,36 @@ extension _DownloadQueueNativeWorker on DownloadQueueNotifier {
}
final contexts = <String, _NativeWorkerRequestContext>{};
final requests = <Map<String, dynamic>>[];
for (final item in queuedItems) {
final context = await _buildAndroidNativeWorkerRequest(item, settings);
if (context == null) {
_log.w(
'Native worker gate rejected ${item.track.name}; falling back to Dart queue',
);
return false;
}
contexts[item.id] = context;
requests.add({
Map<String, dynamic> encodeRequest(
DownloadItem item,
_NativeWorkerRequestContext context,
) {
return {
'contract_version': DownloadRequestPayload.nativeWorkerContractVersion,
'item_id': item.id,
'track_name': item.track.name,
'artist_name': item.track.artistName,
'item_json': jsonEncode(item.toJson()),
'request_json': context.requestJson,
});
};
}
// Only the first request blocks startup. The rest are prepared with a
// small metadata pipeline and appended to the already-running native
// worker, reducing time-to-first-byte for large albums/playlists.
final firstItem = queuedItems.first;
final firstContext = await _buildAndroidNativeWorkerRequest(
firstItem,
settings,
);
if (firstContext == null) {
_log.w(
'Native worker gate rejected ${firstItem.track.name}; falling back to Dart queue',
);
return false;
}
contexts[firstItem.id] = firstContext;
if (!canStartForegroundDownloadForLifecycle(
WidgetsBinding.instance.lifecycleState,
)) {
@@ -601,9 +605,10 @@ extension _DownloadQueueNativeWorker on DownloadQueueNotifier {
final runId = _newNativeWorkerRunId();
await _persistNativeWorkerRunId(runId);
final reconciledIds = <String>{};
Future<void>? preparationFuture;
try {
await PlatformBridge.startNativeDownloadWorker(
requests: requests,
requests: [encodeRequest(firstItem, firstContext)],
settings: {
'worker': 'android_native',
'version': 1,
@@ -613,9 +618,62 @@ extension _DownloadQueueNativeWorker on DownloadQueueNotifier {
'created_at': DateTime.now().toIso8601String(),
'save_download_history': settings.saveDownloadHistory,
'download_network_mode': settings.downloadNetworkMode,
'concurrent_downloads': settings.concurrentDownloads.clamp(1, 3),
'finalizer_concurrency': 1,
'preparation_streaming': true,
'expected_total_items': queuedItems.length,
},
);
preparationFuture = () async {
var nextIndex = 1;
final preparationConcurrency = min(2, queuedItems.length - 1);
try {
await Future.wait(
List.generate(preparationConcurrency, (_) async {
while (nextIndex < queuedItems.length) {
final index = nextIndex++;
final item = queuedItems[index];
try {
final context = await _buildAndroidNativeWorkerRequest(
item,
settings,
);
if (context == null) {
_log.w(
'Native worker gate rejected ${item.track.name}; leaving it queued for the Dart worker',
);
continue;
}
contexts[item.id] = context;
await PlatformBridge.appendNativeDownloadWorkerRequests(
runId: runId,
requests: [encodeRequest(item, context)],
);
} catch (e, stack) {
_log.e(
'Could not prepare native request for ${item.track.name}: $e',
e,
stack,
);
}
}
}),
);
} finally {
try {
await PlatformBridge.finishNativeDownloadWorkerPreparation(
runId: runId,
);
} catch (_) {
// Do not leave the foreground worker waiting forever on an open
// preparation channel if the final hand-off fails.
await PlatformBridge.cancelNativeDownloadWorker();
rethrow;
}
}
}();
final runStartWait = Stopwatch()..start();
var lastStateSerial = 0;
while (true) {
@@ -694,6 +752,13 @@ extension _DownloadQueueNativeWorker on DownloadQueueNotifier {
_failedInSession++;
}
} finally {
if (preparationFuture != null) {
try {
await preparationFuture;
} catch (e) {
_log.w('Native worker preparation pipeline stopped: $e');
}
}
state = state.copyWith(isProcessing: false, currentDownload: null);
_stopConnectivityMonitoring();
try {
+42
View File
@@ -50,6 +50,46 @@ List<String>? _tryDecodeStringListPreference(String rawJson, String key) {
}
}
class ExtensionDownloadTransferPolicy {
final int maxAttempts;
final String resumePolicy;
final bool persistentCheckpoint;
final int maxParallelSegments;
final int maxConcurrentDownloads;
const ExtensionDownloadTransferPolicy({
this.maxAttempts = 3,
this.resumePolicy = 'none',
this.persistentCheckpoint = false,
this.maxParallelSegments = 3,
this.maxConcurrentDownloads = 3,
});
factory ExtensionDownloadTransferPolicy.fromCapabilities(
Map<String, dynamic> capabilities,
) {
final raw = capabilities['downloadTransfer'];
if (raw is! Map) return const ExtensionDownloadTransferPolicy();
final values = Map<String, dynamic>.from(raw);
int boundedInt(String key, int fallback, int min, int max) {
final value = values[key];
final parsed = value is num ? value.round() : fallback;
return parsed.clamp(min, max).toInt();
}
final requestedResume = values['resumePolicy']?.toString().trim();
final resumePolicy = requestedResume == 'validated' ? 'validated' : 'none';
return ExtensionDownloadTransferPolicy(
maxAttempts: boundedInt('maxAttempts', 3, 1, 8),
resumePolicy: resumePolicy,
persistentCheckpoint:
resumePolicy == 'validated' && values['persistentCheckpoint'] == true,
maxParallelSegments: boundedInt('maxParallelSegments', 3, 1, 8),
maxConcurrentDownloads: boundedInt('maxConcurrentDownloads', 3, 1, 3),
);
}
}
/// First enabled custom-search extension, preferring ones marked primary.
Extension? defaultSearchExtension(List<Extension> extensions) {
return extensions
@@ -239,6 +279,8 @@ class Extension {
bool get hasPostProcessing => postProcessing?.enabled ?? false;
bool get hasServiceHealth => serviceHealth.isNotEmpty;
bool get hasHomeFeed => capabilities['homeFeed'] == true;
ExtensionDownloadTransferPolicy get downloadTransferPolicy =>
ExtensionDownloadTransferPolicy.fromCapabilities(capabilities);
bool get requiresNativeContainerConversion =>
capabilities['requiresContainerConversion'] == true ||
capabilities['requiresNativeContainerConversion'] == true;
@@ -60,6 +60,7 @@ class DownloadRequestPayload {
final String qualityVariant;
final bool qualityVariantCollisionOnly;
final String songLinkRegion;
final int networkConcurrencyLimit;
const DownloadRequestPayload({
this.contractVersion = nativeWorkerContractVersion,
@@ -121,6 +122,7 @@ class DownloadRequestPayload {
this.qualityVariant = '',
this.qualityVariantCollisionOnly = false,
this.songLinkRegion = 'US',
this.networkConcurrencyLimit = 3,
});
Map<String, dynamic> toJson() {
@@ -184,6 +186,7 @@ class DownloadRequestPayload {
'quality_variant': qualityVariant,
'quality_variant_collision_only': qualityVariantCollisionOnly,
'songlink_region': songLinkRegion,
'network_concurrency_limit': networkConcurrencyLimit,
};
}
@@ -251,6 +254,7 @@ class DownloadRequestPayload {
qualityVariant: qualityVariant,
qualityVariantCollisionOnly: qualityVariantCollisionOnly,
songLinkRegion: songLinkRegion,
networkConcurrencyLimit: networkConcurrencyLimit,
);
}
}
+28
View File
@@ -1192,6 +1192,34 @@ class PlatformBridge {
}
}
static Future<void> appendNativeDownloadWorkerRequests({
required String runId,
required List<Map<String, dynamic>> requests,
}) async {
if (requests.isEmpty) return;
final payloadDir = await _nativeWorkerPayloadDir();
final stamp = DateTime.now().microsecondsSinceEpoch;
final requestPath = '${payloadDir.path}/append_$stamp.json';
await File(requestPath).writeAsString(jsonEncode(requests), flush: true);
try {
await _channel.invokeMethod('appendNativeDownloadWorkerRequests', {
'run_id': runId,
'requests_path': requestPath,
});
} catch (_) {
unawaited(_deleteFileIfExists(requestPath));
rethrow;
}
}
static Future<void> finishNativeDownloadWorkerPreparation({
required String runId,
}) async {
await _channel.invokeMethod('finishNativeDownloadWorkerPreparation', {
'run_id': runId,
});
}
static Future<void> _deleteFileIfExists(String path) async {
try {
final file = File(path);