2

I have SSD storage and this code takes 32 seconds to move ~200 files and ~40 folders to the same storage in either debug or release mode. total size of folder is ~30 MB.

How can i make this faster?

// moves content from local folder to target folder.
async Task MoveContent(IStorageFolder source, IStorageFolder destination)
{
    foreach(var item in await source.GetItemsAsync())
    {
        switch (item)
        {
            case IStorageFile sourceFile:
                await sourceFile.MoveAsync(destination, sourceFile.Name, NameCollisionOption.ReplaceExisting);
                break;
            case IStorageFolder sourceSubFolder:
                var destinationSubFolder = await destination.CreateFolderAsync(sourceSubFolder.Name, CreationCollisionOption.ReplaceExisting);
                await MoveContent(sourceSubFolder, destinationSubFolder);
                break;
        }
    }
}

And I call it like this

await MoveContent(extractionFolder, targetFolder);

Note that extractionFolder is in ApplicationData.Current.LocalCacheFolder and targetFolder is any folder chosen by user via FolderPicker

Martin Zikmund
  • 38,440
  • 7
  • 70
  • 91
M.kazem Akhgary
  • 18,645
  • 8
  • 57
  • 118
  • You can't really improve the plain copying process itself but you could copy the directories parallel. – devsmn Mar 01 '19 at 10:35
  • In windows I can simply cut/paste folder and that takes 0 time, what a shame, UWP doesn't have facility to move folders? @Shawn – M.kazem Akhgary Mar 01 '19 at 10:37
  • AFAIK, File reading writing from UWP is supposed to be slow because of a lot of security check etc, what you ca do is, Using a Full Trust Process to launch a normal win32 exe and use that to handle file reading writing to avoid these security checks. – Muzib Mar 01 '19 at 10:39
  • when you run the code, is any of your devices running on 100%? – Dark Templar Mar 01 '19 at 15:27

3 Answers3

3

There are several issues with the code you posted:

  1. You fire off File I/O operations one by one, and wait for their completion. Since File I/O in UWP is brokered, that involves calling into another process. Since most of the time is spent communicating between processes, you get bottlenecked by your own waits. Your disk isn't active at all during that time.

  2. WinRT file I/O API is utter garbage performance wise. You want to avoid it as much as you can. Since you have proper access to the source path, you should use C# DirectoryInfo class to enumerate files. Then, instead of using MoveAsync (since you no longer have source as IStorageItem), use C# File I/O.

With these changes, it manages to complete my synthetic test case (40 folders, with 5 files in them each) takes 300 ms, compared to 12 seconds using your code. That is 30 times faster. This could get much faster if we were allowed to use Win32 APIs like MoveFile, but unfortunately there is no way to currently do it for folders and files picked by file/folder pickers.

