Replace QHash with std::unordered_map inside ProFileCache

It looks like it happens that while holding a reference
to ProFileCache::parsed_files elements we are modifying
this container by adding / removing items.
In general it's undefined behavior.

In addition some other fixes have been made. Comments
inside the code.

Task-number: QTCREATORBUG-26351
Change-Id: I7f98c813ff6b7bd612757e7a14a5e6835affd21e
Reviewed-by: Eike Ziller <eike.ziller@qt.io>
Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org>
Reviewed-by: Christian Kandeler <christian.kandeler@qt.io>
This commit is contained in:
Jarek Kobus
2021-09-29 14:23:42 +02:00
parent 3a02e03729
commit 2f8b541fe5
2 changed files with 119 additions and 44 deletions

View File

@@ -49,9 +49,9 @@ ProFileCache::ProFileCache()
ProFileCache::~ProFileCache() ProFileCache::~ProFileCache()
{ {
foreach (const Entry &ent, parsed_files) for (const auto &keyValuePair : parsed_files)
if (ent.pro) if (keyValuePair.second.pro)
ent.pro->deref(); keyValuePair.second.pro->deref();
QMakeVfs::deref(); QMakeVfs::deref();
} }
@@ -73,19 +73,36 @@ void ProFileCache::discardFile(int id)
auto it = parsed_files.find(id); auto it = parsed_files.find(id);
if (it != parsed_files.end()) { if (it != parsed_files.end()) {
#ifdef PROPARSER_THREAD_SAFE #ifdef PROPARSER_THREAD_SAFE
if (it->locker) { Entry &entry = it->second;
if (!it->locker->done) { Entry::Locker *locker = entry.locker;
++it->locker->waiters; if (locker) { // Either it's still being prepared or it's already done but someone
it->locker->cond.wait(&mutex); // else started waiting for it when it wasn't ready yet
if (!--it->locker->waiters) { // and is still waiting.
delete it->locker; if (!locker->done) {
it->locker = 0; ++locker->waiters;
locker->cond.wait(&mutex); // Mutex is unlocked and relocked,
// everything may happen in this time window
it = parsed_files.find(id); // In meantime the iterator could have
// been invalidated, search for it again
Q_ASSERT(it != parsed_files.end()); // This would mean that some other
// waiter removed our entry, but only
// the last waiter should do it
if (!--locker->waiters) {
delete locker;
} else {
// There are still other waiters. Do nothing now.
locker->removeOnLastWaiter = true;
return;
} }
} else if (locker->waiters) {
// It's done but some waiters are still awaiting to be woken.
locker->removeOnLastWaiter = true; // Mark it for other waiters
return;
} }
} }
#endif #endif
if (it->pro) if (entry.pro)
it->pro->deref(); entry.pro->deref();
parsed_files.erase(it); parsed_files.erase(it);
} }
} }
@@ -95,26 +112,53 @@ void ProFileCache::discardFiles(const QString &prefix, QMakeVfs *vfs)
#ifdef PROPARSER_THREAD_SAFE #ifdef PROPARSER_THREAD_SAFE
QMutexLocker lck(&mutex); QMutexLocker lck(&mutex);
#endif #endif
auto it = parsed_files.begin(), end = parsed_files.end(); auto it = parsed_files.begin();
while (it != end) { while (it != parsed_files.end()) {
const int id = it->first;
// Note: this is empty for virtual files from other VFSes. // Note: this is empty for virtual files from other VFSes.
QString fn = vfs->fileNameForId(it.key()); const QString fn = vfs->fileNameForId(id);
if (fn.startsWith(prefix)) { if (fn.startsWith(prefix)) {
#ifdef PROPARSER_THREAD_SAFE #ifdef PROPARSER_THREAD_SAFE
if (it->locker) { bool continueFromScratch = false;
if (!it->locker->done) { Entry &entry = it->second;
++it->locker->waiters; Entry::Locker *locker = entry.locker;
it->locker->cond.wait(&mutex); if (locker) { // Either it's still being prepared or it's already done but someone
if (!--it->locker->waiters) { // else started waiting for it when it wasn't ready yet
delete it->locker; // and is still waiting.
it->locker = 0; if (!locker->done) { // It's still being prepared.
++locker->waiters;
locker->cond.wait(&mutex); // Mutex is unlocked and relocked,
// everything may happen in this time window
it = parsed_files.find(id); // In meantime the iterator could have
// been invalidated, search for it again
Q_ASSERT(it != parsed_files.end()); // This would mean that some other
// waiter removed our entry, but only
// the last waiter should do it
if (!--locker->waiters) {
delete locker; // No more waiters, remove locker and entry
continueFromScratch = true;
} else {
// There are still other waiters. Do nothing now
locker->removeOnLastWaiter = true;
it = parsed_files.begin(); // Start from scratch, as new matching
// entries could have appeared.
// If we hit it again, we will skip it
// as the locker is already marked as done
continue;
} }
} else if (locker->waiters) {
// It's done but some waiters are still awaiting to be woken.
locker->removeOnLastWaiter = true; // Mark it for other waiters
++it; // We skip it for now, other waiter will do the job
continue;
} }
} }
#endif #endif
if (it->pro) if (entry.pro)
it->pro->deref(); entry.pro->deref();
it = parsed_files.erase(it); it = parsed_files.erase(it);
if (continueFromScratch)
it = parsed_files.begin();
} else { } else {
++it; ++it;
} }
@@ -187,20 +231,44 @@ ProFile *QMakeParser::parsedProFile(const QString &fileName, ParseFlags flags)
if ((flags & ParseUseCache) && m_cache) { if ((flags & ParseUseCache) && m_cache) {
ProFileCache::Entry *ent; ProFileCache::Entry *ent;
#ifdef PROPARSER_THREAD_SAFE #ifdef PROPARSER_THREAD_SAFE
QMutexLocker locker(&m_cache->mutex); QMutexLocker lck(&m_cache->mutex);
#endif #endif
auto it = m_cache->parsed_files.find(id); auto it = m_cache->parsed_files.find(id);
if (it != m_cache->parsed_files.end()) { if (it != m_cache->parsed_files.end()) {
ent = &*it; ent = &it->second;
#ifdef PROPARSER_THREAD_SAFE #ifdef PROPARSER_THREAD_SAFE
if (ent->locker && !ent->locker->done) { ProFileCache::Entry::Locker *locker = ent->locker;
++ent->locker->waiters; if (locker) { // Either it's still being prepared or it's already done but someone
QThreadPool::globalInstance()->releaseThread(); // else started waiting for it when it wasn't ready yet
ent->locker->cond.wait(locker.mutex()); // and is still waiting.
QThreadPool::globalInstance()->reserveThread(); if (!locker->done) { // It's still being prepared.
if (!--ent->locker->waiters) { ++locker->waiters;
delete ent->locker; QThreadPool::globalInstance()->releaseThread();
ent->locker = 0; locker->cond.wait(lck.mutex()); // Mutex is unlocked and relocked,
// everything may happen in this time window
QThreadPool::globalInstance()->reserveThread();
it = m_cache->parsed_files.find(id); // In meantime the iterator could have
// been invalidated, search for it again
Q_ASSERT(it != m_cache->parsed_files.end()); // This would mean that some other
// waiter removed our entry, but only
// the last waiter should do it
const bool remove = locker->removeOnLastWaiter; // Some waiter marked this
// entry for removal.
if (!--locker->waiters) { // If we were the last waiter
delete locker; // we do it now
ent->locker = 0;
if (remove) {
if (ent->pro)
ent->pro->deref();
m_cache->parsed_files.erase(it);
return nullptr;
}
}
if (remove)
return nullptr;
} else if (locker->removeOnLastWaiter) {
return nullptr;
} }
} }
#endif #endif
@@ -210,7 +278,7 @@ ProFile *QMakeParser::parsedProFile(const QString &fileName, ParseFlags flags)
ent = &m_cache->parsed_files[id]; ent = &m_cache->parsed_files[id];
#ifdef PROPARSER_THREAD_SAFE #ifdef PROPARSER_THREAD_SAFE
ent->locker = new ProFileCache::Entry::Locker; ent->locker = new ProFileCache::Entry::Locker;
locker.unlock(); lck.unlock(); // We unlock the mutex and open a time window
#endif #endif
QString contents; QString contents;
if (readFile(id, flags, &contents)) { if (readFile(id, flags, &contents)) {
@@ -222,11 +290,17 @@ ProFile *QMakeParser::parsedProFile(const QString &fileName, ParseFlags flags)
} }
ent->pro = pro; ent->pro = pro;
#ifdef PROPARSER_THREAD_SAFE #ifdef PROPARSER_THREAD_SAFE
locker.relock(); lck.relock(); // We relock it back and close the window. Someone could have added a new
if (ent->locker->waiters) { // entry to the parsed_files list (or removed one). However, no one
ent->locker->done = true; // was awoken for our id in this time window.
it = m_cache->parsed_files.find(id); // In meantime the iterator could have
// been invalidated, search for it again
Q_ASSERT(it != m_cache->parsed_files.end()); // This would mean that our reference
// is also invalid
if (ent->locker->waiters) { // Someone runs getter or wants to remove it
ent->locker->done = true; // The structure is ready
ent->locker->cond.wakeAll(); ent->locker->cond.wakeAll();
} else { } else { // No one was interested, remove locker as everything is ready now
delete ent->locker; delete ent->locker;
ent->locker = 0; ent->locker = 0;
} }

View File

@@ -29,13 +29,14 @@
#include "qmakevfs.h" #include "qmakevfs.h"
#include "proitems.h" #include "proitems.h"
#include <qhash.h>
#include <qstack.h> #include <qstack.h>
#ifdef PROPARSER_THREAD_SAFE #ifdef PROPARSER_THREAD_SAFE
# include <qmutex.h> # include <qmutex.h>
# include <qwaitcondition.h> # include <qwaitcondition.h>
#endif #endif
#include <unordered_map>
QT_BEGIN_NAMESPACE QT_BEGIN_NAMESPACE
class QMAKE_EXPORT QMakeParserHandler class QMAKE_EXPORT QMakeParserHandler
{ {
@@ -208,16 +209,16 @@ private:
ProFile *pro; ProFile *pro;
#ifdef PROPARSER_THREAD_SAFE #ifdef PROPARSER_THREAD_SAFE
struct Locker { struct Locker {
Locker() : waiters(0), done(false) {}
QWaitCondition cond; QWaitCondition cond;
int waiters; int waiters = 0;
bool done; bool done = false;
bool removeOnLastWaiter = false;
}; };
Locker *locker; Locker *locker;
#endif #endif
}; };
QHash<int, Entry> parsed_files; std::unordered_map<int, Entry> parsed_files;
#ifdef PROPARSER_THREAD_SAFE #ifdef PROPARSER_THREAD_SAFE
QMutex mutex; QMutex mutex;
#endif #endif