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

Cache TreeItems between runs in EditorHelpSearch #77471

Merged
merged 1 commit into from
Jan 17, 2024

Conversation

AThousandShips
Copy link
Member

@AThousandShips AThousandShips commented May 25, 2023

Split off from #62524

Depends on #77554, which it includes

@AThousandShips
Copy link
Member Author

AThousandShips commented May 25, 2023

Realized I can remove some checks from slice, will update that too

Edit: Will also make it scroll to the first object on empty search as it otherwise keeps the original scroll which is annoying (at least without saving folding, only affects empty search), will add this change in a bit

@AThousandShips AThousandShips changed the title Use index for empty Editor Search Use index for empty Editor Help Search May 25, 2023
@YuriSizov
Copy link
Contributor

YuriSizov commented May 26, 2023

only used for empty searches, have not attempted to use it for searches with hierarchy, main focus is to reduce overhead building the hierarchy

So this is my main concern here. If we were to introduce some sort of cache, we should use it for all searches, not just for empty searches. With this PR we would be adding complexity as a temporary fix for only a part of use cases (and a small part at that). We would still need a complete solution that covers other cases, so it's likely that the code here is going to be reworked anyway. I think that it's a problem. There are also existing discrepancies in the current code regarding how we treat empty and non-empty searches, which I find weird (why are we sometimes ignoring scripts, for instance?) and which we should not aim to preserve.

What are your main blockers from implementing a proper caching mechanism? And what exactly do we want to cache to speed things up? I think that main bottlenecks are filtering and building the tree. And especially for empty searches building the hierarchy of classes itself is a very small part, I think. We basically already have everything we need in DocData, it's just flattened out.

So I'm thinking, hierarchy would rarely change compared to the number of searches we need to run every time a user works with the editor normally. Perhaps we should have a hierarchal structure in DocData itself, in addition to the flat class_list? We would update it alongside class_list, and that ensures that we only do the expensive stuff when the data changes. Then we basically don't need your index in its current form and can generate the tree from that hierarchal data directly, right?

And with #77446 merged we could start caching TreeItems, so we don't free and recreate them constantly. That can be a simple hashmap of class names and TreeItems.

I'm not sure if we can speed up filtering itself, but we can at least ensure we keep the results for a particular set of filters, so changing display options doesn't trigger pointless searches. This is where I see a thing like your index being useful. But it needs to be implemented differently, of course.

Let me know what you think!


PS. The implementation looks mostly fine! I would have a few nitpicks, but no point mentioning them for now since I'd prefer a different approach all together as explained above.

@AThousandShips
Copy link
Member Author

AThousandShips commented May 26, 2023

Good points, will look into the details of this, the original issue was that filling the tree without the index was very slow in the original PR

Will look into transferring the system for this to the DocData directly, shouldn't be too complicated (thinking possibly SelfList to make tracking easier, only issue is lack of sorting there, will see), and good points, the prospects of caching nodes would greatly help here, and yes probably main block to caching would be the lack of this ability offered by #77446. I think the main issue is the lack of Parent -> Child relationship, the other way around is trivial from inherits but finding the classes that inherit from it is a different issue, this can even be put into ClassDB to fetch all classes inheriting from X

The benefit mainly for speed of having the index I believe is that it prevents adding nodes at random points in the tree at each step, as the doc order is not related to the hiearchy order

Will take a deeper look at how some of this might be achieved tomorrow

@AThousandShips
Copy link
Member Author

AThousandShips commented May 27, 2023

Minor update, based on some tentative tests:

  • Intending to (initially) use just DocData and DocTools to maintain the hierarchy, will investigate the possibility of integrating it into ClassDB and Object but it's a bit more of a significant addition and I don't think adding it to the doc data as well (like how the parent class is both on docs and ClassDB)
  • Will put this to use for EditorHelp as well eliminating the need to fill the Imherited by: each time, and see any other places it might be relevant
  • Will then dig into the Tree PR to work out how to connect the two

Will open a separate PR for the doc data stuff because it touches some other aspects and feel best to get it as one step

