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

Fix loading scene for every request on script file for workspace completion #101091

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

dekaravanhoc
Copy link

@dekaravanhoc dekaravanhoc commented Jan 3, 2025

For workspace completion the Scene for the script is loaded for every completion request and freed afterwards. Resulting in performance issues for scenes including large resources.

Loaded Scenes are now cached for each script. Not loading them again and only freeing them when the connection is terminated or the language server stopped.

This PR fixes #100934


I have no experience in C++ development - feedback is of course more than welcome.
Unit Tests are green and auto formatting should have worked too.

Bugsquad edit (keywords for easier searching): LSP, language server

@dekaravanhoc dekaravanhoc requested a review from a team as a code owner January 3, 2025 19:59
@dekaravanhoc dekaravanhoc force-pushed the lsp/fix_performance_scene_loading_workspace_completion branch from 186fd24 to c2ac0a5 Compare January 3, 2025 20:30
@HolonProduction
Copy link
Member

This should remove the delay for subsequent completion requests, the delay would still be there for the initial request. I think we could improve on this by using the textDocument/didOpen and textDocument/didClose signals to manage the cache. This way Godot could start loading the owner, when the script gets opened. Depending on the time between opening and typing something (which can be quite a bit in bigger projects from my experience) this might completely remove the delay or decrease it a bit.

Also Godot resource loading system has a builtin cache which would definitely come in handy with the preloading approach. As long as the loaded resource isn't freed it will be reused in the future. We could just cache the references to the loaded scene and re instantiate it since this shouldn't cause much overhead (this would also allow replacing SceneCache with a normal HashMap).

This could work kinda like this:

  • in didOpen:
    • use ResourceLoader.load_threaded_request to start loading the owner
    • add an invalid Ref to the hashmap for the path (we need to keep track of this in case the user opens files but does never complete anything)
  • in completion:
    • use ResourceLoader.load_threaded_request again to make sure a loading process is going on for completions after the first one
    • obtain the resource with ResourceLoader.load_threaded_get (blocking like before if the resource isn't fully loaded yet)
    • then safe the resource into the hash map under the path (this will keep the resource in memory thus automatically using the resource loader cache in the future)
  • in didClose (and on disconnect for external editor crashes):
    • remove entry from hash map and if entry was invalid use ResourceLoader.load_threaded_get to prevent leaking results

@dekaravanhoc
Copy link
Author

Hey thanks,

the SceneCache is only a wrapper for a HashMap with a Scene specific clear() function.

Also as these Scenes are loaded resources and their sub resources also, the ResourceLoader already uses the cache for all of them, if there are used in another loaded Scene.

For the rest, sounds reasonable. I'll look into it. Also I have really fast completion times even at the beginning now with a production build and large resources (~1GB loaded), as the main slowdown came from the queue of loading->freeing->loading->freeing.

@HolonProduction
Copy link
Member

Also as these Scenes are loaded resources and their sub resources also, the ResourceLoader already uses the cache for all of them, if there are used in another loaded Scene.

Yeah right, my main reason for suggesting only relying on the resource loader cache was the idea with the loading headstart, which might get harder to implement when caching the nodes.

For the rest, sounds reasonable. I'll look into it. Also I have really fast completion times even at the beginning now with a production build and large resources (~1GB loaded), as the main slowdown came from the queue of loading->freeing->loading->freeing.

That's interesting, I still get the same slow down on the first completion request, with this PR and my MRP (no production build). Maybe we are hitting different bottle necks with loading 🤔

@dekaravanhoc
Copy link
Author

That's interesting, I still get the same slow down on the first completion request, with this PR and my MRP (no production build). Maybe we are hitting different bottle necks with loading 🤔

Ah k. I used a production build (ucrt). On my dev build it was in deed still very slow, even for the first input. But I did not stop time, so could just be my imagination over being happy that it worked. 🙃
That said, I think, its a good idea to load the scene when opening the script.

@AThousandShips AThousandShips added this to the 4.x milestone Jan 4, 2025
@dekaravanhoc dekaravanhoc force-pushed the lsp/fix_performance_scene_loading_workspace_completion branch from c2ac0a5 to 44565a1 Compare January 6, 2025 12:41
@dekaravanhoc
Copy link
Author

I moved all logic for the Scene loading into its own class SceneCache inside language_server_protocol.

Threaded resource Loading is added and used, when "use thread" is activated for the language server.
Its quite nice, as the loading now does not block the server for auto completion etc. And as far as I can see, the Scene is only used for Node path completion - definitively a win on scenes with large resources.

Using threaded resource loading is a bit of a hassle, as there is no option to kill a specific running task. (Or at least I did not found one.) So the server needs to wait for a running task and clean it afterwards, if the client disconnects or closes the document.
Also running multiple tasks in parallel that might load the same sub resources can bring up errors in the editor, so I opted for one task at a time.

But for now, I think it works quite well.

@dekaravanhoc dekaravanhoc force-pushed the lsp/fix_performance_scene_loading_workspace_completion branch from 44565a1 to bb22257 Compare January 6, 2025 15:50
Copy link
Member

@HolonProduction HolonProduction left a comment

Choose a reason for hiding this comment

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

In general I think that when the completion request arrives and the scene isn't loaded yet, we should not continue without owner and instead wait for loading to finish. The owner is not only used for node path completion, but also for argument options (e.g. suggesting animation names when doing $AnimationPlayer.play()).

Otherwise two subsequent completion requests could lead to different results, I think that's not really great for the user experience.

@dekaravanhoc
Copy link
Author

Hey, thank you very much for reviewing. :)

