diff options
author | Jan Arve Sæther <jan-arve.saether@qt.io> | 2020-04-15 16:18:03 +0200 |
---|---|---|
committer | Jan Arve Sæther <jan-arve.saether@qt.io> | 2020-05-04 15:11:12 +0200 |
commit | cc77a0bc549ce8f0b218661e7ae0e82e5b89e0da (patch) | |
tree | d78d8902579be745ea823b10e56dcc2c4b06b89f /src/imports/layouts/qquicklayout.cpp | |
parent | 788a3a183f8c49c1a88270c1456c3d47423df240 (diff) |
Improve performance when dynamically adding items to a layout
This was especially noticeable when a Repeater was populating the
children of a layout, and its model was dynamically changed: When the
model was changed, the repeater removed one item at a time until all
items were removed, then applied the new model and then added all the
new items for the new model.
The layout reacted to that by doing a full sync of the QML layout into
the internal gridlayout engine each time an item got removed or added.
For very large layouts (or layouts that have complex size hints to
calculate), this caused a major slowdown.
This patch fixes that by postponing the sync until we get a
updatePolish(), basically replacing most calls to updateLayoutItems()
(which does the sync) with a call to invalidate() (which schedules a
polish). It will also get rid of some binding loop warnings due to this
change.
This means that there is a small change in behavior:
impicitWidth, implicitHeight and Layout.{min,max}imum{Width,Height}
might in some cases be incorrect until the updatePolish() have been
done. (This is however consistent with how Row/Column/Grid behaves)
The creation test in qmlbench was quite simple, so it did not suffer
from the most severe performance issues, but we did not regress:
> compareresults:
auto/creation/layouts/delegates_rowlayout.qml: improvement by 3.91%
auto/creation/layouts/delegates_columnlayout.qml: improvement by 6.59%
auto/creation/layouts/delegates_gridlayout.qml: improvement by 1.83%
Overall average of differences: 4.11%
And for the gridlayout_large.qml (Repeater with 1000 dynamically
changing items):
> compareresults:
auto/layouts/gridlayout_large.qml: improvement by 66477.78%
Overall average of differences: 66477.78%
[ChangeLog][Qt Quick Layouts] Performance improvements to Qt Quick
Layouts. This has the small side-effect that size hints
(implicitWidth/implicithHeight etc) changes are not immediately emitted
after a layout has been modified (e.g item added)
Pick-to: 5.15
Fixes: QTBUG-71839
Fixes: QTBUG-65121
Fixes: QTBUG-66017
Change-Id: I6922efe449134246df66b177992e4442747bc8fb
Reviewed-by: Mitch Curtis <mitch.curtis@qt.io>
Diffstat (limited to 'src/imports/layouts/qquicklayout.cpp')
-rw-r--r-- | src/imports/layouts/qquicklayout.cpp | 105 |
1 files changed, 82 insertions, 23 deletions
diff --git a/src/imports/layouts/qquicklayout.cpp b/src/imports/layouts/qquicklayout.cpp index a2a3ce2cc4..9e981f7de6 100644 --- a/src/imports/layouts/qquicklayout.cpp +++ b/src/imports/layouts/qquicklayout.cpp @@ -41,6 +41,7 @@ #include <QEvent> #include <QtCore/qcoreapplication.h> #include <QtCore/private/qnumeric_p.h> +#include <QtCore/qstack.h> #include <QtCore/qmath.h> #include <QtQml/qqmlinfo.h> #include <limits> @@ -380,7 +381,7 @@ void QQuickLayoutAttached::setRow(int row) { if (row >= 0 && row != m_row) { m_row = row; - repopulateLayout(); + invalidateItem(); emit rowChanged(); } } @@ -401,7 +402,7 @@ void QQuickLayoutAttached::setColumn(int column) { if (column >= 0 && column != m_column) { m_column = column; - repopulateLayout(); + invalidateItem(); emit columnChanged(); } } @@ -628,7 +629,7 @@ void QQuickLayoutAttached::setRowSpan(int span) { if (span != m_rowSpan) { m_rowSpan = span; - repopulateLayout(); + invalidateItem(); emit rowSpanChanged(); } } @@ -648,7 +649,7 @@ void QQuickLayoutAttached::setColumnSpan(int span) { if (span != m_columnSpan) { m_columnSpan = span; - repopulateLayout(); + invalidateItem(); emit columnSpanChanged(); } } @@ -669,20 +670,12 @@ qreal QQuickLayoutAttached::sizeHint(Qt::SizeHint which, Qt::Orientation orienta void QQuickLayoutAttached::invalidateItem() { - if (!m_changesNotificationEnabled) - return; qCDebug(lcQuickLayouts) << "QQuickLayoutAttached::invalidateItem"; if (QQuickLayout *layout = parentLayout()) { layout->invalidate(item()); } } -void QQuickLayoutAttached::repopulateLayout() -{ - if (QQuickLayout *layout = parentLayout()) - layout->updateLayoutItems(); -} - QQuickLayout *QQuickLayoutAttached::parentLayout() const { QQuickItem *parentItem = item(); @@ -700,10 +693,41 @@ QQuickItem *QQuickLayoutAttached::item() const return qobject_cast<QQuickItem *>(parent()); } +qreal QQuickLayoutPrivate::getImplicitWidth() const +{ + Q_Q(const QQuickLayout); + if (q->invalidated()) { + QQuickLayoutPrivate *that = const_cast<QQuickLayoutPrivate*>(this); + that->implicitWidth = q->sizeHint(Qt::PreferredSize).width(); + } + return implicitWidth; +} + +qreal QQuickLayoutPrivate::getImplicitHeight() const +{ + Q_Q(const QQuickLayout); + if (q->invalidated()) { + QQuickLayoutPrivate *that = const_cast<QQuickLayoutPrivate*>(this); + that->implicitHeight = q->sizeHint(Qt::PreferredSize).height(); + } + return implicitHeight; +} + +void QQuickLayoutPrivate::applySizeHints() const { + Q_Q(const QQuickLayout); + QQuickLayout *that = const_cast<QQuickLayout*>(q); + QQuickLayoutAttached *info = attachedLayoutObject(that, true); + + const QSizeF min = q->sizeHint(Qt::MinimumSize); + const QSizeF max = q->sizeHint(Qt::MaximumSize); + const QSizeF pref = q->sizeHint(Qt::PreferredSize); + info->setMinimumImplicitSize(min); + info->setMaximumImplicitSize(max); + that->setImplicitSize(pref.width(), pref.height()); +} QQuickLayout::QQuickLayout(QQuickLayoutPrivate &dd, QQuickItem *parent) : QQuickItem(dd, parent) - , m_dirty(false) , m_inUpdatePolish(false) , m_polishInsideUpdatePolish(0) { @@ -734,6 +758,20 @@ void QQuickLayout::updatePolish() { qCDebug(lcQuickLayouts) << "updatePolish() ENTERING" << this; m_inUpdatePolish = true; + + // Might have become "undirty" before we reach this updatePolish() + // (e.g. if somebody queried for implicitWidth it will immediately + // calculate size hints) + if (invalidated()) { + // Ensure that all invalidated layouts are synced and valid again. Since + // ensureLayoutItemsUpdated() will also call applySizeHints(), and sizeHint() will call its + // childrens sizeHint(), and sizeHint() will call ensureLayoutItemsUpdated(), this will be done + // recursive as we want. + // Note that we need to call ensureLayoutItemsUpdated() *before* we query width() and height(), + // because width()/height() might return their implicitWidth/implicitHeight (e.g. for a layout + // with no explicitly specified size, (nor anchors.fill: parent)) + ensureLayoutItemsUpdated(); + } rearrange(QSizeF(width(), height())); m_inUpdatePolish = false; qCDebug(lcQuickLayouts) << "updatePolish() LEAVING" << this; @@ -750,10 +788,13 @@ void QQuickLayout::componentComplete() void QQuickLayout::invalidate(QQuickItem * /*childItem*/) { - if (m_dirty) + Q_D(QQuickLayout); + if (invalidated()) return; - m_dirty = true; + qCDebug(lcQuickLayouts) << "QQuickLayout::invalidate()" << this; + d->m_dirty = true; + d->m_dirtyArrangement = true; if (!qobject_cast<QQuickLayout *>(parentItem())) { @@ -775,7 +816,6 @@ void QQuickLayout::invalidate(QQuickItem * /*childItem*/) bool QQuickLayout::shouldIgnoreItem(QQuickItem *child, QQuickLayoutAttached *&info, QSizeF *sizeHints) const { - Q_D(const QQuickLayout); bool ignoreItem = true; QQuickItemPrivate *childPrivate = QQuickItemPrivate::get(child); if (childPrivate->explicitVisible) { @@ -794,8 +834,6 @@ bool QQuickLayout::shouldIgnoreItem(QQuickItem *child, QQuickLayoutAttached *&in if (!ignoreItem && childPrivate->isTransparentForPositioner()) ignoreItem = true; - if (ignoreItem) - d->m_ignoredItems << child; return ignoreItem; } @@ -806,6 +844,17 @@ void QQuickLayout::checkAnchors(QQuickItem *item) const qmlWarning(item) << "Detected anchors on an item that is managed by a layout. This is undefined behavior; use Layout.alignment instead."; } +void QQuickLayout::ensureLayoutItemsUpdated() const +{ + Q_D(const QQuickLayout); + if (!invalidated()) + return; + const_cast<QQuickLayout*>(this)->updateLayoutItems(); + d->m_dirty = false; + d->applySizeHints(); +} + + void QQuickLayout::itemChange(ItemChange change, const ItemChangeData &value) { if (change == ItemChildAddedChange) { @@ -816,14 +865,14 @@ void QQuickLayout::itemChange(ItemChange change, const ItemChangeData &value) d->m_hasItemChangeListeners = true; qCDebug(lcQuickLayouts) << "ChildAdded" << item; if (isReady()) - updateLayoutItems(); + invalidate(); } else if (change == ItemChildRemovedChange) { QQuickItem *item = value.item; qmlobject_disconnect(item, QQuickItem, SIGNAL(baselineOffsetChanged(qreal)), this, QQuickLayout, SLOT(invalidateSenderItem())); QQuickItemPrivate::get(item)->removeItemChangeListener(this, changeTypes); qCDebug(lcQuickLayouts) << "ChildRemoved" << item; if (isReady()) - updateLayoutItems(); + invalidate(); } QQuickItem::itemChange(change, value); } @@ -877,10 +926,20 @@ void QQuickLayout::deactivateRecur() } } +bool QQuickLayout::invalidated() const +{ + return d_func()->m_dirty; +} + +bool QQuickLayout::invalidatedArrangement() const +{ + return d_func()->m_dirtyArrangement; +} + void QQuickLayout::itemSiblingOrderChanged(QQuickItem *item) { Q_UNUSED(item); - updateLayoutItems(); + invalidate(); } void QQuickLayout::itemImplicitWidthChanged(QQuickItem *item) @@ -909,7 +968,7 @@ void QQuickLayout::itemVisibilityChanged(QQuickItem *item) void QQuickLayout::rearrange(const QSizeF &/*size*/) { - m_dirty = false; + d_func()->m_dirtyArrangement = false; } @@ -1188,7 +1247,7 @@ void QQuickLayout::dumpLayoutTreeRecursive(int level, QString &buf) const buf += formatLine("%1 {").arg(QQmlMetaType::prettyTypeName(this)); ++level; buf += formatLine("// Effective calculated values:"); - buf += formatLine("sizeHintDirty: %2").arg(m_dirty); + buf += formatLine("sizeHintDirty: %2").arg(invalidated()); QSizeF min = sizeHint(Qt::MinimumSize); buf += formatLine("sizeHint.min : [%1, %2]").arg(f2s(min.width()), 5).arg(min.height(), 5); QSizeF pref = sizeHint(Qt::PreferredSize); |