aboutsummaryrefslogtreecommitdiffstats
path: root/src/qml/types
diff options
context:
space:
mode:
authorRichard Moe Gustavsen <richard.gustavsen@qt.io>2018-07-19 13:06:19 +0200
committerSimon Hausmann <simon.hausmann@qt.io>2018-08-02 09:50:39 +0000
commit6e65da97fe84e99b786698f89fdc8408333f3682 (patch)
treeed45a0b5ab2456c2150d1d752bebfa66fb6d0f3f /src/qml/types
parent382812cd2917180a160917d24b11825a7eeb2cf1 (diff)
QQmlTableInstanceModel: implement support for reusing delegate items
This patch will make it possible for TableView to reuse delegate items. The API lets TableView (or any kind of view in theory) specify if an item is reusable at the time the item is released. Reusing delegate items is specified per item, meaning that the view can choose to release some items as reusable, but others not. If the view never releases any items as reusable, no items will be added to the pool, and hence, no items will be reused. If the view releases an item as reusable, the model will move it to the reuse pool rather than destroying it (if the items ref-count is zero, and not persisted). The next time the view asks the model for a new item, the model will first check if the pool already contains an item, and if so, use it. Otherwise it will create a new item, like before. Items in the pool are supposed to rest there for a short while. Ideally only from the time items are flicked out on one side of the view, until we reuse them when new items are flicked in on the oppsite side. One big reason for this is that we have no way of hibernating items in QML, so they will effectively stay alive while inside the pool. This is not ideal, but still a huge performance improvement over what we had before, where all items are always created from scratch. If hibernating objects will ever be possible, it should be easy to extends the current logic to take advantage of this. Already existing tests for QQuickTableView should verify that no regressions are introduced with this patch. Since recycling of items is driven from the view, auto tests for this functionality is included with the patch for implementing reuse support in TableView (coming in a subsequent patch). Change-Id: I28aa687251ce3e7e1de0b1c799fdbf44d8867d45 Reviewed-by: Mitch Curtis <mitch.curtis@qt.io>
Diffstat (limited to 'src/qml/types')
-rw-r--r--src/qml/types/qqmldelegatemodel.cpp2
-rw-r--r--src/qml/types/qqmldelegatemodel_p_p.h2
-rw-r--r--src/qml/types/qqmltableinstancemodel.cpp232
-rw-r--r--src/qml/types/qqmltableinstancemodel_p.h23
4 files changed, 212 insertions, 47 deletions
diff --git a/src/qml/types/qqmldelegatemodel.cpp b/src/qml/types/qqmldelegatemodel.cpp
index 7a128d7070..d9adb7808e 100644
--- a/src/qml/types/qqmldelegatemodel.cpp
+++ b/src/qml/types/qqmldelegatemodel.cpp
@@ -2005,6 +2005,8 @@ QQmlDelegateModelItem::QQmlDelegateModelItem(QQmlDelegateModelItemMetaType *meta
, object(nullptr)
, attached(nullptr)
, incubationTask(nullptr)
+ , delegate(nullptr)
+ , poolTime(0)
, objectRef(0)
, scriptRef(0)
, groups(0)
diff --git a/src/qml/types/qqmldelegatemodel_p_p.h b/src/qml/types/qqmldelegatemodel_p_p.h
index e87f8d4440..43d288b5c5 100644
--- a/src/qml/types/qqmldelegatemodel_p_p.h
+++ b/src/qml/types/qqmldelegatemodel_p_p.h
@@ -143,6 +143,8 @@ public:
QPointer<QObject> object;
QPointer<QQmlDelegateModelAttached> attached;
QQDMIncubationTask *incubationTask;
+ QQmlComponent *delegate;
+ int poolTime;
int objectRef;
int scriptRef;
int groups;
diff --git a/src/qml/types/qqmltableinstancemodel.cpp b/src/qml/types/qqmltableinstancemodel.cpp
index bab70ecaa6..6604e009c1 100644
--- a/src/qml/types/qqmltableinstancemodel.cpp
+++ b/src/qml/types/qqmltableinstancemodel.cpp
@@ -91,6 +91,37 @@ QQmlTableInstanceModel::~QQmlTableInstanceModel()
// the view needs to be deleted first (and thereby release all
// its items), before this destructor is run.
Q_ASSERT(m_modelItems.isEmpty());
+
+ drainReusableItemsPool(0);
+}
+
+QQmlDelegateModelItem *QQmlTableInstanceModel::resolveModelItem(int index)
+{
+ // Check if an item for the given index is already loaded and ready
+ QQmlDelegateModelItem *modelItem = m_modelItems.value(index, nullptr);
+ if (modelItem)
+ return modelItem;
+
+ QQmlComponent *delegate = m_delegate;
+
+ // Check if the pool contains an item that can be reused
+ modelItem = takeFromReusableItemsPool(delegate);
+ if (modelItem) {
+ reuseItem(modelItem, index);
+ m_modelItems.insert(index, modelItem);
+ return modelItem;
+ }
+
+ // Create a new item from scratch
+ modelItem = m_adaptorModel.createItem(m_metaType, index);
+ if (modelItem) {
+ modelItem->delegate = delegate;
+ m_modelItems.insert(index, modelItem);
+ return modelItem;
+ }
+
+ qWarning() << Q_FUNC_INFO << "failed creating a model item for index: " << index;
+ return nullptr;
}
QObject *QQmlTableInstanceModel::object(int index, QQmlIncubator::IncubationMode incubationMode)
@@ -99,18 +130,9 @@ QObject *QQmlTableInstanceModel::object(int index, QQmlIncubator::IncubationMode
Q_ASSERT(index >= 0 && index < m_adaptorModel.count());
Q_ASSERT(m_qmlContext && m_qmlContext->isValid());
- // Check if we've already created an item for the given index
- QQmlDelegateModelItem *modelItem = m_modelItems.value(index, nullptr);
-
- if (!modelItem) {
- // Create a new model item
- modelItem = m_adaptorModel.createItem(m_metaType, index);
- if (!modelItem) {
- qWarning() << Q_FUNC_INFO << "Adaptor model failed creating a model item";
- return nullptr;
- }
- m_modelItems.insert(index, modelItem);
- }
+ QQmlDelegateModelItem *modelItem = resolveModelItem(index);
+ if (!modelItem)
+ return nullptr;
if (modelItem->object) {
// The model item has already been incubated. So
@@ -119,41 +141,12 @@ QObject *QQmlTableInstanceModel::object(int index, QQmlIncubator::IncubationMode
return modelItem->object;
}
- // Guard the model item temporarily so that it's not deleted from
- // incubatorStatusChanged(), in case the incubation is done synchronously.
- modelItem->scriptRef++;
-
- if (modelItem->incubationTask) {
- // We're already incubating the model item from a previous request. If the previous call requested
- // the item async, but the current request needs it sync, we need to force-complete the incubation.
- const bool sync = (incubationMode == QQmlIncubator::Synchronous || incubationMode == QQmlIncubator::AsynchronousIfNested);
- if (sync && modelItem->incubationTask->incubationMode() == QQmlIncubator::Asynchronous)
- modelItem->incubationTask->forceCompletion();
- } else {
- modelItem->incubationTask = new QQmlTableInstanceModelIncubationTask(this, modelItem, incubationMode);
-
- QQmlContextData *ctxt = new QQmlContextData;
- QQmlContext *creationContext = m_delegate->creationContext();
- ctxt->setParent(QQmlContextData::get(creationContext ? creationContext : m_qmlContext.data()));
- ctxt->contextObject = modelItem;
- modelItem->contextData = ctxt;
-
- QQmlComponentPrivate::get(m_delegate)->incubateObject(
- modelItem->incubationTask,
- m_delegate,
- m_qmlContext->engine(),
- ctxt,
- QQmlContextData::get(m_qmlContext));
- }
-
- // Remove the temporary guard
- modelItem->scriptRef--;
- Q_ASSERT(modelItem->scriptRef == 0);
-
+ // The object is not ready, and needs to be incubated
+ incubateModelItem(modelItem, incubationMode);
if (!isDoneIncubating(modelItem))
return nullptr;
- // When incubation is done, the task should be removed
+ // Incubation is done, so the task should be removed
Q_ASSERT(!modelItem->incubationTask);
if (!modelItem->object) {
@@ -173,7 +166,7 @@ QObject *QQmlTableInstanceModel::object(int index, QQmlIncubator::IncubationMode
return modelItem->object;
}
-QQmlInstanceModel::ReleaseFlags QQmlTableInstanceModel::release(QObject *object)
+QQmlInstanceModel::ReleaseFlags QQmlTableInstanceModel::release(QObject *object, ReusableFlag reusable)
{
Q_ASSERT(object);
auto modelItem = qvariant_cast<QQmlDelegateModelItem *>(object->property(kModelItemTag));
@@ -188,11 +181,19 @@ QQmlInstanceModel::ReleaseFlags QQmlTableInstanceModel::release(QObject *object)
// are e.g delivered async, and the user flicks back and forth quicker than the loading can catch
// up with. The view might then find that the object is no longer visible and should be released.
// We detect this case in incubatorStatusChanged(), and delete it there instead. But from the callers
- // points of view, it should consider it destroyed.
+ // point of view, it should consider it destroyed.
return QQmlDelegateModel::Destroyed;
}
+ // The item is not referenced by anyone
m_modelItems.remove(modelItem->index);
+
+ if (reusable == Reusable) {
+ insertIntoReusableItemsPool(modelItem);
+ return QQmlInstanceModel::Referenced;
+ }
+
+ // The item is not reused or referenced by anyone, so just delete it
modelItem->destroyObject();
emit destroyingItem(object);
@@ -220,6 +221,139 @@ void QQmlTableInstanceModel::cancel(int index)
delete modelItem;
}
+void QQmlTableInstanceModel::insertIntoReusableItemsPool(QQmlDelegateModelItem *modelItem)
+{
+ // Currently, the only way for a view to reuse items is to call QQmlTableInstanceModel::release()
+ // with the second argument explicitly set to QQmlTableInstanceModel::Reusable. If the released
+ // item is no longer referenced, it will be added to the pool. Reusing of items can be specified
+ // per item, in case certain items cannot be recycled.
+ // A QQmlDelegateModelItem knows which delegate its object was created from. So when we are
+ // about to create a new item, we first check if the pool contains an item based on the same
+ // delegate from before. If so, we take it out of the pool (instead of creating a new item), and
+ // update all its context-, and attached properties.
+ // When a view is recycling items, it should call QQmlTableInstanceModel::drainReusableItemsPool()
+ // regularly. As there is currently no logic to 'hibernate' items in the pool, they are only
+ // meant to rest there for a short while, ideally only from the time e.g a row is unloaded
+ // on one side of the view, and until a new row is loaded on the opposite side. In-between
+ // this time, the application will see the item as fully functional and 'alive' (just not
+ // visible on screen). Since this time is supposed to be short, we don't take any action to
+ // notify the application about it, since we don't want to trigger any bindings that can
+ // disturb performance.
+ // A recommended time for calling drainReusableItemsPool() is each time a view has finished
+ // loading e.g a new row or column. If there are more items in the pool after that, it means
+ // that the view most likely doesn't need them anytime soon. Those items should be destroyed to
+ // not consume resources.
+ // Depending on if a view is a list or a table, it can sometimes be performant to keep
+ // items in the pool for a bit longer than one "row out/row in" cycle. E.g for a table, if the
+ // number of visible rows in a view is much larger than the number of visible columns.
+ // In that case, if you flick out a row, and then flick in a column, you would throw away a lot
+ // of items in the pool if completely draining it. The reason is that unloading a row places more
+ // items in the pool than what ends up being recycled when loading a new column. And then, when you
+ // next flick in a new row, you would need to load all those drained items again from scratch. For
+ // that reason, you can specify a maxPoolTime to the drainReusableItemsPool() that allows you to keep
+ // items in the pool for a bit longer, effectively keeping more items in circulation.
+ // A recommended maxPoolTime would be equal to the number of dimenstions in the view, which
+ // means 1 for a list view and 2 for a table view. If you specify 0, all items will be drained.
+ Q_ASSERT(!modelItem->incubationTask);
+ Q_ASSERT(!modelItem->isObjectReferenced());
+ Q_ASSERT(!modelItem->isReferenced());
+ Q_ASSERT(modelItem->object);
+
+ modelItem->poolTime = 0;
+ m_reusableItemsPool.append(modelItem);
+ emit itemPooled(modelItem->index, modelItem->object);
+}
+
+QQmlDelegateModelItem *QQmlTableInstanceModel::takeFromReusableItemsPool(const QQmlComponent *delegate)
+{
+ // Find the oldest item in the pool that was made from the same delegate as
+ // the given argument, remove it from the pool, and return it.
+ if (m_reusableItemsPool.isEmpty())
+ return nullptr;
+
+ for (auto it = m_reusableItemsPool.begin(); it != m_reusableItemsPool.end(); ++it) {
+ if ((*it)->delegate != delegate)
+ continue;
+ auto modelItem = *it;
+ m_reusableItemsPool.erase(it);
+ return modelItem;
+ }
+
+ return nullptr;
+}
+
+void QQmlTableInstanceModel::drainReusableItemsPool(int maxPoolTime)
+{
+ // Rather than releasing all pooled items upon a call to this function, each
+ // item has a poolTime. The poolTime specifies for how many loading cycles an item
+ // has been resting in the pool. And for each invocation of this function, poolTime
+ // will increase. If poolTime is equal to, or exceeds, maxPoolTime, it will be removed
+ // from the pool and released. This way, the view can tweak a bit for how long
+ // items should stay in "circulation", even if they are not recycled right away.
+ for (auto it = m_reusableItemsPool.begin(); it != m_reusableItemsPool.end();) {
+ auto modelItem = *it;
+ modelItem->poolTime++;
+ if (modelItem->poolTime <= maxPoolTime) {
+ ++it;
+ } else {
+ it = m_reusableItemsPool.erase(it);
+ release(modelItem->object, NotReusable);
+ }
+ }
+}
+
+void QQmlTableInstanceModel::reuseItem(QQmlDelegateModelItem *item, int newModelIndex)
+{
+ // Update the context properties index, row and column on
+ // the delegate item, and inform the application about it.
+ const int newRow = m_adaptorModel.rowAt(newModelIndex);
+ const int newColumn = m_adaptorModel.columnAt(newModelIndex);
+ item->setModelIndex(newModelIndex, newRow, newColumn);
+
+ // Notify the application that all 'dynamic'/role-based context data has
+ // changed as well (their getter function will use the updated index).
+ auto const itemAsList = QList<QQmlDelegateModelItem *>() << item;
+ auto const updateAllRoles = QVector<int>();
+ m_adaptorModel.notify(itemAsList, newModelIndex, 1, updateAllRoles);
+
+ // Inform the view that the item is recycled. This will typically result
+ // in the view updating its own attached delegate item properties.
+ emit itemReused(newModelIndex, item->object);
+}
+
+void QQmlTableInstanceModel::incubateModelItem(QQmlDelegateModelItem *modelItem, QQmlIncubator::IncubationMode incubationMode)
+{
+ // Guard the model item temporarily so that it's not deleted from
+ // incubatorStatusChanged(), in case the incubation is done synchronously.
+ modelItem->scriptRef++;
+
+ if (modelItem->incubationTask) {
+ // We're already incubating the model item from a previous request. If the previous call requested
+ // the item async, but the current request needs it sync, we need to force-complete the incubation.
+ const bool sync = (incubationMode == QQmlIncubator::Synchronous || incubationMode == QQmlIncubator::AsynchronousIfNested);
+ if (sync && modelItem->incubationTask->incubationMode() == QQmlIncubator::Asynchronous)
+ modelItem->incubationTask->forceCompletion();
+ } else {
+ modelItem->incubationTask = new QQmlTableInstanceModelIncubationTask(this, modelItem, incubationMode);
+
+ QQmlContextData *ctxt = new QQmlContextData;
+ QQmlContext *creationContext = modelItem->delegate->creationContext();
+ ctxt->setParent(QQmlContextData::get(creationContext ? creationContext : m_qmlContext.data()));
+ ctxt->contextObject = modelItem;
+ modelItem->contextData = ctxt;
+
+ QQmlComponentPrivate::get(modelItem->delegate)->incubateObject(
+ modelItem->incubationTask,
+ modelItem->delegate,
+ m_qmlContext->engine(),
+ ctxt,
+ QQmlContextData::get(m_qmlContext));
+ }
+
+ // Remove the temporary guard
+ modelItem->scriptRef--;
+}
+
void QQmlTableInstanceModel::incubatorStatusChanged(QQmlTableInstanceModelIncubationTask *incubationTask, QQmlIncubator::Status status)
{
QQmlDelegateModelItem *modelItem = incubationTask->modelItemToIncubate;
@@ -299,6 +433,10 @@ QVariant QQmlTableInstanceModel::model() const
void QQmlTableInstanceModel::setModel(const QVariant &model)
{
+ // Pooled items are still accessible/alive for the application, and
+ // needs to stay in sync with the model. So we need to drain the pool
+ // completely when the model changes.
+ drainReusableItemsPool(0);
m_adaptorModel.setModel(model, this, m_qmlContext->engine());
}
@@ -337,5 +475,7 @@ void QQmlTableInstanceModelIncubationTask::statusChanged(QQmlIncubator::Status s
tableInstanceModel->incubatorStatusChanged(this, status);
}
+#include "moc_qqmltableinstancemodel_p.cpp"
+
QT_END_NAMESPACE
diff --git a/src/qml/types/qqmltableinstancemodel_p.h b/src/qml/types/qqmltableinstancemodel_p.h
index fb222d8bc4..23243e7f0e 100644
--- a/src/qml/types/qqmltableinstancemodel_p.h
+++ b/src/qml/types/qqmltableinstancemodel_p.h
@@ -80,7 +80,15 @@ public:
class Q_QML_PRIVATE_EXPORT QQmlTableInstanceModel : public QQmlInstanceModel
{
+ Q_OBJECT
+
public:
+
+ enum ReusableFlag {
+ NotReusable,
+ Reusable
+ };
+
QQmlTableInstanceModel(QQmlContext *qmlContext, QObject *parent = nullptr);
~QQmlTableInstanceModel() override;
@@ -99,15 +107,25 @@ public:
const QAbstractItemModel *abstractItemModel() const override;
QObject *object(int index, QQmlIncubator::IncubationMode incubationMode = QQmlIncubator::AsynchronousIfNested) override;
- ReleaseFlags release(QObject *) override;
+ ReleaseFlags release(QObject *object) override { return release(object, NotReusable); }
+ ReleaseFlags release(QObject *object, ReusableFlag reusable);
void cancel(int) override;
+ void insertIntoReusableItemsPool(QQmlDelegateModelItem *modelItem);
+ QQmlDelegateModelItem *takeFromReusableItemsPool(const QQmlComponent *delegate);
+ void drainReusableItemsPool(int maxPoolTime);
+ void reuseItem(QQmlDelegateModelItem *item, int newModelIndex);
+
QQmlIncubator::Status incubationStatus(int index) override;
QString stringValue(int, const QString &) override { Q_UNREACHABLE(); return QString(); }
void setWatchedRoles(const QList<QByteArray> &) override { Q_UNREACHABLE(); }
int indexOf(QObject *, QObject *) const override { Q_UNREACHABLE(); return 0; }
+Q_SIGNALS:
+ void itemPooled(int index, QObject *object);
+ void itemReused(int index, QObject *object);
+
private:
QQmlComponent *m_delegate = nullptr;
QQmlAdaptorModel m_adaptorModel;
@@ -115,11 +133,14 @@ private:
QQmlDelegateModelItemMetaType *m_metaType;
QHash<int, QQmlDelegateModelItem *> m_modelItems;
+ QList<QQmlDelegateModelItem *> m_reusableItemsPool;
QList<QQmlIncubator *> m_finishedIncubationTasks;
+ void incubateModelItem(QQmlDelegateModelItem *modelItem, QQmlIncubator::IncubationMode incubationMode);
void incubatorStatusChanged(QQmlTableInstanceModelIncubationTask *dmIncubationTask, QQmlIncubator::Status status);
void deleteIncubationTaskLater(QQmlIncubator *incubationTask);
void deleteAllFinishedIncubationTasks();
+ QQmlDelegateModelItem *resolveModelItem(int index);
static bool isDoneIncubating(QQmlDelegateModelItem *modelItem);
static void deleteModelItemLater(QQmlDelegateModelItem *modelItem);