Ah ok, I did not look to deep into the usage of the scene itself for auto completion. I get were you are coming from, but I think it is the better user experience to instantly get an auto completion from the language server after opening a script.
Completion suggestions change in both cases. If you wait it changes basically every time from only editor suggestions to editor suggestion + ls. If you do not wait it only changes for the specific completions.
I think the coolest thing, about the threaded resource load is, that it does not block any other language server operations.

@dekaravanhoc dekaravanhoc force-pushed the lsp/fix_performance_scene_loading_workspace_completion branch from bb22257 to 8149c3b Compare January 10, 2025 13:41
@dekaravanhoc dekaravanhoc force-pushed the lsp/fix_performance_scene_loading_workspace_completion branch from 8149c3b to 2cadadd Compare January 30, 2025 14:27
@dekaravanhoc
Copy link
Author

Hey,

so from my side it is finished. Worked the last weeks with this in 4.3 and in master and got no problems.
Let me know if something is off.

Best regards :)

@akien-mga
Copy link
Member

Just a quick update, this still needs a maintainer approval, but as a side note:

Your commit seems not to be linked to your GitHub account. See: Why are my commits linked to the wrong user? for more info.

…letion

For workspace completion the Scene for the script is loaded for every
completion request and freed afterwards. Resulting in performance issues
for scenes including large resources.

Loaded Scenes are now cached for each script and loaded when opening the
script. Not loading them again and only freeing them when the connection
is terminated, the language server stopped or the script is closed.

Add use of a thread for resource loading

If use thread is enabled in the language server, threaded load from
ResourceLoader will be utilized.

Only one load task will be running at a time.

Remove unused header
@dekaravanhoc dekaravanhoc force-pushed the lsp/fix_performance_scene_loading_workspace_completion branch from 2cadadd to 8693381 Compare February 22, 2025 18:39
@dekaravanhoc
Copy link
Author

Just a quick update, this still needs a maintainer approval, but as a side note:

Your commit seems not to be linked to your GitHub account. See: Why are my commits linked to the wrong user? for more info.

Thanks for the heads up. I added the e-mail adress. Should be working now.
Also pushed the latest rebased state.

Copy link
Member

@HolonProduction HolonProduction left a comment

Choose a reason for hiding this comment

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

Sorry for the long delay, had some exams to write.

I tested the PR locally and it seems to work fine.

I have to come back to one thing I brought up before though:

In general I think that when the completion request arrives and the scene isn't loaded yet, we should not continue without owner and instead wait for loading to finish.

I was going to say, that this is not a deal breaker for me, but I read a bit more into the spec due to some other PR and noticed that implementation of file management (didOpen/didClose) is not required. I don't know any editor that does not implement those, but since it is part of the spec we should have a contingency plan for it. The easiest one, would be to block the current thread and finish loading inside of get (this would only happen on the first request, and not at all if didOpen is implemented and finished before).

