1

I am trying to calculate folder size but the problem is; it is working fast for D:\ drive or another folders, but whenever I try to click on C:\ drive, app is freezing for a while approximately 7-8 seconds. (My drive list are on treeview) When I remove folder size, everything is ok. Do you guys have any idea about this?

   public FolderModel(string folderPath)
    {
        try
        {

            //File = new FileInfo(folderPath);
            //FolderInfo = new DirectoryInfo(folderPath);
            //_createdTime = FolderInfo.CreationTime.ToShortDateString();
            //_folderName = FolderInfo.Name;
            //_folderPath = folderPath;
            //Fileextension = File.Extension.ToLower();
            //this.Children = new ObservableCollection<FolderModel>();

            _folderSize = CalculatorSize(GetDirectorySize(folderPath));
           
        }
        catch (Exception e)
        {
            //
        }
    }




    internal string CalculatorSize(long bytes)
    {
        var suffix = new[] { "B", "KB", "MB", "GB", "TB" };
        float byteNumber = bytes;
        for (var i = 0; i < suffix.Length; i++)
        {
            if (byteNumber < 1000)
            {
                if (i == 0)
                    return $"{byteNumber} {suffix[i]}";
                else
                    return $"{byteNumber:0.#0} {suffix[i]}";
            }
            else
            {
                byteNumber /= 1024;
            }
        }
        return $"{byteNumber:N} {suffix[suffix.Length - 1]}";
    }



    internal static long GetDirectorySize(string directoryPath)
    {
        try
        {
            if (Directory.Exists(directoryPath))
            {
                var d = new DirectoryInfo(directoryPath);
                return d.EnumerateFiles("*", SearchOption.AllDirectories).Sum(fi => fi.Length);
            }

            return new FileInfo(directoryPath).Length;
        }
        catch (UnauthorizedAccessException)
        {
            return 0;
        }
        catch (FileNotFoundException)
        {
            return 0;
        }
        catch (DirectoryNotFoundException)
        {
            return 0;
        }
    }