Edit: might be easier to integrate into ClassDB than I initially thought, but will still be needed in DocData for script classes, will see

@AThousandShips AThousandShips marked this pull request as draft May 27, 2023 10:41
@YuriSizov
Copy link
Contributor

I wouldn't touch ClassDB, unless we absolutely have to for some reason. Let's keep this localized to the editor and the docs.

@AThousandShips AThousandShips modified the milestones: 4.1, 4.x Jun 12, 2023
@YuriSizov YuriSizov modified the milestones: 4.x, 4.3 Sep 27, 2023
@AThousandShips AThousandShips marked this pull request as ready for review December 2, 2023 13:26
@AThousandShips AThousandShips changed the title Use index for empty Editor Help Search Cache TreeItems between runs in EditorHelpSearch Dec 2, 2023
@AThousandShips AThousandShips force-pushed the help_search_index branch 2 times, most recently from b2b8cb7 to a1f5213 Compare December 7, 2023 12:20
@AThousandShips AThousandShips force-pushed the help_search_index branch 3 times, most recently from d573db9 to 29b4b4c Compare January 11, 2024 17:43
@YuriSizov
Copy link
Contributor

YuriSizov commented Jan 16, 2024

So I did some performance testing with this patch, and there are some improvements, but I would say not where it matters the most. This suggests that the biggest task during the search is, well, the search itself, the filtering. So tree optimizations don't help a lot.

Here's what I've got.

This PR but with _find_or_create_item always creating item
# Open & close

EditorHelp search done in 27150 usec
EditorHelp search done in 22821 usec
EditorHelp search done in 23037 usec
EditorHelp search done in 23033 usec
EditorHelp search done in 23357 usec
EditorHelp search done in 22984 usec

# Type CanvasItem

EditorHelp search done in 16151 usec
EditorHelp search done in 61174 usec
EditorHelp search done in 38129 usec
EditorHelp search done in 36029 usec
EditorHelp search done in 37685 usec
EditorHelp search done in 33603 usec
EditorHelp search done in 30664 usec
EditorHelp search done in 29356 usec

# Select all and delete

EditorHelp search done in 19702 usec

# Type CanvasItem

EditorHelp search done in 16901 usec
EditorHelp search done in 60786 usec
EditorHelp search done in 37567 usec
EditorHelp search done in 35557 usec
EditorHelp search done in 35710 usec
EditorHelp search done in 36061 usec
EditorHelp search done in 29840 usec
EditorHelp search done in 29485 usec

# Select all and delete

EditorHelp search done in 20904 usec

# Type CanvasItem

EditorHelp search done in 16352 usec
EditorHelp search done in 62959 usec
EditorHelp search done in 37910 usec
EditorHelp search done in 36326 usec
EditorHelp search done in 34051 usec
EditorHelp search done in 36893 usec
EditorHelp search done in 29998 usec
EditorHelp search done in 31087 usec

# Select all and delete

EditorHelp search done in 20287 usec

I then properly reverted to the base commit (your pre-requisite PR) and repeated the test, numbers were pretty much the same.

This PR as is
# Open & close

EditorHelp search done in 29836 usec
EditorHelp search done in 23291 usec
EditorHelp search done in 23281 usec
EditorHelp search done in 23235 usec
EditorHelp search done in 23128 usec

# Type CanvasItem

EditorHelp search done in 11300 usec
EditorHelp search done in 60133 usec
EditorHelp search done in 34215 usec
EditorHelp search done in 36310 usec
EditorHelp search done in 34973 usec
EditorHelp search done in 29777 usec
EditorHelp search done in 30237 usec

# Select all and delete

EditorHelp search done in 11583 usec

# Type CanvasItem

EditorHelp search done in 11552 usec
EditorHelp search done in 47853 usec
EditorHelp search done in 40483 usec
EditorHelp search done in 36908 usec
EditorHelp search done in 38016 usec
EditorHelp search done in 41106 usec
EditorHelp search done in 34926 usec
EditorHelp search done in 33553 usec
EditorHelp search done in 30547 usec
EditorHelp search done in 28900 usec

# Select all and delete

