Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve Scene Tree editor performance #99700

Merged
merged 1 commit into from
Dec 16, 2024

Conversation

hpvb
Copy link
Member

@hpvb hpvb commented Nov 26, 2024

We now cache the Node*<>TreeItem* mapping in the SceneTreeEditor. This allows us to make targeted updates to the Tree used to display the scene tree in the editor.

Previously on almost all changes to the scene tree the editor would rebuild the entire widget, causing a large number of deallocations an allocations. We now carefully manipulate the Tree widget in-situ saving a large number of these allocations.

In order to know what Nodes need to be updated we add a editor_state_changed signal to Node, this is a TOOLS_ENABLED, editor-only signal fired when changes to Node happen that are relevant to editor state.

We also now make sure that when nodes are moved/renamed we don't check expensive properties that cannot contain NodePaths. This saves a lot of time when SceneTreeDock renames a node in a scene with a lot of MeshInstances. This makes renaming nodes go from ~27 seconds to ~2 seconds on large scenes.

SceneTreeEditor instances will now also not do all of the potentially expensive update work if they are invisible. This behavior is turned off by default so it won't affect existing users. This change allows the editor to only update SceneTreeEditors that actually in view. In practice this means that for most changes instead of updating 6
SceneTreeEditors we only update 1 instantly, and the others only when they become visible.

There is definitely more that could be done, but this is already a massive improvement. In complex scenes we see an improvement of 10x, things that used to take ~30 seconds now only take 2.

This fixes #83460

I want to thank @KoBeWi, @TokisanGames, @a-johnston, @daniel080400 for their tireless testing. And @AeioMuch for their testing and providing a fix for the hover issue.

@hpvb hpvb added topic:editor performance cherrypick:4.3 Considered for cherry-picking into a future 4.3.x release labels Nov 26, 2024
@hpvb hpvb requested review from a team as code owners November 26, 2024 01:39
@hpvb
Copy link
Member Author

hpvb commented Nov 26, 2024

Before: (this scene has about 15,000 nodes)

Screencast.From.2024-11-26.02-11-22.mp4

After:

Screencast.From.2024-11-26.02-10-35.mp4

Copy link
Contributor

@arkology arkology left a comment

Choose a reason for hiding this comment

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

Stealing @AThousandShips 's job 🤣

editor/gui/scene_tree_editor.cpp Outdated Show resolved Hide resolved
editor/gui/scene_tree_editor.cpp Outdated Show resolved Hide resolved
editor/gui/scene_tree_editor.cpp Outdated Show resolved Hide resolved
editor/gui/scene_tree_editor.cpp Outdated Show resolved Hide resolved
@AThousandShips AThousandShips added this to the 4.x milestone Nov 26, 2024
editor/gui/scene_tree_editor.h Outdated Show resolved Hide resolved
scene/gui/tree.cpp Show resolved Hide resolved
@hpvb hpvb force-pushed the scene_tree_editor_performance branch 2 times, most recently from 6aefc0b to 9a25be0 Compare November 26, 2024 12:03
@hpvb hpvb requested a review from a team as a code owner November 26, 2024 12:20
@hpvb hpvb force-pushed the scene_tree_editor_performance branch from 431094f to 05519d4 Compare November 26, 2024 12:29
@hpvb hpvb requested review from arkology, bruvzg and KoBeWi November 26, 2024 12:32
Copy link
Contributor

@arkology arkology left a comment

Choose a reason for hiding this comment

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

I confirm that birdies are in the nest (dots at the end of the comments)😃

@hpvb hpvb force-pushed the scene_tree_editor_performance branch from 05519d4 to d4d55d0 Compare November 26, 2024 13:20
@KoBeWi
Copy link
Member

KoBeWi commented Nov 26, 2024

Changing valid types does not update the nodes (they stay disabled/enabled).

Can be tested with this simple EditorScript:

extends EditorScript
func _run() -> void:
	EditorInterface.popup_node_selector(func(n): print(n), ["Node2D"])

Run it, then remove "Node2D" and run again. Nodes that didn't match originally will stay disabled.

@hpvb hpvb force-pushed the scene_tree_editor_performance branch 2 times, most recently from c5fca34 to e076f60 Compare November 26, 2024 16:15
@hpvb

This comment was marked as outdated.

doc/classes/TreeItem.xml Outdated Show resolved Hide resolved
@hpvb hpvb force-pushed the scene_tree_editor_performance branch from e076f60 to 8851ae1 Compare November 26, 2024 17:18
@hpvb hpvb force-pushed the scene_tree_editor_performance branch from 47465d3 to 987fdd4 Compare December 11, 2024 19:30
@aaronfranke
Copy link
Member

