WinUI 3: Finding faster file loading for TreeView # 2 - FireCubeStudios/Mica-Studio GitHub Wiki

This is the second blog post detailing the journey of creating my "Mica Studio" code editor. This post explores methods for searching files/folders in a folder. Then it goes on about the performance implications of TreeView in WinUI 3.

The premise

A core feature of Mica Studio is the ability to open a folder and display all files and folders in that folder on a TreeView. The contents of a folder are displayed in alphabetical order with all folders shown before all files. The first blog goes a little bit over the current implementation but basically the TreeView displays a tree like data structure of nodes which can have child nodes. It is a given that folders can have child nodes consisting of files/folders meanwhile files are leaf nodes and can not contain any children for now. I want to figure out what is the fastest way to display files/folders in the TreeView when opening a folder.

The Mica Studio TreeView node contains file name, a corresponding string "path" and most importantly an ObservableCollection of children which we display with x:Bind. Our TreeView implements lazy loading so we only load the files/folders when a folder node is expanded. The class currently used looks something like:

public partial class FolderNode : ObservableObject, IExplorerParentNode
{
       public string DisplayName;
       public string FilePath;
       public ObservableCollection<IExplorerNode> Children; // All the files and folders in this folder

       public void Expanding(); // Called when expanding a folder in the TreeView (load the items in this call)
}

To benchmark the performance we will be using a simple StopWatch class. The current tests will involve opening the System32 folder which on my computer has 4681 items. I also decided to test how fast it would be to iterates folder contents without displaying to the UI and then with displaying to the UI aka x:Bind to the "children" ObservableCollection

Using WinRT FileSystem API

The WinRT File API is very nice to use so it was the first API I used to get contents of a folder. The code is very straightforward:

	StorageFolder folder = await StorageFolder.GetFolderFromPathAsync(FilePath);

	// Get subfolders
	var subFolders = await folder.GetFoldersAsync();
	foreach (var subFolder in subFolders)
		Children.Add(new FolderNode(subFolder.Path));

	// Get files
	var files = await folder.GetFilesAsync();
	foreach (var file in files)
		Children.Add(new FileNode(file.Path));
  • To load System32: 11773 ms
  • To load System32 while displaying to TreeView UI: 17026 ms

These results were surpsingly slow for me but upon digging further it turns out WinUI 3 interop with CSWinRT and some other stuff causes the WinRT File API to be slow. It might be interesting to see if it is faster in UWP or C++ WinUI 3.

Using the System.IO FileSystem API

The System.IO API is easy to use too eventhough it does not have as many convenience features as the WinRT API.

	// Get subfolders in this folder
	var subFolders = Directory.GetDirectories(FilePath);
	foreach (var subFolder in subFolders)
		Children2.Add(new FolderNode(subFolder));

        // Get files in this folder
        var files = Directory.GetFiles(FilePath);
        foreach (var file in files)
		Children2.Add(new FileNode(file));
  • To load System32: 15 ms
  • To load System32 while displaying to TreeView UI: 4346 ms

This was considerably faster than the WinRT API especially when iterating folder contents without displaying it to the UI.

Using Win32 FileSystem API directly

I decided to try the Win32 API directly with p/invoke calls to FindNextFileW and other functions. ChatGPT generated the code for this test and the results are:

  • To load System32: 59 ms
  • To load System32 while displaying to TreeView UI: 11302 ms

Optimising the UI

I could not find any other methods on iterating files/folders easily to try so it was now time to look into optimising the fastest method found which was the System.IO one. I took a closer look at the code by setting break points at each step to see how long they take and here were the results:

  • 7 ms - to call Directory.GetDirectories
  • 38 ms - to loop over folders and add them to ObservableCollection
  • 9 ms - to call Directory.GetFiles
  • 4350 ms - to loop over files and add them to ObservableCollection

Almost all of the time was taken in the UI updates which was quite shocking as I have loaded large amounts of items in lists before in WinUI apps yet it did not take seconds to load. After doing some research I found out that TreeView actually has some performance issues like no Virtualisation on non root nodes. I decided to try to load items but put them into a ListView instead. In my TreeView I used an ItemTemplateSelector to render folder items and file items differently and I did the same with the ListView too.

The results with ListView timed with breakpoints:

  • 5 ms - to call Directory.GetDirectories
  • 13 ms - to loop over folders and add them to ObservableCollection
  • 5 ms - to call Directory.GetFiles
  • 114 ms - to loop over files and add them to ObservableCollection

This was a MASSIVE performance improvement and visually i could also see the items loaded near instantaneously. I also timed it with the StopWatch class and it came to around 58ms to load items and display it in the UI. Simply switching from TreeView to ListView made loading items go from 4 seconds to 0.058 seconds.

However switching to a ListView also means I lose a critical benefit of the TreeView which is the ability to display a hierarchical list with expanding and collapsing nodes that contain nested items. Instead an alternative I might have to do is to render a ListView in a ListView which might look bad. I could also somehow render all nested items in the ListView and use stylings to indent them.

Reimplementing TreeView

I decided to try the ListView in ListView idea and see how it would look and perform. The implementation was trivial and it looked good if you only open one folder but when nesting it quickly got out of hand. Furthermore the nested ListView actually seems to lose any performance gains. It seems virtualisation gets disabled if it is nested.

It seemed to me that the better course of action would be to render all files and folders including nested sub directories all in one ListView bound to a single ObservableCollection. This will hopefully keep the performance gains of Virtualisation and I could easier styling by only having one ListView. This of course needs a good way to manage everything in one ObservableCollection which would be quite a challenge to implement. Due to this I have decided to write an upcoming blog post dedicated to this custom high performance TreeView recreation designed for Mica Studio which will also have some additional features the current TreeView does not have.