mirror of
https://github.com/MaskRay/ccls.git
synced 2025-02-16 13:48:04 +00:00
Use condition variables instead of sleeping.
While tricky to do because we have multiple queues, this reduces a lot of unnecessary delay. e2e time goes down from 10-15ms on average to 0-3ms on average. Loading from cache is also nearly instant on the cquery codebase.
This commit is contained in:
parent
63908e3aa0
commit
1b2f5896dc
@ -80,8 +80,8 @@ struct IpcManager {
|
||||
static IpcManager* instance() {
|
||||
return instance_;
|
||||
}
|
||||
static void CreateInstance() {
|
||||
instance_ = new IpcManager();
|
||||
static void CreateInstance(MultiQueueWaiter* waiter) {
|
||||
instance_ = new IpcManager(waiter);
|
||||
}
|
||||
|
||||
std::unique_ptr<ThreadedQueue<std::unique_ptr<BaseIpcMessage>>> threaded_queue_for_client_;
|
||||
@ -114,9 +114,9 @@ struct IpcManager {
|
||||
}
|
||||
|
||||
private:
|
||||
IpcManager() {
|
||||
threaded_queue_for_client_ = MakeUnique<ThreadedQueue<std::unique_ptr<BaseIpcMessage>>>();
|
||||
threaded_queue_for_server_ = MakeUnique<ThreadedQueue<std::unique_ptr<BaseIpcMessage>>>();
|
||||
IpcManager(MultiQueueWaiter* waiter) {
|
||||
threaded_queue_for_client_ = MakeUnique<ThreadedQueue<std::unique_ptr<BaseIpcMessage>>>(waiter);
|
||||
threaded_queue_for_server_ = MakeUnique<ThreadedQueue<std::unique_ptr<BaseIpcMessage>>>(waiter);
|
||||
}
|
||||
};
|
||||
|
||||
@ -1073,18 +1073,20 @@ bool IndexMain_DoCreateIndexUpdate(
|
||||
return true;
|
||||
}
|
||||
|
||||
void IndexJoinIndexUpdates(Index_OnIndexedQueue* queue_on_indexed) {
|
||||
bool IndexMergeIndexUpdates(Index_OnIndexedQueue* queue_on_indexed) {
|
||||
optional<Index_OnIndexed> root = queue_on_indexed->TryDequeue();
|
||||
if (!root)
|
||||
return;
|
||||
return false;
|
||||
|
||||
bool did_merge = false;
|
||||
while (true) {
|
||||
optional<Index_OnIndexed> to_join = queue_on_indexed->TryDequeue();
|
||||
if (!to_join) {
|
||||
queue_on_indexed->Enqueue(std::move(*root));
|
||||
return;
|
||||
return did_merge;
|
||||
}
|
||||
|
||||
did_merge = true;
|
||||
Timer time;
|
||||
root->update.Merge(to_join->update);
|
||||
time.ResetAndPrint("[indexer] Joining two querydb updates");
|
||||
@ -1095,6 +1097,7 @@ void IndexMain(
|
||||
IndexerConfig* config,
|
||||
FileConsumer::SharedState* file_consumer_shared,
|
||||
Project* project,
|
||||
MultiQueueWaiter* waiter,
|
||||
Index_DoIndexQueue* queue_do_index,
|
||||
Index_DoIdMapQueue* queue_do_id_map,
|
||||
Index_OnIdMappedQueue* queue_on_id_mapped,
|
||||
@ -1112,15 +1115,20 @@ void IndexMain(
|
||||
// index.
|
||||
bool did_index = IndexMain_DoIndex(config, file_consumer_shared, project, queue_do_index, queue_do_id_map);
|
||||
bool did_create_update = IndexMain_DoCreateIndexUpdate(queue_on_id_mapped, queue_on_indexed);
|
||||
if (!did_index && !did_create_update) {
|
||||
bool did_merge = false;
|
||||
|
||||
// Nothing to index and no index updates to create, so join some already
|
||||
// created index updates to reduce work on querydb thread.
|
||||
IndexJoinIndexUpdates(queue_on_indexed);
|
||||
// Nothing to index and no index updates to create, so join some already
|
||||
// created index updates to reduce work on querydb thread.
|
||||
if (!did_index && !did_create_update)
|
||||
did_merge = IndexMergeIndexUpdates(queue_on_indexed);
|
||||
|
||||
// TODO: use CV to wakeup?
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(25));
|
||||
}
|
||||
// We didn't do any work, so wait for a notification.
|
||||
if (!did_index && !did_create_update && !did_merge)
|
||||
waiter->Wait({
|
||||
queue_do_index,
|
||||
queue_on_id_mapped,
|
||||
queue_on_indexed
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -1173,9 +1181,10 @@ void IndexMain(
|
||||
|
||||
|
||||
|
||||
void QueryDbMainLoop(
|
||||
bool QueryDbMainLoop(
|
||||
IndexerConfig* config,
|
||||
QueryDatabase* db,
|
||||
MultiQueueWaiter* waiter,
|
||||
Index_DoIndexQueue* queue_do_index,
|
||||
Index_DoIdMapQueue* queue_do_id_map,
|
||||
Index_OnIdMappedQueue* queue_on_id_mapped,
|
||||
@ -1186,8 +1195,11 @@ void QueryDbMainLoop(
|
||||
CompletionManager* completion_manager) {
|
||||
IpcManager* ipc = IpcManager::instance();
|
||||
|
||||
bool did_work = false;
|
||||
|
||||
std::vector<std::unique_ptr<BaseIpcMessage>> messages = ipc->GetMessages(IpcManager::Destination::Server);
|
||||
for (auto& message : messages) {
|
||||
did_work = true;
|
||||
std::cerr << "[querydb] Processing message " << IpcIdToString(message->method_id) << std::endl;
|
||||
|
||||
switch (message->method_id) {
|
||||
@ -1223,7 +1235,7 @@ void QueryDbMainLoop(
|
||||
std::cerr << "[querydb] Starting " << indexer_count << " indexers" << std::endl;
|
||||
for (int i = 0; i < indexer_count; ++i) {
|
||||
new std::thread([&]() {
|
||||
IndexMain(config, file_consumer_shared, project, queue_do_index, queue_do_id_map, queue_on_id_mapped, queue_on_indexed);
|
||||
IndexMain(config, file_consumer_shared, project, waiter, queue_do_index, queue_do_id_map, queue_on_id_mapped, queue_on_indexed);
|
||||
});
|
||||
}
|
||||
|
||||
@ -1738,6 +1750,7 @@ void QueryDbMainLoop(
|
||||
if (!request)
|
||||
break;
|
||||
|
||||
did_work = true;
|
||||
|
||||
Index_OnIdMapped response;
|
||||
Timer time;
|
||||
@ -1760,6 +1773,8 @@ void QueryDbMainLoop(
|
||||
if (!response)
|
||||
break;
|
||||
|
||||
did_work = true;
|
||||
|
||||
Timer time;
|
||||
|
||||
for (auto& updated_file : response->update.files_def_update) {
|
||||
@ -1787,14 +1802,16 @@ void QueryDbMainLoop(
|
||||
db->ApplyIndexUpdate(&response->update);
|
||||
time.ResetAndPrint("[querydb] Applying index update");
|
||||
}
|
||||
|
||||
return did_work;
|
||||
}
|
||||
|
||||
void QueryDbMain(IndexerConfig* config) {
|
||||
void QueryDbMain(IndexerConfig* config, MultiQueueWaiter* waiter) {
|
||||
// Create queues.
|
||||
Index_DoIndexQueue queue_do_index;
|
||||
Index_DoIdMapQueue queue_do_id_map;
|
||||
Index_OnIdMappedQueue queue_on_id_mapped;
|
||||
Index_OnIndexedQueue queue_on_indexed;
|
||||
Index_DoIndexQueue queue_do_index(waiter);
|
||||
Index_DoIdMapQueue queue_do_id_map(waiter);
|
||||
Index_OnIdMappedQueue queue_on_id_mapped(waiter);
|
||||
Index_OnIndexedQueue queue_on_indexed(waiter);
|
||||
|
||||
Project project;
|
||||
WorkingFiles working_files;
|
||||
@ -1805,8 +1822,15 @@ void QueryDbMain(IndexerConfig* config) {
|
||||
SetCurrentThreadName("querydb");
|
||||
QueryDatabase db;
|
||||
while (true) {
|
||||
QueryDbMainLoop(config, &db, &queue_do_index, &queue_do_id_map, &queue_on_id_mapped, &queue_on_indexed, &project, &file_consumer_shared, &working_files, &completion_manager);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
bool did_work = QueryDbMainLoop(config, &db, waiter, &queue_do_index, &queue_do_id_map, &queue_on_id_mapped, &queue_on_indexed, &project, &file_consumer_shared, &working_files, &completion_manager);
|
||||
if (!did_work) {
|
||||
IpcManager* ipc = IpcManager::instance();
|
||||
waiter->Wait({
|
||||
ipc->threaded_queue_for_server_.get(),
|
||||
&queue_do_id_map,
|
||||
&queue_on_indexed
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1978,34 +2002,42 @@ void LanguageServerStdinLoop(IndexerConfig* config, std::unordered_map<IpcId, Ti
|
||||
|
||||
|
||||
|
||||
void StdoutMainLoop(std::unordered_map<IpcId, Timer>* request_times) {
|
||||
void StdoutMain(std::unordered_map<IpcId, Timer>* request_times, MultiQueueWaiter* waiter) {
|
||||
SetCurrentThreadName("stdout");
|
||||
IpcManager* ipc = IpcManager::instance();
|
||||
|
||||
std::vector<std::unique_ptr<BaseIpcMessage>> messages = ipc->GetMessages(IpcManager::Destination::Client);
|
||||
for (auto& message : messages) {
|
||||
std::cerr << "[stdout] Processing message " << IpcIdToString(message->method_id) << std::endl;
|
||||
while (true) {
|
||||
std::vector<std::unique_ptr<BaseIpcMessage>> messages = ipc->GetMessages(IpcManager::Destination::Client);
|
||||
if (messages.empty()) {
|
||||
waiter->Wait({ipc->threaded_queue_for_client_.get()});
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (message->method_id) {
|
||||
case IpcId::Cout: {
|
||||
auto msg = static_cast<Ipc_Cout*>(message.get());
|
||||
for (auto& message : messages) {
|
||||
std::cerr << "[stdout] Processing message " << IpcIdToString(message->method_id) << std::endl;
|
||||
|
||||
Timer time = (*request_times)[msg->original_ipc_id];
|
||||
time.ResetAndPrint("[e2e] Running " + std::string(IpcIdToString(msg->original_ipc_id)));
|
||||
switch (message->method_id) {
|
||||
case IpcId::Cout: {
|
||||
auto msg = static_cast<Ipc_Cout*>(message.get());
|
||||
|
||||
std::cout << msg->content;
|
||||
std::cout.flush();
|
||||
break;
|
||||
}
|
||||
Timer time = (*request_times)[msg->original_ipc_id];
|
||||
time.ResetAndPrint("[e2e] Running " + std::string(IpcIdToString(msg->original_ipc_id)));
|
||||
|
||||
default: {
|
||||
std::cerr << "[stdout] Unhandled IPC message " << IpcIdToString(message->method_id) << std::endl;
|
||||
exit(1);
|
||||
std::cout << msg->content;
|
||||
std::cout.flush();
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
std::cerr << "[stdout] Unhandled IPC message " << IpcIdToString(message->method_id) << std::endl;
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void LanguageServerMain(IndexerConfig* config) {
|
||||
void LanguageServerMain(IndexerConfig* config, MultiQueueWaiter* waiter) {
|
||||
std::unordered_map<IpcId, Timer> request_times;
|
||||
|
||||
// Start stdin reader. Reading from stdin is a blocking operation so this
|
||||
@ -2015,17 +2047,13 @@ void LanguageServerMain(IndexerConfig* config) {
|
||||
});
|
||||
|
||||
// Start querydb thread. querydb will start indexer threads as needed.
|
||||
new std::thread([&config]() {
|
||||
QueryDbMain(config);
|
||||
new std::thread([&]() {
|
||||
QueryDbMain(config, waiter);
|
||||
});
|
||||
|
||||
// We run a dedicated thread for writing to stdout because there can be an
|
||||
// unknown number of delays when output information.
|
||||
SetCurrentThreadName("stdout");
|
||||
while (true) {
|
||||
StdoutMainLoop(&request_times);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(2));
|
||||
}
|
||||
StdoutMain(&request_times, waiter);
|
||||
}
|
||||
|
||||
|
||||
@ -2079,7 +2107,8 @@ void LanguageServerMain(IndexerConfig* config) {
|
||||
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
IpcManager::CreateInstance();
|
||||
MultiQueueWaiter waiter;
|
||||
IpcManager::CreateInstance(&waiter);
|
||||
|
||||
//bool loop = true;
|
||||
//while (loop)
|
||||
@ -2107,7 +2136,7 @@ int main(int argc, char** argv) {
|
||||
else if (HasOption(options, "--language-server")) {
|
||||
//std::cerr << "Running language server" << std::endl;
|
||||
IndexerConfig config;
|
||||
LanguageServerMain(&config);
|
||||
LanguageServerMain(&config, &waiter);
|
||||
return 0;
|
||||
}
|
||||
else {
|
||||
|
@ -3,29 +3,67 @@
|
||||
#include <optional.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <queue>
|
||||
#include <mutex>
|
||||
#include <condition_variable>
|
||||
|
||||
// TODO: cleanup includes.
|
||||
|
||||
struct BaseThreadQueue {
|
||||
virtual bool IsEmpty() = 0;
|
||||
};
|
||||
|
||||
struct MultiQueueWaiter {
|
||||
std::mutex m;
|
||||
std::condition_variable cv;
|
||||
|
||||
bool HasState(std::initializer_list<BaseThreadQueue*> queues) {
|
||||
for (BaseThreadQueue* queue : queues) {
|
||||
if (!queue->IsEmpty())
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void Wait(std::initializer_list<BaseThreadQueue*> queues) {
|
||||
// We cannot have a single condition variable wait on all of the different
|
||||
// mutexes, so we have a global condition variable that every queue will
|
||||
// notify. After it is notified we check to see if any of the queues have
|
||||
// data; if they do, we return.
|
||||
//
|
||||
// We repoll every 5 seconds because it's not possible to atomically check
|
||||
// the state of every queue and then setup the condition variable. So, if
|
||||
// Wait() is called, HasState() returns false, and then in the time after
|
||||
// HasState() is called data gets posted but before we begin waiting for
|
||||
// the condition variable, we will miss the notification. The timeout of 5
|
||||
// means that if this happens we will delay operation for 5 seconds.
|
||||
|
||||
while (!HasState(queues)) {
|
||||
std::unique_lock<std::mutex> l(m);
|
||||
cv.wait_for(l, std::chrono::seconds(5));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// A threadsafe-queue. http://stackoverflow.com/a/16075550
|
||||
template <class T>
|
||||
class ThreadedQueue {
|
||||
struct ThreadedQueue : public BaseThreadQueue {
|
||||
public:
|
||||
ThreadedQueue(MultiQueueWaiter* waiter) : waiter_(waiter) {}
|
||||
|
||||
// Add an element to the front of the queue.
|
||||
void PriorityEnqueue(T&& t) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
priority_.push(std::move(t));
|
||||
cv_.notify_one();
|
||||
waiter_->cv.notify_all();
|
||||
}
|
||||
|
||||
// Add an element to the queue.
|
||||
void Enqueue(T&& t) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
queue_.push(std::move(t));
|
||||
cv_.notify_one();
|
||||
waiter_->cv.notify_all();
|
||||
}
|
||||
|
||||
// Return all elements in the queue.
|
||||
@ -45,6 +83,12 @@ public:
|
||||
return result;
|
||||
}
|
||||
|
||||
bool IsEmpty() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
return queue_.empty();
|
||||
}
|
||||
|
||||
/*
|
||||
// Get the "front"-element.
|
||||
// If the queue is empty, wait untill an element is avaiable.
|
||||
T Dequeue() {
|
||||
@ -64,6 +108,7 @@ public:
|
||||
queue_.pop();
|
||||
return val;
|
||||
}
|
||||
*/
|
||||
|
||||
// Get the first element from the queue without blocking. Returns a null
|
||||
// value if the queue is empty.
|
||||
@ -85,7 +130,7 @@ public:
|
||||
|
||||
private:
|
||||
std::queue<T> priority_;
|
||||
std::queue<T> queue_;
|
||||
mutable std::mutex mutex_;
|
||||
std::condition_variable cv_;
|
||||
std::queue<T> queue_;
|
||||
MultiQueueWaiter* waiter_;
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user