Here's the code.

        async Task MoveContentFast(IStorageFolder source, IStorageFolder destination)
        {
            await Task.Run(() =>
            {
                MoveContextImpl(new DirectoryInfo(source.Path), destination);
            });
        }

        private void MoveContextImpl(DirectoryInfo sourceFolderInfo, IStorageFolder destination)
        {
            var tasks = new List<Task>();

            var destinationAccess = destination as IStorageFolderHandleAccess;

            foreach (var item in sourceFolderInfo.EnumerateFileSystemInfos())
            {
                if ((item.Attributes & System.IO.FileAttributes.Directory) != 0)
                {
                    tasks.Add(destination.CreateFolderAsync(item.Name, CreationCollisionOption.ReplaceExisting).AsTask().ContinueWith((destinationSubFolder) =>
                    {
                        MoveContextImpl((DirectoryInfo)item, destinationSubFolder.Result);
                    }));
                }
                else
                {
                    if (destinationAccess == null)
                    {
                        // Slower, pre 14393 OS build path
                        tasks.Add(WindowsRuntimeStorageExtensions.OpenStreamForWriteAsync(destination, item.Name, CreationCollisionOption.ReplaceExisting).ContinueWith((openTask) =>
                        {
                            using (var stream = openTask.Result)
                            {
                                var sourceBytes = File.ReadAllBytes(item.FullName);
                                stream.Write(sourceBytes, 0, sourceBytes.Length);
                            }

                            File.Delete(item.FullName);
                        }));
                    }
                    else
                    {
                        int hr = destinationAccess.Create(item.Name, HANDLE_CREATION_OPTIONS.CREATE_ALWAYS, HANDLE_ACCESS_OPTIONS.WRITE, HANDLE_SHARING_OPTIONS.SHARE_NONE, HANDLE_OPTIONS.NONE, IntPtr.Zero, out SafeFileHandle file);
                        if (hr < 0)
                            Marshal.ThrowExceptionForHR(hr);

                        using (file)
                        {
                            using (var stream = new FileStream(file, FileAccess.Write))
                            {
                                var sourceBytes = File.ReadAllBytes(item.FullName);
                                stream.Write(sourceBytes, 0, sourceBytes.Length);
                            }
                        }

                        File.Delete(item.FullName);
                    }
                }
            }

            Task.WaitAll(tasks.ToArray());
        }

        [ComImport]
        [Guid("DF19938F-5462-48A0-BE65-D2A3271A08D6")]
        [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
        internal interface IStorageFolderHandleAccess
        {
            [PreserveSig]
            int Create(
                [MarshalAs(UnmanagedType.LPWStr)] string fileName,
                HANDLE_CREATION_OPTIONS creationOptions,
                HANDLE_ACCESS_OPTIONS accessOptions,
                HANDLE_SHARING_OPTIONS sharingOptions,
                HANDLE_OPTIONS options,
                IntPtr oplockBreakingHandler,
                out SafeFileHandle interopHandle); // using Microsoft.Win32.SafeHandles
        }

        internal enum HANDLE_CREATION_OPTIONS : uint
        {
            CREATE_NEW = 0x1,
            CREATE_ALWAYS = 0x2,
            OPEN_EXISTING = 0x3,
            OPEN_ALWAYS = 0x4,
            TRUNCATE_EXISTING = 0x5,
        }

        [Flags]
        internal enum HANDLE_ACCESS_OPTIONS : uint
        {
            NONE = 0,
            READ_ATTRIBUTES = 0x80,
            // 0x120089
            READ = SYNCHRONIZE | READ_CONTROL | READ_ATTRIBUTES | FILE_READ_EA | FILE_READ_DATA,
            // 0x120116
            WRITE = SYNCHRONIZE | READ_CONTROL | FILE_WRITE_ATTRIBUTES | FILE_WRITE_EA | FILE_APPEND_DATA | FILE_WRITE_DATA,
            DELETE = 0x10000,

            READ_CONTROL = 0x00020000,
            SYNCHRONIZE = 0x00100000,
            FILE_READ_DATA = 0x00000001,
            FILE_WRITE_DATA = 0x00000002,
            FILE_APPEND_DATA = 0x00000004,
            FILE_READ_EA = 0x00000008,
            FILE_WRITE_EA = 0x00000010,
            FILE_EXECUTE = 0x00000020,
            FILE_WRITE_ATTRIBUTES = 0x00000100,
        }

        [Flags]
        internal enum HANDLE_SHARING_OPTIONS : uint
        {
            SHARE_NONE = 0,
            SHARE_READ = 0x1,
            SHARE_WRITE = 0x2,
            SHARE_DELETE = 0x4
        }

        [Flags]
        internal enum HANDLE_OPTIONS : uint
        {
            NONE = 0,
            OPEN_REQUIRING_OPLOCK = 0x40000,
            DELETE_ON_CLOSE = 0x4000000,
            SEQUENTIAL_SCAN = 0x8000000,
            RANDOM_ACCESS = 0x10000000,
            NO_BUFFERING = 0x20000000,
            OVERLAPPED = 0x40000000,
            WRITE_THROUGH = 0x80000000
        }
Sunius
  • 2,789
  • 18
  • 30
  • 1
    **hint hint** @Peter Torr - MSFT :) – Sunius Mar 03 '19 at 06:53
  • Unfortunately this doesn't play well with `broadFileSystemAccess`. I'll see if it can be re-worked to use the UWP FileIO APIs in some places you're using the .NET FileIO APIs. I hope so. The performance of the UWP copy/move APIs per-file is _terrible_.... – kayleeFrye_onDeck Aug 16 '19 at 03:40
  • It doesn't look like it. I made a lot of changes but as soon as I hit `var sourceBytes = File.ReadAllBytes(item.FullName);` I got stuck. I commented out the recursion because I was just trying to see if I could get the happy path working first. Here's my gist if you want to take it and cover that final-ish gap. I don't know much about interfacing with byte streams and I'm getting sleepy x_x lol. Cool stuff either way! :) I'm not even sure this can take advantage of your performance boosts due to all the extra async options, but I was willing to test it out =/ – kayleeFrye_onDeck Aug 16 '19 at 04:05
  • It might help if I actually left the url.. LOL https://gist.github.com/the-nose-knows/bc86f2a4e6baa53f96b716178c1dbc24 – kayleeFrye_onDeck Aug 16 '19 at 04:09
  • 1
    My code will only work if the destination is in brokered path, while the source isn't. If your source is in a path an app can't usually access, you'll need to use `IStorageFolderHandleAccess` interface to read it. – Sunius Aug 16 '19 at 18:32
  • I ended up just writing a dedicated class for all my app's fileIO, and made everything within the sandbox .NET FileIO, and everything externally UWP FileIO. Huge performance differences! Also, I switched from await by-file handling to task-batches sent to Task.WhenAll. Not amazingly faster, but still faster. – kayleeFrye_onDeck Aug 17 '19 at 02:25
