From 744502033aa4f70f0e6399983cfa1e976f262e3a Mon Sep 17 00:00:00 2001 From: Shiyu Date: Sun, 17 Aug 2025 10:04:12 +0800 Subject: [PATCH] Fix: use lru cache for active files in server (#177) --- docs/clice.toml | 4 + include/Server/Config.h | 1 + include/Server/Server.h | 84 +++++++- src/Server/Document.cpp | 247 ++++++++++++------------ src/Server/Feature.cpp | 29 +-- src/Server/Lifecycle.cpp | 3 + src/Server/Server.cpp | 40 ++++ tests/unit/Server/ActiveFileManager.cpp | 108 +++++++++++ 8 files changed, 368 insertions(+), 148 deletions(-) create mode 100644 tests/unit/Server/ActiveFileManager.cpp diff --git a/docs/clice.toml b/docs/clice.toml index a7328a92..08f58a91 100644 --- a/docs/clice.toml +++ b/docs/clice.toml @@ -13,6 +13,10 @@ # Compile commands directories to search for compile_commands.json files. compile_commands_dirs = ["${workspace}/build"] + # Maximum number of active files to keep in memory. If the number of active files + # exceeds this limit, the least recently used files will be removed. + # The default value is 8. Whatever the number you set, the minimum is 1, the maximum is 512. + max_active_file = 8 # Cache configuration for storing precompiled headers and modules. [cache] diff --git a/include/Server/Config.h b/include/Server/Config.h index 7d53fc8e..5ad9068b 100644 --- a/include/Server/Config.h +++ b/include/Server/Config.h @@ -17,6 +17,7 @@ void init(std::string_view workplace); struct ServerOptions { std::vector compile_commands_dirs = {"${workspace}/build"}; + size_t max_active_file = 8; }; struct CacheOptions { diff --git a/include/Server/Server.h b/include/Server/Server.h index dffc4324..5b207356 100644 --- a/include/Server/Server.h +++ b/include/Server/Server.h @@ -39,6 +39,82 @@ struct OpenFile { std::unique_ptr next; }; +/// A manager for all OpenFile with LRU cache. +class ActiveFileManager { +public: + /// Use shared_ptr to manage the lifetime of OpenFile object in async function. + using ActiveFile = std::shared_ptr; + + /// A double-linked list to store all opened files. While the `first` field of pair (each node + /// of list) refers to a key in `index`, the `second` field refers to the OpenFile object. + /// In another word, the `index` holds the ownership of path and the `items` holds the + /// ownership of OpenFile object. + using ListContainer = std::list>; + + struct ActiveFileIterator : public ListContainer::const_iterator {}; + + constexpr static size_t DefaultMaxActiveFileNum = 8; + constexpr static size_t UnlimitedActiveFileNum = 512; + +public: + /// Create an ActiveFileManager with a default size. + ActiveFileManager() : capability(DefaultMaxActiveFileNum) {} + + ActiveFileManager(const ActiveFileManager&) = delete; + ActiveFileManager& operator= (const ActiveFileManager&) = delete; + + /// Set the maximum active file count and it will be clamped to [1, UnlimitedActiveFileNum]. + void set_capability(size_t size) { + // Use static_cast to make MSVC happy. + capability = std::clamp(size, static_cast(1), UnlimitedActiveFileNum); + } + + /// Get the maximum size of the cache. + size_t max_size() const { + return capability; + } + + /// Get the current size of the cache. + size_t size() const { + return index.size(); + } + + /// Try get OpenFile from manager, default construct one if not exists. + [[nodiscard]] ActiveFile& get_or_add(llvm::StringRef path); + + /// Add a OpenFile to the manager. + ActiveFile& add(llvm::StringRef path, OpenFile file); + + [[nodiscard]] bool contains(llvm::StringRef path) const { + return index.contains(path); + } + + ActiveFileIterator begin() const { + return ActiveFileIterator(items.begin()); + } + + ActiveFileIterator end() const { + return ActiveFileIterator(items.end()); + } + +private: + ActiveFile& lru_put_impl(llvm::StringRef path, OpenFile file); + +private: + /// The maximum size of the cache. + size_t capability; + + /// The first element is the most recently used, and the last + /// element is the least recently used. + /// When a file is accessed, it will be moved to the front of the list. + /// When a new file is added, if the size exceeds the maximum size, + /// the last element will be removed. + ListContainer items; + + /// A map from path to the iterator of the list. + llvm::StringMap index; +}; + class Server { public: Server(); @@ -106,10 +182,10 @@ private: async::Task<> build_ast(std::string file, std::string content); - async::Task add_document(std::string path, std::string content); + async::Task> add_document(std::string path, std::string content); private: - async::Task<> publish_diagnostics(std::string path, OpenFile* file); + async::Task<> publish_diagnostics(std::string path, std::shared_ptr file); async::Task<> on_did_open(proto::DidOpenTextDocumentParams params); @@ -146,8 +222,8 @@ private: /// The compilation database. CompilationDatabase database; - /// All opening files, TODO: use a LRU cache. - llvm::StringMap opening_files; + /// All opening files. + ActiveFileManager opening_files; PathMapping mapping; }; diff --git a/src/Server/Document.cpp b/src/Server/Document.cpp index ff4f599b..b4d56221 100644 --- a/src/Server/Document.cpp +++ b/src/Server/Document.cpp @@ -67,7 +67,7 @@ void Server::load_cache_info() { } /// Update the PCH info. - opening_files[*file].pch = std::move(info); + opening_files.get_or_add(*file)->pch = std::move(info); } } @@ -80,11 +80,11 @@ void Server::save_cache_info() { json["pchs"] = json::Array(); for(auto& [file, open_file]: opening_files) { - if(!open_file.pch) { + if(!open_file->pch) { continue; } - auto& pch = *open_file.pch; + auto& pch = *open_file->pch; json::Object object; object["file"] = file; object["path"] = pch.path; @@ -104,7 +104,11 @@ void Server::save_cache_info() { return; } - auto clean_up = llvm::make_scope_exit([&temp_path]() { llvm::sys::fs::remove(temp_path); }); + auto clean_up = llvm::make_scope_exit([&temp_path]() { + if(auto errc = llvm::sys::fs::remove(temp_path); errc != std::error_code{}) { + log::warn("Fail to remove temporary file: {}", errc.message()); + } + }); std::error_code EC; llvm::raw_fd_ostream os(temp_path, EC, llvm::sys::fs::OF_None); @@ -132,117 +136,120 @@ void Server::save_cache_info() { log::info("Save cache info successfully"); } +namespace { + +bool check_pch_update(llvm::StringRef content, + std::uint32_t bound, + CompilationDatabase::LookupInfo& info, + PCHInfo& pch) { + if(content.substr(0, bound) != pch.preamble) { + return true; + } + + if(info.arguments != pch.arguments) { + return true; + } + + /// Check deps. + for(auto& dep: pch.deps) { + fs::file_status status; + auto error = fs::status(dep, status, true); + if(error || std::chrono::duration_cast( + status.getLastModificationTime().time_since_epoch()) + .count() > pch.mtime) { + return true; + } + } + + return false; +} + +/// The actual PCH build task. +async::Task build_pch_task(CompilationDatabase::LookupInfo& info, + std::shared_ptr open_file, + std::string path, + std::uint32_t bound, + std::string content, + std::shared_ptr> diagnostics) { + if(!fs::exists(config::cache.dir)) { + auto error = fs::create_directories(config::cache.dir); + if(error) { + log::warn("Fail to create directory for PCH building: {}", config::cache.dir); + co_return false; + } + } + + /// Everytime we build a new pch, the old diagnostics should be discarded. + diagnostics->clear(); + + CompilationParams params; + params.output_file = path::join(config::cache.dir, path::filename(path) + ".pch"); + params.arguments = std::move(info.arguments); + params.diagnostics = diagnostics; + params.add_remapped_file(path, content, bound); + + PCHInfo pch; + + std::string command; + for(auto argument: params.arguments) { + command += " "; + command += argument; + } + + log::info("Start building PCH for {}, command: [{}]", path, command); + + std::string message; + std::vector links; + + bool success = co_await async::submit([¶ms, &pch, &message, &links] -> bool { + /// PCH file is written until destructing, Add a single block + /// for it. + auto unit = compile(params, pch); + if(!unit) { + message = std::move(unit.error()); + return false; + } + + links = feature::document_links(*unit); + /// TODO: index PCH file, etc + return true; + }); + + if(!success) { + log::warn("Building PCH fails for {}, Because: {}", path, message); + for(auto& diagnostic: *diagnostics) { + log::warn("{}", diagnostic.message); + } + co_return false; + } + + log::info("Building PCH successfully for {}", path); + + /// Update the built PCH info. + open_file->pch = std::move(pch); + open_file->pch_includes = std::move(links); + + /// Resume waiters on this event. + open_file->pch_built_event.set(); + open_file->pch_built_event.clear(); + + co_return true; +}; + +} // namespace + async::Task Server::build_pch(std::string file, std::string content) { auto bound = compute_preamble_bound(content); - - auto open_file = &opening_files[file]; auto info = database.get_command(file, true, true); - - auto check_pch_update = [&content, &bound, &info](PCHInfo& pch) { - if(content.substr(0, bound) != pch.preamble) { - return true; - } - - if(info.arguments != pch.arguments) { - return true; - } - - /// Check deps. - for(auto& dep: pch.deps) { - fs::file_status status; - auto error = fs::status(dep, status, true); - if(error || std::chrono::duration_cast( - status.getLastModificationTime().time_since_epoch()) - .count() > pch.mtime) { - return true; - } - } - - return false; - }; + auto& open_file = opening_files.get_or_add(file); /// Check update ... - if(open_file->pch && !check_pch_update(*open_file->pch)) { + if(open_file->pch && !check_pch_update(content, bound, info, *open_file->pch)) { /// If not need update, return directly. log::info("PCH is already up-to-date for {}", file); co_return true; } - /// The actual PCH build task. - constexpr static auto PCHBuildTask = - [](CompilationDatabase::LookupInfo& info, - OpenFile* open_file, - std::string path, - std::uint32_t bound, - std::string content, - std::shared_ptr> diagnostics) -> async::Task { - if(!fs::exists(config::cache.dir)) { - auto error = fs::create_directories(config::cache.dir); - if(error) { - log::warn("Fail to create directory for PCH building: {}", config::cache.dir); - co_return false; - } - } - - /// Everytime we build a new pch, the old diagnostics should be discarded. - diagnostics->clear(); - - CompilationParams params; - params.output_file = path::join(config::cache.dir, path::filename(path) + ".pch"); - params.arguments = std::move(info.arguments); - params.diagnostics = diagnostics; - params.add_remapped_file(path, content, bound); - - PCHInfo pch; - - std::string command; - for(auto argument: params.arguments) { - command += " "; - command += argument; - } - - log::info("Start building PCH for {}, command: [{}]", path, command); - - std::string message; - std::vector links; - - bool success = co_await async::submit([¶ms, &pch, &message, &links] -> bool { - /// PCH file is written until destructing, Add a single block - /// for it. - auto unit = compile(params, pch); - if(!unit) { - message = std::move(unit.error()); - return false; - } - - links = feature::document_links(*unit); - /// TODO: index PCH file, etc - return true; - }); - - if(!success) { - log::warn("Building PCH fails for {}, Because: {}", path, message); - for(auto& diagnostic: *diagnostics) { - log::warn("{}", diagnostic.message); - } - co_return false; - } - - log::info("Building PCH successfully for {}", path); - - /// Update the built PCH info. - open_file->pch = std::move(pch); - open_file->pch_includes = std::move(links); - - /// Resume waiters on this event. - open_file->pch_built_event.set(); - open_file->pch_built_event.clear(); - - co_return true; - }; - - open_file = &opening_files[file]; - /// If there is already an PCH build task, cancel it. auto& task = open_file->pch_build_task; if(!task.empty()) { @@ -257,13 +264,10 @@ async::Task Server::build_pch(std::string file, std::string content) { } /// Schedule the new building task. - task = PCHBuildTask(info, open_file, file, bound, std::move(content), open_file->diagnostics); - + task = build_pch_task(info, open_file, file, bound, std::move(content), open_file->diagnostics); if(co_await task) { - /// FIXME: At this point, task has already been finished, destroy it - /// directly. + /// FIXME: At this point, task has already been finished, destroy it directly. task.release().destroy(); - co_return true; } @@ -272,7 +276,7 @@ async::Task Server::build_pch(std::string file, std::string content) { } async::Task<> Server::build_ast(std::string path, std::string content) { - auto file = &opening_files[path]; + auto file = opening_files.get_or_add(path); /// Try get the lock, the waiter on the lock will be resumed when /// guard is destroyed. @@ -284,12 +288,11 @@ async::Task<> Server::build_ast(std::string path, std::string content) { co_return; } - auto pch = opening_files[path].pch; + auto pch = file->pch; if(!pch) { log::fatal("Expected PCH built at this point"); } - file = &opening_files[path]; CompilationParams params; params.arguments = database.get_command(path, true, true).arguments; params.add_remapped_file(path, content); @@ -311,7 +314,6 @@ async::Task<> Server::build_ast(std::string path, std::string content) { /// FIXME: Index the source file. /// co_await indexer.index(*ast); - file = &opening_files[path]; /// Update built AST info. file->ast = std::make_shared(std::move(*ast)); @@ -321,11 +323,11 @@ async::Task<> Server::build_ast(std::string path, std::string content) { log::info("Building AST successfully for {}", path); } -async::Task Server::add_document(std::string path, std::string content) { - auto& openFile = opening_files[path]; - openFile.content = content; +async::Task> Server::add_document(std::string path, std::string content) { + auto& openFile = opening_files.get_or_add(path); + openFile->content = content; - auto& task = openFile.ast_build_task; + auto& task = openFile->ast_build_task; /// If there is already an AST build task, cancel it. if(!task.empty()) { @@ -343,12 +345,11 @@ async::Task Server::add_document(std::string path, std::string conten task = build_ast(std::move(path), std::move(content)); task.schedule(); - co_return &opening_files[path]; + co_return openFile; } -async::Task<> Server::publish_diagnostics(std::string path, OpenFile* file) { +async::Task<> Server::publish_diagnostics(std::string path, std::shared_ptr file) { auto guard = co_await file->ast_built_lock.try_lock(); - file = &opening_files[path]; if(file->ast) { auto diagnostics = feature::diagnostics(kind, mapping, *file->ast); co_await notify("textDocument/publishDiagnostics", @@ -363,7 +364,7 @@ async::Task<> Server::on_did_open(proto::DidOpenTextDocumentParams params) { auto path = mapping.to_path(params.textDocument.uri); auto file = co_await add_document(path, std::move(params.textDocument.text)); if(file->diagnostics) { - co_await publish_diagnostics(path, file); + co_await publish_diagnostics(path, std::move(file)); } co_return; } @@ -372,7 +373,7 @@ async::Task<> Server::on_did_change(proto::DidChangeTextDocumentParams params) { auto path = mapping.to_path(params.textDocument.uri); auto file = co_await add_document(path, std::move(params.contentChanges[0].text)); if(file->diagnostics) { - co_await publish_diagnostics(path, file); + co_await publish_diagnostics(path, std::move(file)); } co_return; } diff --git a/src/Server/Feature.cpp b/src/Server/Feature.cpp index 6b586f55..c891a2e0 100644 --- a/src/Server/Feature.cpp +++ b/src/Server/Feature.cpp @@ -12,17 +12,15 @@ namespace clice { async::Task Server::on_completion(proto::CompletionParams params) { auto path = mapping.to_path(params.textDocument.uri); - auto opening_file = &opening_files[path]; - auto content = opening_file->content; - auto offset = to_offset(kind, content, params.position); + auto opening_file = opening_files.get_or_add(path); if(!opening_file->pch_build_task.empty()) { co_await opening_file->pch_built_event; } - opening_file = &opening_files[path]; + auto& content = opening_file->content; + auto offset = to_offset(kind, content, params.position); auto& pch = opening_file->pch; - { /// Set compilation params ... . CompilationParams params; @@ -40,13 +38,10 @@ async::Task Server::on_completion(proto::CompletionParams params) { async::Task Server::on_hover(proto::HoverParams params) { auto path = mapping.to_path(params.textDocument.uri); - auto opening_file = &opening_files[path]; - + auto opening_file = opening_files.get_or_add(path); auto guard = co_await opening_file->ast_built_lock.try_lock(); auto offset = to_offset(kind, opening_file->content, params.position); - opening_file = &opening_files[path]; - auto ast = opening_file->ast; if(!ast) { co_return json::Value(nullptr); @@ -65,11 +60,9 @@ async::Task Server::on_hover(proto::HoverParams params) { async::Task Server::on_document_symbol(proto::DocumentSymbolParams params) { auto path = mapping.to_path(params.textDocument.uri); - auto opening_file = &opening_files[path]; + auto opening_file = opening_files.get_or_add(path); auto guard = co_await opening_file->ast_built_lock.try_lock(); - opening_file = &opening_files[path]; - auto ast = opening_file->ast; if(!ast) { co_return json::Value(nullptr); @@ -115,9 +108,7 @@ async::Task Server::on_document_symbol(proto::DocumentSymbolParams async::Task Server::on_document_link(proto::DocumentLinkParams params) { auto path = mapping.to_path(params.textDocument.uri); - auto opening_file = &opening_files[path]; - opening_file = &opening_files[path]; - + auto opening_file = opening_files.get_or_add(path); auto guard = co_await opening_file->ast_built_lock.try_lock(); auto ast = opening_file->ast; @@ -146,10 +137,9 @@ async::Task Server::on_document_link(proto::DocumentLinkParams para async::Task Server::on_folding_range(proto::FoldingRangeParams params) { auto path = mapping.to_path(params.textDocument.uri); - auto opening_file = &opening_files[path]; + auto opening_file = opening_files.get_or_add(path); auto guard = co_await opening_file->ast_built_lock.try_lock(); - opening_file = &opening_files[path]; auto ast = opening_file->ast; if(!ast) { co_return json::Value(nullptr); @@ -185,12 +175,9 @@ async::Task Server::on_folding_range(proto::FoldingRangeParams para async::Task Server::on_semantic_token(proto::SemanticTokensParams params) { auto path = mapping.to_path(params.textDocument.uri); - auto opening_file = &opening_files[path]; - + auto opening_file = opening_files.get_or_add(path); auto guard = co_await opening_file->ast_built_lock.try_lock(); - opening_file = &opening_files[path]; - auto ast = opening_file->ast; if(!ast) { co_return json::Value(nullptr); diff --git a/src/Server/Lifecycle.cpp b/src/Server/Lifecycle.cpp index 531e10be..6e9221ab 100644 --- a/src/Server/Lifecycle.cpp +++ b/src/Server/Lifecycle.cpp @@ -18,6 +18,9 @@ async::Task Server::on_initialize(proto::InitializeParams params) { /// Initialize configuration. config::init(workspace); + /// Set server options. + opening_files.set_capability(config::server.max_active_file); + /// Load compile commands.json for(auto& dir: config::server.compile_commands_dirs) { auto content = fs::read(dir + "/compile_commands.json"); diff --git a/src/Server/Server.cpp b/src/Server/Server.cpp index d2936a8e..c00933d7 100644 --- a/src/Server/Server.cpp +++ b/src/Server/Server.cpp @@ -3,6 +3,46 @@ namespace clice { +ActiveFileManager::ActiveFile& ActiveFileManager::lru_put_impl(llvm::StringRef path, + OpenFile file) { + /// If the file is not in the chain, create a new OpenFile. + if(items.size() >= capability) { + /// If the size exceeds the maximum size, remove the last element. + index.erase(items.back().first); + items.pop_back(); + } + items.emplace_front(path, std::make_shared(std::move(file))); + + // fix the ownership of the StringRef of the path. + auto [added, _] = index.insert({path, items.begin()}); + items.front().first = added->getKey(); + + return items.front().second; +} + +ActiveFileManager::ActiveFile& ActiveFileManager::get_or_add(llvm::StringRef path) { + auto iter = index.find(path); + if(iter == index.end()) { + return lru_put_impl(path, OpenFile{}); + } + + // If the file is in the chain, move it to the front. + items.splice(items.begin(), items, iter->second); + return iter->second->second; +} + +ActiveFileManager::ActiveFile& ActiveFileManager::add(llvm::StringRef path, OpenFile file) { + auto iter = index.find(path); + if(iter == index.end()) { + return lru_put_impl(path, std::move(file)); + } + iter->second->second = std::make_shared(std::move(file)); + + // If the file is in the chain, move it to the front. + items.splice(items.begin(), items, iter->second); + return iter->second->second; +} + async::Task<> Server::request(llvm::StringRef method, json::Value params) { co_await async::net::write(json::Object{ {"jsonrpc", "2.0" }, diff --git a/tests/unit/Server/ActiveFileManager.cpp b/tests/unit/Server/ActiveFileManager.cpp new file mode 100644 index 00000000..db48387c --- /dev/null +++ b/tests/unit/Server/ActiveFileManager.cpp @@ -0,0 +1,108 @@ +#include "Test/Test.h" +#include "Server/Server.h" + +namespace clice::testing { + +namespace { + +suite<"ActiveFileManager"> active_file_manager = [] { + using Manager = ActiveFileManager; + + test("MaxSize") = [] { + Manager actives; + + expect(that % actives.max_size() == Manager::DefaultMaxActiveFileNum); + + actives.set_capability(0); + expect(that % actives.max_size() == 1); + + actives.set_capability(std::numeric_limits::max()); + expect(that % actives.max_size() <= Manager::UnlimitedActiveFileNum); + }; + + test("LruAlgorithm") = [] { + Manager actives; + actives.set_capability(1); + + expect(that % actives.size() == 0); + + auto& first = actives.add("first", OpenFile{.version = 1}); + expect(that % actives.size() == 1); + expect(that % actives.contains("first") == true); + expect(that % first->version == 1); + + auto& second = actives.add("second", OpenFile{.version = 2}); + expect(that % actives.size() == 1); + }; + + test("IteratorBasic") = [] { + Manager actives; + actives.set_capability(3); + + actives.add("first", OpenFile{.version = 1}); + actives.add("second", OpenFile{.version = 2}); + actives.add("third", OpenFile{.version = 3}); + expect(that % actives.size() == 3); + + auto iter = actives.begin(); + expect(that % iter != actives.end()); + expect(that % iter->first == "third"); + expect(that % iter->second->version == 3); + + iter++; + expect(that % iter != actives.end()); + expect(that % iter->first == "second"); + expect(that % iter->second->version == 2); + + iter++; + expect(that % iter != actives.end()); + expect(that % iter->first == "first"); + expect(that % iter->second->version == 1); + + iter++; + expect(iter == actives.end()); + }; + + test("IteratorCheck") = [] { + ActiveFileManager manager; + + constexpr static size_t TotalInsertedNum = 10; + constexpr static size_t MaxActiveFileNum = 3; + manager.set_capability(MaxActiveFileNum); + + // insert file from (1 .. TotalInsertedNum). + // so there should be (TotalInsertedNum - MaxActiveFileNum) after inserted + for(uint32_t i = 1; i <= TotalInsertedNum; i++) { + std::string fpath = std::format("{}", i); + OpenFile object{.version = i}; + + auto& inseted = manager.add(fpath, std::move(object)); + std::optional new_added_entry = manager.get_or_add(fpath); + expect(that % new_added_entry.has_value()); + auto new_added = std::move(new_added_entry).value(); + expect(that % inseted == new_added); + expect(that % new_added != nullptr); + expect(that % new_added->version == i); + + auto& [path, openfile] = *manager.begin(); + expect(that % path == fpath); + expect(that % openfile->version == new_added->version); + } + + expect(that % manager.size() == manager.max_size()); + + // the remain file should be in reversed order. + auto iter = manager.begin(); + int i = TotalInsertedNum; + while(iter != manager.end()) { + auto& [path, openfile] = *iter; + expect(that % path == std::to_string(i)); + expect(that % openfile->version == i); + iter++; + i--; + } + }; +}; + +} // namespace +} // namespace clice::testing