In order to know what Nodes need to be updated we add a editor_state_changed signal to Node, this is a TOOLS_ENABLED, editor-only signal fired when changes to Node happen that are relevant to editor state.

I'm not a fan of even more editor-specific code in non-editor code. Why not add the signal onto the object using Object::add_user_signal? https://docs.godotengine.org/en/stable/classes/class_object.html#class-object-method-add-user-signal

Copy link
Contributor

@AeioMuch AeioMuch left a comment

Choose a reason for hiding this comment

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

For what it's worth, I've hammered the TPS Demo project's scenes trees with random changes, undid them, redid them, etc and haven't been able to break it.

@hpvb
Copy link
Member Author

hpvb commented Dec 12, 2024

I'm not a fan of even more editor-specific code in non-editor code. Why not add the signal onto the object using Object::add_user_signal? https://docs.godotengine.org/en/stable/classes/class_object.html#class-object-method-add-user-signal

After this pr we can remove several editor only signals as well, it if sadly not possible to add the signals at runtime as they get emitted from parts of node that just don't have signals. Like when a node gets added to a persistent group.

However the ultimate net effect is going to be less overall editor specific code in node.

@WrobotGames
Copy link

I tried this on my Jungle Demo scene and (compared to 4.4dev6) the time it takes for re-parenting a node is reduced from ~49 seconds to ~1 second. Amazing work! I, however, feels like the scene takes a bit longer to load in editor, (~10% longer).

@hpvb
Copy link
Member Author

hpvb commented Dec 12, 2024

I tried this on my Jungle Demo scene and (compared to 4.4dev6) the time it takes for re-parenting a node is reduced from ~49 seconds to ~1 second. Amazing work! I, however, feels like the scene takes a bit longer to load in editor, (~10% longer).

If you would be willing to share your project with me in some fashion I can have a look at that also. But this would likely be addressed in a follow-up pr. You can find me on the rocket.chat with username hp if you'd like to discuss in private.

@akien-mga
Copy link
Member

I think the two commits could be squashed together, there isn't much reason to implement the functionality in one commit and expose it in the next, we usually implement and expose at the same time.

Comment on lines +2087 to +2091
if (Object::cast_to<Material>(resource)) {
// For performance reasons, assume that Materials don't have NodePaths in them.
// TODO This check could be removed when String performance has improved.
break;
}
Copy link
Member

Choose a reason for hiding this comment

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

Can't this use the PROPERTY_HINT_NO_NODEPATH hint you added instead?

Copy link
Member Author

Choose a reason for hiding this comment

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

No, the problem here is that getting the property list itself is super slow. By the time I get the hint it is already too late.

Copy link
Member Author

Choose a reason for hiding this comment

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

https://github.com/godotengine/godot/blob/master/scene/resources/material.cpp#L2404

This is the function that I need to avoid calling. There's a way to make it a bit faster too, but even then it is still pretty slow.

Copy link
Member

Choose a reason for hiding this comment

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

Now I realized this might break ViewportTextures in materials. They won't update paths anymore.

Copy link
Member Author

Choose a reason for hiding this comment

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

They still work, I forgot to put that as a test in my list of things I tested. But that was specifically tested.

@hpvb
Copy link
Member Author

hpvb commented Dec 12, 2024

I think the two commits could be squashed together, there isn't much reason to implement the functionality in one commit and expose it in the next, we usually implement and expose at the same time.

I put it in two separate commits in case any backport to 4.3 wouldn't want to expose it. I can squash them though if that is not actually desired.

@akien-mga
Copy link
Member

I think the two commits could be squashed together, there isn't much reason to implement the functionality in one commit and expose it in the next, we usually implement and expose at the same time.

I put it in two separate commits in case any backport to 4.3 wouldn't want to expose it. I can squash them though if that is not actually desired.

I'd say it's fine to squash. If we do a 4.3 backport, I think exposing a minor new functionality wouldn't be a big deal compared to the very significant refactoring of both editor code and core logic, which might be riskier than just a new function.

From a quick test it wouldn't be a simple git cherry-pick either but a proper backport, so if we do one we can also decide at that time to not expose the method (but really I don't think exposing new methods is a big deal - if it's there, we can as well offer it).

We now cache the Node*<>TreeItem* mapping in the SceneTreeEditor. This
allows us to make targeted updates to the Tree used to display the scene
tree in the editor.

