2
\$\begingroup\$

Currently, I have a code which downloads a file from FTP to local hard disk. It then uploads a file in chunks to Azure. Finally, it deletes the file from local and ftp. This code is very slow though. Just wanted to know how to improve it.

    private async Task UploadToBlobJobAsync(FtpConfiguration ftpConfiguration, BlobStorageConfiguration blobStorageConfiguration, string fileExtension)
    {
        try
        {
               ftpConfiguration.FileExtension = fileExtension;

                var filesToProcess = FileHelper.GetAllFileNames(ftpConfiguration).ToList();
                
                var batchSize = 4;
                List<Task> uploadBlobToStorageTasks = new List<Task>(batchSize);

                for (int i = 0; i < filesToProcess.Count(); i += batchSize)
                {
                    // calculated the remaining items to avoid an OutOfRangeException
                    batchSize = filesToProcess.Count() - i > batchSize ? batchSize : filesToProcess.Count() - i;

                    for (int j = i; j < i + batchSize; j++)
                    {
                        var fileName = filesToProcess[j];
                        var localFilePath = SaveFileToLocalAndGetLocation(ftpConfiguration, ftpConfiguration.FolderPath, fileName);

                        // Spin off a background task to process the file we just downloaded
                        uploadBlobToStorageTasks.Add(Task.Run(() =>
                        {
                            // Process the file
                            UploadFile(ftpConfiguration, blobStorageConfiguration, fileName, localFilePath).ConfigureAwait(false);
                        }));
                    }

                    Task.WaitAll(uploadBlobToStorageTasks.ToArray());
                    uploadBlobToStorageTasks.Clear();
                }
        }
        catch (Exception ex)
        {
        }
    }

    private async Task UploadFile(FtpConfiguration ftpConfiguration, BlobStorageConfiguration blobStorageConfiguration, string fileName, string localFilePath)
    {
        try
        {
            await UploadLargeFiles(GetBlobStorageConfiguration(blobStorageConfiguration), fileName, localFilePath).ConfigureAwait(false);
    FileHelper.DeleteFile(ftpConfiguration, fileName); // delete file from ftp
        }
        catch (Exception exception)
        {
        }
    }

   private async Task UploadLargeFiles(BlobStorageConfiguration blobStorageConfiguration, string fileName, string localFilePath)
    {
        try
        {
            var output = await UploadFileAsBlockBlob(localFilePath, blobStorageConfiguration).ConfigureAwait(false);

            // delete the file from local
            Logger.LogInformation($"Deleting {fileName} from the local folder. Path is {localFilePath}.");

            if (File.Exists(localFilePath))
            {
                File.Delete(localFilePath);
            }
        }
        catch (Exception ex)
        {
        }
    }

    private async Task UploadFileAsBlockBlob(string sourceFilePath, BlobStorageConfiguration blobStorageConfiguration)
    {
        string fileName = Path.GetFileName(sourceFilePath);
        try
        {
            var storageAccount = CloudStorageAccount.Parse(blobStorageConfiguration.ConnectionString);
            var blobClient = storageAccount.CreateCloudBlobClient();
            var cloudContainer = blobClient.GetContainerReference(blobStorageConfiguration.Container);
            await cloudContainer.CreateIfNotExistsAsync().ConfigureAwait(false);

            var directory = cloudContainer.GetDirectoryReference(blobStorageConfiguration.Path);
            var blob = directory.GetBlockBlobReference(fileName);

            var blocklist = new HashSet<string>();

            byte[] bytes = File.ReadAllBytes(sourceFilePath);

            const long pageSizeInBytes = 10485760 * 20; // 20mb at a time
            long prevLastByte = 0;
            long bytesRemain = bytes.Length;

            do
            {
                long bytesToCopy = Math.Min(bytesRemain, pageSizeInBytes);
                byte[] bytesToSend = new byte[bytesToCopy];

                Array.Copy(bytes, prevLastByte, bytesToSend, 0, bytesToCopy);

                prevLastByte += bytesToCopy;
                bytesRemain -= bytesToCopy;

                // create blockId
                string blockId = Guid.NewGuid().ToString();
                string base64BlockId = Convert.ToBase64String(Encoding.UTF8.GetBytes(blockId));

                await blob.PutBlockAsync(base64BlockId, new MemoryStream(bytesToSend, true), null).ConfigureAwait(false);

                blocklist.Add(base64BlockId);
            }
            while (bytesRemain > 0);

            // post blocklist
            await blob.PutBlockListAsync(blocklist).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
        }
    }
\$\endgroup\$
4
  • \$\begingroup\$ const long pageSizeInBytes = 10485760 * 20; // 20mb at a time That's 200 MB, not 20. Is your calculation wrong or your comment? \$\endgroup\$
    – RobH
    Commented Aug 19, 2020 at 15:00
  • 1
    \$\begingroup\$ Why do you need to save the file locally first? Can you show the SaveFileToLocalAndGetLocation code? I would assume you would be getting a stream from the FTP and use that stream for the blob stream. \$\endgroup\$ Commented Aug 19, 2020 at 15:46
  • \$\begingroup\$ Actually I tried reading filestream but its quite slow \$\endgroup\$
    – Gauravsa
    Commented Aug 19, 2020 at 18:26
  • \$\begingroup\$ @Gauravsa Can please elaborate on this statement: This code is very slow though? How did you measure it? Which part of it is slow? (download / local file save / upload)? Did you tried with different files and workloads? etc. \$\endgroup\$ Commented Aug 25, 2020 at 14:45

1 Answer 1

1
\$\begingroup\$

First, don't write to disk anything you don't need to. It's not entirely clear what your goal is here, but I fail to see why you would have such a need in the first place.

That said, if you take your send function in a vacuum, what it does right now is:

  1. Read your whole (as you say) large file in memory

  2. For each chunk, you allocate a whole new array, copy the chunk over, put a MemoryStream on top of it and then send it over.

That is not how streaming is done.

Instead, you should open a file stream without reading anything, then loop over it for as many chunks as you need and read each chunk individually in one pre-allocated buffer (don't keep allocating new byte arrays), get the base64 representation if you really need it, and send the chunk, then keep looping. Your garbage collector will thank you.

\$\endgroup\$
2
  • \$\begingroup\$ reading 1 gb file in memory is slow. I am trying now to save the file to local disk. I will try ur suggestion and will let u know \$\endgroup\$
    – Gauravsa
    Commented Aug 20, 2020 at 7:31
  • \$\begingroup\$ You're doing the same reading, it's just that the generational GC that .Net uses works very poorly (by design) with very large buffers that you recreate over and over. It's the GC specifically you're fighting with the old implementation. \$\endgroup\$
    – Blindy
    Commented Aug 20, 2020 at 14:27

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.