1

To improve the performance of your code, you could try enumerating all files in the folder and subfolders at once instead of throughout the folder structure (folder by folder):

var results = storageFolder.CreateFileQueryWithOptions(
                  new QueryOptions() { FolderDepth = FolderDepth.Deep } );
var files = (await results.GetFilesAsync()).ToArray();

Where storageFolder is the folder you want to move. The custom file query has FolderDepth setting set to Deep so that it returns all files from the whole folder structure. After running this, files array will contain all the files and you can then move them. This will be at least a tad faster than enumerating all folders one by one. You just have to make sure to always check the appropriate subfolders are created in the target location.

Finally you could try to parallelize the move Tasks - for example moving three files at once. You can create multiple Task instances and await them all using Task.WhenAll.

Copy-paste solution

Another quick and dirty solution would be to use StorageFolder.CopyAsync() method to copy the folder to the new location and delete the original (this is even suggested in Docs):

There is not currently a "MoveAsync" or similar method. One simple implementation of moving a folder might be to get the desired folder, copy it to the desired location, and then delete the original folder.

However, the cost of additional storage space is not very appealing and may not even improve the performance, because copying is more costly than moving.

Martin Zikmund
  • 38,440
  • 7
  • 70
  • 91
0

UWP currently doesn't have anything like MoveAsync as of Aug. 2019. This answer achieves similar behavior to a MoveAsync function, and assumes you're working outside of your UWP App sandbox/Local State, because within the sandbox you can use classic much faster System.IO methods from .NET. Just use the latter inside your sandbox, otherwise you can use this ad-hoc:

public static async Task Move_Directory_Async(
    StorageFolder           sourceDir,
    StorageFolder           destParentDir,
    CreationCollisionOption repDirOpt,
    NameCollisionOption     repFilesOpt)
{
    try
    {
        if (sourceDir == null)
            return;

        List<Task> copies = new List<Task>();
        var files = await sourceDir.GetFilesAsync();
        if (files == null || files.Count == 0)
            await destParentDir.CreateFolderAsync(sourceDir.Name);
        else
        {
            await destParentDir.CreateFolderAsync(sourceDir.Name, repDirOpt);
            foreach (var file in files)
                copies.Add(file.CopyAsync(destParentDir, file.Name, repFilesOpt).AsTask());
        }

        await sourceDir.DeleteAsync(StorageDeleteOption.PermanentDelete);
        await Task.WhenAll(copies);
    }
    catch(Exception ex)
    {
        //Handle any needed cleanup tasks here
        throw new Exception(
          $"A fatal exception triggered within Move_Directory_Async:\r\n{ex.Message}", ex);
    }
}
kayleeFrye_onDeck
  • 6,648
  • 5
  • 69
  • 80