BionicCode
  • 1
  • 4
  • 28
  • 44
  • 2
    Make use of asynchronous programming. Which will then make the UI responsive even though you are still busy doing calculations. For reference check this [link](https://mithunvp.com/building-responsive-ui-using-async-await-csharp/) – Harish Nov 30 '21 at 11:35
  • Are you only interested in drives? Which .NET version are you using? – BionicCode Nov 30 '21 at 12:12
  • @BionicCode, I am using .Net Framework 4.7.2 No, not only drives. I have treeview which contains desktop, documents, folders, directories, drives... But it freezes only when I expand C:\ drive. after 7-8 seconds, it expandes and again when I expand childerens, it freezes again. –  Nov 30 '21 at 12:46
  • @Harish Thanks, I tried but result is the same. Or maybe I couldn't do it. –  Nov 30 '21 at 12:47

1 Answers1

2

You must enumerate the folder on a background thread.

Suggestions to improve performance
When using the DriveInfo API you can further improve the performance for the case that the folder path is a drive. In this case, you can omit the enumeration of the complete drive, which usually takes a while.
Furthermore, your current implementation aborts the calculation when the enumeration throws the UnauthorizedAccessException exception. You don't want that. You want the algorithm to ignore forbidden filesystem paths.

The following two examples show a fixed and improved version of your implementation.
The first solution targets the modern .NET Standard 2.1 compliant .NET versions.
The second solution targets the old .NET Framework.

.NET Standard 2.1 (.NET Core 3.0, .NET 5)

When using a .NET version compatible with .NET Standard 2.1 like .NET Core 3.0 and .NET 5 you can eliminate the exception handling. Using EnumerationOptions as an argument allows the API to ignore inaccessible directories, which significantly improves performance (no more UnauthorizedAccessException exceptions) and readability:

internal static async Task<bool> TryGetDirectorySize(string directoryPath, out long spaceUsedInBytes)
{
  spaceUsedInBytes = -1;
  var drives = DriveInfo.GetDrives();
  DriveInfo targetDrive = drives.FirstOrDefault(drive => drive.Name.Equals(directoryPath, StringComparison.OrdinalIgnoreCase));

  // Directory is a drive: skip the expensive enumeration of complete drive.
  if (targetDrive != null)
  {
    spaceUsedInBytes = targetDrive.TotalSize - targetDrive.TotalFreeSpace;
    return true;
  }

  if (!Directory.Exists(folderPath))
  {
    return false;
  }

  // Consider to make this local variable a private property
  var enumerationOptions = new EnumerationOptions { RecurseSubdirectories = true };

  var targetFolderInfo = new DirectoryInfo(directoryPath);
  spaceUsedInBytes = await Task.Run(
    () => targetFolderInfo.EnumerateFiles("*", enumerationOptions)
      .Sum(fileInfo => fileInfo.Length));

  return true;
}

.NET Framework

A .NET Framework compliant version. It fixes the issue with your original code where the enumeration is aborted as soon as an UnauthorizedAccessException exception is thrown. This version continues to enumerate all remaining directories using recursion:

internal static async Task<long> GetDirectorySize(string directoryPath)
{
  long spaceUsedInBytes = -1;
  var drives = DriveInfo.GetDrives();
  DriveInfo targetDrive =  drives.FirstOrDefault(drive => drive.Name.Equals(directoryPath, StringComparison.OrdinalIgnoreCase));

  // Directory is a drive: skip enumeration of complete drive.
  if (targetDrive != null)
  {
    spaceUsedInBytes = targetDrive.TotalSize - targetDrive.TotalFreeSpace;
    return spaceUsedInBytes;
  }

  var targetDirectoryInfo = new DirectoryInfo(directoryPath);
  spaceUsedInBytes = await Task.Run(() => SumDirectorySize(targetDirectoryInfo));
  return spaceUsedInBytes;
}

private static long SumDirectorySize(DirectoryInfo parentDirectoryInfo)
{
  long spaceUsedInBytes = 0;
  try
  {
    spaceUsedInBytes = parentDirectoryInfo.EnumerateFiles("*", SearchOption.TopDirectoryOnly)
      .Sum(fileInfo => fileInfo.Length);
  }
  catch (UnauthorizedAccessException)
  {
    return 0;
  }

  foreach (var subdirectoryInfo in parentDirectoryInfo.EnumerateDirectories("*", SearchOption.TopDirectoryOnly))
  {
    spaceUsedInBytes += SumDirectorySize(subdirectoryInfo);
  }

  return spaceUsedInBytes;
}

How to instantiate a type that requires to run async operations on construction

FolderModel.cs

class FolderModel
{
  // Make a constructor private to force instantiation using the factory method
  private FolderModel(string folderPath)
  {
    // Do non-async initialization
  }

  // Async factory method: add constructor parameters to async factory method
  public static async Task<FolderModel> CreateAsync(string folderPath)
  {
    var instance = new FolderModel(folderPath);
    await instance.InitializeAsync(folderPath);
    return instance;
  }

  // Define member as protected virtual to allow derived classes to add initialization routines
  protected virtual async Task InitializeAsync(string directoryPath)
  {
    // Consider to throw an exception here ONLY in case the folder is generated programmatically.
    // If folder is retrieved from user input, use input validation 
    // or even better use a folder picker dialog
    // to ensure that the provided path is always valid!
    if (!Directory.Exists(directoryPath))
    {
      throw new DirectoryNotFoundException($"Invalid directory path '{directoryPath}'.");
    }

    long folderSize = await GetDirectorySize(directoryPath);

    // TODO::Do something with the 'folderSize' value 
    // and execute other async code if necessary
  }
}

Usage

// Create an instance of FolderModel example
private async Task SomeMethod()
{
  // Always await async methods (methods that return a Task).
  // Call static CreateAsync method instead of the constructor.
  FolderModel folderModel = await FolderModel.CreateAsync(@"C:\");
}

In a more advanced scenario when you want to defer the initialization for example because you want to avoid to allocate expensive resources that are not needed now or never, you can make the instance call InitializeAsync when a certain member that depends on these resources is referenced or you can make the constructor and the InitializeAsync method public to allow the user of the class to call InitializeAsync explicitly.

Sandeep Jadhav
  • 815
  • 1
  • 10
  • 27
BionicCode
  • 1
  • 4
  • 28
  • 44
  • `You must enumerate the folder on a background thread.` -- Or you can use the async await approach described [here](https://mithunvp.com/building-responsive-ui-using-async-await-csharp/), which runs on the *same* thread. – Robert Harvey Nov 30 '21 at 14:15
  • @RobertHarvey My example does exactly the same: it uses `await Task.Run`. While async/await does not necessarily run on a different thread, operations started with Task.Run do. Natural or pure async methods do not require an extra thread, that's true. Task.Run does. As far as I've seen, your provided example uses Task.Run, therefore it does not run on the caller's thread context. – BionicCode Nov 30 '21 at 14:38
  • Hmm... The [StorageFolder Class](https://learn.microsoft.com/en-us/uwp/api/Windows.Storage.StorageFolder?redirectedfrom=MSDN&view=winrt-22000) has async versions of these methods. It's unclear to me whether they use a thread under the hood. https://stackoverflow.com/questions/719020 – Robert Harvey Nov 30 '21 at 14:56
  • @RobertHarvey I don't think they use a thread. It will use low-level hardware features to implement a pure async operation. I know StorageFolder from UWP. While it exposes an async API it perfoms very bad for aggregation operations like sum up all file sizes. Where StorageFolder returns a complete collection as a result, Directory.EnuerateXyz returns a lazy evaluating IEnumerable. StorageFolder is good when you need the result immediately. Aside from that the OP use the old .NET Framework. I believe the Windows.Storage namespace won't be available for .NET Framework. – BionicCode Nov 30 '21 at 16:09
  • @BionicCode thanks for your comment. But I got some errors that you shared for .NET Framework. Here is it; https://i.stack.imgur.com/Wm0lr.png –  Dec 01 '21 at 05:42
  • @SerhatÖzyıldız I have fixed it, sorry for that inconvenience. – BionicCode Dec 01 '21 at 09:36
  • 1
    @SerhatÖzyıldız And don't forget to await the GetDirectorySize() call! `await GetDirectorySize(@"C:\");` – BionicCode Dec 01 '21 at 10:09
  • Hey, @BionicCode, I didn't use await, The code is not freezing my application, but my text shows only "System.Threading.Tasks.Task'1[System.Int64]" and I coludnt convert it –  Dec 01 '21 at 11:19
  • @SerhatÖzyıldız Yes, because you MUST await the method in order to unwrap the Task. You always must await async methods! – BionicCode Dec 01 '21 at 11:51
  • @SerhatÖzyıldız And never call async methods from the constructor. – BionicCode Dec 01 '21 at 11:54
  • Thanks for your advices! Where shouuld I put await GetDirectory(fpath) exactly ? –  Dec 01 '21 at 13:46
  • @SerhatÖzyıldız Where do you call it? Can you edit your question to show where you call it? – BionicCode Dec 01 '21 at 13:46
  • @BionicCode _folderSize = CalculatorSize(GetDirectorySize(folderPath)); I call it like this before methods, in FolderModel construction. Now I am trying this; _folderSize = GetDirectorySize(folderPath).ToString(); –  Dec 01 '21 at 18:12
  • Can you please update your question to show where you call the constructor of FolderModel? I can then show you the pattern how you create/initialize instances that require async method calls. – BionicCode Dec 01 '21 at 18:44
  • 1
    @SerhatÖzyıldız I have updated the answer to give you an examplke how to instantiate a class that requires async initialization. I have also moved the `Directory.Exists` check from the `GetDirectorySize()` method to the instantiation routine. You should use user input validation or more preferably a folder picker dialog: – BionicCode Dec 01 '21 at 19:58
  • 1
    @SerhatÖzyıldız install [Windows-API-Code-Pack-1.1.4](https://www.nuget.org/packages/Microsoft-WindowsAPICodePack-Core/) via NuGet manager and use the `CommonOpenFileDialog` or alternatively use the [FolderBrowserDialog](https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.folderbrowserdialog?redirectedfrom=MSDN&view=windowsdesktop-6.0#examples). – BionicCode Dec 01 '21 at 19:58
  • @BionicCode, I appreciate for your effort! Thank you for everything. I will try asap, and will let you know the result.. –  Dec 02 '21 at 05:22