Merge pull request #114559 from HolonProduction/lsp/fix-performance-scene-loading-workspace-completion

LSP: Fix loading scene for every request on script file for workspace completion
This commit is contained in:
Thaddeus Crews 2026-02-06 08:18:04 -06:00
commit 56f3e2611d
No known key found for this signature in database
GPG key ID: 8C6E5FEB5FC03CCC
8 changed files with 276 additions and 58 deletions

View file

@ -148,6 +148,9 @@ Error GDScriptLanguageProtocol::on_client_connected() {
void GDScriptLanguageProtocol::on_client_disconnected(const int &p_client_id) {
clients.erase(p_client_id);
if (clients.is_empty()) {
scene_cache.clear();
}
EditorNode::get_log()->add_message("[LSP] Disconnected", EditorLog::MSG_TYPE_EDITOR);
}
@ -269,6 +272,8 @@ void GDScriptLanguageProtocol::poll(int p_limit_usec) {
on_client_connected();
}
scene_cache.poll();
HashMap<int, Ref<LSPeer>>::Iterator E = clients.begin();
while (E != clients.end()) {
Ref<LSPeer> peer = E->value;
@ -315,6 +320,7 @@ void GDScriptLanguageProtocol::stop() {
peer->connection->disconnect_from_host();
}
scene_cache.clear();
server->stop();
}
@ -447,6 +453,8 @@ void GDScriptLanguageProtocol::lsp_did_open(const Dictionary &p_params) {
client->managed_files[path] = document;
client->parse_script(path);
scene_cache.request_load(path);
}
void GDScriptLanguageProtocol::lsp_did_change(const Dictionary &p_params) {
@ -492,6 +500,8 @@ void GDScriptLanguageProtocol::lsp_did_close(const Dictionary &p_params) {
/// A close notification requires a previous open notification to be sent.
ERR_FAIL_COND_MSG(!was_opened, "LSP: Client is closing file without opening it.");
scene_cache.unload(path);
}
void GDScriptLanguageProtocol::resolve_related_symbols(const LSP::TextDocumentPositionParams &p_doc_pos, List<const LSP::DocumentSymbol *> &r_list) {

View file

@ -32,6 +32,7 @@
#include "gdscript_text_document.h"
#include "gdscript_workspace.h"
#include "scene_cache.h"
#include "core/io/stream_peer_tcp.h"
#include "core/io/tcp_server.h"
@ -92,6 +93,7 @@ private:
static GDScriptLanguageProtocol *singleton;
HashMap<int, Ref<LSPeer>> clients;
SceneCache scene_cache;
Ref<TCPServer> server;
int latest_client_id = LSP_NO_CLIENT;
int next_client_id = 0;
@ -119,6 +121,8 @@ public:
_FORCE_INLINE_ static GDScriptLanguageProtocol *get_singleton() { return singleton; }
_FORCE_INLINE_ Ref<GDScriptWorkspace> get_workspace() { return workspace; }
_FORCE_INLINE_ Ref<GDScriptTextDocument> get_text_document() { return text_document; }
_FORCE_INLINE_ SceneCache *get_scene_cache() { return &scene_cache; }
_FORCE_INLINE_ bool is_initialized() const { return _initialized; }
void poll(int p_limit_usec);

View file

@ -41,7 +41,6 @@
#include "editor/editor_node.h"
#include "editor/file_system/editor_file_system.h"
#include "editor/settings/editor_settings.h"
#include "scene/resources/packed_scene.h"
void GDScriptWorkspace::_bind_methods() {
ClassDB::bind_method(D_METHOD("apply_new_signal"), &GDScriptWorkspace::apply_new_signal);
@ -592,51 +591,6 @@ void GDScriptWorkspace::publish_diagnostics(const String &p_path) {
GDScriptLanguageProtocol::get_singleton()->notify_client("textDocument/publishDiagnostics", params);
}
void GDScriptWorkspace::_get_owners(EditorFileSystemDirectory *efsd, String p_path, List<String> &owners) {
if (!efsd) {
return;
}
for (int i = 0; i < efsd->get_subdir_count(); i++) {
_get_owners(efsd->get_subdir(i), p_path, owners);
}
for (int i = 0; i < efsd->get_file_count(); i++) {
Vector<String> deps = efsd->get_file_deps(i);
bool found = false;
for (int j = 0; j < deps.size(); j++) {
if (deps[j] == p_path) {
found = true;
break;
}
}
if (!found) {
continue;
}
owners.push_back(efsd->get_file_path(i));
}
}
Node *GDScriptWorkspace::_get_owner_scene_node(String p_path) {
Node *owner_scene_node = nullptr;
List<String> owners;
_get_owners(EditorFileSystem::get_singleton()->get_filesystem(), p_path, owners);
for (const String &owner : owners) {
NodePath owner_path = owner;
Ref<Resource> owner_res = ResourceLoader::load(String(owner_path));
if (Object::cast_to<PackedScene>(owner_res.ptr())) {
Ref<PackedScene> owner_packed_scene = Ref<PackedScene>(Object::cast_to<PackedScene>(*owner_res));
owner_scene_node = owner_packed_scene->instantiate();
break;
}
}
return owner_scene_node;
}
void GDScriptWorkspace::completion(const LSP::CompletionParams &p_params, List<ScriptLanguage::CodeCompletionOption> *r_options) {
String path = get_file_path(p_params.textDocument.uri);
String call_hint;
@ -644,7 +598,7 @@ void GDScriptWorkspace::completion(const LSP::CompletionParams &p_params, List<S
const ExtendGDScriptParser *parser = GDScriptLanguageProtocol::get_singleton()->get_parse_result(path);
if (parser) {
Node *owner_scene_node = _get_owner_scene_node(path);
Node *owner_scene_node = GDScriptLanguageProtocol::get_singleton()->get_scene_cache()->get(path);
Array stack;
Node *current = nullptr;
@ -670,9 +624,6 @@ void GDScriptWorkspace::completion(const LSP::CompletionParams &p_params, List<S
String code = parser->get_text_for_completion(p_params.position);
GDScriptLanguage::get_singleton()->complete_code(code, path, current, r_options, forced, call_hint);
if (owner_scene_node) {
memdelete(owner_scene_node);
}
}
}

View file

@ -35,15 +35,11 @@
#include "godot_lsp.h"
#include "core/variant/variant.h"
#include "editor/file_system/editor_file_system.h"
class GDScriptWorkspace : public RefCounted {
GDCLASS(GDScriptWorkspace, RefCounted);
private:
void _get_owners(EditorFileSystemDirectory *efsd, String p_path, List<String> &owners);
Node *_get_owner_scene_node(String p_path);
#ifndef DISABLE_DEPRECATED
void didDeleteFiles() {}
Error parse_script(const String &p_path, const String &p_content) {

View file

@ -34,6 +34,15 @@
#include "core/object/class_db.h"
#include "core/templates/list.h"
// Enable additional LSP related logging.
//#define DEBUG_LSP
#ifdef DEBUG_LSP
#define LOG_LSP(...) print_line("[ LSP -", __FILE__, ":", __LINE__, "-", __func__, "] -", ##__VA_ARGS__)
#else
#define LOG_LSP(...)
#endif
namespace LSP {
typedef String DocumentUri;

View file

@ -0,0 +1,186 @@
/**************************************************************************/
/* scene_cache.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#include "scene_cache.h"
#include "godot_lsp.h"
#include "core/io/resource_loader.h"
#include "editor/file_system/editor_file_system.h"
#include "scene/resources/packed_scene.h"
void SceneCache::_get_owner_paths(EditorFileSystemDirectory *p_dir, const String &p_script_path, LocalVector<String> &r_owner_paths) {
if (!p_dir) {
return;
}
for (int i = 0; i < p_dir->get_subdir_count(); i++) {
_get_owner_paths(p_dir->get_subdir(i), p_script_path, r_owner_paths);
}
for (int i = 0; i < p_dir->get_file_count(); i++) {
if (p_dir->get_file_deps(i).has(p_script_path)) {
r_owner_paths.push_back(p_dir->get_file_path(i));
}
}
}
void SceneCache::_finalize_scene_load() {
ERR_FAIL_COND(current_loaded_owner.is_empty() || script_path_queue.is_empty());
Ref<PackedScene> scene_res = ResourceLoader::load_threaded_get(current_loaded_owner);
if (scene_res.is_valid()) {
cache[script_path_queue[0]] = scene_res->instantiate();
} else {
cache[script_path_queue[0]] = nullptr;
}
LOG_LSP("Scene cached for script:", script_path_queue[0]);
LOG_LSP("pending_script_queue length:", script_path_queue.size() - 1);
script_path_queue.remove_at(0);
current_loaded_owner = String();
}
void SceneCache::poll() {
if (current_loaded_owner.is_empty()) {
// No load ongoing, start the next one.
if (EditorFileSystem::get_singleton()->is_scanning() || script_path_queue.is_empty()) {
return;
}
LocalVector<String> owners;
_get_owner_paths(EditorFileSystem::get_singleton()->get_filesystem(), script_path_queue[0], owners);
for (const String &owner : owners) {
if (ResourceLoader::load_threaded_request(owner) == Error::OK) {
current_loaded_owner = owner;
LOG_LSP("Scene load started for:", current_loaded_owner);
break;
}
}
if (current_loaded_owner.is_empty()) {
cache[script_path_queue[0]] = nullptr;
LOG_LSP("No scene found for script:", script_path_queue[0]);
script_path_queue.remove_at(0);
LOG_LSP("pending_script_queue length:", script_path_queue.size());
}
} else {
ERR_FAIL_COND(script_path_queue.is_empty());
// There is an ongoing load. Check the status.
ResourceLoader::ThreadLoadStatus status = ResourceLoader::load_threaded_get_status(current_loaded_owner);
if (status == ResourceLoader::THREAD_LOAD_IN_PROGRESS) {
return;
}
if (status == ResourceLoader::THREAD_LOAD_LOADED) {
_finalize_scene_load();
} else {
LOG_LSP("Scene load failure for:", current_loaded_owner);
cache[script_path_queue[0]] = nullptr;
script_path_queue.remove_at(0);
current_loaded_owner = String();
}
}
}
Node *SceneCache::get(const String &p_script_path) {
if (!script_path_queue.is_empty() && script_path_queue[0] == p_script_path && !current_loaded_owner.is_empty()) {
_finalize_scene_load();
} else {
script_path_queue.erase(p_script_path);
}
if (Node **entry = cache.getptr(p_script_path)) {
return *entry;
}
// Fallback to blocking load. This could happen if the open request was only recently sent.
// TODO: This could also happen when multiple clients are connected.
LocalVector<String> owners;
_get_owner_paths(EditorFileSystem::get_singleton()->get_filesystem(), p_script_path, owners);
for (const String &owner : owners) {
Ref<PackedScene> scene = ResourceLoader::load(owner);
if (scene.is_valid()) {
Node *instance = scene->instantiate();
cache[p_script_path] = instance;
return instance;
}
}
cache[p_script_path] = nullptr;
return nullptr;
}
void SceneCache::request_load(const String &p_script_path) {
if (!cache.has(p_script_path) && !script_path_queue.has(p_script_path)) {
script_path_queue.push_back(p_script_path);
LOG_LSP("Scene load requested for:", p_script_path);
LOG_LSP("pending_script_queue length:", script_path_queue.size());
}
}
void SceneCache::unload(const String &p_script_path) {
if (!script_path_queue.is_empty() && script_path_queue[0] == p_script_path && !current_loaded_owner.is_empty()) {
_ALLOW_DISCARD_ ResourceLoader::load_threaded_get(current_loaded_owner);
script_path_queue.remove_at(0);
current_loaded_owner = String();
} else {
script_path_queue.erase(p_script_path);
}
if (!cache.has(p_script_path)) {
return;
}
memdelete_notnull(cache[p_script_path]);
cache.erase(p_script_path);
LOG_LSP("Cache cleared for path:", p_script_path);
}
void SceneCache::clear() {
if (!current_loaded_owner.is_empty()) {
_ALLOW_DISCARD_ ResourceLoader::load_threaded_get(current_loaded_owner);
current_loaded_owner = String();
}
script_path_queue.clear();
for (const KeyValue<String, Node *> &E : cache) {
memdelete_notnull(E.value);
}
cache.clear();
LOG_LSP("Cache cleared.");
}

View file

@ -0,0 +1,65 @@
/**************************************************************************/
/* scene_cache.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "core/string/ustring.h"
#include "core/templates/hash_map.h"
#include "core/templates/local_vector.h"
class Node;
class EditorFileSystemDirectory;
class PackedScene;
/**
* Used to load and cache scene instances for LSP autocompletion.
*
* This implementation is not thread safe.
*/
class SceneCache {
// Always contains the path to the scene which is currently loaded via the `ResourceLoader`.
// If this is not empty, `script_path_queue` must have at least one element.
String current_loaded_owner;
LocalVector<String> script_path_queue;
HashMap<String, Node *> cache;
void _get_owner_paths(EditorFileSystemDirectory *p_dir, const String &p_script_path, LocalVector<String> &r_owner_paths);
void _finalize_scene_load();
public:
void poll();
void clear();
void request_load(const String &p_script_path);
void unload(const String &p_script_path);
Node *get(const String &p_script_path);
};

View file

@ -42,10 +42,7 @@
#include "../language_server/godot_lsp.h"
#include "core/io/dir_access.h"
#include "core/io/file_access_pack.h"
#include "core/os/os.h"
#include "editor/doc/editor_help.h"
#include "editor/editor_node.h"
#include "editor/file_system/editor_file_system.h"
#include "modules/gdscript/gdscript_analyzer.h"
#include "modules/regex/regex.h"