Home > front end >  Calculating folder size freezes application while trying to calculate 'C:\' in WPF
Calculating folder size freezes application while trying to calculate 'C:\' in WPF

Time:12-02

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;
        }
    }

CodePudding user response:

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

FolderModel.cs

class FolderModel
{
  // Make 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 intialization 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 explicitely.

  • Related