diff --git a/lib/blocs/upload/upload_cubit.dart b/lib/blocs/upload/upload_cubit.dart index 0c75dea7c..01c4a733c 100644 --- a/lib/blocs/upload/upload_cubit.dart +++ b/lib/blocs/upload/upload_cubit.dart @@ -104,6 +104,10 @@ class UploadCubit extends Cubit { bool _hasEmittedWarning = false; bool _uploadIsInProgress = false; + /// Pre-computed file lengths cache. Populated in parallel at the start of + /// upload preparation to avoid redundant sequential file.ioFile.length reads. + Map _fileLengthCache = {}; + /// Target folder late Drive _targetDrive; late FolderEntry _targetFolder; @@ -566,7 +570,8 @@ class UploadCubit extends Cubit { int size = 0; for (final file in _files) { - size += await file.ioFile.length; + size += _fileLengthCache[file.getIdentifier()] ?? + await file.ioFile.length; } return size; @@ -738,22 +743,27 @@ class UploadCubit extends Cubit { _removeFilesWithFolderNameConflicts(); - for (final file in _files) { - final fileName = file.ioFile.name; - final existingFileIds = await _driveDao - .filesInFolderWithName( - driveId: _targetDrive.id, - parentFolderId: file.parentFolderId, - name: fileName, - ) - .map((f) => f.id) - .get(); - - if (existingFileIds.isNotEmpty) { - final existingFileId = existingFileIds.first; + // Parallel conflict detection: check all files concurrently instead of + // one sequential DB query per file. + final conflictResults = await Future.wait( + _files.map((file) async { + final existingFileIds = await _driveDao + .filesInFolderWithName( + driveId: _targetDrive.id, + parentFolderId: file.parentFolderId, + name: file.ioFile.name, + ) + .map((f) => f.id) + .get(); + return (file: file, existingIds: existingFileIds); + }), + ); + for (final result in conflictResults) { + if (result.existingIds.isNotEmpty) { + final existingFileId = result.existingIds.first; logger.d('Found conflicting file. Existing file id: $existingFileId'); - _conflictingFiles[file.getIdentifier()] = existingFileId; + _conflictingFiles[result.file.getIdentifier()] = existingFileId; } } @@ -826,9 +836,9 @@ class UploadCubit extends Cubit { Future verifyFilesAboveWarningLimit() async { emit(UploadPreparationInProgress()); - /// This delay is necessary. Once we start the upload checks, we will perform high computational tasks. - /// This delay ensures the previous state (UploadPreparationInProgress) is updated before starting the upload checks. - await Future.delayed(const Duration(milliseconds: 100)); + // Yield to allow the UI to render UploadPreparationInProgress before + // starting potentially heavy computation. + await Future.delayed(Duration.zero); if (!_targetDrive.isPrivate) { if (await _uploadFileSizeChecker.hasFileAboveWarningSizeLimit( @@ -943,6 +953,16 @@ class UploadCubit extends Cubit { _targetFolder = await _driveDao.folderById(folderId: _parentFolderId).getSingle(); + // Pre-compute all file lengths in parallel to avoid redundant sequential + // reads across warning check, conflict detection, and plan creation. + final lengths = await Future.wait( + _files.map((f) async => await f.ioFile.length), + ); + _fileLengthCache = { + for (var i = 0; i < _files.length; i++) + _files[i].getIdentifier(): lengths[i], + }; + // TODO: check if the backend refreshed the balance instead of a timer if (isRetryingToPayWithTurbo) { emit(UploadPreparationInProgress()); @@ -1046,11 +1066,6 @@ class UploadCubit extends Cubit { ); try { - if (await _profileCubit.checkIfWalletMismatch()) { - emit(UploadWalletMismatch()); - return; - } - final containsSupportedImageTypeForThumbnailGeneration = _files.any( (element) => supportedImageTypesInFilePreview.contains( element.ioFile.contentType, @@ -1087,9 +1102,9 @@ class UploadCubit extends Cubit { _uploadThumbnail = false; } - if (manifestFileEntries.isNotEmpty) { + if (manifestFileEntries.isNotEmpty && _ants.isEmpty) { try { - await _arnsRepository + _ants = await _arnsRepository .getAntRecordsForWallet(_auth.currentUser.walletAddress); } catch (e) { logger.e( diff --git a/lib/core/upload/uploader.dart b/lib/core/upload/uploader.dart index 73ad96e63..75ab4d952 100644 --- a/lib/core/upload/uploader.dart +++ b/lib/core/upload/uploader.dart @@ -287,19 +287,15 @@ class UploadPreparer { }) : _uploadPlanUtils = uploadPlanUtils; Future prepareFileUpload(UploadParams params) async { - final uploadPlanForAR = await _mountUploadPlan( - params: params, - method: UploadMethod.ar, - ); - - final uploadPlanForTurbo = await _mountUploadPlan( - params: params, - method: UploadMethod.turbo, - ); + // Create both upload plans in parallel — they're independent. + final plans = await Future.wait([ + _mountUploadPlan(params: params, method: UploadMethod.ar), + _mountUploadPlan(params: params, method: UploadMethod.turbo), + ]); return UploadPlansPreparation( - uploadPlanForAr: uploadPlanForAR, - uploadPlanForTurbo: uploadPlanForTurbo, + uploadPlanForAr: plans[0], + uploadPlanForTurbo: plans[1], ); } @@ -442,14 +438,17 @@ class UploadPaymentEvaluator { // rejection. final freeAllowanceFuture = getFreeAllowance(); - /// Check the balance of the user - /// If we can't get the balance, turbo won't be available - turboBalance = await _getTurboBalance(canUseTurbo: _canUseTurbo); - - final arBundleSizes = await sizeUtils - .getSizeOfAllBundles(uploadPlanForAR.bundleUploadHandles); - final arFileSizes = await sizeUtils - .getSizeOfAllV2Files(uploadPlanForAR.fileV2UploadHandles); + // Fetch turbo balance and compute AR sizes in parallel — these are + // independent operations (1 network call + 2 local computations). If the + // balance call fails turbo is marked unavailable inside _getTurboBalance. + final parallelResults = await Future.wait([ + _getTurboBalance(canUseTurbo: _canUseTurbo), + sizeUtils.getSizeOfAllBundles(uploadPlanForAR.bundleUploadHandles), + sizeUtils.getSizeOfAllV2Files(uploadPlanForAR.fileV2UploadHandles), + ]); + turboBalance = parallelResults[0] as TurboBalanceInterface; + final arBundleSizes = parallelResults[1] as int; + final arFileSizes = parallelResults[2] as int; bool isUploadEligibleToTurbo = uploadPlanForTurbo.fileV2UploadHandles.isEmpty && @@ -459,31 +458,40 @@ class UploadPaymentEvaluator { int turboBundleSizes = 0; - /// Calculate the upload with Turbo if possible - if (isUploadEligibleToTurbo) { - turboBundleSizes = await sizeUtils - .getSizeOfAllBundles(uploadPlanForTurbo.bundleUploadHandles); - + // Calculate AR and Turbo costs in parallel — they're independent. + final arCostFuture = () async { try { - turboCostEstimate = await _turboUploadCostCalculator.calculateCost( - totalSize: turboBundleSizes, + return await _uploadCostEstimateCalculatorForAR.calculateCost( + totalSize: arBundleSizes + arFileSizes, ); } catch (e) { - _isTurboAvailableToUploadAllFiles = false; + logger.e('Failed to get AR cost estimate, falling back to zero', e); + return UploadCostEstimate.zero(); } + }(); + + Future? turboCostFuture; + if (isUploadEligibleToTurbo) { + turboBundleSizes = await sizeUtils + .getSizeOfAllBundles(uploadPlanForTurbo.bundleUploadHandles); + turboCostFuture = () async { + try { + return await _turboUploadCostCalculator.calculateCost( + totalSize: turboBundleSizes, + ); + } catch (e) { + _isTurboAvailableToUploadAllFiles = false; + return UploadCostEstimate.zero(); + } + }(); } - /// Calculate the upload cost with AR (D2N). If the gateway is - /// unavailable, fall back to a zero estimate so the modal can still - /// show Turbo as an option instead of crashing entirely. - UploadCostEstimate arCostEstimate; - try { - arCostEstimate = await _uploadCostEstimateCalculatorForAR.calculateCost( - totalSize: arBundleSizes + arFileSizes, - ); - } catch (e) { - logger.e('Failed to get AR cost estimate, falling back to zero', e); - arCostEstimate = UploadCostEstimate.zero(); + // Await both cost calculations (they've been running in parallel). The AR + // future already falls back to a zero estimate on gateway failure so the + // modal can still offer Turbo instead of crashing. + UploadCostEstimate arCostEstimate = await arCostFuture; + if (turboCostFuture != null) { + turboCostEstimate = await turboCostFuture; } final freeAllowance = await freeAllowanceFuture;