Comment on lines +34 to +35
#include "core/error/error_list.h"
#include "core/io/resource_loader.h"
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
#include "core/error/error_list.h"
#include "core/io/resource_loader.h"

Comment on lines +34 to +35
#include "core/templates/hash_map.h"
#include "core/variant/array.h"
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
#include "core/templates/hash_map.h"
#include "core/variant/array.h"

friend class GDScriptLanguageProtocol;

HashMap<String, Node *> cache;
Ref<GDScriptWorkspace> workspace;
Copy link
Member

Choose a reason for hiding this comment

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

The whole LSP is managed through a singleton. No need to keep a reference here, the workspace can be accessed through GDScriptLanguageProtocol::get_singleton()->get_workspace() instead.

@@ -345,4 +351,209 @@ GDScriptLanguageProtocol::GDScriptLanguageProtocol() {
set_scope("completionItem", text_document.ptr());
set_scope("workspace", workspace.ptr());
workspace->root = ProjectSettings::get_singleton()->get_resource_path();
scene_cache.workspace = workspace;
}

Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
//-----------------------------------------------------------------------------

Copy link
Member

Choose a reason for hiding this comment

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

Not sure what did go wrong here, and why github is also marking code. I just want to add a separator between the two classes.

scene_cache.workspace = workspace;
}

void SceneCache::_get_owners(EditorFileSystemDirectory *p_efsd, const String &p_path, List<String> &r_owners) {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
void SceneCache::_get_owners(EditorFileSystemDirectory *p_efsd, const String &p_path, List<String> &r_owners) {
void SceneCache::_get_owners(EditorFileSystemDirectory *p_dir, const String &p_path, List<String> &r_owners) {

There already is a problem with cryptic names in parts of the codebase and efsd doesn't seem like any commonly known abbreviation. Let's use a name that doesn't require looking up the type to understand.

Comment on lines +552 to +554
if (E.value) {
memdelete(E.value);
}
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
if (E.value) {
memdelete(E.value);
}
memdelete_notnull(E.value);

Comment on lines +75 to +76
void set(const String &p_path);
void set_for_uri(const String &p_uri);
Copy link
Member

Choose a reason for hiding this comment

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

Please rename those to something like request or enqueue, to make clear that this does not instantly set the value.

@@ -82,6 +83,7 @@ void GDScriptTextDocument::didChange(const Variant &p_param) {
doc.text = evt.text;
}
sync_script_content(doc.uri, doc.text);
GDScriptLanguageProtocol::get_singleton()->get_scene_cache()->get_for_uri(doc.uri);
Copy link
Member

Choose a reason for hiding this comment

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

The return value is discarded. Why do we need this here?

* Does only one threaded request to the ResourceLoader at a time.
* Because loading the same subresources in parallel can bring up errors in the editor.
* */
void SceneCache::_add_owner_scene_request(String p_path = "") {
Copy link
Member

Choose a reason for hiding this comment

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

This method is doing too much (otherwise you wouldn't need a private default value).

We should split this up in one method for enqueuing new paths. And one for updating the queue. (The former can obviously call to the later)

}
}

void SceneCache::_check_thread_for_cache_update() {
Copy link
Member

Choose a reason for hiding this comment

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

Splitting this function up would make sense. The part beginning from ResourceLoader::load_threaded_get should be its own function. This would allow to easily remove the custom spin locks from erase and clear.

scene_cache.workspace = workspace;
}

void SceneCache::_get_owners(EditorFileSystemDirectory *p_efsd, const String &p_path, List<String> &r_owners) {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
void SceneCache::_get_owners(EditorFileSystemDirectory *p_efsd, const String &p_path, List<String> &r_owners) {
void SceneCache::_get_owners(EditorFileSystemDirectory *p_efsd, const String &p_path, LocalVector<String> &r_owners) {

List should not be used in new Code anymore as per the c++ usage guidelines

@HolonProduction
Copy link
Member

Instead of marking all usages: List should not be used in new Code anymore as per the c++ usage guidelines. The best way to do this here, would probably to use a LocalVector instead.

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.

LSP performance issues on "slower" devices
4 participants