EditorHelp search done in 11745 usec

----

# Type CanvasItem

EditorHelp search done in 11428 usec
EditorHelp search done in 59534 usec
EditorHelp search done in 36870 usec
EditorHelp search done in 34049 usec
EditorHelp search done in 37174 usec
EditorHelp search done in 36284 usec
EditorHelp search done in 36710 usec
EditorHelp search done in 31183 usec

# Select all and delete

EditorHelp search done in 11986 usec

# Type CanvasItem

EditorHelp search done in 11826 usec
EditorHelp search done in 47993 usec
EditorHelp search done in 39987 usec
EditorHelp search done in 35682 usec
EditorHelp search done in 37178 usec
EditorHelp search done in 37434 usec
EditorHelp search done in 39563 usec
EditorHelp search done in 33644 usec
EditorHelp search done in 30011 usec
EditorHelp search done in 29245 usec

# Select all and delete

EditorHelp search done in 11873 usec

# Type CanvasItem

EditorHelp search done in 11087 usec
EditorHelp search done in 48177 usec
EditorHelp search done in 39633 usec
EditorHelp search done in 36763 usec
EditorHelp search done in 35209 usec
EditorHelp search done in 38964 usec
EditorHelp search done in 34553 usec
EditorHelp search done in 36050 usec
EditorHelp search done in 29682 usec
EditorHelp search done in 30392 usec

# Select all and delete

EditorHelp search done in 12001 usec

# Type CanvasItem

EditorHelp search done in 11399 usec
EditorHelp search done in 49362 usec
EditorHelp search done in 37216 usec
EditorHelp search done in 36895 usec
EditorHelp search done in 39983 usec
EditorHelp search done in 36929 usec
EditorHelp search done in 35785 usec
EditorHelp search done in 30559 usec
EditorHelp search done in 32560 usec

# Select all and delete

EditorHelp search done in 12434 usec

My testing routine was to open and close it a few times, then open it and type "CanvasItem" then select all and delete it, also a few times. You can see that:

  • First open after the editor startup is always slower.
  • This PR speeds up "empty" searches significantly.
  • All other searches deviate a lot, but don't appear faster or slower in general.
  • With this PR the first time you type "CanvasItem" the very first search is slower and on par with that from before, but ever repeated search is slightly faster.

Benchmarking is done with this patch:

diff --git a/editor/editor_help_search.cpp b/editor/editor_help_search.cpp
index afe2f97f47..b62c9cca10 100644
--- a/editor/editor_help_search.cpp
+++ b/editor/editor_help_search.cpp
@@ -48,6 +48,7 @@ void EditorHelpSearch::_update_results() {
 		search_flags |= SEARCH_SHOW_HIERARCHY;
 	}
 
+	search_started = OS::get_singleton()->get_ticks_usec();
 	search = Ref<Runner>(memnew(Runner(results_tree, results_tree, &tree_cache, term, search_flags)));
 	set_process(true);
 }
