Skip to content

Optimize tree size computation and the scene tree dock filter - #110759

Merged
Repiteo merged 1 commit into
godotengine:masterfrom
precup:scene-tree-dock-optimizations
Jan 28, 2026
Merged

Optimize tree size computation and the scene tree dock filter#110759
Repiteo merged 1 commit into
godotengine:masterfrom
precup:scene-tree-dock-optimizations

Conversation

@precup

@precup precup commented Sep 21, 2025

Copy link
Copy Markdown
Contributor

These are fixes for the last issues I've encountered related to large node counts in the editor. You can trigger them in a variety of ways, but they all come back to either:

  • updating the filter causes O(m*n) behavior where n is the current selection size and m is the number of items in the tree
  • calculating font height so many times that the constant factors on it overwhelm the runtime

For the filter issue, here are two ways to see the problem:

  • Attempt to type in the Scene Tree dock's filter while a large (5k+) number of nodes are selected. This can take upwards of a minute per letter typed for large enough selections.
  • Make a large selection, switch to an arbitrary other scene's tab, and then attempt to switch back. Switching scenes updates the filter, which will lag the editor out and dominate the scene switch time.

The core of the issue is that when updating the filter, it attempts to scroll to every selected item in turn. This seems equivalent to scrolling to the first item and them scrolling to the last item, so that's what this PR updates it to do, instead. Since scrolling to an item is O(m) on the number of items, that cuts an O(m*n) algorithm (which is O(m^2) with a large enough selection) down to just O(m).

For the font height calculation:
This commonly triggers any time that you redraw the scene tree and whenever you mouse over the scene tree, since it checks what your mouse is over. This doesn't seem like it should be that expensive, but it turns out it is, and the difference in responsiveness of the Scene Tree dock with it cached is massive. You can very obviously see a visual difference just by opening a scene with 20k+ nodes and trying to scroll around and interact with the Scene Tree dock.

One thing I'm nore sure of: this PR assumes that font and font_size in theme_cache only change when the theme changes. This is fine as far as I know, but I'm not exactly sure how theme_cache works, so I wanted to call it out. If this isn't safe to cache on the longer term, it still seems worth caching on a per frame basis somehow.


It's data time!
Typing "Label" into a filter with 25k items that all match and 1 item selected: 1.5s before, 1.5s after
Typing "Label" into a filter with 25k items that all match and 1k items selected: 74s before, 1.5s after
Here's a flamegraph of the before, stuck in the scroll_to_selected swamp, which itself is mostly spent getting font heights:
image

Switching to a scene with 100k items, 1 item selected: 5.5s before, 4.9s after
Switching to a scene with 100k items, 1k items selected: manually killed 10 minutes in before, 4.9s after
Here's a flamegraph that might give you deja vu:
image

The font height calculation can seen as a problem just scrolling through and clicking around a large Scene Tree dock, here's me doing that for a little bit:
Before
image

After
image

I also recorded a little video of the difference. Pretty clear how impactful this is.
Before
https://imgur.com/g7VQimY
After
https://imgur.com/mdmfZWA


All of the data in this PR was collected on builds built with scons optimize=speed_trace production=yes debug_symbols=yes platform=linuxbsd dev_build=no use_llvm=yes. The before numbers aren't actually from master, they're from master with my other editor performance PRs cherry-picked on, as well. The other, existing performance problems make these tests difficult to run without them. The sample project I've been using to test is
editor-optimization-test-scenes.zip.

With this PR and my other optimization PRs applied, I'm able to work reasonably comfortably on a scene with 100,000 nodes in it!

@precup
precup requested a review from a team as a code owner September 21, 2025 20:39
@precup
precup requested a review from a team September 21, 2025 20:39
@precup
precup force-pushed the scene-tree-dock-optimizations branch from 4126f6d to a64397d Compare September 21, 2025 22:09
@AThousandShips AThousandShips added this to the 4.x milestone Sep 22, 2025
Comment thread scene/gui/tree.cpp Outdated
@KoBeWi

KoBeWi commented Oct 2, 2025

Copy link
Copy Markdown
Member

I see more usages of theme_cache.font->get_height(theme_cache.font_size) in Tree that could be replaced.

Comment thread editor/scene/scene_tree_editor.cpp
@precup
precup force-pushed the scene-tree-dock-optimizations branch from a64397d to 505edc7 Compare October 23, 2025 05:53
@precup
precup force-pushed the scene-tree-dock-optimizations branch from 505edc7 to 357cace Compare October 23, 2025 05:54
@clayjohn
clayjohn requested a review from KoBeWi October 27, 2025 05:45
if (n && editor_selection->is_selected(n)) {
if (p_scroll_to_selected) {
// Needs to be deferred to account for possible root visibility change.
callable_mp(tree, &Tree::scroll_to_item).call_deferred(p_parent, false);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this (or the one above) is called for every selected item, so the Tree will needlessly scroll to each item, eventually settling on the last one. This can be queued with something like

void _queue_scroll_to_item(TreeItem *p_item) {
	if (!queued_item) {
		callable_mp(this, &SceneTreeEditor::_scroll_to_item).call_deferred();
	}
	queued_item = p_item;
}

void _scroll_to_item() {
	tree->scroll_to_item(queued_item );
	queued_item  = nullptr;
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It used to be called every time, but this PR actually prevents that via the condition update on line 1041. p_scroll_to_selected will only be true the first time a child is found, and will be false subsequently, so it actually only scrolls to the first element it finds. That's the main performance boost this PR provides, actually.

Calling it a single time on the last item only isn't ideal. Really, you want to call it on the first element once, and then the last element once, because doing so will frame as many of the elements as possible. If you do it only to the first or last element, it might not scroll to show as many of the selected elements. In order to preserve that behavior, I used this somewhat complicated boolean passing instead of a solution like the one you proposed.

I could accomplish the same result as what I'm doing now via a similar method, though, if you'd prefer.

@akien-mga akien-mga modified the milestones: 4.x, 4.7 Dec 17, 2025
@Repiteo
Repiteo merged commit 0620af8 into godotengine:master Jan 28, 2026
20 checks passed
@Repiteo

Repiteo commented Jan 28, 2026

Copy link
Copy Markdown
Contributor

Thanks!

rivie13 pushed a commit to rivie13/Phoenix-Agentic-Engine that referenced this pull request Feb 16, 2026
…imizations

Optimize tree size computation and the scene tree dock filter
BendyLand pushed a commit to BendyLand/voltaire that referenced this pull request Aug 2, 2026
…imizations

Optimize tree size computation and the scene tree dock filter
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants