Repository navigation
editor: Improve filesystem dock and import performance - #124138
Open
stuartcarnie wants to merge 1 commit into
Open
stuartcarnie wants to merge 1 commit into
stuartcarnie wants to merge 1 commit into
Conversation
stuartcarnie
force-pushed
the
editor_perf_lots_of_files
branch
from
October 3, 2026 22:25
1b28f97 to
9dae1db
Compare
Contributor
Contributor
Author
|
@Maran23 yes, thanks for spotting that 🤦🏻 – I've fixed it |
stuartcarnie
force-pushed
the
editor_perf_lots_of_files
branch
from
October 6, 2026 23:10
9dae1db to
51cbd48
Compare
stuartcarnie
marked this pull request as ready for review
October 9, 2026 22:57
Contributor
|
I've seen this PR: #116464, which seems to slightly conflict with this one? It may be good to resolve that one first, since it seems simpler. But it's a draft, so I'm not sure that anything can be done.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Summary
Helps #123948
In the MPR with 80,000 PNG files, importing the assets kept the main thread busy and made the editor unresponsive. When the import finished, the editor then generated a thumbnail for every PNG file and redrew the FileSystem dock many times per frame. This PR is the culmination of several paper cuts in a few areas found by profiling with Instruments.
The main hotspots, as identified in Instruments, were the following:
DirAccessUnixconstructor: repeated the same work twice.EditorFileSystem::_find_file: created aDirAccessobject and allocated strings on every call.EditorFileSystem::reimport_files: the main thread spun in a tight loop while worker threads imported files.DirAccessMacOS::is_hidden: usedNSURLfor every directory entry, which was slow.Here is a video showing opening the MRP from #123948 with 80,000 PNG files for the first time, such that it imports all the
.pngfiles, creating associated metadata,.md5hash files and.ctexcompressed texture equivalents.FullImport4.8-dev7.mp4
FullImport-optimised.mp4
spritesnode in File System dockThe last two CPU usages are particularly important, as they show how much more efficient the code is. My machine has 16 cores. Those with fewer cores will see even greater improvements to wall times.
Speeding Up Large Imports
By default, Godot enables safe backup / rename when writing files:
It's possible on slower drives that this might introduce additional overhead during a large initial import. I would say for most SSDs today, it's probably not much of an issue.
This feature is enabled by default, which means writing a file will
When importing 80,000 PNGs, 3 files are written for every
.png:<png_file_name>.import.godot/imports/<png_file_name>.ctex.godot/imports/<png_file_name>.md5DirAccessUnixconstructorDirAccess::create()is called constantly during scans and imports, and in the original profile theDirAccessUnixconstructor used 16.9 s of CPU time. The constructor read the current directory withgetcwd(), then calledchange_dir()with that same directory.change_dir()calledgetcwd()again and then calledchdir()twice, ending withcurrent_dirholding the value it already had. I wonder if the call was left over from #11149, which in 2017 changedchange_dir()to stop resolving the path withgetcwd()and moved that step into the constructor. After that change the call no longer did anything.The fix was to remove the redundant call to
change_dir()call from the constructor.Result
The constructor's CPU time dropped from 16.9 s to 6.1 s. After the
_find_filechange, which stops creating aDirAccesson every call (as it wasn't necessary), the constructor used 0.01 s.EditorFileSystem::_find_fileThis function finds a file in the editor's in-memory file tree, and the import threads call it frequently. In the original profile it used 46.6 s of CPU time. It had three primary hot spots:
DirAccesson every call, which paid the constructor cost above. The object was only needed when a folder was missing from the tree, which I'll assume is rare. Also, it checked ares://path with anACCESS_FILESYSTEMobject, which cannot readres://paths, so that check always failed. The check was added in Fix removing a folder that contains a file is not removed from the FileSystem Dock #94435.to_lower()is called on every folder and file name. Each call allocated a new string.Vector<String>, allocating morelocalize_path(), which unconditionally allocates several strings, yet the localized path was almost always the same as the original, wasting CPU and memory allocationsFixes
DirAccessonly when a folder is missing from the tree, throughDirAccess::dir_exists_absolute(). This picks the right access type forres://paths, so the check now works._span_equals, compares twoSpan<char32_t>values. When case matters it uses the existingSpancomparison. When case does not matter it converts only the characters that differ, withString::char_lowercase().String::char_lowercase()an inline function that handles ASCII characters directly. Other characters still use the lookup table inustring.cpp, through the new private function_char_lowercase_table(). The result is the same as before for every character. Other callers ofchar_lowercase()also get the faster ASCII path.Span<char32_t>, and skiplocalize_path()when the path is already a simpleres://path.Result
_find_fileCPU time dropped from 46.6 s to 0.35 s, about 130 times less.Caution
This function has a data race under specific conditions, as it is called from multiple threads. Normally
_find_filedoesn't mutate anything, but if the directory doesn't exist, it mutates the file system tree without protecting it with a synchronisation primitive like an RWLock.EditorFileSystem::reimport_filesWhile worker threads import files, the main thread spins in a loop that keeps the progress dialog updated. On every pass it called
get_file(), which allocates a new string, andProgressDialog::task_step().task_step()returns early unless 200 ms have passed since the last update, so almost every iteration did nothing useful. The loop was just spinning and consuming the main thread. The loop also showed the wrong file name for batches that did not start at index 0, because it usedreimport_files[imported_count]instead ofreimport_files[from + imported_count]. That has been the case since #98385.Fixes
reimport_files[from + imported_count], so it shows the right file name.Result
The main-thread CPU time during the import dropped from 42.0 s to 6.8 s, and
ProgressDialog::task_step()from 7.4 s to 0.2 s. The import finished in 29.4 s instead of 42.1 sFileSystem dock thumbnails
Once the import has completed, the editor stayed slow / unresponsive while it generated thumbnails, and most main-thread time went to redrawing the FileSystem dock tree. In tree-only mode, the dock creates a tree item for every file in the project, including files in collapsed folders, and requested a thumbnail for each one. When each thumbnail arrived, the dock called
set_icon(), which makes the tree redraw. The editor runs the deferred call queue once per physics step and twice per frame, so the tree was redrawing / invalidating many times per frame.Fix
NOTIFICATION_INTERNAL_PROCESS. The tree redraws at most once per frame because of thumbnails.The following shows navigating the tree right after the import has completed:
expand-tree-before.mp4
expand-tree-after.mp4
DirAccessMacOS::is_hiddenis_hidden()is called for every entry in every directory the editor lists. On macOS it built a full path, created anNSURLand asked it forNSURLIsHiddenKey. This was added in #42381 so the file dialog hides the same folders as Finder, such as/usrand~/Library. This is a really expensive call:Fix
Use
getattrlistat(). An item is hidden if its name starts with a dot or it has theUF_HIDDENflag. This is the same ruleNSURLIsHiddenKeyuses, and the names come from the file system, so.and..give the same result as before. I verified this by writing a small program to compare 1,462 directory entries, and it gave the same answer asNSURLIsHiddenKey.Result
is_hidden()main-thread CPU time dropped from 1.25 s to 0.33 s, about 3.8 times less, and directory scanning (_scan_new_dir()) from 1.39 s to 0.48 s.Note
This doesn't account for the additional CPU time spent releasing the temporary objects (NSURL, NSString, etc) that are handled by the
NSAutoReleasePoolafter each event loop iteration