@@ -128,6 +129,8 @@ void EditorHelpSearch::_notification(int p_what) {
 			if (search.is_valid()) {
 				if (search->work()) {
 					// Search done.
+					uint64_t search_ended = OS::get_singleton()->get_ticks_usec();
+					print_verbose(vformat("EditorHelp search done in %s usec", search_ended - search_started));
 
 					// Only point to the match if it's a new search, and not just reopening a old one.
 					if (!old_search) {
diff --git a/editor/editor_help_search.h b/editor/editor_help_search.h
index e4980d6ff7..e2afa6ee88 100644
--- a/editor/editor_help_search.h
+++ b/editor/editor_help_search.h
@@ -66,6 +66,7 @@ class EditorHelpSearch : public ConfirmationDialog {
 
 	class Runner;
 	Ref<Runner> search;
+	uint64_t search_started = 0;
 
 	struct TreeCache {
 		HashMap<String, TreeItem *> item_cache;

Copy link
Contributor

@YuriSizov YuriSizov left a comment

Choose a reason for hiding this comment

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

There is at least some improvement here, and the implementation is pretty simple, so I'm keen on approving this so we can merge it once #77554 is done.

r_item = tree_cache->item_cache[p_item_meta];

// Remove from cache.
tree_cache->item_cache.erase(p_item_meta);
Copy link
Contributor

@YuriSizov YuriSizov Jan 16, 2024

Choose a reason for hiding this comment

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

I wasn't sure why we would clear the cache here, since the HashMap should update existing items anyway on insert. But removing this line doesn't introduce any performance improvements (and I may have introduced a leak with this, didn't debug much).

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 would risk double deleting items on close as any item already in the tree is deleted when clearing it, but might work out as it's deferred I realize, but unsure

Copy link
Contributor

Choose a reason for hiding this comment

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

Well, doesn't matter if removing it doesn't help. Just thought I'd mention 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.

Thank you 🙂

@AThousandShips
Copy link
Member Author

Once I've pushed some changes you can also compare the same performance with the complete PR, where I hope it'll help more with larger empty searches 🙂, probably areas where more can be shaved off as well

@AThousandShips
Copy link
Member Author

And thank you for verifying the performance, wasn't sure how I'd do it rigorously myself

@AThousandShips
Copy link
Member Author

AThousandShips commented Jan 16, 2024

Did some further performance checking and realized that the clearing of the tree also matters, so these are my results, using:

diff --git a/editor/editor_help_search.cpp b/editor/editor_help_search.cpp
index 229eb79e11..dd4d6ebbdc 100644
--- a/editor/editor_help_search.cpp
+++ b/editor/editor_help_search.cpp
@@ -48,6 +48,7 @@ void EditorHelpSearch::_update_results() {
                search_flags |= SEARCH_SHOW_HIERARCHY;
        }

+       search_started = OS::get_singleton()->get_ticks_usec();
        search = Ref<Runner>(memnew(Runner(results_tree, results_tree, &tree_cache, term, search_flags)));
        set_process(true);
 }
@@ -128,6 +129,8 @@ void EditorHelpSearch::_notification(int p_what) {
                        if (search.is_valid()) {
                                if (search->work()) {
                                        // Search done.
+                                       uint64_t search_ended = OS::get_singleton()->get_ticks_usec();
+                                       print_verbose(vformat("EditorHelp search done in %s usec", search_ended - search_started));

                                        // Only point to the match if it's a new search, and not just reopening a old one.
                                        if (!old_search) {
@@ -445,6 +448,8 @@ bool EditorHelpSearch::Runner::_phase_match_classes() {
 }

 void EditorHelpSearch::Runner::_populate_cache() {
+       results_tree->clear();
+
        root_item = results_tree->get_root();

        if (root_item) {
@@ -659,7 +664,7 @@ TreeItem *EditorHelpSearch::Runner::_create_class_hierarchy(const ClassMatch &p_

 bool EditorHelpSearch::Runner::_find_or_create_item(TreeItem *p_parent, const String &p_item_meta, TreeItem *&r_item) {
        // Attempt to find in cache.
-       if (tree_cache->item_cache.has(p_item_meta)) {
+       if (false && tree_cache->item_cache.has(p_item_meta)) {
                r_item = tree_cache->item_cache[p_item_meta];

                // Remove from cache.
diff --git a/editor/editor_help_search.h b/editor/editor_help_search.h
index e4980d6ff7..e2afa6ee88 100644
--- a/editor/editor_help_search.h
+++ b/editor/editor_help_search.h
@@ -66,6 +66,7 @@ class EditorHelpSearch : public ConfirmationDialog {

        class Runner;
        Ref<Runner> search;
+       uint64_t search_started = 0;

        struct TreeCache {
                HashMap<String, TreeItem *> item_cache;

For the first, and just remove the results_tree->clear(); line for the second case

No caching, clears tree (this is really what should be compared with, or we'll have leaking objects)

# Open, empty search
EditorHelp search done in 53760 usec

# Type in `CanvasItem`
EditorHelp search done in 18775 usec
EditorHelp search done in 45658 usec
EditorHelp search done in 43117 usec
EditorHelp search done in 29130 usec
EditorHelp search done in 27321 usec
EditorHelp search done in 26683 usec
EditorHelp search done in 24487 usec
EditorHelp search done in 22271 usec
EditorHelp search done in 22081 usec

# Clear search
EditorHelp search done in 21015 usec

# Type in `CanvasItem` again
EditorHelp search done in 13893 usec
EditorHelp search done in 18153 usec
EditorHelp search done in 44394 usec
EditorHelp search done in 41381 usec
EditorHelp search done in 26954 usec
EditorHelp search done in 24407 usec
EditorHelp search done in 21838 usec
EditorHelp search done in 20886 usec
EditorHelp search done in 20379 usec

# Clear search again
EditorHelp search done in 13812 usec

No caching, no clearing of tree

# Open, empty search
EditorHelp search done in 35600 usec

# Type in `CanvasItem`
EditorHelp search done in 12201 usec
EditorHelp search done in 42929 usec
EditorHelp search done in 29858 usec
EditorHelp search done in 27547 usec
EditorHelp search done in 27164 usec
EditorHelp search done in 25825 usec
EditorHelp search done in 23611 usec
EditorHelp search done in 22020 usec
EditorHelp search done in 21571 usec
EditorHelp search done in 21011 usec

# Clear search
EditorHelp search done in 15888 usec

# Type in `CanvasItem` again
EditorHelp search done in 12774 usec
EditorHelp search done in 45586 usec
EditorHelp search done in 29901 usec
EditorHelp search done in 27730 usec
EditorHelp search done in 26266 usec
EditorHelp search done in 25097 usec
EditorHelp search done in 23159 usec
EditorHelp search done in 22676 usec
EditorHelp search done in 21303 usec
EditorHelp search done in 20992 usec

# Clear search again
EditorHelp search done in 15651 usec

Caching

# Open, empty search
EditorHelp search done in 35782 usec

# Type in `CanvasItem`
EditorHelp search done in 9196 usec
EditorHelp search done in 42856 usec
EditorHelp search done in 28382 usec
EditorHelp search done in 26510 usec
EditorHelp search done in 25689 usec
EditorHelp search done in 24787 usec
EditorHelp search done in 22825 usec
EditorHelp search done in 22427 usec
EditorHelp search done in 21719 usec
EditorHelp search done in 21309 usec

# Clear search
EditorHelp search done in 9587 usec

# Type in `CanvasItem` again
EditorHelp search done in 9159 usec
EditorHelp search done in 35010 usec
EditorHelp search done in 29025 usec
EditorHelp search done in 26107 usec
EditorHelp search done in 25557 usec
EditorHelp search done in 24929 usec
EditorHelp search done in 23414 usec
EditorHelp search done in 22101 usec
EditorHelp search done in 21785 usec
EditorHelp search done in 21321 usec

# Clear search again
EditorHelp search done in 9568 usec

I'd appropriate a comparison by you too @YuriSizov if you're able, because it looks like your results don't take this into account from your description and stats

@YuriSizov
Copy link
Contributor

YuriSizov commented Jan 16, 2024

because it looks like your results don't take this into account from your description and stats

Well, as noted I also checked times without this commit completely, and they were in the same ballpark. I think what you demonstrate here aligns with my numbers. Overall you get a ~45-50% improvement on "empty" searches, but the rest of the runs remain consistent with or without the patch. For you it's 20-25k usec, for me it was 30-35k usec.

Unless you see something else in this data?

@AThousandShips
Copy link
Member Author

AThousandShips commented Jan 16, 2024

My bad missed the note that the data was the same, noticed some increase with the clear applied but it's largely equivalent, it was more pronounced on the final one and didn't test as much here and it's largely negligible, it did seem more pronounced especially when clearing a very large tree (see the first search after empty) 🙂

@YuriSizov YuriSizov merged commit 039bc52 into godotengine:master Jan 17, 2024
15 checks passed
@AThousandShips AThousandShips deleted the help_search_index branch January 17, 2024 17:59
@YuriSizov
Copy link
Contributor

Thanks!

@AThousandShips
Copy link
Member Author

Thank you!

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.

2 participants