Preload code completion, maintain LRU cache for multiple completion files.

This commit is contained in:
Jacob Dufault 2017-05-25 23:40:38 -07:00
parent 2e3e1e0427
commit cdc268d549
5 changed files with 214 additions and 101 deletions

View File

@ -238,9 +238,9 @@ void BuildDetailString(CXCompletionString completion_string, std::string& label,
void EnsureDocumentParsed(CompletionSession* session,
std::unique_ptr<clang::TranslationUnit>* tu,
std::unique_ptr<clang::Index>* 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<std::string> args = session->file.args;
@ -249,40 +249,51 @@ void EnsureDocumentParsed(CompletionSession* session,
std::vector<CXUnsavedFile> unsaved = session->working_files->AsUnsavedFiles();
std::cerr << "[complete] Creating completion session with arguments " << StringJoin(args) << std::endl;
*index = MakeUnique<clang::Index>(0 /*excludeDeclarationsFromPCH*/, 0 /*displayDiagnostics*/);
*tu = MakeUnique<clang::TranslationUnit>(index->get(), session->file.filename, args, unsaved, Flags());
*tu = MakeUnique<clang::TranslationUnit>(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<std::string> path = completion_manager->reparse_request.Take();
CompletionManager::ParseRequest request = completion_manager->parse_requests_.Dequeue();
// 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;
}
CompletionSession* session = completion_manager->GetOrOpenSession(*path);
std::unique_ptr<clang::TranslationUnit> parsing;
std::unique_ptr<clang::Index> parsing_index;
EnsureDocumentParsed(session, &parsing, &session->index);
EnsureDocumentParsed(session, &parsing, &parsing_index);
// Swap out active.
std::lock_guard<std::mutex> 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<std::mutex> 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<CompletionManager::CompletionRequest> request = completion_manager->completion_request.Take();
std::unique_ptr<CompletionManager::CompletionRequest> request = completion_manager->completion_request_.Take();
std::string path = request->location.textDocument.uri.GetPath();
CompletionSession* session = completion_manager->TryGetSession(path, true /*create_if_needed*/);
CompletionSession* session = completion_manager->GetOrOpenSession(request->location.textDocument.uri.GetPath());
std::lock_guard<std::mutex> lock(session->usage_lock);
EnsureDocumentParsed(session, &session->active, &session->active_index);
std::lock_guard<std::mutex> lock(session->tu_lock);
EnsureDocumentParsed(session, &session->tu, &session->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<CXUnsavedFile> unsaved = completion_manager->working_files->AsUnsavedFiles();
std::vector<CXUnsavedFile> 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<CompletionSession> 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<CompletionRequest>();
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.
//
std::lock_guard<std::mutex> 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<CompletionSession>(
project_->FindCompilationEntryForFile(filename), working_files_));
parse_requests_.Enqueue(ParseRequest(filename));
}
// Create new session. Note that this will block.
std::cerr << "[complete] Creating new code completion session for " << filename << std::endl;
optional<Project::Entry> 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<CompletionSession>(*entry, working_files));
return sessions[sessions.size() - 1].get();
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<std::mutex> 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<CompletionSession>(
project_->FindCompilationEntryForFile(filename), working_files_));
parse_requests_.PriorityEnqueue(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::NotifySave(const std::string& filename) {
//
// On save, always reparse.
//
std::lock_guard<std::mutex> lock(session->usage_lock);
session->active.reset();
session->active_index.reset();
std::lock_guard<std::mutex> 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<CompletionSession>(
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<std::string>(filename));
parse_requests_.PriorityEnqueue(ParseRequest(filename));
}
CompletionSession* CompletionManager::TryGetSession(const std::string& filename, bool create_if_needed) {
std::lock_guard<std::mutex> 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<CompletionSession>(
project_->FindCompilationEntryForFile(filename), working_files_));
session = edit_sessions_.TryGetEntry(filename);
}
return session;
}

View File

@ -3,66 +3,101 @@
#include "libclangmm/Index.h"
#include "libclangmm/TranslationUnit.h"
#include "project.h"
#include "threaded_queue.h"
#include "working_files.h"
#include <clang-c/Index.h>
#include <functional>
#include <memory>
#include <mutex>
#include <string>
// 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<std::chrono::time_point<std::chrono::high_resolution_clock>> tu_last_parsed_at;
// Acquired when |tu| is being used.
std::mutex tu_lock;
// The active translation unit.
std::unique_ptr<clang::TranslationUnit> active;
std::unique_ptr<clang::Index> 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<clang::TranslationUnit> updated;
//std::unique_ptr<clang::Index> updated_index;
std::unique_ptr<clang::TranslationUnit> tu;
CompletionSession(const Project::Entry& file, WorkingFiles* working_files);
~CompletionSession();
};
struct CompletionManager {
std::vector<std::unique_ptr<CompletionSession>> sessions;
Config* config;
Project* project;
WorkingFiles* working_files;
struct LruSessionCache {
std::vector<std::unique_ptr<CompletionSession>> 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<CompletionSession> session);
};
struct CompletionManager {
using OnComplete = std::function<void(NonElidedVector<lsCompletionItem> results, NonElidedVector<lsDiagnostic> diagnostics)>;
struct ParseRequest {
ParseRequest(const std::string& path);
std::chrono::time_point<std::chrono::high_resolution_clock> request_time;
std::string path;
};
struct CompletionRequest {
lsTextDocumentPositionParams location;
OnComplete on_complete;
};
AtomicObject<CompletionRequest> completion_request;
// Request that the given path be reparsed.
AtomicObject<std::string> 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<CompletionRequest> completion_request_;
// Parse requests. The path may already be parsed, in which case it should be
// reparsed.
ThreadedQueue<ParseRequest> parse_requests_;
};

View File

@ -1869,27 +1869,31 @@ bool QueryDbMainLoop(
Timer time;
auto msg = static_cast<Ipc_TextDocumentDidOpen*>(message.get());
std::string path = msg->params.textDocument.uri.GetPath();
WorkingFile* working_file = working_files->OnOpen(msg->params);
optional<std::string> cached_file_contents = LoadCachedFileContents(config, msg->params.textDocument.uri.GetPath());
optional<std::string> 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<IndexFile> cache = LoadCachedIndex(config, msg->params.textDocument.uri.GetPath());
std::unique_ptr<IndexFile> 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<Ipc_TextDocumentDidChange*>(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;

View File

@ -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);

View File

@ -50,7 +50,12 @@ struct MultiQueueWaiter {
template <class T>
struct ThreadedQueue : public BaseThreadQueue {
public:
ThreadedQueue(MultiQueueWaiter* waiter) : waiter_(waiter) {}
ThreadedQueue() {
owned_waiter_ = MakeUnique<MultiQueueWaiter>();
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<std::mutex> 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<T> queue_;
MultiQueueWaiter* waiter_;
std::unique_ptr<MultiQueueWaiter> owned_waiter_;
};