aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorChristian Kandeler <christian.kandeler@qt.io>2023-07-10 15:19:29 +0200
committerChristian Kandeler <christian.kandeler@qt.io>2023-09-05 08:07:50 +0000
commitae82e039eb74b092604910a1f3f7fa4887397cbc (patch)
tree69d53083cd122e7089f7dca1cb335a7f7124a2c1
parent51e58390f2b34b27d15e7f287353451a6a969a33 (diff)
Loader: Resolve products in parallel
The resolveProduct() function is now executed for several products simultaneously, with the (relatively few) accesses to common resources guarded by mutexes. Using Qt Creator as a mid-to-large-sized test project, we see the following changes in the time it takes to resolve the project on some example machines: - Linux (36 cores): 10.5s -> 4.8s - Linux (8 cores): 17s -> 6.5s - macOS (6 cores): 41s -> 16s - Windows (8 cores): 20s -> 9s Unsurprisingly, the speed-up does not scale with the number of processors, as there are typically lots of inter-product dependencies and some expensive resources such as Probes are shared globally. However, we do see a factor of two to three across all the hardware and OS configuarations, which is a good practical result for users. Note that running with -j1, i.e. forcing the use of only a single core, takes the same amount of time everywhere as it did without the patch, so there is no scheduling overhead in the single-core case. The results of our benchmarker tool look interesting. Here they are for qbs and Qt Creator, respectively: ========== Performance data for Resolving ========== (qbs) Old instruction count: 9121688266 New instruction count: 15736125513 Relative change: +72 % Old peak memory usage: 84155384 Bytes New peak memory usage: 187776736 Bytes Relative change: +123 % ========== Performance data for Resolving ========== (QtC) Old instruction count: 59901017190 New instruction count: 65227937765 Relative change: +8 % Old peak memory usage: 621560008 Bytes New peak memory usage: 761732040 Bytes Relative change: +22 % The increased peak memory usage is to be expected, as there are now several JS engines running in parallel. The instruction count increase is likely due to a higher amount of deferrals. Importantly, it appears to go down massively with increased project size, so it does not seem that the parallelism hides a serious per-thread slowdown. Change-Id: Ib4d9ca9aa0687c1056ff82f9805b565cc5a35894 Reviewed-by: Ivan Komissarov <ABBAPOH@gmail.com>
-rw-r--r--src/lib/corelib/api/internaljobs.cpp4
-rw-r--r--src/lib/corelib/language/evaluator.cpp54
-rw-r--r--src/lib/corelib/language/evaluator.h9
-rw-r--r--src/lib/corelib/language/item.cpp72
-rw-r--r--src/lib/corelib/language/item.h40
-rw-r--r--src/lib/corelib/language/itempool.cpp3
-rw-r--r--src/lib/corelib/language/itempool.h2
-rw-r--r--src/lib/corelib/language/scriptengine.h3
-rw-r--r--src/lib/corelib/loader/dependenciesresolver.cpp54
-rw-r--r--src/lib/corelib/loader/itemreader.cpp8
-rw-r--r--src/lib/corelib/loader/itemreaderastvisitor.cpp14
-rw-r--r--src/lib/corelib/loader/itemreaderastvisitor.h6
-rw-r--r--src/lib/corelib/loader/loaderutils.cpp137
-rw-r--r--src/lib/corelib/loader/loaderutils.h84
-rw-r--r--src/lib/corelib/loader/moduleloader.cpp6
-rw-r--r--src/lib/corelib/loader/moduleproviderloader.cpp1
-rw-r--r--src/lib/corelib/loader/probesresolver.cpp18
-rw-r--r--src/lib/corelib/loader/probesresolver.h4
-rw-r--r--src/lib/corelib/loader/productresolver.cpp6
-rw-r--r--src/lib/corelib/loader/productscollector.cpp1
-rw-r--r--src/lib/corelib/loader/productsresolver.cpp446
-rw-r--r--src/lib/corelib/loader/projectresolver.cpp18
-rw-r--r--src/lib/corelib/logging/categories.cpp1
-rw-r--r--src/lib/corelib/logging/categories.h1
-rw-r--r--src/lib/corelib/tools/progressobserver.h8
-rw-r--r--tests/auto/api/tst_api.cpp9
26 files changed, 807 insertions, 202 deletions
diff --git a/src/lib/corelib/api/internaljobs.cpp b/src/lib/corelib/api/internaljobs.cpp
index 46ee20bbf..35766efa4 100644
--- a/src/lib/corelib/api/internaljobs.cpp
+++ b/src/lib/corelib/api/internaljobs.cpp
@@ -77,8 +77,8 @@ public:
{
std::lock_guard<std::mutex> lock(m_cancelMutex);
m_canceled = true;
- if (scriptEngine())
- scriptEngine()->cancel();
+ for (ScriptEngine * const engine : scriptEngines())
+ engine->cancel();
}
private:
diff --git a/src/lib/corelib/language/evaluator.cpp b/src/lib/corelib/language/evaluator.cpp
index 824dc8d10..ef9376194 100644
--- a/src/lib/corelib/language/evaluator.cpp
+++ b/src/lib/corelib/language/evaluator.cpp
@@ -96,6 +96,7 @@ Evaluator::~Evaluator()
valuesToFree << data;
for (const JSValue cachedValue : evalData->valueCache)
JS_FreeValue(m_scriptEngine->context(), cachedValue);
+ evalData->item->removeObserver(this);
delete evalData;
}
for (const auto &scopes : std::as_const(m_fileContextScopesMap)) {
@@ -224,24 +225,13 @@ JSValue Evaluator::scriptValue(const Item *item)
const auto edata = new EvaluationData;
edata->evaluator = this;
edata->item = item;
- edata->item->setObserver(this);
+ edata->item->addObserver(this);
scriptValue = JS_NewObjectClass(m_scriptEngine->context(), m_scriptClass);
attachPointerTo(scriptValue, edata);
return scriptValue;
}
-void Evaluator::clearCache(const Item *item)
-{
- const auto data = attachedPointer<EvaluationData>(m_scriptValueMap.value(item),
- m_scriptEngine->dataWithPtrClass());
- if (data) {
- for (const auto value : std::as_const(data->valueCache))
- JS_FreeValue(m_scriptEngine->context(), value);
- data->valueCache.clear();
- }
-}
-
void Evaluator::handleEvaluationError(const Item *item, const QString &name)
{
throwOnEvaluationError(m_scriptEngine, [&item, &name] () {
@@ -287,6 +277,42 @@ Evaluator::FileContextScopes Evaluator::fileContextScopes(const FileContextConst
return result;
}
+// This is the only function in this class that can be called from a thread that is not
+// the evaluating one. For this reason, we do not clear the cache here, as that would
+// incur enourmous synchronization overhead. Instead, we mark the item's cache as invalidated
+// and do the actual clearing only at the very few places where the cache is actually accessed.
+void Evaluator::invalidateCache(const Item *item)
+{
+ std::lock_guard lock(m_cacheInvalidationMutex);
+ m_invalidatedCaches << item;
+}
+
+void Evaluator::clearCache(const Item *item)
+{
+ std::lock_guard lock(m_cacheInvalidationMutex);
+ if (const auto data = attachedPointer<EvaluationData>(m_scriptValueMap.value(item),
+ m_scriptEngine->dataWithPtrClass())) {
+ clearCache(*data);
+ m_invalidatedCaches.remove(data->item);
+ }
+}
+
+void Evaluator::clearCacheIfInvalidated(EvaluationData &edata)
+{
+ std::lock_guard lock(m_cacheInvalidationMutex);
+ if (const auto it = m_invalidatedCaches.find(edata.item); it != m_invalidatedCaches.end()) {
+ clearCache(edata);
+ m_invalidatedCaches.erase(it);
+ }
+}
+
+void Evaluator::clearCache(EvaluationData &edata)
+{
+ for (const auto value : std::as_const(edata.valueCache))
+ JS_FreeValue(m_scriptEngine->context(), value);
+ edata.valueCache.clear();
+}
+
void throwOnEvaluationError(ScriptEngine *engine,
const std::function<CodeLocation()> &provideFallbackCodeLocation)
{
@@ -862,7 +888,7 @@ static void collectValuesFromNextChain(
struct EvalResult { JSValue v = JS_UNDEFINED; bool found = false; };
static EvalResult getEvalProperty(ScriptEngine *engine, JSValue obj, const Item *item,
- const QString &name, const EvaluationData *data)
+ const QString &name, EvaluationData *data)
{
Evaluator * const evaluator = data->evaluator;
const bool isModuleInstance = item->type() == ItemType::ModuleInstance
@@ -881,6 +907,7 @@ static EvalResult getEvalProperty(ScriptEngine *engine, JSValue obj, const Item
evaluator->propertyDependencies());
JSValue result;
if (evaluator->cachingEnabled()) {
+ data->evaluator->clearCacheIfInvalidated(*data);
const auto result = data->valueCache.constFind(name);
if (result != data->valueCache.constEnd()) {
if (debugProperties)
@@ -903,6 +930,7 @@ static EvalResult getEvalProperty(ScriptEngine *engine, JSValue obj, const Item
qDebug() << "[SC] cache miss " << name << ": "
<< resultToString(engine->context(), result);
if (evaluator->cachingEnabled()) {
+ data->evaluator->clearCacheIfInvalidated(*data);
const auto it = data->valueCache.find(name);
if (it != data->valueCache.end()) {
JS_FreeValue(engine->context(), it.value());
diff --git a/src/lib/corelib/language/evaluator.h b/src/lib/corelib/language/evaluator.h
index f174d858c..d86e08eb1 100644
--- a/src/lib/corelib/language/evaluator.h
+++ b/src/lib/corelib/language/evaluator.h
@@ -49,11 +49,13 @@
#include <QtCore/qhash.h>
#include <functional>
+#include <mutex>
#include <optional>
#include <stack>
namespace qbs {
namespace Internal {
+class EvaluationData;
class FileTags;
class Logger;
class PropertyDeclaration;
@@ -101,6 +103,8 @@ public:
void setCachingEnabled(bool enabled) { m_valueCacheEnabled = enabled; }
bool cachingEnabled() const { return m_valueCacheEnabled; }
void clearCache(const Item *item);
+ void invalidateCache(const Item *item);
+ void clearCacheIfInvalidated(EvaluationData &edata);
PropertyDependencies &propertyDependencies() { return m_propertyDependencies; }
void clearPropertyDependencies() { m_propertyDependencies.clear(); }
@@ -115,9 +119,10 @@ public:
bool isNonDefaultValue(const Item *item, const QString &name) const;
private:
- void onItemPropertyChanged(Item *item) override { clearCache(item); }
+ void onItemPropertyChanged(Item *item) override { invalidateCache(item); }
bool evaluateProperty(JSValue *result, const Item *item, const QString &name,
bool *propertyWasSet);
+ void clearCache(EvaluationData &edata);
ScriptEngine * const m_scriptEngine;
const JSClassID m_scriptClass;
@@ -126,6 +131,8 @@ private:
QString m_pathPropertiesBaseDir;
PropertyDependencies m_propertyDependencies;
std::stack<QualifiedId> m_requestedProperties;
+ std::mutex m_cacheInvalidationMutex;
+ Set<const Item *> m_invalidatedCaches;
bool m_valueCacheEnabled = false;
};
diff --git a/src/lib/corelib/language/item.cpp b/src/lib/corelib/language/item.cpp
index 7ab542bd0..d7ad9f5f5 100644
--- a/src/lib/corelib/language/item.cpp
+++ b/src/lib/corelib/language/item.cpp
@@ -67,6 +67,8 @@ Item *Item::create(ItemPool *pool, ItemType type)
Item *Item::clone(ItemPool &pool) const
{
+ assertModuleLocked();
+
Item *dup = create(&pool, type());
dup->m_id = m_id;
dup->m_location = m_location;
@@ -117,6 +119,7 @@ QString Item::typeName() const
bool Item::hasProperty(const QString &name) const
{
+ assertModuleLocked();
const Item *item = this;
do {
if (item->m_properties.contains(name))
@@ -128,11 +131,13 @@ bool Item::hasProperty(const QString &name) const
bool Item::hasOwnProperty(const QString &name) const
{
+ assertModuleLocked();
return m_properties.contains(name);
}
ValuePtr Item::property(const QString &name) const
{
+ assertModuleLocked();
ValuePtr value;
const Item *item = this;
do {
@@ -145,6 +150,7 @@ ValuePtr Item::property(const QString &name) const
ValuePtr Item::ownProperty(const QString &name) const
{
+ assertModuleLocked();
return m_properties.value(name);
}
@@ -200,6 +206,28 @@ bool Item::isOfTypeOrhasParentOfType(ItemType type) const
return false;
}
+void Item::addObserver(ItemObserver *observer) const
+{
+ // Cached Module properties never change.
+ if (m_type == ItemType::Module)
+ return;
+
+ std::lock_guard lock(m_observersMutex);
+ if (!qEnvironmentVariableIsEmpty("QBS_SANITY_CHECKS"))
+ QBS_CHECK(!contains(m_observers, observer));
+ m_observers << observer;
+}
+
+void Item::removeObserver(ItemObserver *observer) const
+{
+ if (m_type == ItemType::Module)
+ return;
+ std::lock_guard lock(m_observersMutex);
+ const auto it = std::find(m_observers.begin(), m_observers.end(), observer);
+ QBS_CHECK(it != m_observers.end());
+ m_observers.erase(it);
+}
+
PropertyDeclaration Item::propertyDeclaration(const QString &name, bool allowExpired) const
{
auto it = m_propertyDeclarations.find(name);
@@ -234,17 +262,13 @@ void Item::addModule(const Item::Module &module)
m_modules.push_back(module);
}
-void Item::setObserver(ItemObserver *observer) const
-{
- QBS_ASSERT(!observer || !m_observer, return); // warn if accidentally overwritten
- m_observer = observer;
-}
-
void Item::setProperty(const QString &name, const ValuePtr &value)
{
+ assertModuleLocked();
m_properties.insert(name, value);
- if (m_observer)
- m_observer->onItemPropertyChanged(this);
+ std::lock_guard lock(m_observersMutex);
+ for (ItemObserver * const observer : m_observers)
+ observer->onItemPropertyChanged(this);
}
void Item::dump() const
@@ -261,6 +285,7 @@ bool Item::isPresentModule() const
void Item::setupForBuiltinType(DeprecationWarningMode deprecationMode, Logger &logger)
{
+ assertModuleLocked();
const BuiltinDeclarations &builtins = BuiltinDeclarations::instance();
const auto properties = builtins.declarationsForType(type()).properties();
for (const PropertyDeclaration &pd : properties) {
@@ -342,8 +367,39 @@ void Item::dump(int indentation) const
}
}
+void Item::lockModule() const
+{
+ QBS_CHECK(m_type == ItemType::Module);
+ m_moduleMutex.lock();
+#ifndef NDEBUG
+ QBS_CHECK(!m_moduleLocked);
+ m_moduleLocked = true;
+#endif
+}
+
+void Item::unlockModule() const
+{
+ QBS_CHECK(m_type == ItemType::Module);
+#ifndef NDEBUG
+ QBS_CHECK(m_moduleLocked);
+ m_moduleLocked = false;
+#endif
+ m_moduleMutex.unlock();
+}
+
+// This safeguard verifies that all contexts which access Module properties have really
+// acquired the lock via ModuleItemLocker, as they must.
+void Item::assertModuleLocked() const
+{
+#ifndef NDEBUG
+ if (m_type == ItemType::Module)
+ QBS_CHECK(m_moduleLocked);
+#endif
+}
+
void Item::removeProperty(const QString &name)
{
+ assertModuleLocked();
m_properties.remove(name);
}
diff --git a/src/lib/corelib/language/item.h b/src/lib/corelib/language/item.h
index e3d0e6104..7f81d53b5 100644
--- a/src/lib/corelib/language/item.h
+++ b/src/lib/corelib/language/item.h
@@ -53,6 +53,8 @@
#include <QtCore/qlist.h>
#include <QtCore/qmap.h>
+#include <atomic>
+#include <mutex>
#include <vector>
namespace qbs {
@@ -63,6 +65,7 @@ namespace Internal {
class ItemObserver;
class ItemPool;
class Logger;
+class ModuleItemLocker;
class ProductContext;
class QBS_AUTOTEST_EXPORT Item : public QbsQmlJS::Managed
@@ -70,6 +73,7 @@ class QBS_AUTOTEST_EXPORT Item : public QbsQmlJS::Managed
friend class ASTPropertiesItemHandler;
friend class ItemPool;
friend class ItemReaderASTVisitor;
+ friend class ModuleItemLocker;
Q_DISABLE_COPY(Item)
Item(ItemType type) : m_type(type) {}
@@ -111,8 +115,8 @@ public:
const QList<Item *> &children() const { return m_children; }
QList<Item *> &children() { return m_children; }
Item *child(ItemType type, bool checkForMultiple = true) const;
- const PropertyMap &properties() const { return m_properties; }
- PropertyMap &properties() { return m_properties; }
+ const PropertyMap &properties() const { assertModuleLocked(); return m_properties; }
+ PropertyMap &properties() { assertModuleLocked(); return m_properties; }
const PropertyDeclarationMap &propertyDeclarations() const { return m_propertyDeclarations; }
PropertyDeclaration propertyDeclaration(const QString &name, bool allowExpired = true) const;
@@ -138,9 +142,10 @@ public:
JSSourceValuePtr sourceProperty(const QString &name) const;
VariantValuePtr variantProperty(const QString &name) const;
bool isOfTypeOrhasParentOfType(ItemType type) const;
- void setObserver(ItemObserver *observer) const;
+ void addObserver(ItemObserver *observer) const;
+ void removeObserver(ItemObserver *observer) const;
void setProperty(const QString &name, const ValuePtr &value);
- void setProperties(const PropertyMap &props) { m_properties = props; }
+ void setProperties(const PropertyMap &props) { assertModuleLocked(); m_properties = props; }
void removeProperty(const QString &name);
void setPropertyDeclaration(const QString &name, const PropertyDeclaration &declaration);
void setPropertyDeclarations(const PropertyDeclarationMap &decls);
@@ -176,7 +181,12 @@ private:
void dump(int indentation) const;
- mutable ItemObserver *m_observer = nullptr;
+ void lockModule() const;
+ void unlockModule() const;
+ void assertModuleLocked() const;
+
+ mutable std::vector<ItemObserver *> m_observers;
+ mutable std::mutex m_observersMutex;
QString m_id;
CodeLocation m_location;
Item *m_prototype = nullptr;
@@ -190,6 +200,10 @@ private:
PropertyDeclarationMap m_expiredPropertyDeclarations;
Modules m_modules;
ItemType m_type;
+ mutable std::mutex m_moduleMutex;
+#ifndef NDEBUG
+ mutable std::atomic_bool m_moduleLocked = false;
+#endif
};
inline bool operator<(const Item::Module &m1, const Item::Module &m2) { return m1.name < m2.name; }
@@ -198,6 +212,22 @@ Item *createNonPresentModule(ItemPool &pool, const QString &name, const QString
Item *module);
void setScopeForDescendants(Item *item, Item *scope);
+// This mechanism is needed because Module items are shared between products (not doing so
+// would be prohibitively expensive).
+// The competing accesses are between
+// - Attaching a temporary qbs module for evaluating the Module condition.
+// - Cloning the module when creating an instance.
+// - Directly accessing Module properties, which happens rarely (as opposed to properties of
+// an instance).
+class ModuleItemLocker
+{
+public:
+ ModuleItemLocker(const Item &item) : m_item(item) { item.lockModule(); }
+ ~ModuleItemLocker() { m_item.unlockModule(); }
+private:
+ const Item &m_item;
+};
+
} // namespace Internal
} // namespace qbs
diff --git a/src/lib/corelib/language/itempool.cpp b/src/lib/corelib/language/itempool.cpp
index 6552f92ef..a89124d60 100644
--- a/src/lib/corelib/language/itempool.cpp
+++ b/src/lib/corelib/language/itempool.cpp
@@ -53,6 +53,9 @@ ItemPool::~ItemPool()
Item *ItemPool::allocateItem(const ItemType &type)
{
+ // TODO: This mutex seems to be hotly contested. Try using per-thread item pools.
+ std::lock_guard lock(m_mutex);
+
const auto item = new (&m_pool) Item(type);
m_items.push_back(item);
return item;
diff --git a/src/lib/corelib/language/itempool.h b/src/lib/corelib/language/itempool.h
index ef4be7639..7c679720c 100644
--- a/src/lib/corelib/language/itempool.h
+++ b/src/lib/corelib/language/itempool.h
@@ -43,6 +43,7 @@
#include <parser/qmljsmemorypool_p.h>
#include <tools/qbs_export.h>
+#include <mutex>
#include <vector>
namespace qbs {
@@ -62,6 +63,7 @@ public:
private:
QbsQmlJS::MemoryPool m_pool;
+ std::mutex m_mutex;
std::vector<Item *> m_items;
};
diff --git a/src/lib/corelib/language/scriptengine.h b/src/lib/corelib/language/scriptengine.h
index 942b7519c..4a55392e3 100644
--- a/src/lib/corelib/language/scriptengine.h
+++ b/src/lib/corelib/language/scriptengine.h
@@ -58,6 +58,7 @@
#include <QtCore/qprocess.h>
#include <QtCore/qstring.h>
+#include <atomic>
#include <functional>
#include <memory>
#include <mutex>
@@ -360,7 +361,7 @@ private:
std::unordered_map<QString, JSValue> m_jsFileCache;
bool m_propertyCacheEnabled = true;
bool m_active = false;
- bool m_canceling = false;
+ std::atomic_bool m_canceling = false;
QHash<PropertyCacheKey, QVariant> m_propertyCache;
PropertySet m_propertiesRequestedInScript;
QHash<QString, PropertySet> m_propertiesRequestedFromArtifact;
diff --git a/src/lib/corelib/loader/dependenciesresolver.cpp b/src/lib/corelib/loader/dependenciesresolver.cpp
index b9d3baaff..262b875d0 100644
--- a/src/lib/corelib/loader/dependenciesresolver.cpp
+++ b/src/lib/corelib/loader/dependenciesresolver.cpp
@@ -148,12 +148,11 @@ public:
std::list<DependenciesResolvingState> stateStack;
private:
- bool hasDependencyToUnresolvedProduct() const override;
+ std::pair<ProductDependency, ProductContext *> pendingDependency() const override;
- void setSearchPathsForProduct();
+ void setSearchPathsForProduct(LoaderState &loaderState);
ProductContext &m_product;
- LoaderState &m_loaderState;
};
class DependenciesResolver
@@ -192,7 +191,7 @@ private:
LoaderState &m_loaderState;
ProductContext &m_product;
- const Deferral m_deferral;
+ Deferral m_deferral;
};
static bool haveSameSubProject(const ProductContext &p1, const ProductContext &p2);
@@ -256,6 +255,8 @@ void DependenciesResolver::resolve()
state.pendingResolvedDependencies = multiplexDependency(*state.currentDependsItem);
state.currentDependsItem.reset();
+ m_deferral = Deferral::Allowed; // We made progress.
+
continue;
}
@@ -336,6 +337,7 @@ HandleDependency DependenciesResolver::handleResolvedDependencies()
state.pendingResolvedDependencies.pop();
continue;
}
+ m_deferral = Deferral::Allowed; // We made progress.
break;
}
@@ -594,6 +596,7 @@ Item *DependenciesResolver::findMatchingModule(
dependency.name, dependency.fallbackMode)) {
QBS_CHECK(moduleItem->type() == ItemType::Module);
Item * const proto = moduleItem;
+ ModuleItemLocker locker(*moduleItem);
moduleItem = moduleItem->clone(m_loaderState.itemPool());
moduleItem->setPrototype(proto); // For parameter declarations.
return moduleItem;
@@ -1105,9 +1108,9 @@ QVariantMap safeToVariant(JSContext *ctx, const JSValue &v)
}
DependenciesContextImpl::DependenciesContextImpl(ProductContext &product, LoaderState &loaderState)
- : m_product(product), m_loaderState(loaderState)
+ : m_product(product)
{
- setSearchPathsForProduct();
+ setSearchPathsForProduct(loaderState);
// Initialize the state with the direct Depends items of the product item.
DependenciesResolvingState newState{product.item,};
@@ -1120,25 +1123,44 @@ DependenciesContextImpl::DependenciesContextImpl(ProductContext &product, Loader
FullyResolvedDependsItem::makeBaseDependency());
}
-bool DependenciesContextImpl::hasDependencyToUnresolvedProduct() const
+std::pair<ProductDependency, ProductContext *> DependenciesContextImpl::pendingDependency() const
{
QBS_CHECK(!stateStack.empty());
- if (stateStack.front().pendingResolvedDependencies.empty())
- return false;
- const ProductContext * const dep
- = stateStack.front().pendingResolvedDependencies.front().product;
- return dep && m_loaderState.topLevelProject().isProductQueuedForHandling(*dep);
+ if (stateStack.front().currentDependsItem
+ && !stateStack.front().currentDependsItem->productTypes.empty()) {
+ qCDebug(lcLoaderScheduling) << "product" << m_product.displayName()
+ << "to be delayed because of bulk dependency";
+ return {ProductDependency::Bulk, nullptr};
+ }
+ if (!stateStack.front().pendingResolvedDependencies.empty()) {
+ if (ProductContext * const dep = stateStack.front().pendingResolvedDependencies
+ .front().product) {
+ if (m_product.project->topLevelProject->isProductQueuedForHandling(*dep)) {
+ qCDebug(lcLoaderScheduling) << "product" << m_product.displayName()
+ << "to be delayed because of dependency "
+ "to unfinished product" << dep->displayName();
+ return {ProductDependency::Single, dep};
+ } else {
+ qCDebug(lcLoaderScheduling) << "product" << m_product.displayName()
+ << "to be re-scheduled, as dependency "
+ << dep->displayName()
+ << "appears to have finished in the meantime";
+ return {ProductDependency::None, dep};
+ }
+ }
+ }
+ return {ProductDependency::None, nullptr};
}
-void DependenciesContextImpl::setSearchPathsForProduct()
+void DependenciesContextImpl::setSearchPathsForProduct(LoaderState &loaderState)
{
QBS_CHECK(m_product.searchPaths.isEmpty());
- m_product.searchPaths = m_loaderState.itemReader().readExtraSearchPaths(m_product.item);
- Settings settings(m_loaderState.parameters().settingsDirectory());
+ m_product.searchPaths = loaderState.itemReader().readExtraSearchPaths(m_product.item);
+ Settings settings(loaderState.parameters().settingsDirectory());
const QStringList prefsSearchPaths = Preferences(&settings, m_product.profileModuleProperties)
.searchPaths();
- const QStringList &currentSearchPaths = m_loaderState.itemReader().allSearchPaths();
+ const QStringList &currentSearchPaths = loaderState.itemReader().allSearchPaths();
for (const QString &p : prefsSearchPaths) {
if (!currentSearchPaths.contains(p) && FileInfo(p).exists())
m_product.searchPaths << p;
diff --git a/src/lib/corelib/loader/itemreader.cpp b/src/lib/corelib/loader/itemreader.cpp
index e0ad9897d..0638d1af5 100644
--- a/src/lib/corelib/loader/itemreader.cpp
+++ b/src/lib/corelib/loader/itemreader.cpp
@@ -214,6 +214,14 @@ void ItemReader::handleAllPropertyOptionsItems(Item *item)
Item *ItemReader::setupItemFromFile(const QString &filePath, const CodeLocation &referencingLocation)
{
Item *item = readFile(filePath, referencingLocation);
+
+ // This is technically not needed, because files are only set up once and then served
+ // from a cache. But it simplifies the checks in item.cpp if we require the locking invariant
+ // to always hold.
+ std::unique_ptr<ModuleItemLocker> locker;
+ if (item->type() == ItemType::Module)
+ locker = std::make_unique<ModuleItemLocker>(*item);
+
handleAllPropertyOptionsItems(item);
return item;
}
diff --git a/src/lib/corelib/loader/itemreaderastvisitor.cpp b/src/lib/corelib/loader/itemreaderastvisitor.cpp
index 9968d2c92..6932e4f91 100644
--- a/src/lib/corelib/loader/itemreaderastvisitor.cpp
+++ b/src/lib/corelib/loader/itemreaderastvisitor.cpp
@@ -75,6 +75,8 @@ ItemReaderASTVisitor::ItemReaderASTVisitor(ItemReaderVisitorState &visitorState,
{
}
+ItemReaderASTVisitor::~ItemReaderASTVisitor() = default;
+
bool ItemReaderASTVisitor::visit(AST::UiProgram *uiProgram)
{
ASTImportsHandler importsHandler(m_visitorState, m_logger, m_file);
@@ -135,10 +137,15 @@ bool ItemReaderASTVisitor::visit(AST::UiObjectDefinition *ast)
item->m_type = itemType;
- if (m_item)
+ if (m_item) {
Item::addChild(m_item, item); // Add this item to the children of the parent item.
- else
+ } else {
m_item = item; // This is the root item.
+ if (itemType == ItemType::Module) {
+ QBS_CHECK(!m_moduleItemLocker);
+ m_moduleItemLocker = std::make_unique<ModuleItemLocker>(*m_item);
+ }
+ }
if (ast->initializer) {
Item *mdi = m_visitorState.mostDerivingItem();
@@ -335,6 +342,9 @@ void ItemReaderASTVisitor::inheritItem(Item *dst, const Item *src)
dst->setPropertyDeclaration(pd.name(), pd);
}
+ std::unique_ptr<ModuleItemLocker> locker;
+ if (src->type() == ItemType::Module)
+ locker = std::make_unique<ModuleItemLocker>(*src);
for (auto it = src->properties().constBegin(); it != src->properties().constEnd(); ++it) {
ValuePtr &v = dst->m_properties[it.key()];
if (!v) {
diff --git a/src/lib/corelib/loader/itemreaderastvisitor.h b/src/lib/corelib/loader/itemreaderastvisitor.h
index a102b2821..c47501f2f 100644
--- a/src/lib/corelib/loader/itemreaderastvisitor.h
+++ b/src/lib/corelib/loader/itemreaderastvisitor.h
@@ -49,6 +49,8 @@
#include <QtCore/qhash.h>
#include <QtCore/qstringlist.h>
+#include <memory.h>
+
namespace qbs {
class CodeLocation;
@@ -56,12 +58,15 @@ namespace Internal {
class Item;
class ItemPool;
class ItemReaderVisitorState;
+class ModuleItemLocker;
class ItemReaderASTVisitor : public QbsQmlJS::AST::Visitor
{
public:
ItemReaderASTVisitor(ItemReaderVisitorState &visitorState, FileContextPtr file,
ItemPool *itemPool, Logger &logger);
+ ~ItemReaderASTVisitor();
+
void checkItemTypes() { doCheckItemTypes(rootItem()); }
Item *rootItem() const { return m_item; }
@@ -88,6 +93,7 @@ private:
Logger &m_logger;
QHash<QStringList, QString> m_typeNameToFile;
Item *m_item = nullptr;
+ std::unique_ptr<ModuleItemLocker> m_moduleItemLocker;
ItemType m_instanceItemType = ItemType::ModuleInstancePlaceholder;
};
diff --git a/src/lib/corelib/loader/loaderutils.cpp b/src/lib/corelib/loader/loaderutils.cpp
index 3c8deb0ad..62e5c7cea 100644
--- a/src/lib/corelib/loader/loaderutils.cpp
+++ b/src/lib/corelib/loader/loaderutils.cpp
@@ -45,6 +45,7 @@
#include <language/filecontext.h>
#include <language/language.h>
#include <language/resolvedfilecontext.h>
+#include <language/scriptengine.h>
#include <language/value.h>
#include <logging/categories.h>
#include <logging/translator.h>
@@ -139,14 +140,10 @@ void TopLevelProjectContext::checkCancelation()
throw CancelException();
}
-bool TopLevelProjectContext::isCanceled() const
-{
- return m_canceled;
-}
-
QString TopLevelProjectContext::sourceCodeForEvaluation(const JSSourceValueConstPtr &value)
{
- QString &code = m_sourceCode[value->sourceCode()];
+ std::lock_guard lock(m_sourceCode.mutex);
+ QString &code = m_sourceCode.data[value->sourceCode()];
if (!code.isNull())
return code;
code = value->sourceCodeForEvaluation();
@@ -157,7 +154,8 @@ ScriptFunctionPtr TopLevelProjectContext::scriptFunctionValue(Item *item, const
{
const JSSourceValuePtr value = item->sourceProperty(name);
QBS_CHECK(value);
- ScriptFunctionPtr &script = m_scriptFunctionMap[value->location()];
+ std::lock_guard lock(m_scriptFunctionMap.mutex);
+ ScriptFunctionPtr &script = m_scriptFunctionMap.data[value->location()];
if (!script.get()) {
script = ScriptFunction::create();
const PropertyDeclaration decl = item->propertyDeclaration(name);
@@ -171,8 +169,9 @@ ScriptFunctionPtr TopLevelProjectContext::scriptFunctionValue(Item *item, const
QString TopLevelProjectContext::sourceCodeAsFunction(const JSSourceValueConstPtr &value,
const PropertyDeclaration &decl)
{
- QString &scriptFunction = m_scriptFunctions[std::make_pair(value->sourceCode(),
- decl.functionArgumentNames())];
+ std::lock_guard lock(m_scriptFunctions.mutex);
+ QString &scriptFunction = m_scriptFunctions.data[std::make_pair(value->sourceCode(),
+ decl.functionArgumentNames())];
if (!scriptFunction.isNull())
return scriptFunction;
const QString args = decl.functionArgumentNames().join(QLatin1Char(','));
@@ -199,34 +198,28 @@ const ResolvedFileContextPtr &TopLevelProjectContext::resolvedFileContext(
return result;
}
-void TopLevelProjectContext::setCanceled()
-{
- m_canceled = true;
-}
-
-int TopLevelProjectContext::productCount() const
-{
- return m_productsByName.size();
-}
-
void TopLevelProjectContext::removeProductToHandle(const ProductContext &product)
{
- m_productsToHandle.remove(&product);
+ std::unique_lock lock(m_productsToHandle.mutex);
+ m_productsToHandle.data.remove(&product);
}
bool TopLevelProjectContext::isProductQueuedForHandling(const ProductContext &product) const
{
- return m_productsToHandle.contains(&product);
+ std::shared_lock lock(m_productsToHandle.mutex);
+ return m_productsToHandle.data.contains(&product);
}
void TopLevelProjectContext::addDisabledItem(Item *item)
{
- m_disabledItems << item;
+ std::unique_lock lock(m_disabledItems.mutex);
+ m_disabledItems.data << item;
}
bool TopLevelProjectContext::isDisabledItem(Item *item) const
{
- return m_disabledItems.contains(item);
+ std::shared_lock lock(m_disabledItems.mutex);
+ return m_disabledItems.data.contains(item);
}
void TopLevelProjectContext::setProgressObserver(ProgressObserver *observer)
@@ -238,7 +231,8 @@ ProgressObserver *TopLevelProjectContext::progressObserver() const { return m_pr
void TopLevelProjectContext::addQueuedError(const ErrorInfo &error)
{
- m_queuedErrors << error;
+ std::lock_guard lock(m_queuedErrors.mutex);
+ m_queuedErrors.data << error;
}
void TopLevelProjectContext::addProfileConfig(const QString &profileName,
@@ -255,9 +249,9 @@ std::optional<QVariantMap> TopLevelProjectContext::profileConfig(const QString &
return it.value().toMap();
}
-void TopLevelProjectContext::addProjectLevelProbes(const std::vector<ProbeConstPtr> &probes)
+void TopLevelProjectContext::addProjectLevelProbe(const ProbeConstPtr &probe)
{
- m_probesInfo.projectLevelProbes << probes;
+ m_probesInfo.projectLevelProbes << probe;
}
const std::vector<ProbeConstPtr> TopLevelProjectContext::projectLevelProbes() const
@@ -272,8 +266,9 @@ void TopLevelProjectContext::addProduct(ProductContext &product)
void TopLevelProjectContext::addProductByType(ProductContext &product, const FileTags &tags)
{
+ std::unique_lock lock(m_productsByType.mutex);
for (const FileTag &tag : tags)
- m_productsByType.insert({tag, &product});
+ m_productsByType.data.insert({tag, &product});
}
ProductContext *TopLevelProjectContext::productWithNameAndConstraint(
@@ -304,9 +299,10 @@ std::vector<ProductContext *> TopLevelProjectContext::productsWithNameAndConstra
std::vector<ProductContext *> TopLevelProjectContext::productsWithTypeAndConstraint(
const FileTags &tags, const std::function<bool (ProductContext &)> &constraint)
{
+ std::shared_lock lock(m_productsByType.mutex);
std::vector<ProductContext *> matchingProducts;
for (const FileTag &typeTag : tags) {
- const auto range = m_productsByType.equal_range(typeTag);
+ const auto range = m_productsByType.data.equal_range(typeTag);
for (auto it = range.first; it != range.second; ++it) {
if (constraint(*it->second))
matchingProducts.push_back(it->second);
@@ -349,6 +345,11 @@ QVariantMap TopLevelProjectContext::multiplexConfiguration(const QString &id) co
return it->second;
}
+std::lock_guard<std::mutex> TopLevelProjectContext::moduleProvidersCacheLock()
+{
+ return std::lock_guard<std::mutex>(m_moduleProvidersCacheMutex);
+}
+
void TopLevelProjectContext::setModuleProvidersCache(const ModuleProvidersCache &cache)
{
m_moduleProvidersCache = cache;
@@ -370,13 +371,15 @@ ModuleProviderInfo &TopLevelProjectContext::addModuleProvider(const ModuleProvid
void TopLevelProjectContext::addParameterDeclarations(const Item *moduleProto,
const Item::PropertyDeclarationMap &decls)
{
- m_parameterDeclarations.insert({moduleProto, decls});
+ std::unique_lock lock(m_parameterDeclarations.mutex);
+ m_parameterDeclarations.data.insert({moduleProto, decls});
}
Item::PropertyDeclarationMap TopLevelProjectContext::parameterDeclarations(Item *moduleProto) const
{
- if (const auto it = m_parameterDeclarations.find(moduleProto);
- it != m_parameterDeclarations.end()) {
+ std::shared_lock lock(m_parameterDeclarations.mutex);
+ if (const auto it = m_parameterDeclarations.data.find(moduleProto);
+ it != m_parameterDeclarations.data.end()) {
return it->second;
}
return {};
@@ -386,7 +389,8 @@ QString TopLevelProjectContext::findModuleDirectory(
const QualifiedId &module, const QString &searchPath,
const std::function<QString()> &findOnDisk)
{
- auto &path = m_modulePathCache[{searchPath, module}];
+ std::lock_guard lock(m_modulePathCache.mutex);
+ auto &path = m_modulePathCache.data[{searchPath, module}];
if (!path)
path = findOnDisk();
return *path;
@@ -395,7 +399,8 @@ QString TopLevelProjectContext::findModuleDirectory(
QStringList TopLevelProjectContext::getModuleFilesForDirectory(
const QString &dir, const std::function<QStringList ()> &findOnDisk)
{
- auto &list = m_moduleFilesPerDirectory[dir];
+ std::lock_guard lock(m_moduleFilesPerDirectory.mutex);
+ auto &list = m_moduleFilesPerDirectory.data[dir];
if (!list)
list = findOnDisk();
return *list;
@@ -403,22 +408,25 @@ QStringList TopLevelProjectContext::getModuleFilesForDirectory(
void TopLevelProjectContext::removeModuleFileFromDirectoryCache(const QString &filePath)
{
- const auto it = m_moduleFilesPerDirectory.find(FileInfo::path(filePath));
- QBS_CHECK(it != m_moduleFilesPerDirectory.end());
+ std::lock_guard lock(m_moduleFilesPerDirectory.mutex);
+ const auto it = m_moduleFilesPerDirectory.data.find(FileInfo::path(filePath));
+ QBS_CHECK(it != m_moduleFilesPerDirectory.data.end());
it->second->removeOne(filePath);
}
void TopLevelProjectContext::addUnknownProfilePropertyError(const Item *moduleProto,
const ErrorInfo &error)
{
- m_unknownProfilePropertyErrors[moduleProto].push_back(error);
+ std::unique_lock lock(m_unknownProfilePropertyErrors.mutex);
+ m_unknownProfilePropertyErrors.data[moduleProto].push_back(error);
}
const std::vector<ErrorInfo> &TopLevelProjectContext::unknownProfilePropertyErrors(
const Item *moduleProto) const
{
- if (const auto it = m_unknownProfilePropertyErrors.find(moduleProto);
- it != m_unknownProfilePropertyErrors.end()) {
+ std::shared_lock lock(m_unknownProfilePropertyErrors.mutex);
+ if (const auto it = m_unknownProfilePropertyErrors.data.find(moduleProto);
+ it != m_unknownProfilePropertyErrors.data.end()) {
return it->second;
}
static const std::vector<ErrorInfo> empty;
@@ -428,7 +436,8 @@ const std::vector<ErrorInfo> &TopLevelProjectContext::unknownProfilePropertyErro
Item *TopLevelProjectContext::getModulePrototype(const QString &filePath, const QString &profile,
const std::function<Item *()> &produce)
{
- auto &prototypeList = m_modulePrototypes[filePath];
+ std::lock_guard lock(m_modulePrototypes.mutex);
+ auto &prototypeList = m_modulePrototypes.data[filePath];
for (const auto &prototype : prototypeList) {
if (prototype.second == profile)
return prototype.first;
@@ -446,6 +455,11 @@ void TopLevelProjectContext::addLocalProfile(const QString &name, const QVariant
m_localProfiles.insert(name, values);
}
+std::lock_guard<std::mutex> TopLevelProjectContext::probesCacheLock()
+{
+ return std::lock_guard<std::mutex>(m_probesMutex);
+}
+
void TopLevelProjectContext::setOldProjectProbes(const std::vector<ProbeConstPtr> &oldProbes)
{
for (const ProbeConstPtr& probe : oldProbes)
@@ -493,27 +507,41 @@ ProbeConstPtr TopLevelProjectContext::findCurrentProbe(const CodeLocation &locat
return {};
}
+void TopLevelProjectContext::collectDataFromEngine(const ScriptEngine &engine)
+{
+ const auto project = dynamic_cast<TopLevelProject *>(m_projects.front()->project.get());
+ QBS_CHECK(project);
+ project->canonicalFilePathResults.insert(engine.canonicalFilePathResults());
+ project->fileExistsResults.insert(engine.fileExistsResults());
+ project->directoryEntriesResults.insert(engine.directoryEntriesResults());
+ project->fileLastModifiedResults.insert(engine.fileLastModifiedResults());
+ project->environment.insert(engine.environment());
+ project->buildSystemFiles.unite(engine.imports());
+}
+
class LoaderState::Private
{
public:
- Private(LoaderState &q, const SetupProjectParameters &parameters, ItemPool &itemPool,
- Evaluator &evaluator, Logger &logger)
- : parameters(parameters), itemPool(itemPool), evaluator(evaluator), logger(logger),
- itemReader(q)
+ Private(LoaderState &q, const SetupProjectParameters &parameters,
+ TopLevelProjectContext &topLevelProject, ItemPool &itemPool, Evaluator &evaluator,
+ Logger &logger)
+ : parameters(parameters), topLevelProject(topLevelProject), itemPool(itemPool),
+ evaluator(evaluator), logger(logger), itemReader(q)
{}
const SetupProjectParameters &parameters;
+ TopLevelProjectContext &topLevelProject;
ItemPool &itemPool;
Evaluator &evaluator;
Logger &logger;
- TopLevelProjectContext topLevelProject;
ItemReader itemReader;
};
-LoaderState::LoaderState(const SetupProjectParameters &parameters, ItemPool &itemPool,
+LoaderState::LoaderState(const SetupProjectParameters &parameters,
+ TopLevelProjectContext &topLevelProject, ItemPool &itemPool,
Evaluator &evaluator, Logger &logger)
- : d(makePimpl<Private>(*this, parameters, itemPool, evaluator, logger))
+ : d(makePimpl<Private>(*this, parameters, topLevelProject, itemPool, evaluator, logger))
{
d->itemReader.init();
}
@@ -740,9 +768,10 @@ bool ProductContext::dependenciesResolvingPending() const
&& !product && !delayedError.hasError();
}
-bool ProductContext::hasDependencyToUnresolvedProduct() const
+std::pair<ProductDependency, ProductContext *> ProductContext::pendingDependency() const
{
- return dependenciesContext && dependenciesContext->hasDependencyToUnresolvedProduct();
+ return dependenciesContext ? dependenciesContext->pendingDependency()
+ : std::make_pair(ProductDependency::None, nullptr);
}
TimingData &TimingData::operator+=(const TimingData &other)
@@ -766,7 +795,8 @@ DependenciesContext::~DependenciesContext() = default;
ItemReaderCache::AstCacheEntry &ItemReaderCache::retrieveOrSetupCacheEntry(
const QString &filePath, const std::function<void (AstCacheEntry &)> &setup)
{
- AstCacheEntry &entry = m_astCache[filePath];
+ std::lock_guard lock(m_astCache.mutex);
+ AstCacheEntry &entry = m_astCache.data[filePath];
if (!entry.ast) {
setup(entry);
m_filesRead << filePath;
@@ -777,7 +807,8 @@ ItemReaderCache::AstCacheEntry &ItemReaderCache::retrieveOrSetupCacheEntry(
const QStringList &ItemReaderCache::retrieveOrSetDirectoryEntries(
const QString &dir, const std::function<QStringList ()> &findOnDisk)
{
- auto &entries = m_directoryEntries[dir];
+ std::lock_guard lock(m_directoryEntries.mutex);
+ auto &entries = m_directoryEntries.data[dir];
if (!entries)
entries = findOnDisk();
return *entries;
@@ -785,12 +816,14 @@ const QStringList &ItemReaderCache::retrieveOrSetDirectoryEntries(
bool ItemReaderCache::AstCacheEntry::addProcessingThread()
{
- return m_processingThreads.insert(std::this_thread::get_id()).second;
+ std::lock_guard lock(m_processingThreads.mutex);
+ return m_processingThreads.data.insert(std::this_thread::get_id()).second;
}
void ItemReaderCache::AstCacheEntry::removeProcessingThread()
{
- m_processingThreads.remove(std::this_thread::get_id());
+ std::lock_guard lock(m_processingThreads.mutex);
+ m_processingThreads.data.remove(std::this_thread::get_id());
}
} // namespace qbs::Internal
diff --git a/src/lib/corelib/loader/loaderutils.h b/src/lib/corelib/loader/loaderutils.h
index 8f09e5873..4b78acb08 100644
--- a/src/lib/corelib/loader/loaderutils.h
+++ b/src/lib/corelib/loader/loaderutils.h
@@ -56,9 +56,12 @@
#include <QStringList>
#include <QVariant>
+#include <atomic>
#include <functional>
#include <memory>
+#include <mutex>
#include <optional>
+#include <shared_mutex>
#include <thread>
#include <utility>
#include <vector>
@@ -73,15 +76,23 @@ class Logger;
class ProductContext;
class ProgressObserver;
class ProjectContext;
+class ScriptEngine;
using ModulePropertiesPerGroup = std::unordered_map<const Item *, QualifiedIdSet>;
using FileLocations = QHash<std::pair<QString, QString>, CodeLocation>;
enum class FallbackMode { Enabled, Disabled };
enum class Deferral { Allowed, NotAllowed };
+enum class ProductDependency { None, Single, Bulk };
class CancelException { };
+template<typename DataType, typename MutexType = std::shared_mutex>
+struct GuardedData {
+ DataType data;
+ mutable MutexType mutex;
+};
+
class TimingData
{
public:
@@ -95,6 +106,7 @@ public:
qint64 groupsResolving = 0;
qint64 preparingProducts = 0;
qint64 resolvingProducts = 0;
+ qint64 schedulingProducts = 0;
qint64 probes = 0;
qint64 propertyEvaluation = 0;
qint64 propertyChecking = 0;
@@ -114,7 +126,7 @@ public:
void removeProcessingThread();
private:
- Set<std::thread::id> m_processingThreads;
+ GuardedData<Set<std::thread::id>, std::recursive_mutex> m_processingThreads;
};
const Set<QString> &filesRead() const { return m_filesRead; }
@@ -125,15 +137,15 @@ public:
private:
Set<QString> m_filesRead;
- std::unordered_map<QString, std::optional<QStringList>> m_directoryEntries; // TODO: Merge with module dir entries cache?
- std::unordered_map<QString, AstCacheEntry> m_astCache;
+ GuardedData<std::unordered_map<QString, std::optional<QStringList>>, std::mutex> m_directoryEntries; // TODO: Merge with module dir entries cache?
+ GuardedData<std::unordered_map<QString, AstCacheEntry>, std::mutex> m_astCache;
};
class DependenciesContext
{
public:
virtual ~DependenciesContext();
- virtual bool hasDependencyToUnresolvedProduct() const = 0;
+ virtual std::pair<ProductDependency, ProductContext *> pendingDependency() const = 0;
bool dependenciesResolved = false;
};
@@ -145,7 +157,7 @@ public:
QString displayName() const;
void handleError(const ErrorInfo &error);
bool dependenciesResolvingPending() const;
- bool hasDependencyToUnresolvedProduct() const;
+ std::pair<ProductDependency, ProductContext *> pendingDependency() const;
QString name;
QString buildDirectory;
@@ -188,17 +200,17 @@ public:
ScriptFunctionPtr scriptFunctionValue(Item *item, const QString &name);
QString sourceCodeAsFunction(const JSSourceValueConstPtr &value,
const PropertyDeclaration &decl);
- const ResolvedFileContextPtr &resolvedFileContext(const FileContextConstPtr &ctx);
- void setCanceled();
+ void setCanceled() { m_canceled = true; }
void checkCancelation();
- bool isCanceled() const;
+ bool isCanceled() const { return m_canceled; }
- int productCount() const;
+ int productCount() const { return m_productsByName.size(); }
- void addProductToHandle(const ProductContext &product) { m_productsToHandle << &product; }
+ void addProductToHandle(const ProductContext &product) { m_productsToHandle.data << &product; }
void removeProductToHandle(const ProductContext &product);
bool isProductQueuedForHandling(const ProductContext &product) const;
+ int productsToHandleCount() const { return m_productsToHandle.data.size(); }
void addDisabledItem(Item *item);
bool isDisabledItem(Item *item) const;
@@ -210,7 +222,7 @@ public:
const std::vector<ProjectContext *> &projects() const { return m_projects; }
void addQueuedError(const ErrorInfo &error);
- const std::vector<ErrorInfo> &queuedErrors() const { return m_queuedErrors; }
+ const std::vector<ErrorInfo> &queuedErrors() const { return m_queuedErrors.data; }
void setProfileConfigs(const QVariantMap &profileConfigs) { m_profileConfigs = profileConfigs; }
void addProfileConfig(const QString &profileName, const QVariantMap &profileConfig);
@@ -243,13 +255,15 @@ public:
Set<QString> buildSystemFiles() const { return m_itemReaderCache.filesRead(); }
+ std::lock_guard<std::mutex> moduleProvidersCacheLock();
void setModuleProvidersCache(const ModuleProvidersCache &cache);
const ModuleProvidersCache &moduleProvidersCache() const { return m_moduleProvidersCache; }
ModuleProviderInfo *moduleProvider(const ModuleProvidersCacheKey &key);
ModuleProviderInfo &addModuleProvider(const ModuleProvidersCacheKey &key,
const ModuleProviderInfo &provider);
- void addParameterDeclarations(const Item *moduleProto, const Item::PropertyDeclarationMap &decls);
+ void addParameterDeclarations(const Item *moduleProto,
+ const Item::PropertyDeclarationMap &decls);
Item::PropertyDeclarationMap parameterDeclarations(Item *moduleProto) const;
// An empty string means no matching module directory was found.
@@ -271,10 +285,11 @@ public:
const QVariantMap localProfiles() { return m_localProfiles; }
using ProbeFilter = std::function<bool(const ProbeConstPtr &)>;
+ std::lock_guard<std::mutex> probesCacheLock();
void setOldProjectProbes(const std::vector<ProbeConstPtr> &oldProbes);
void setOldProductProbes(const QHash<QString, std::vector<ProbeConstPtr>> &oldProbes);
void addNewlyResolvedProbe(const ProbeConstPtr &probe);
- void addProjectLevelProbes(const std::vector<ProbeConstPtr> &probes);
+ void addProjectLevelProbe(const ProbeConstPtr &probe);
const std::vector<ProbeConstPtr> projectLevelProbes() const;
ProbeConstPtr findOldProjectProbe(const QString &id, const ProbeFilter &filter) const;
ProbeConstPtr findOldProductProbe(const QString &productName, const ProbeFilter &filter) const;
@@ -294,43 +309,53 @@ public:
void incProductDeferrals() { ++m_productDeferrals; }
int productDeferrals() const { return m_productDeferrals; }
+ void collectDataFromEngine(const ScriptEngine &engine);
+
private:
+ const ResolvedFileContextPtr &resolvedFileContext(const FileContextConstPtr &ctx);
+
std::vector<ProjectContext *> m_projects;
- Set<const ProductContext *> m_productsToHandle;
+ GuardedData<Set<const ProductContext *>> m_productsToHandle;
std::multimap<QString, ProductContext *> m_productsByName;
- std::unordered_map<QStringView, QString> m_sourceCode;
+ GuardedData<std::unordered_map<QStringView, QString>, std::mutex> m_sourceCode;
std::unordered_map<QString, QVariantMap> m_multiplexConfigsById;
- QHash<CodeLocation, ScriptFunctionPtr> m_scriptFunctionMap;
- std::unordered_map<std::pair<QStringView, QStringList>, QString> m_scriptFunctions;
+ GuardedData<QHash<CodeLocation, ScriptFunctionPtr>, std::mutex> m_scriptFunctionMap;
+ GuardedData<std::unordered_map<std::pair<QStringView, QStringList>, QString>,
+ std::mutex> m_scriptFunctions;
std::unordered_map<FileContextConstPtr, ResolvedFileContextPtr> m_fileContextMap;
Set<QString> m_projectNamesUsedInOverrides;
Set<QString> m_productNamesUsedInOverrides;
- Set<Item *> m_disabledItems;
- std::vector<ErrorInfo> m_queuedErrors;
+ GuardedData<Set<Item *>> m_disabledItems;
+ GuardedData<std::vector<ErrorInfo>, std::mutex> m_queuedErrors;
QString m_buildDirectory;
QVariantMap m_profileConfigs;
ProgressObserver *m_progressObserver = nullptr;
TimingData m_timingData;
ModuleProvidersCache m_moduleProvidersCache;
+ std::mutex m_moduleProvidersCacheMutex;
QVariantMap m_localProfiles;
ItemReaderCache m_itemReaderCache;
// For fast look-up when resolving Depends.productTypes.
// The contract is that it contains fully handled, error-free, enabled products.
- std::multimap<FileTag, ProductContext *> m_productsByType;
+ GuardedData<std::multimap<FileTag, ProductContext *>> m_productsByType;
// The keys are module prototypes.
- std::unordered_map<const Item *, Item::PropertyDeclarationMap> m_parameterDeclarations;
- std::unordered_map<const Item *, std::vector<ErrorInfo>> m_unknownProfilePropertyErrors;
+ GuardedData<std::unordered_map<const Item *,
+ Item::PropertyDeclarationMap>> m_parameterDeclarations;
+ GuardedData<std::unordered_map<const Item *,
+ std::vector<ErrorInfo>>> m_unknownProfilePropertyErrors;
// The keys are search path + module name, the values are directories.
- QHash<std::pair<QString, QualifiedId>, std::optional<QString>> m_modulePathCache;
+ GuardedData<QHash<std::pair<QString, QualifiedId>, std::optional<QString>>,
+ std::mutex> m_modulePathCache;
// The keys are file paths, the values are module prototype items accompanied by a profile.
- std::unordered_map<QString, std::vector<std::pair<Item *, QString>>> m_modulePrototypes;
-
- std::map<QString, std::optional<QStringList>> m_moduleFilesPerDirectory;
+ GuardedData<std::unordered_map<QString, std::vector<std::pair<Item *, QString>>>,
+ std::mutex> m_modulePrototypes;
+ GuardedData<std::map<QString, std::optional<QStringList>>,
+ std::mutex> m_moduleFilesPerDirectory;
struct {
QHash<QString, std::vector<ProbeConstPtr>> oldProjectProbes;
@@ -343,11 +368,12 @@ private:
quint64 probesCachedCurrent = 0;
quint64 probesCachedOld = 0;
} m_probesInfo;
+ std::mutex m_probesMutex;
FileTime m_lastResolveTime;
+ std::atomic_bool m_canceled = false;
int m_productDeferrals = 0;
- bool m_canceled = false;
};
class ProjectContext
@@ -378,8 +404,8 @@ public:
class LoaderState
{
public:
- LoaderState(const SetupProjectParameters &parameters, ItemPool &itemPool, Evaluator &evaluator,
- Logger &logger);
+ LoaderState(const SetupProjectParameters &parameters, TopLevelProjectContext &topLevelProject,
+ ItemPool &itemPool, Evaluator &evaluator, Logger &logger);
~LoaderState();
Evaluator &evaluator();
diff --git a/src/lib/corelib/loader/moduleloader.cpp b/src/lib/corelib/loader/moduleloader.cpp
index 71f886765..c937e8b22 100644
--- a/src/lib/corelib/loader/moduleloader.cpp
+++ b/src/lib/corelib/loader/moduleloader.cpp
@@ -190,6 +190,7 @@ Item *ModuleLoader::load()
moduleItem = candidates.at(0).item;
} else {
for (auto &candidate : candidates) {
+ ModuleItemLocker lock(*candidate.item);
candidate.priority = m_loaderState.evaluator()
.intValue(candidate.item, StringConstants::priorityProperty(),
candidate.priority);
@@ -250,6 +251,9 @@ Item *ModuleLoader::createAndInitModuleItem(const QString &moduleName, const QSt
return nullptr;
}
+ // Not technically needed, but we want to keep the invariant in item.cpp.
+ ModuleItemLocker locker(*module);
+
module->setProperty(StringConstants::nameProperty(), VariantValue::create(moduleName));
if (moduleName == StringConstants::qbsModule()) {
module->setProperty(QStringLiteral("hostPlatform"),
@@ -302,6 +306,8 @@ Item *ModuleLoader::createAndInitModuleItem(const QString &moduleName, const QSt
bool ModuleLoader::evaluateModuleCondition(Item *module, const QString &fullModuleName)
{
+ ModuleItemLocker locker(*module);
+
// Temporarily make the product's qbs module instance available, so the condition
// can use qbs.targetOS etc.
class TempQbsModuleProvider {
diff --git a/src/lib/corelib/loader/moduleproviderloader.cpp b/src/lib/corelib/loader/moduleproviderloader.cpp
index 9272a9f8e..9ab1fd14f 100644
--- a/src/lib/corelib/loader/moduleproviderloader.cpp
+++ b/src/lib/corelib/loader/moduleproviderloader.cpp
@@ -169,6 +169,7 @@ ModuleProviderLoader::findOrCreateProviderInfo(
const QVariantMap &qbsModule)
{
const QVariantMap config = product.providerConfig->value(name.toString()).toMap();
+ std::lock_guard lock(m_loaderState.topLevelProject().moduleProvidersCacheLock());
ModuleProvidersCacheKey cacheKey{name.toString(), {}, config, qbsModule, int(lookupType)};
// TODO: get rid of non-eager providers and eliminate following if-logic
// first, try to find eager provider (stored with an empty module name)
diff --git a/src/lib/corelib/loader/probesresolver.cpp b/src/lib/corelib/loader/probesresolver.cpp
index 59b86420d..b38366900 100644
--- a/src/lib/corelib/loader/probesresolver.cpp
+++ b/src/lib/corelib/loader/probesresolver.cpp
@@ -92,18 +92,20 @@ void ProbesResolver::resolveProbes(ProductContext &productContext, Item *item)
EvalContext::ProbeExecution);
for (Item * const child : item->children()) {
if (child->type() == ItemType::Probe)
- productContext.probes.push_back(resolveProbe(productContext, item, child));
+ resolveProbe(productContext, item, child);
}
}
-ProbeConstPtr ProbesResolver::resolveProbe(ProductContext &productContext, Item *parent,
+void ProbesResolver::resolveProbe(ProductContext &productContext, Item *parent,
Item *probe)
{
qCDebug(lcModuleLoader) << "Resolving Probe at " << probe->location().toString();
- m_loaderState.topLevelProject().incrementProbesCount();
const QString &probeId = probeGlobalId(probe);
if (Q_UNLIKELY(probeId.isEmpty()))
throw ErrorInfo(Tr::tr("Probe.id must be set."), probe->location());
+ const bool isProjectLevelProbe
+ = parent->type() == ItemType::Project
+ || productContext.name.startsWith(StringConstants::shadowProductPrefix());
const JSSourceValueConstPtr configureScript
= probe->sourceProperty(StringConstants::configureProperty());
QBS_CHECK(configureScript);
@@ -130,8 +132,9 @@ ProbeConstPtr ProbesResolver::resolveProbe(ProductContext &productContext, Item
const bool condition = evaluator.boolValue(probe, StringConstants::conditionProperty());
const QString &sourceCode = configureScript->sourceCode().toString();
ProbeConstPtr resolvedProbe;
- if (parent->type() == ItemType::Project
- || productContext.name.startsWith(StringConstants::shadowProductPrefix())) {
+ std::lock_guard lock(m_loaderState.topLevelProject().probesCacheLock());
+ m_loaderState.topLevelProject().incrementProbesCount();
+ if (isProjectLevelProbe) {
resolvedProbe = findOldProjectProbe(probeId, condition, initialProperties, sourceCode);
} else {
resolvedProbe = findOldProductProbe(productContext.uniqueName(), condition,
@@ -228,7 +231,10 @@ ProbeConstPtr ProbesResolver::resolveProbe(ProductContext &productContext, Item
importedFilesUsedInConfigure);
m_loaderState.topLevelProject().addNewlyResolvedProbe(resolvedProbe);
}
- return resolvedProbe;
+ if (isProjectLevelProbe)
+ m_loaderState.topLevelProject().addProjectLevelProbe(resolvedProbe);
+ else
+ productContext.probes << resolvedProbe;
}
ProbeConstPtr ProbesResolver::findOldProjectProbe(
diff --git a/src/lib/corelib/loader/probesresolver.h b/src/lib/corelib/loader/probesresolver.h
index 2b68ed261..4c49861d3 100644
--- a/src/lib/corelib/loader/probesresolver.h
+++ b/src/lib/corelib/loader/probesresolver.h
@@ -46,8 +46,6 @@
#include <QString>
-#include <vector>
-
namespace qbs::Internal {
class Item;
class LoaderState;
@@ -72,7 +70,7 @@ private:
bool probeMatches(const ProbeConstPtr &probe, bool condition,
const QVariantMap &initialProperties, const QString &configureScript,
CompareScript compareScript) const;
- ProbeConstPtr resolveProbe(ProductContext &productContext, Item *parent, Item *probe);
+ void resolveProbe(ProductContext &productContext, Item *parent, Item *probe);
LoaderState &m_loaderState;
};
diff --git a/src/lib/corelib/loader/productresolver.cpp b/src/lib/corelib/loader/productresolver.cpp
index 3c1191601..694b12c0a 100644
--- a/src/lib/corelib/loader/productresolver.cpp
+++ b/src/lib/corelib/loader/productresolver.cpp
@@ -190,10 +190,8 @@ void resolveProduct(ProductContext &product, Deferral deferral, LoaderState &loa
if (product.dependenciesResolvingPending())
return;
- if (product.name.startsWith(StringConstants::shadowProductPrefix())) {
- loaderState.topLevelProject().addProjectLevelProbes(product.probes);
+ if (product.name.startsWith(StringConstants::shadowProductPrefix()))
return;
- }
// TODO: The weird double-forwarded error handling can hopefully be simplified now.
try {
@@ -522,7 +520,6 @@ void ProductResolverStage2::resolveProductFully()
{
Item * const item = m_product.item;
const ResolvedProductPtr product = m_product.product;
- product->project->products.push_back(product);
Evaluator &evaluator = m_loaderState.evaluator();
product->name = evaluator.stringValue(item, StringConstants::nameProperty());
@@ -1331,6 +1328,7 @@ void ExportsResolver::collectPropertiesForModuleInExportItem(const Item::Module
modulePrototype = modulePrototype->prototype();
if (!modulePrototype) // Can happen for broken products in relaxed mode.
return;
+ ModuleItemLocker locker(*modulePrototype);
const Item::PropertyMap &props = modulePrototype->properties();
ExportedModuleDependency dep;
dep.name = module.name.toString();
diff --git a/src/lib/corelib/loader/productscollector.cpp b/src/lib/corelib/loader/productscollector.cpp
index 7732c7d7a..254ec0561 100644
--- a/src/lib/corelib/loader/productscollector.cpp
+++ b/src/lib/corelib/loader/productscollector.cpp
@@ -195,7 +195,6 @@ void ProductsCollector::Private::handleProject(Item *projectItem, ProjectContext
child->setScope(projectContext.scope);
ProbesResolver(loaderState).resolveProbes(dummyProduct, projectItem);
- projectContext.topLevelProject->addProjectLevelProbes(dummyProduct.probes);
collectProfilesFromItems(projectItem, projectContext.scope, loaderState);
diff --git a/src/lib/corelib/loader/productsresolver.cpp b/src/lib/corelib/loader/productsresolver.cpp
index 3bf055ea7..96a9a011c 100644
--- a/src/lib/corelib/loader/productsresolver.cpp
+++ b/src/lib/corelib/loader/productsresolver.cpp
@@ -43,15 +43,52 @@
#include "loaderutils.h"
#include "productresolver.h"
+#include <language/evaluator.h>
+#include <language/language.h>
+#include <language/scriptengine.h>
+#include <logging/categories.h>
#include <tools/profiling.h>
+#include <tools/progressobserver.h>
#include <tools/setupprojectparameters.h>
#include <tools/stringconstants.h>
#include <algorithm>
+#include <condition_variable>
+#include <future>
+#include <mutex>
#include <queue>
+#include <system_error>
+#include <thread>
+#include <unordered_map>
#include <vector>
namespace qbs::Internal {
+namespace {
+struct ThreadInfo {
+ ThreadInfo(std::future<void> &&future, Evaluator *evaluator)
+ : future(std::move(future)), evaluator(evaluator)
+ {}
+ std::future<void> future;
+ Evaluator * const evaluator;
+ bool done = false;
+};
+
+struct ProductWithEvaluator {
+ ProductWithEvaluator(ProductContext &product, Evaluator *evaluator)
+ : product(&product), evaluator(evaluator) {}
+ ProductContext * const product;
+ Evaluator *evaluator;
+};
+
+class ThreadsLocker {
+public:
+ ThreadsLocker(std::launch mode, std::mutex &mutex) {
+ if (mode == std::launch::async)
+ lock = std::make_unique<std::unique_lock<std::mutex>>(mutex);
+ }
+ std::unique_ptr<std::unique_lock<std::mutex>> lock;
+};
+} // namespace
class ProductsResolver
{
@@ -61,15 +98,43 @@ public:
private:
void initialize();
+ void initializeProductQueue();
+ void initializeEvaluatorPool();
void runScheduler();
+ void scheduleNext();
+ bool tryToReserveEvaluator(ProductWithEvaluator &product, Deferral deferral);
+ std::optional<std::pair<ProductContext *, Deferral>>
+ unblockProductWaitingForEvaluator(Evaluator &evaluator);
+ void startJob(const ProductWithEvaluator &product, Deferral deferral);
+ void checkForCancelation();
+ void handleFinishedThreads();
+ void queueProductForScheduling(const ProductWithEvaluator &product, Deferral deferral);
+ void waitForSingleDependency(const ProductWithEvaluator &product, ProductContext &dependency);
+ void waitForBulkDependency(const ProductWithEvaluator &product);
+ void unblockProductsWaitingForDependency(ProductContext &finishedProduct);
void postProcess();
static int dependsItemCount(const Item *item);
static int dependsItemCount(ProductContext &product);
LoaderState &m_loaderState;
- std::queue<std::pair<ProductContext *, int>> m_productsToHandle;
+ std::queue<std::pair<ProductWithEvaluator, int>> m_productsToSchedule;
std::vector<ProductContext *> m_finishedProducts;
+ std::unordered_map<ProductContext *,
+ std::vector<ProductWithEvaluator>> m_waitingForSingleDependency;
+ std::vector<ProductWithEvaluator> m_waitingForBulkDependency;
+ std::unordered_map<Evaluator *,
+ std::queue<std::pair<ProductContext *, Deferral>>> m_waitingForEvaluator;
+ std::unordered_map<ProductContext *, ThreadInfo> m_runningThreads;
+ std::mutex m_threadsMutex;
+ std::condition_variable m_threadsNotifier;
+ std::vector<std::unique_ptr<ScriptEngine>> m_enginePool;
+ std::vector<std::unique_ptr<Evaluator>> m_evaluatorPool;
+ std::vector<Evaluator *> m_availableEvaluators;
+ std::mutex m_cancelingMutex;
+ std::launch m_asyncMode = std::launch::async;
+ int m_maxJobCount = m_loaderState.parameters().maxJobCount();
+ bool m_canceling = false;
};
void resolveProducts(LoaderState &loaderState)
@@ -80,12 +145,24 @@ void resolveProducts(LoaderState &loaderState)
void ProductsResolver::resolve()
{
initialize();
- runScheduler();
+ try {
+ runScheduler();
+ } catch (const ErrorInfo &e) {
+ for (auto &thread : m_runningThreads)
+ thread.second.future.wait();
+ throw e;
+ }
postProcess();
}
void ProductsResolver::initialize()
{
+ initializeProductQueue();
+ initializeEvaluatorPool();
+}
+
+void ProductsResolver::initializeProductQueue()
+{
TopLevelProjectContext &topLevelProject = m_loaderState.topLevelProject();
std::vector<ProductContext *> sortedProducts;
for (ProjectContext * const projectContext : topLevelProject.projects()) {
@@ -98,68 +175,361 @@ void ProductsResolver::initialize()
sortedProducts.insert(it, &product);
}
}
+
for (ProductContext * const product : sortedProducts) {
- m_productsToHandle.emplace(product, -1);
- if (product->shadowProduct)
- m_productsToHandle.emplace(product->shadowProduct.get(), -1);
+ queueProductForScheduling(ProductWithEvaluator(*product, nullptr), Deferral::Allowed);
+ if (product->shadowProduct) {
+ topLevelProject.addProductToHandle(*product->shadowProduct);
+ queueProductForScheduling(ProductWithEvaluator(*product->shadowProduct, nullptr),
+ Deferral::Allowed);
+ }
}
}
+void ProductsResolver::initializeEvaluatorPool()
+{
+ TopLevelProjectContext &topLevelProject = m_loaderState.topLevelProject();
+
+ // Adapt max job count: It makes no sense to have it be higher than the number of products
+ // or what can actually be run concurrently. In both cases, we would simply waste resources.
+ const int maxConcurrency = std::thread::hardware_concurrency();
+ if (maxConcurrency > 0 && maxConcurrency < m_maxJobCount)
+ m_maxJobCount = maxConcurrency;
+ if (m_maxJobCount > topLevelProject.productsToHandleCount())
+ m_maxJobCount = topLevelProject.productsToHandleCount();
+
+ // The number of engines and evaluators we need to allocate here is one less than the
+ // total number of concurrent jobs, as we already have one evaluator that we can re-use.
+ if (m_maxJobCount > 1)
+ m_enginePool.reserve(m_maxJobCount - 1);
+ m_evaluatorPool.reserve(m_enginePool.size());
+ m_availableEvaluators.reserve(m_enginePool.size() + 1);
+ m_availableEvaluators.push_back(&m_loaderState.evaluator());
+ for (std::size_t i = 0; i < m_enginePool.capacity(); ++i) {
+ m_enginePool.emplace_back(
+ ScriptEngine::create(m_loaderState.logger(), EvalContext::PropertyEvaluation));
+ m_enginePool.back()->setEnvironment(m_loaderState.parameters().adjustedEnvironment());
+ m_evaluatorPool.push_back(std::make_unique<Evaluator>(m_enginePool.back().get()));
+ m_availableEvaluators.push_back(m_evaluatorPool.back().get());
+ if (topLevelProject.progressObserver())
+ topLevelProject.progressObserver()->addScriptEngine(m_enginePool.back().get());
+ }
+ qCDebug(lcLoaderScheduling) << "using" << m_availableEvaluators.size() << "evaluators";
+ if (int(m_availableEvaluators.size()) == 1)
+ m_asyncMode = std::launch::deferred;
+}
+
void ProductsResolver::runScheduler()
{
+ AccumulatingTimer timer(m_loaderState.parameters().logElapsedTime()
+ ? &m_loaderState.topLevelProject().timingData().resolvingProducts
+ : nullptr);
+
+ ThreadsLocker threadsLock(m_asyncMode, m_threadsMutex);
+ while (true) {
+ scheduleNext();
+ if (m_runningThreads.empty())
+ break;
+ if (m_asyncMode == std::launch::async) {
+ qCDebug(lcLoaderScheduling()) << "scheduling paused, waiting for threads to finish";
+ m_threadsNotifier.wait(*threadsLock.lock);
+ }
+ checkForCancelation();
+ handleFinishedThreads();
+ }
+
+ QBS_CHECK(m_productsToSchedule.empty());
+ QBS_CHECK(m_loaderState.topLevelProject().productsToHandleCount() == 0);
+ QBS_CHECK(m_runningThreads.empty());
+ QBS_CHECK(m_waitingForSingleDependency.empty());
+ QBS_CHECK(m_waitingForBulkDependency.empty());
+}
+
+void ProductsResolver::scheduleNext()
+{
TopLevelProjectContext &topLevelProject = m_loaderState.topLevelProject();
AccumulatingTimer timer(m_loaderState.parameters().logElapsedTime()
- ? &topLevelProject.timingData().resolvingProducts : nullptr);
+ ? &topLevelProject.timingData().schedulingProducts : nullptr);
+ while (m_maxJobCount > int(m_runningThreads.size()) && !m_productsToSchedule.empty()) {
+ auto [product, toHandleCountOnInsert] = m_productsToSchedule.front();
+ m_productsToSchedule.pop();
- while (!m_productsToHandle.empty()) {
- const auto [product, queueSizeOnInsert] = m_productsToHandle.front();
- m_productsToHandle.pop();
+ qCDebug(lcLoaderScheduling) << "potentially scheduling product"
+ << product.product->displayName()
+ << "unhandled product count on queue insertion"
+ << toHandleCountOnInsert << "current unhandled product count"
+ << topLevelProject.productsToHandleCount();
- // If the queue of in-progress products has shrunk since the last time we tried handling
+ // If the number of unfinished products has shrunk since the last time we tried handling
// this product, there has been forward progress and we can allow a deferral.
- const Deferral deferral = queueSizeOnInsert == -1
- || queueSizeOnInsert > int(m_productsToHandle.size())
- ? Deferral::Allowed : Deferral::NotAllowed;
-
- // If we already know that there's a dependency to an unresolved product pending,
- // we don't start up the machinery, but put the product back into the queue right away.
- if (deferral == Deferral::Allowed && product->hasDependencyToUnresolvedProduct()) {
- m_productsToHandle.emplace(product, int(m_productsToHandle.size()));
+ const Deferral deferral = toHandleCountOnInsert == -1
+ || toHandleCountOnInsert > topLevelProject.productsToHandleCount()
+ ? Deferral::Allowed : Deferral::NotAllowed;
+
+ if (!tryToReserveEvaluator(product, deferral))
continue;
+
+ startJob(product, deferral);
+ }
+
+ // There are jobs running, so forward progress is still possible.
+ if (!m_runningThreads.empty())
+ return;
+
+ QBS_CHECK(m_productsToSchedule.empty());
+
+ // If we end up here, nothing was scheduled in the loop above, which means that either ...
+ // a) ... we are done or
+ // b) ... we finally need to schedule our bulk dependencies or
+ // c) ... we need to schedule products waiting for an unhandled dependency.
+ // In the latter case, the project has at least one dependency cycle, and the
+ // DependencyResolver will emit an error.
+
+ // a)
+ if (m_waitingForBulkDependency.empty() && m_waitingForSingleDependency.empty())
+ return;
+
+ // b)
+ for (const ProductWithEvaluator &product : m_waitingForBulkDependency)
+ queueProductForScheduling(product, Deferral::NotAllowed);
+ if (!m_productsToSchedule.empty()) {
+ m_waitingForBulkDependency.clear();
+ scheduleNext();
+ return;
+ }
+
+ // c)
+ for (const auto &e : m_waitingForSingleDependency) {
+ for (const ProductWithEvaluator &p : e.second)
+ queueProductForScheduling(p, Deferral::NotAllowed);
+ }
+ QBS_CHECK(!m_productsToSchedule.empty());
+ scheduleNext();
+}
+
+bool ProductsResolver::tryToReserveEvaluator(ProductWithEvaluator &product, Deferral deferral)
+{
+ QBS_CHECK(!m_availableEvaluators.empty());
+ if (!product.evaluator) {
+ product.evaluator = m_availableEvaluators.back();
+ m_availableEvaluators.pop_back();
+ return true;
+ }
+ if (const auto it = std::find(m_availableEvaluators.begin(), m_availableEvaluators.end(),
+ product.evaluator); it != m_availableEvaluators.end()) {
+ m_availableEvaluators.erase(it);
+ return true;
+ }
+ qCDebug(lcLoaderScheduling) << "evaluator" << product.evaluator << " for product"
+ << product.product->displayName()
+ << "not available, adding product to wait queue";
+ m_waitingForEvaluator[product.evaluator].push({product.product, deferral});
+ return false;
+}
+
+std::optional<std::pair<ProductContext *, Deferral>>
+ProductsResolver::unblockProductWaitingForEvaluator(Evaluator &evaluator)
+{
+ auto &waitingForEvaluator = m_waitingForEvaluator[&evaluator];
+ if (waitingForEvaluator.empty())
+ return {};
+ const auto product = waitingForEvaluator.front();
+ waitingForEvaluator.pop();
+ qCDebug(lcLoaderScheduling) << "evaluator" << &evaluator << "now available for product"
+ << product.first->displayName();
+ return product;
+}
+
+void ProductsResolver::startJob(const ProductWithEvaluator &product, Deferral deferral)
+{
+ QBS_CHECK(product.evaluator);
+ qCDebug(lcLoaderScheduling) << "scheduling product" << product.product->displayName()
+ << "with evaluator" << product.evaluator
+ << "and deferral mode" << int(deferral);
+ try {
+ const auto it = m_runningThreads.emplace(product.product, ThreadInfo(std::async(m_asyncMode,
+ [this, product, deferral] {
+ LoaderState loaderState(m_loaderState.parameters(), m_loaderState.topLevelProject(),
+ m_loaderState.itemPool(), *product.evaluator,
+ m_loaderState.logger());
+ loaderState.itemReader().setExtraSearchPathsStack(
+ product.product->project->searchPathsStack);
+ resolveProduct(*product.product, deferral, loaderState);
+
+ // The search paths stack can change during dependency resolution
+ // (due to module providers); check that we've rolled back all the changes
+ QBS_CHECK(loaderState.itemReader().extraSearchPathsStack()
+ == product.product->project->searchPathsStack);
+
+ std::lock_guard cancelingLock(m_cancelingMutex);
+ if (m_canceling)
+ return;
+ ThreadsLocker threadsLock(m_asyncMode, m_threadsMutex);
+ if (const auto it = m_runningThreads.find(product.product);
+ it != m_runningThreads.end()) {
+ it->second.done = true;
+ qCDebug(lcLoaderScheduling) << "thread for product"
+ << product.product->displayName()
+ << "finished, waking up scheduler";
+ m_threadsNotifier.notify_one();
+ }
+ }), product.evaluator));
+
+ // With just one worker thread, the notify/wait overhead would be excessive, so
+ // we run the task synchronously.
+ if (m_asyncMode == std::launch::deferred) {
+ qCDebug(lcLoaderScheduling) << "blocking on product thread immediately";
+ it.first->second.future.wait();
}
+ } catch (const std::system_error &e) {
+ if (e.code() != std::errc::resource_unavailable_try_again)
+ throw e;
+ qCWarning(lcLoaderScheduling) << "failed to create thread";
+ if (m_maxJobCount >= 2) {
+ m_maxJobCount /= 2;
+ qCWarning(lcLoaderScheduling) << "throttling down to" << m_maxJobCount << "jobs";
+ }
+ queueProductForScheduling(product, deferral);
+ m_availableEvaluators.push_back(product.evaluator);
+ }
+}
+
+void ProductsResolver::checkForCancelation()
+{
+ if (m_loaderState.topLevelProject().isCanceled()) {
+ m_cancelingMutex.lock();
+ m_canceling = true;
+ m_cancelingMutex.unlock();
+ for (auto &thread : m_runningThreads)
+ thread.second.future.wait();
+ throw CancelException();
+ }
+}
- m_loaderState.itemReader().setExtraSearchPathsStack(product->project->searchPathsStack);
- resolveProduct(*product, deferral, m_loaderState);
- if (topLevelProject.isCanceled())
- throw CancelException();
+void ProductsResolver::handleFinishedThreads()
+{
+ TopLevelProjectContext &topLevelProject = m_loaderState.topLevelProject();
+ AccumulatingTimer timer(m_loaderState.parameters().logElapsedTime()
+ ? &topLevelProject.timingData().schedulingProducts : nullptr);
+
+ std::vector<std::pair<ProductWithEvaluator, Deferral>> productsToScheduleDirectly;
+ for (auto it = m_runningThreads.begin(); it != m_runningThreads.end();) {
+ ThreadInfo &ti = it->second;
+ if (!ti.done) {
+ ++it;
+ continue;
+ }
+ ti.future.wait();
+ ProductContext &product = *it->first;
+ Evaluator &evaluator = *ti.evaluator;
+ it = m_runningThreads.erase(it);
- // The search paths stack can change during dependency resolution (due to module providers);
- // check that we've rolled back all the changes
- QBS_CHECK(m_loaderState.itemReader().extraSearchPathsStack()
- == product->project->searchPathsStack);
+ qCDebug(lcLoaderScheduling) << "handling finished thread for product"
+ << product.displayName()
+ << "current unhandled product count is"
+ << topLevelProject.productsToHandleCount();
+
+ // If there are products waiting for the evaluator used in the finished thread,
+ // we can start a job for one of them right away (but not in the loop,
+ // because startJob() modifies the thread list we are currently iterating over).
+ if (const auto productInfo = unblockProductWaitingForEvaluator(evaluator)) {
+ productsToScheduleDirectly.emplace_back(
+ ProductWithEvaluator(*productInfo->first, &evaluator), productInfo->second);
+ } else {
+ qCDebug(lcLoaderScheduling) << "making evaluator" << &evaluator << "available again";
+ m_availableEvaluators.push_back(&evaluator);
+ }
// If we encountered a dependency to an in-progress product or to a bulk dependency,
- // we defer handling this product if it hasn't failed yet and there is still
- // forward progress.
- if (product->dependenciesResolvingPending()) {
- m_productsToHandle.emplace(product, int(m_productsToHandle.size()));
+ // we defer handling this product.
+ if (product.dependenciesResolvingPending()) {
+ qCDebug(lcLoaderScheduling) << "dependencies resolving not finished for product"
+ << product.displayName();
+ const auto pending = product.pendingDependency();
+ switch (pending.first) {
+ case ProductDependency::Single:
+ waitForSingleDependency(ProductWithEvaluator(product, &evaluator), *pending.second);
+ break;
+ case ProductDependency::Bulk:
+ waitForBulkDependency(ProductWithEvaluator(product, &evaluator));
+ break;
+ case ProductDependency::None:
+ // This can happen if the dependency has finished in between the check in
+ // DependencyResolver and the one here.
+ QBS_CHECK(pending.second);
+ queueProductForScheduling(ProductWithEvaluator(product, &evaluator),
+ Deferral::Allowed);
+ break;
+ }
topLevelProject.incProductDeferrals();
} else {
- topLevelProject.removeProductToHandle(*product);
- if (!product->name.startsWith(StringConstants::shadowProductPrefix()))
- m_finishedProducts.push_back(product);
- topLevelProject.timingData() += product->timingData;
+ qCDebug(lcLoaderScheduling) << "product" << product.displayName() << "finished";
+ topLevelProject.removeProductToHandle(product);
+ if (!product.name.startsWith(StringConstants::shadowProductPrefix()))
+ m_finishedProducts.push_back(&product);
+ topLevelProject.timingData() += product.timingData;
+ unblockProductsWaitingForDependency(product);
}
}
+
+ for (const auto &productInfo : productsToScheduleDirectly)
+ startJob(productInfo.first, productInfo.second);
+}
+
+void ProductsResolver::queueProductForScheduling(const ProductWithEvaluator &product,
+ Deferral deferral)
+{
+ qCDebug(lcLoaderScheduling) << "queueing product" << product.product->displayName()
+ << "with deferral mode" << int(deferral);
+ m_productsToSchedule.emplace(product, deferral == Deferral::Allowed
+ ? -1 : m_loaderState.topLevelProject().productsToHandleCount());
+}
+
+void ProductsResolver::waitForSingleDependency(const ProductWithEvaluator &product,
+ ProductContext &dependency)
+{
+ qCDebug(lcLoaderScheduling) << "product" << product.product->displayName()
+ << "now waiting for single dependency"
+ << dependency.displayName();
+ m_waitingForSingleDependency[&dependency].push_back(product);
+}
+
+void ProductsResolver::waitForBulkDependency(const ProductWithEvaluator &product)
+{
+ qCDebug(lcLoaderScheduling) << "product" << product.product->displayName()
+ << "now waiting for bulk dependency";
+ m_waitingForBulkDependency.push_back(product);
+}
+
+void ProductsResolver::unblockProductsWaitingForDependency(ProductContext &finishedProduct)
+{
+ const auto it = m_waitingForSingleDependency.find(&finishedProduct);
+ if (it == m_waitingForSingleDependency.end())
+ return;
+
+ qCDebug(lcLoaderScheduling) << "unblocking all products waiting for now-finished product" <<
+ finishedProduct.displayName();
+ for (const ProductWithEvaluator &p : it->second) {
+ qCDebug(lcLoaderScheduling) << " unblocking product" << p.product->displayName();
+ queueProductForScheduling(p, Deferral::Allowed);
+ }
+ m_waitingForSingleDependency.erase(it);
}
void ProductsResolver::postProcess()
{
- // This has to be done at the end, because we need both product and shadow product to be
- // ready, and contrary to what one might assume, there is no proper ordering between them
- // regarding dependency resolving.
- for (ProductContext * const product : m_finishedProducts)
+ for (ProductContext * const product : m_finishedProducts) {
+ if (product->product)
+ product->product->project->products.push_back(product->product);
+
+ // This has to be done in post-processing, because we need both product and shadow product
+ // to be ready, and contrary to what one might assume, there is no proper ordering
+ // between them regarding dependency resolving.
setupExports(*product, m_loaderState);
+ }
+
+ for (const auto &engine : m_enginePool)
+ m_loaderState.topLevelProject().collectDataFromEngine(*engine);
}
int ProductsResolver::dependsItemCount(const Item *item)
diff --git a/src/lib/corelib/loader/projectresolver.cpp b/src/lib/corelib/loader/projectresolver.cpp
index 5774936a3..147d5a618 100644
--- a/src/lib/corelib/loader/projectresolver.cpp
+++ b/src/lib/corelib/loader/projectresolver.cpp
@@ -134,9 +134,10 @@ public:
const SetupProjectParameters setupParams;
ScriptEngine * const engine;
mutable Logger logger;
- Evaluator evaluator{engine};
ItemPool itemPool;
- LoaderState state{setupParams, itemPool, evaluator, logger};
+ Evaluator evaluator{engine};
+ TopLevelProjectContext topLevelProject;
+ LoaderState state{setupParams, topLevelProject, itemPool, evaluator, logger};
Item *rootProjectItem = nullptr;
};
@@ -218,7 +219,7 @@ TopLevelProjectPtr ProjectResolver::resolve()
if (ProgressObserver * const observer = d->state.topLevelProject().progressObserver()) {
observer->initialize(Tr::tr("Resolving project for configuration %1")
.arg(TopLevelProject::deriveId(d->setupParams.finalBuildConfigurationTree())), 1);
- observer->setScriptEngine(d->engine);
+ observer->addScriptEngine(d->engine);
}
const FileTime resolveTime = FileTime::currentTime();
@@ -288,7 +289,7 @@ TopLevelProjectPtr ProjectResolver::Private::resolveTopLevelProject()
if (accumulatedErrors.hasError())
throw accumulatedErrors;
- project->buildSystemFiles = state.topLevelProject().buildSystemFiles();
+ project->buildSystemFiles.unite(state.topLevelProject().buildSystemFiles());
project->profileConfigs = state.topLevelProject().profileConfigs();
const QVariantMap &profiles = state.topLevelProject().localProfiles();
for (auto it = profiles.begin(); it != profiles.end(); ++it)
@@ -297,12 +298,7 @@ TopLevelProjectPtr ProjectResolver::Private::resolveTopLevelProject()
project->moduleProviderInfo.providers = state.topLevelProject().moduleProvidersCache();
project->setBuildConfiguration(setupParams.finalBuildConfigurationTree());
project->overriddenValues = setupParams.overriddenValues();
- project->canonicalFilePathResults = engine->canonicalFilePathResults();
- project->fileExistsResults = engine->fileExistsResults();
- project->directoryEntriesResults = engine->directoryEntriesResults();
- project->fileLastModifiedResults = engine->fileLastModifiedResults();
- project->environment = engine->environment();
- project->buildSystemFiles.unite(engine->imports());
+ state.topLevelProject().collectDataFromEngine(*engine);
makeSubProjectNamesUniqe(project);
checkForDuplicateProductNames(project);
@@ -522,6 +518,8 @@ void ProjectResolver::Private::printProfilingInfo()
state.topLevelProject().timingData().preparingProducts);
print(2, Tr::tr("Setting up Groups took %1."),
state.topLevelProject().timingData().groupsSetup);
+ print(2, Tr::tr("Scheduling products took %1."),
+ state.topLevelProject().timingData().schedulingProducts);
print(2, Tr::tr("Resolving products took %1."),
state.topLevelProject().timingData().resolvingProducts);
print(4, Tr::tr("Property evaluation took %1."),
diff --git a/src/lib/corelib/logging/categories.cpp b/src/lib/corelib/logging/categories.cpp
index 0f844f5b4..5738dc21d 100644
--- a/src/lib/corelib/logging/categories.cpp
+++ b/src/lib/corelib/logging/categories.cpp
@@ -50,6 +50,7 @@ Q_LOGGING_CATEGORY(lcModuleLoader, "qbs.moduleloader", QtCriticalMsg)
Q_LOGGING_CATEGORY(lcPluginManager, "qbs.pluginmanager", QtCriticalMsg)
Q_LOGGING_CATEGORY(lcProjectResolver, "qbs.projectresolver", QtCriticalMsg)
Q_LOGGING_CATEGORY(lcUpToDateCheck, "qbs.uptodate", QtCriticalMsg)
+Q_LOGGING_CATEGORY(lcLoaderScheduling, "qbs.loader.scheduling", QtCriticalMsg)
} // namespace Internal
} // namespace qbs
diff --git a/src/lib/corelib/logging/categories.h b/src/lib/corelib/logging/categories.h
index 40c69845e..c8873c30c 100644
--- a/src/lib/corelib/logging/categories.h
+++ b/src/lib/corelib/logging/categories.h
@@ -53,6 +53,7 @@ Q_DECLARE_LOGGING_CATEGORY(lcModuleLoader)
Q_DECLARE_LOGGING_CATEGORY(lcPluginManager)
Q_DECLARE_LOGGING_CATEGORY(lcProjectResolver)
Q_DECLARE_LOGGING_CATEGORY(lcUpToDateCheck)
+Q_DECLARE_LOGGING_CATEGORY(lcLoaderScheduling)
} // namespace Internal
} // namespace qbs
diff --git a/src/lib/corelib/tools/progressobserver.h b/src/lib/corelib/tools/progressobserver.h
index 9acb4b30c..73a61de37 100644
--- a/src/lib/corelib/tools/progressobserver.h
+++ b/src/lib/corelib/tools/progressobserver.h
@@ -41,6 +41,8 @@
#include <QtCore/qglobal.h>
+#include <vector>
+
QT_BEGIN_NAMESPACE
class QString;
QT_END_NAMESPACE
@@ -66,13 +68,13 @@ public:
// Call this to ensure that the progress bar always goes to 100%.
void setFinished();
- void setScriptEngine(ScriptEngine *engine) { m_scriptEngine = engine; }
+ void addScriptEngine(ScriptEngine *engine) { m_scriptEngines.push_back(engine); }
protected:
- ScriptEngine *scriptEngine() const { return m_scriptEngine; }
+ const std::vector<ScriptEngine *> &scriptEngines() const { return m_scriptEngines; }
private:
- ScriptEngine *m_scriptEngine = nullptr;
+ std::vector<ScriptEngine *> m_scriptEngines;
};
} // namespace Internal
diff --git a/tests/auto/api/tst_api.cpp b/tests/auto/api/tst_api.cpp
index 38e9804a3..5042766d9 100644
--- a/tests/auto/api/tst_api.cpp
+++ b/tests/auto/api/tst_api.cpp
@@ -928,14 +928,7 @@ void TestApi::dependencyOnMultiplexedType()
} else {
QVERIFY(p.name() == "p2");
++p2Count;
-
- // FIXME: This is an odd effect of our current algorithm: We collect the products
- // matching the requested type and add Depends items with their names ("p1" in
- // this case). Later, the algorithm checking for compatibility regarding the
- // multiplexing axes picks the aggregate. However, the aggregate does not have
- // a matching type... It's not entirely clear what the real expected
- // result should be here.
- QCOMPARE(p.dependencies().size(), 2);
+ QVERIFY(p.dependencies().contains("dep"));
}
}
QCOMPARE(depCount, 1);