diff options
author | Christian Kandeler <christian.kandeler@qt.io> | 2023-07-10 15:19:29 +0200 |
---|---|---|
committer | Christian Kandeler <christian.kandeler@qt.io> | 2023-09-05 08:07:50 +0000 |
commit | ae82e039eb74b092604910a1f3f7fa4887397cbc (patch) | |
tree | 69d53083cd122e7089f7dca1cb335a7f7124a2c1 | |
parent | 51e58390f2b34b27d15e7f287353451a6a969a33 (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>
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 ¤tSearchPaths = m_loaderState.itemReader().allSearchPaths(); + const QStringList ¤tSearchPaths = 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 ¶meters, ItemPool &itemPool, - Evaluator &evaluator, Logger &logger) - : parameters(parameters), itemPool(itemPool), evaluator(evaluator), logger(logger), - itemReader(q) + Private(LoaderState &q, const SetupProjectParameters ¶meters, + TopLevelProjectContext &topLevelProject, ItemPool &itemPool, Evaluator &evaluator, + Logger &logger) + : parameters(parameters), topLevelProject(topLevelProject), itemPool(itemPool), + evaluator(evaluator), logger(logger), itemReader(q) {} const SetupProjectParameters ¶meters; + TopLevelProjectContext &topLevelProject; ItemPool &itemPool; Evaluator &evaluator; Logger &logger; - TopLevelProjectContext topLevelProject; ItemReader itemReader; }; -LoaderState::LoaderState(const SetupProjectParameters ¶meters, ItemPool &itemPool, +LoaderState::LoaderState(const SetupProjectParameters ¶meters, + 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 ¶meters, ItemPool &itemPool, Evaluator &evaluator, - Logger &logger); + LoaderState(const SetupProjectParameters ¶meters, 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); |