Previously on almost all changes to the scene tree the editor would
rebuild the entire widget, causing a large number of deallocations an
allocations. We now carefully manipulate the Tree widget in-situ saving
a large number of these allocations.

In order to know what Nodes need to be updated we add a
editor_state_changed signal to Node, this is a TOOLS_ENABLED,
editor-only signal fired when changes to Node happen that are relevant
to editor state.

We also now make sure that when nodes are moved/renamed we don't check
expensive properties that cannot contain NodePaths. This saves a lot of
time when SceneTreeDock renames a node in a scene with a lot of
MeshInstances. This makes renaming nodes go from ~27 seconds to ~2
seconds on large scenes.

SceneTreeEditor instances will now also not do all of the potentially
expensive update work if they are invisible. This behavior is turned off
by default so it won't affect existing users. This change allows the
editor to only update SceneTreeEditors that actually in view. In
practice this means that for most changes instead of updating 6
SceneTreeEditors we only update 1 instantly, and the others only when
they become visible.

There is definitely more that could be done, but this is already a
massive improvement. In complex scenes we see an improvement of 10x,
things that used to take ~30 seconds now only take 2.

This fixes godotengine#83460

I want to thank KoBeWi, TokisanGames, a-johnston, aniel080400 for
their tireless testing. And AeioMuch for their testing and providing a
fix for the hover issue.
@hpvb hpvb force-pushed the scene_tree_editor_performance branch from 987fdd4 to 6f7525c Compare December 12, 2024 21:47
@hpvb
Copy link
Member Author

hpvb commented Dec 12, 2024

From a quick test it wouldn't be a simple git cherry-pick either but a proper backport, so if we do one we can also decide at that time to not expose the method (but really I don't think exposing new methods is a big deal - if it's there, we can as well offer it).

Alright! Squashed.

@hpvb
Copy link
Member Author

hpvb commented Dec 16, 2024

Can we has merge this so it can be tested in the next dev build prz? 👉👈

Copy link
Member

@akien-mga akien-mga left a comment

Choose a reason for hiding this comment

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

YOLO

@akien-mga akien-mga merged commit 08508d2 into godotengine:master Dec 16, 2024
20 checks passed
@akien-mga
Copy link
Member

Thanks!

void SceneTreeEditor::set_marked(const HashSet<Node *> &p_marked, bool p_selectable, bool p_children_selectable) {
_update_if_clean();

_update_marking_list(marked);

Choose a reason for hiding this comment

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

I am getting a crash within this function occasionally when opening the signal connection dialog due to garbage data in marked. Is calling _update_marking_list() twice intended? Commenting out the first call seems to fix the issue, but I can't tell if that breaks anything.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, calling it twice is intentional. Do you have a back trace of the crash?

Copy link
Member Author

Choose a reason for hiding this comment

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

There should never be "garbage data" in marked that is a pre-existing bug then, but I can't figure out how garbage data could get into it.

Copy link
Member Author

Choose a reason for hiding this comment

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

Choose a reason for hiding this comment

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

Sorry, I'm away for a few days. I should have opened an issue for this but I couldn't make a decent reproduction. I think it has to do with the connection dialog keeping data from a closed scene if anyone else can make a reproduction project. If not, I will make one when I get back.

Copy link
Member Author

Choose a reason for hiding this comment

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

It does keep data from a closed scene until it reopens, this is by design because updating the invisible SceneTreeEditors is very costly. It makes everything take 6x as long! :)

I will see if I can come up with a fix for what I hypothesize is the problem.

}

if (p_node->has_signal(SceneStringName(visibility_changed))) {
if (p_node->is_connected(SceneStringName(visibility_changed), callable_mp(this, &SceneTreeEditor::_node_visibility_changed))) {
p_node->disconnect(SceneStringName(visibility_changed), callable_mp(this, &SceneTreeEditor::_node_visibility_changed));
Copy link
Contributor

Choose a reason for hiding this comment

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

This signal is no longer disconnected. Is that okay? Is it part of the performance savings? Will it cause any issues in long running editor sessions?

Copy link
Member Author

Choose a reason for hiding this comment

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

This is by design, the cost of connecting and disconnecting signals here is pretty high, and leaving them connected doesn't really do anything. If it turns out to be a problem it can be changed back, but I have not observed any issues with this.

Copy link
Member Author

Choose a reason for hiding this comment

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

These signals do not fire often, and a signal that's not firing is just a bit of memory, and not a lot.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug cherrypick:4.3 Considered for cherry-picking into a future 4.3.x release performance topic:editor
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Manipulating the scene tree gets progressively slower with more nodes added to the tree in the editor.