diff --git a/src/code_completion.cc b/src/code_completion.cc index acdc0e0a..d48dc084 100644 --- a/src/code_completion.cc +++ b/src/code_completion.cc @@ -238,9 +238,9 @@ void BuildDetailString(CXCompletionString completion_string, std::string& label, void EnsureDocumentParsed(CompletionSession* session, std::unique_ptr* tu, - std::unique_ptr* index) { - // Nothing to do. We already have a translation unit and an index. - if (*tu && *index) + clang::Index* index) { + // Nothing to do. We already have a translation unit. + if (*tu) return; std::vector args = session->file.args; @@ -249,40 +249,51 @@ void EnsureDocumentParsed(CompletionSession* session, std::vector unsaved = session->working_files->AsUnsavedFiles(); std::cerr << "[complete] Creating completion session with arguments " << StringJoin(args) << std::endl; - *index = MakeUnique(0 /*excludeDeclarationsFromPCH*/, 0 /*displayDiagnostics*/); - *tu = MakeUnique(index->get(), session->file.filename, args, unsaved, Flags()); + *tu = MakeUnique(index, session->file.filename, args, unsaved, Flags()); std::cerr << "[complete] Done creating active; did_fail=" << (*tu)->did_fail << std::endl; } void CompletionParseMain(CompletionManager* completion_manager) { while (true) { // Fetching the completion request blocks until we have a request. - std::unique_ptr path = completion_manager->reparse_request.Take(); + CompletionManager::ParseRequest request = completion_manager->parse_requests_.Dequeue(); - CompletionSession* session = completion_manager->GetOrOpenSession(*path); + // If we don't get a session then that means we don't care about the file + // anymore - abandon the request. + CompletionSession* session = completion_manager->TryGetSession(request.path, false /*create_if_needed*/); + if (!session) + continue; + + // If we've parsed it more recently than the request time, don't bother + // reparsing. + if (session->tu_last_parsed_at && + *session->tu_last_parsed_at > request.request_time) { + continue; + } + std::unique_ptr parsing; - std::unique_ptr parsing_index; + EnsureDocumentParsed(session, &parsing, &session->index); - EnsureDocumentParsed(session, &parsing, &parsing_index); - - // Swap out active. - std::lock_guard lock(session->usage_lock); - session->active = std::move(parsing); - session->active_index = std::move(parsing_index); + // Activate new translation unit. + // tu_last_parsed_at is only read by this thread, so it doesn't need to be under the mutex. + session->tu_last_parsed_at = std::chrono::high_resolution_clock::now(); + std::lock_guard lock(session->tu_lock); + session->tu = std::move(parsing); } } void CompletionQueryMain(CompletionManager* completion_manager) { while (true) { // Fetching the completion request blocks until we have a request. - std::unique_ptr request = completion_manager->completion_request.Take(); + std::unique_ptr request = completion_manager->completion_request_.Take(); + std::string path = request->location.textDocument.uri.GetPath(); + CompletionSession* session = completion_manager->TryGetSession(path, true /*create_if_needed*/); + + std::lock_guard lock(session->tu_lock); + EnsureDocumentParsed(session, &session->tu, &session->index); - CompletionSession* session = completion_manager->GetOrOpenSession(request->location.textDocument.uri.GetPath()); - std::lock_guard lock(session->usage_lock); - - EnsureDocumentParsed(session, &session->active, &session->active_index); - + // Language server is 0-based, clang is 1-based. unsigned line = request->location.position.line + 1; unsigned column = request->location.position.character + 1; @@ -291,18 +302,18 @@ void CompletionQueryMain(CompletionManager* completion_manager) { Timer timer; - std::vector unsaved = completion_manager->working_files->AsUnsavedFiles(); + std::vector unsaved = completion_manager->working_files_->AsUnsavedFiles(); timer.ResetAndPrint("[complete] Fetching unsaved files"); timer.Reset(); unsigned const kCompleteOptions = CXCodeComplete_IncludeMacros | CXCodeComplete_IncludeBriefComments; CXCodeCompleteResults* cx_results = clang_codeCompleteAt( - session->active->cx_tu, + session->tu->cx_tu, session->file.filename.c_str(), line, column, unsaved.data(), (unsigned)unsaved.size(), kCompleteOptions); if (!cx_results) { - std::cerr << "[complete] Code completion failed" << std::endl; + timer.ResetAndPrint("[complete] Code completion failed"); request->on_complete({}, {}); continue; } @@ -355,6 +366,7 @@ void CompletionQueryMain(CompletionManager* completion_manager) { } timer.ResetAndPrint("[complete] Build diagnostics"); + clang_disposeCodeCompleteResults(cx_results); timer.ResetAndPrint("[complete] clang_disposeCodeCompleteResults"); @@ -367,12 +379,32 @@ void CompletionQueryMain(CompletionManager* completion_manager) { } // namespace CompletionSession::CompletionSession(const Project::Entry& file, WorkingFiles* working_files) - : file(file), working_files(working_files) {} + : file(file), working_files(working_files), index(0 /*excludeDeclarationsFromPCH*/, 0 /*displayDiagnostics*/) {} CompletionSession::~CompletionSession() {} +LruSessionCache::LruSessionCache(int max_entries) : max_entries_(max_entries) {} + +CompletionSession* LruSessionCache::TryGetEntry(const std::string& filename) { + for (int i = 0; i < entries_.size(); ++i) { + if (entries_[i]->file.filename == filename) + return entries_[i].get(); + } + return nullptr; +} + +void LruSessionCache::InsertEntry(std::unique_ptr session) { + if (entries_.size() >= max_entries_) + entries_.pop_back(); + entries_.insert(entries_.begin(), std::move(session)); +} + +CompletionManager::ParseRequest::ParseRequest(const std::string& path) + : path(path), request_time(std::chrono::high_resolution_clock::now()) {} + CompletionManager::CompletionManager(Config* config, Project* project, WorkingFiles* working_files) - : config(config), project(project), working_files(working_files) { + : config_(config), project_(project), working_files_(working_files), + view_sessions_(kMaxViewSessions), edit_sessions_(kMaxEditSessions) { new std::thread([&]() { SetCurrentThreadName("completequery"); CompletionQueryMain(this); @@ -385,47 +417,81 @@ CompletionManager::CompletionManager(Config* config, Project* project, WorkingFi } void CompletionManager::CodeComplete(const lsTextDocumentPositionParams& completion_location, const OnComplete& on_complete) { + // completion thread will create the CompletionSession if needed. + auto request = MakeUnique(); request->location = completion_location; request->on_complete = on_complete; - completion_request.Set(std::move(request)); + completion_request_.Set(std::move(request)); } -CompletionSession* CompletionManager::GetOrOpenSession(const std::string& filename) { - // Try to find existing session. - for (auto& session : sessions) { - if (session->file.filename == filename) - return session.get(); - } +void CompletionManager::NotifyView(const std::string& filename) { + // + // On view, we reparse only if the file has not been parsed. The existence of + // a CompletionSession instance implies the file is already parsed or will be + // parsed soon. + // - // Create new session. Note that this will block. - std::cerr << "[complete] Creating new code completion session for " << filename << std::endl; - optional entry = project->FindCompilationEntryForFile(filename); - if (!entry) { - std::cerr << "[complete] Unable to find compilation entry" << std::endl; - entry = Project::Entry(); - entry->filename = filename; - } - else { - std::cerr << "[complete] Found compilation entry" << std::endl; - } - sessions.push_back(MakeUnique(*entry, working_files)); - return sessions[sessions.size() - 1].get(); + std::lock_guard lock(sessions_lock_); + + if (view_sessions_.TryGetEntry(filename)) + return; + + std::cerr << "[complete] Creating new edit code completion session for " << filename << std::endl; + view_sessions_.InsertEntry(MakeUnique( + project_->FindCompilationEntryForFile(filename), working_files_)); + parse_requests_.Enqueue(ParseRequest(filename)); } -void CompletionManager::UpdateActiveSession(const std::string& filename) { - // Drop all sessions except for |filename|. - for (auto& session : sessions) { - if (session->file.filename == filename) - continue; +void CompletionManager::NotifyEdit(const std::string& filename) { + // + // On edit, we reparse only if the file has not been parsed. The existence of + // a CompletionSession instance implies the file is already parsed or will be + // parsed soon. + // - std::lock_guard lock(session->usage_lock); - session->active.reset(); - session->active_index.reset(); + std::lock_guard lock(sessions_lock_); + + if (edit_sessions_.TryGetEntry(filename)) + return; + + std::cerr << "[complete] Creating new edit code completion session for " << filename << std::endl; + edit_sessions_.InsertEntry(MakeUnique( + project_->FindCompilationEntryForFile(filename), working_files_)); + parse_requests_.PriorityEnqueue(ParseRequest(filename)); +} + +void CompletionManager::NotifySave(const std::string& filename) { + // + // On save, always reparse. + // + + std::lock_guard lock(sessions_lock_); + + if (!edit_sessions_.TryGetEntry(filename)) { + std::cerr << "[complete] Creating new edit code completion session for " << filename << std::endl; + edit_sessions_.InsertEntry(MakeUnique( + project_->FindCompilationEntryForFile(filename), working_files_)); } - // Reparse |filename|. - // TODO: Instead of actually reparsing it, see if we can hook into the - // indexer and steal the translation unit from there.. - reparse_request.Set(MakeUnique(filename)); + parse_requests_.PriorityEnqueue(ParseRequest(filename)); +} + +CompletionSession* CompletionManager::TryGetSession(const std::string& filename, bool create_if_needed) { + std::lock_guard lock(sessions_lock_); + + CompletionSession* session = edit_sessions_.TryGetEntry(filename); + + if (!session) + session = view_sessions_.TryGetEntry(filename); + + if (!session && create_if_needed) { + // Create new session. Default to edited_sessions_ since invoking code + // completion almost certainly implies an edit. + edit_sessions_.InsertEntry(MakeUnique( + project_->FindCompilationEntryForFile(filename), working_files_)); + session = edit_sessions_.TryGetEntry(filename); + } + + return session; } \ No newline at end of file diff --git a/src/code_completion.h b/src/code_completion.h index 9dbf9119..e25f7ff3 100644 --- a/src/code_completion.h +++ b/src/code_completion.h @@ -3,66 +3,101 @@ #include "libclangmm/Index.h" #include "libclangmm/TranslationUnit.h" #include "project.h" +#include "threaded_queue.h" #include "working_files.h" #include #include +#include #include +#include // TODO: rename this file to clang_completion.h/cc struct CompletionSession { Project::Entry file; WorkingFiles* working_files; + clang::Index index; - // Acquired when the session is being used. - std::mutex usage_lock; + // When |tu| was last parsed. + optional> tu_last_parsed_at; + + // Acquired when |tu| is being used. + std::mutex tu_lock; // The active translation unit. - std::unique_ptr active; - std::unique_ptr active_index; - - // Updated translation unit. If |is_updated_ready| is true, then |updated| - // contains more recent state than |active| and the two should be swapped. - // - // TODO: implement this. Needs changes in Refresh and CodeComplete. - //bool is_updated_ready = false; - //std::unique_ptr updated; - //std::unique_ptr updated_index; + std::unique_ptr tu; CompletionSession(const Project::Entry& file, WorkingFiles* working_files); ~CompletionSession(); }; -struct CompletionManager { - std::vector> sessions; - Config* config; - Project* project; - WorkingFiles* working_files; +struct LruSessionCache { + std::vector> entries_; + int max_entries_; + LruSessionCache(int max_entries); + + // Fetches the entry for |filename| and updates it's usage so it is less + // likely to be evicted. + CompletionSession* TryGetEntry(const std::string& filename); + // Inserts an entry. Evicts the oldest unused entry if there is no space. + void InsertEntry(std::unique_ptr session); +}; + +struct CompletionManager { using OnComplete = std::function results, NonElidedVector diagnostics)>; + struct ParseRequest { + ParseRequest(const std::string& path); + + std::chrono::time_point request_time; + std::string path; + }; struct CompletionRequest { lsTextDocumentPositionParams location; OnComplete on_complete; }; - AtomicObject completion_request; - - // Request that the given path be reparsed. - AtomicObject reparse_request; - CompletionManager(Config* config, Project* project, WorkingFiles* working_files); // Start a code completion at the given location. |on_complete| will run when // completion results are available. |on_complete| may run on any thread. - void CodeComplete(const lsTextDocumentPositionParams& completion_location, const OnComplete& on_complete); + void CodeComplete(const lsTextDocumentPositionParams& completion_location, + const OnComplete& on_complete); - CompletionSession* GetOrOpenSession(const std::string& filename); + // Notify the completion manager that |filename| has been viewed and we + // should begin preloading completion data. + void NotifyView(const std::string& filename); + // Notify the completion manager that |filename| has been edited. + void NotifyEdit(const std::string& filename); + // Notify the completion manager that |filename| has been saved. This + // triggers a reparse. + void NotifySave(const std::string& filename); - // Set the new active session. We will drop clang state for all other - // sessions and begin reparsing the session for |filename| to ensure - // completions are always fast. - void UpdateActiveSession(const std::string& filename); + CompletionSession* TryGetSession(const std::string& filename, bool create_if_needed); + + // TODO: make these configurable. + const int kMaxViewSessions = 3; + const int kMaxEditSessions = 10; + + // Global state. + Config* config_; + Project* project_; + WorkingFiles* working_files_; + + // Sessions which have never had a real text-edit applied, but are preloaded + // to give a fast initial experience. + LruSessionCache view_sessions_; + // Completion sessions which have been edited. + LruSessionCache edit_sessions_; + // Mutex which protects |view_sessions_| and |edit_sessions_|. + std::mutex sessions_lock_; + + // Request a code completion at the given location. + AtomicObject completion_request_; + // Parse requests. The path may already be parsed, in which case it should be + // reparsed. + ThreadedQueue parse_requests_; }; \ No newline at end of file diff --git a/src/command_line.cc b/src/command_line.cc index a4975e53..85bef084 100644 --- a/src/command_line.cc +++ b/src/command_line.cc @@ -1869,27 +1869,31 @@ bool QueryDbMainLoop( Timer time; auto msg = static_cast(message.get()); + std::string path = msg->params.textDocument.uri.GetPath(); WorkingFile* working_file = working_files->OnOpen(msg->params); - optional cached_file_contents = LoadCachedFileContents(config, msg->params.textDocument.uri.GetPath()); + optional cached_file_contents = LoadCachedFileContents(config, path); if (cached_file_contents) working_file->SetIndexContent(*cached_file_contents); else working_file->SetIndexContent(working_file->buffer_content); - std::unique_ptr cache = LoadCachedIndex(config, msg->params.textDocument.uri.GetPath()); + std::unique_ptr cache = LoadCachedIndex(config, path); if (cache && !cache->skipped_by_preprocessor.empty()) PublishInactiveLines(working_file, cache->skipped_by_preprocessor); time.ResetAndPrint("[querydb] Loading cached index file for DidOpen (blocks CodeLens)"); include_completion->AddFile(working_file->filename); + completion_manager->NotifyView(path); break; } case IpcId::TextDocumentDidChange: { auto msg = static_cast(message.get()); + std::string path = msg->params.textDocument.uri.GetPath(); working_files->OnChange(msg->params); + completion_manager->NotifyEdit(path); break; } @@ -1927,7 +1931,8 @@ bool QueryDbMainLoop( queue_do_index->PriorityEnqueue(Index_DoIndex(index_type, project->FindCompilationEntryForFile(path), working_file->buffer_content, true /*is_interactive*/)); } - completion_manager->UpdateActiveSession(path); + + completion_manager->NotifySave(path); break; } @@ -2416,8 +2421,11 @@ bool QueryDbMainLoop( response.id = msg->id; lsDocumentUri file_as_uri = msg->params.textDocument.uri; + std::string path = file_as_uri.GetPath(); - QueryFile* file = FindFile(db, file_as_uri.GetPath()); + completion_manager->NotifyView(path); + + QueryFile* file = FindFile(db, path); if (!file) { std::cerr << "Unable to find file " << msg->params.textDocument.uri.GetPath() << std::endl; break; diff --git a/src/libclangmm/TranslationUnit.cc b/src/libclangmm/TranslationUnit.cc index 92571de4..fac21792 100644 --- a/src/libclangmm/TranslationUnit.cc +++ b/src/libclangmm/TranslationUnit.cc @@ -25,7 +25,9 @@ TranslationUnit::TranslationUnit(Index* index, //std::cerr << "Parsing " << filepath << " with args " << StringJoin(args) << std::endl; - //CXErrorCode error_code = clang_parseTranslationUnit2FullArgv( + //cx_tu = clang_createTranslationUnitFromSourceFile( + // index->cx_index, filepath.c_str(), args.size(), args.data(), (unsigned)unsaved_files.size(), unsaved_files.data()); + CXErrorCode error_code = clang_parseTranslationUnit2( index->cx_index, filepath.c_str(), args.data(), (int)args.size(), unsaved_files.data(), (unsigned)unsaved_files.size(), flags, &cx_tu); diff --git a/src/threaded_queue.h b/src/threaded_queue.h index 7936f713..35b54cb5 100644 --- a/src/threaded_queue.h +++ b/src/threaded_queue.h @@ -50,7 +50,12 @@ struct MultiQueueWaiter { template struct ThreadedQueue : public BaseThreadQueue { public: - ThreadedQueue(MultiQueueWaiter* waiter) : waiter_(waiter) {} + ThreadedQueue() { + owned_waiter_ = MakeUnique(); + waiter_ = owned_waiter_.get(); + } + + explicit ThreadedQueue(MultiQueueWaiter* waiter) : waiter_(waiter) {} // Add an element to the front of the queue. void PriorityEnqueue(T&& t) { @@ -88,27 +93,23 @@ public: return priority_.empty() && queue_.empty(); } - /* - // Get the "front"-element. - // If the queue is empty, wait untill an element is avaiable. + // Get the first element from the queue. Blocks until one is available. T Dequeue() { std::unique_lock lock(mutex_); - while (priority_.empty() && queue_.empty()) { - // release lock as long as the wait and reaquire it afterwards. - cv_.wait(lock); - } + waiter_->cv.wait(lock, [&]() { + return !priority_.empty() || !queue_.empty(); + }); if (!priority_.empty()) { auto val = std::move(priority_.front()); priority_.pop(); - return val; + return std::move(val); } auto val = std::move(queue_.front()); queue_.pop(); - return val; + return std::move(val); } - */ // Get the first element from the queue without blocking. Returns a null // value if the queue is empty. @@ -133,4 +134,5 @@ public: mutable std::mutex mutex_; std::queue queue_; MultiQueueWaiter* waiter_; + std::unique_ptr owned_waiter_; };