feat(StatusQ): Faster version of SFPM's ExpressionSorter
Provides FastExpressionSorter component compatible with SortFilterProxyModel. In comparison to original ExpressionSorter it allows to define which role values should be provided to the expression's context and improves performance significantly. Closes: #13062
This commit is contained in:
parent
a6cf37278c
commit
56493fa5e1
|
@ -93,6 +93,7 @@ add_library(StatusQ SHARED
|
|||
include/StatusQ/concatmodel.h
|
||||
include/StatusQ/fastexpressionfilter.h
|
||||
include/StatusQ/fastexpressionrole.h
|
||||
include/StatusQ/fastexpressionsorter.h
|
||||
include/StatusQ/leftjoinmodel.h
|
||||
include/StatusQ/modelutilsinternal.h
|
||||
include/StatusQ/permissionutilsinternal.h
|
||||
|
@ -109,6 +110,7 @@ add_library(StatusQ SHARED
|
|||
src/concatmodel.cpp
|
||||
src/fastexpressionfilter.cpp
|
||||
src/fastexpressionrole.cpp
|
||||
src/fastexpressionsorter.cpp
|
||||
src/leftjoinmodel.cpp
|
||||
src/modelutilsinternal.cpp
|
||||
src/permissionutilsinternal.cpp
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
#pragma once
|
||||
|
||||
#include <sorters/sorter.h>
|
||||
|
||||
#include <QQmlContext>
|
||||
#include <QQmlExpression>
|
||||
#include <QQmlScriptString>
|
||||
|
||||
#include <memory>
|
||||
|
||||
class FastExpressionSorter : public qqsfpm::Sorter
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(QQmlScriptString expression READ expression
|
||||
WRITE setExpression NOTIFY expressionChanged)
|
||||
|
||||
Q_PROPERTY(QStringList expectedRoles READ expectedRoles
|
||||
WRITE setExpectedRoles NOTIFY expectedRolesChanged)
|
||||
public:
|
||||
using qqsfpm::Sorter::Sorter;
|
||||
|
||||
const QQmlScriptString& expression() const;
|
||||
void setExpression(const QQmlScriptString& scriptString);
|
||||
|
||||
void proxyModelCompleted(const qqsfpm::QQmlSortFilterProxyModel& proxyModel) override;
|
||||
|
||||
void setExpectedRoles(const QStringList& expectedRoles);
|
||||
const QStringList& expectedRoles() const;
|
||||
|
||||
Q_SIGNALS:
|
||||
void expressionChanged();
|
||||
void expectedRolesChanged();
|
||||
|
||||
protected:
|
||||
int compare(const QModelIndex& sourceLeft, const QModelIndex& sourceRight,
|
||||
const qqsfpm::QQmlSortFilterProxyModel& proxyModel) const override;
|
||||
|
||||
private:
|
||||
void updateContext(const qqsfpm::QQmlSortFilterProxyModel& proxyModel);
|
||||
void updateExpression();
|
||||
|
||||
QQmlScriptString m_scriptString;
|
||||
|
||||
std::unique_ptr<QQmlExpression> m_expression;
|
||||
std::unique_ptr<QQmlContext> m_context;
|
||||
|
||||
QStringList m_expectedRoles;
|
||||
};
|
|
@ -0,0 +1,177 @@
|
|||
#include "StatusQ/fastexpressionsorter.h"
|
||||
|
||||
#include <qqmlsortfilterproxymodel.h>
|
||||
|
||||
using namespace qqsfpm;
|
||||
|
||||
/*!
|
||||
\qmltype FastExpressionSorter
|
||||
\inherits Sorter
|
||||
\inqmlmodule StatusQ
|
||||
\brief A custom sorter similar to (and based on) SFPM's ExpressionSorter but
|
||||
optimized to access only explicitly indicated roles.
|
||||
|
||||
A FastExpressionSorter, similarly as \l ExpressionSorter, is a \l Sorter
|
||||
allowing to implement sorting based on a javascript expression.
|
||||
However in FastExpressionSorter's expression context there are available
|
||||
only roles explicitly listed in \l expectedRoles property:
|
||||
|
||||
\code
|
||||
SortFilterProxyModel {
|
||||
sourceModel: mySourceModel
|
||||
sorter: FastExpressionSorter {
|
||||
expression: modelLeft.a < modelRight.a
|
||||
expectedRoles: ["a"]
|
||||
}
|
||||
}
|
||||
\endcode
|
||||
|
||||
By accessing only needed roles, the performance is significantly better in
|
||||
comparison to ExpressionSorter.
|
||||
*/
|
||||
|
||||
/*!
|
||||
\qmlproperty expression FastExpressionSorter::expression
|
||||
|
||||
See ExpressionSorter::expression for details. Unlike the original
|
||||
ExpressionSorter only roles explicitly declared via expectedRoles are accessible.
|
||||
*/
|
||||
const QQmlScriptString& FastExpressionSorter::expression() const
|
||||
{
|
||||
return m_scriptString;
|
||||
}
|
||||
|
||||
void FastExpressionSorter::setExpression(const QQmlScriptString& scriptString)
|
||||
{
|
||||
if (m_scriptString == scriptString)
|
||||
return;
|
||||
|
||||
m_scriptString = scriptString;
|
||||
updateExpression();
|
||||
|
||||
emit expressionChanged();
|
||||
invalidate();
|
||||
}
|
||||
|
||||
void FastExpressionSorter::proxyModelCompleted(const QQmlSortFilterProxyModel& proxyModel)
|
||||
{
|
||||
updateContext(proxyModel);
|
||||
}
|
||||
|
||||
/*!
|
||||
\qmlproperty list<string> FastExpressionSorter::expectedRoles
|
||||
|
||||
List of role names intended to be available in the expression's context.
|
||||
*/
|
||||
void FastExpressionSorter::setExpectedRoles(const QStringList& expectedRoles)
|
||||
{
|
||||
if (m_expectedRoles == expectedRoles)
|
||||
return;
|
||||
|
||||
m_expectedRoles = expectedRoles;
|
||||
emit expectedRolesChanged();
|
||||
|
||||
invalidate();
|
||||
}
|
||||
|
||||
const QStringList &FastExpressionSorter::expectedRoles() const
|
||||
{
|
||||
return m_expectedRoles;
|
||||
}
|
||||
|
||||
bool evaluateBoolExpression(QQmlExpression& expression)
|
||||
{
|
||||
QVariant variantResult = expression.evaluate();
|
||||
if (expression.hasError()) {
|
||||
qWarning() << expression.error();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (variantResult.canConvert<bool>())
|
||||
return variantResult.toBool();
|
||||
|
||||
qWarning("%s:%i:%i : Can't convert result to bool",
|
||||
expression.sourceFile().toUtf8().data(),
|
||||
expression.lineNumber(),
|
||||
expression.columnNumber());
|
||||
return false;
|
||||
}
|
||||
|
||||
int FastExpressionSorter::compare(const QModelIndex& sourceLeft,
|
||||
const QModelIndex& sourceRight,
|
||||
const QQmlSortFilterProxyModel& proxyModel) const
|
||||
{
|
||||
if (m_scriptString.isEmpty())
|
||||
return 0;
|
||||
|
||||
QVariantMap modelLeftMap, modelRightMap;
|
||||
QHash<int, QByteArray> roles = proxyModel.roleNames();
|
||||
|
||||
QQmlContext context(qmlContext(this));
|
||||
|
||||
for (auto it = roles.cbegin(); it != roles.cend(); ++it) {
|
||||
auto role = it.key();
|
||||
auto name = it.value();
|
||||
|
||||
if (!m_expectedRoles.contains(name))
|
||||
continue;
|
||||
|
||||
modelLeftMap.insert(name, proxyModel.sourceData(sourceLeft, role));
|
||||
modelRightMap.insert(name, proxyModel.sourceData(sourceRight, role));
|
||||
}
|
||||
modelLeftMap.insert(QStringLiteral("index"), sourceLeft.row());
|
||||
modelRightMap.insert(QStringLiteral("index"), sourceRight.row());
|
||||
|
||||
QQmlExpression expression(m_scriptString, &context);
|
||||
|
||||
context.setContextProperty(QStringLiteral("modelLeft"), modelLeftMap);
|
||||
context.setContextProperty(QStringLiteral("modelRight"), modelRightMap);
|
||||
|
||||
if (evaluateBoolExpression(expression))
|
||||
return -1;
|
||||
|
||||
context.setContextProperty(QStringLiteral("modelLeft"), modelRightMap);
|
||||
context.setContextProperty(QStringLiteral("modelRight"), modelLeftMap);
|
||||
|
||||
if (evaluateBoolExpression(expression))
|
||||
return 1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void FastExpressionSorter::updateContext(const QQmlSortFilterProxyModel& proxyModel)
|
||||
{
|
||||
m_context = std::make_unique<QQmlContext>(qmlContext(this));
|
||||
|
||||
QVariantMap modelLeftMap, modelRightMap;
|
||||
|
||||
const auto roleNames = proxyModel.roleNames();
|
||||
for (const QByteArray& name : roleNames) {
|
||||
if (!m_expectedRoles.contains(name))
|
||||
continue;
|
||||
|
||||
modelLeftMap.insert(name, {});
|
||||
modelRightMap.insert(name, {});
|
||||
}
|
||||
modelLeftMap.insert(QStringLiteral("index"), -1);
|
||||
modelRightMap.insert(QStringLiteral("index"), -1);
|
||||
|
||||
m_context->setContextProperty(QStringLiteral("modelLeft"), modelLeftMap);
|
||||
m_context->setContextProperty(QStringLiteral("modelRight"), modelRightMap);
|
||||
|
||||
updateExpression();
|
||||
}
|
||||
|
||||
void FastExpressionSorter::updateExpression()
|
||||
{
|
||||
if (!m_context)
|
||||
return;
|
||||
|
||||
m_expression = std::make_unique<QQmlExpression>(m_scriptString,
|
||||
m_context.get());
|
||||
|
||||
connect(m_expression.get(), &QQmlExpression::valueChanged, this,
|
||||
&FastExpressionSorter::invalidate);
|
||||
m_expression->setNotifyOnValueChanged(true);
|
||||
m_expression->evaluate();
|
||||
}
|
|
@ -7,6 +7,7 @@
|
|||
#include "StatusQ/concatmodel.h"
|
||||
#include "StatusQ/fastexpressionfilter.h"
|
||||
#include "StatusQ/fastexpressionrole.h"
|
||||
#include "StatusQ/fastexpressionsorter.h"
|
||||
#include "StatusQ/leftjoinmodel.h"
|
||||
#include "StatusQ/modelutilsinternal.h"
|
||||
#include "StatusQ/permissionutilsinternal.h"
|
||||
|
@ -42,6 +43,7 @@ public:
|
|||
|
||||
qmlRegisterType<FastExpressionFilter>("StatusQ", 0, 1, "FastExpressionFilter");
|
||||
qmlRegisterType<FastExpressionRole>("StatusQ", 0, 1, "FastExpressionRole");
|
||||
qmlRegisterType<FastExpressionSorter>("StatusQ", 0, 1, "FastExpressionSorter");
|
||||
|
||||
qmlRegisterType<LeftJoinModel>("StatusQ", 0, 1, "LeftJoinModel");
|
||||
qmlRegisterType<SubmodelProxyModel>("StatusQ", 0, 1, "SubmodelProxyModel");
|
||||
|
|
|
@ -0,0 +1,120 @@
|
|||
import QtQml 2.15
|
||||
import QtQuick 2.15
|
||||
import QtTest 1.15
|
||||
|
||||
import SortFilterProxyModel 0.2
|
||||
|
||||
import StatusQ 0.1
|
||||
import StatusQ.TestHelpers 0.1
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
Component {
|
||||
id: testComponent
|
||||
|
||||
QtObject {
|
||||
property int d: 1
|
||||
|
||||
property alias sorterEnabled: sorter.enabled
|
||||
|
||||
readonly property ListModel source: ListModel {
|
||||
id: listModel
|
||||
|
||||
ListElement { a: 1; b: 11; c: 101 }
|
||||
ListElement { a: 2; b: 12; c: 102 }
|
||||
ListElement { a: 3; b: 13; c: 103 }
|
||||
ListElement { a: 4; b: 14; c: 104 }
|
||||
ListElement { a: 5; b: 15; c: 105 }
|
||||
ListElement { a: 6; b: 16; c: 106 }
|
||||
ListElement { a: 7; b: 17; c: 107 }
|
||||
}
|
||||
|
||||
readonly property ModelAccessObserverProxy observer: ModelAccessObserverProxy {
|
||||
id: observerProxy
|
||||
|
||||
property int accessCounter: 0
|
||||
|
||||
sourceModel: listModel
|
||||
|
||||
onDataAccessed: accessCounter++
|
||||
}
|
||||
|
||||
readonly property SortFilterProxyModel model: SortFilterProxyModel {
|
||||
id: testModel
|
||||
|
||||
sourceModel: observerProxy
|
||||
|
||||
sorters: FastExpressionSorter {
|
||||
id: sorter
|
||||
|
||||
expression: {
|
||||
return d ? modelLeft.a < modelRight.a
|
||||
: modelLeft.a > modelRight.a
|
||||
}
|
||||
|
||||
expectedRoles: ["a"]
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
readonly property SignalSpy rowsRemovedSpy: SignalSpy {
|
||||
target: testModel
|
||||
signalName: "rowsRemoved"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TestCase {
|
||||
name: "FastExpressionSorter"
|
||||
|
||||
function test_basicSorting() {
|
||||
const obj = createTemporaryObject(testComponent, root)
|
||||
const count = obj.model.count
|
||||
|
||||
compare(count, 7)
|
||||
verify(obj.observer.accessCounter
|
||||
< count * Math.ceil(Math.log2(count)) * 2)
|
||||
|
||||
compare(obj.model.get(0).a, 1)
|
||||
compare(obj.model.get(1).a, 2)
|
||||
compare(obj.model.get(6).a, 7)
|
||||
}
|
||||
|
||||
function test_filteringAfterContextChange() {
|
||||
const obj = createTemporaryObject(testComponent, root)
|
||||
const count = obj.model.count
|
||||
|
||||
obj.observer.accessCounter = 0
|
||||
|
||||
obj.d = 0
|
||||
|
||||
verify(obj.observer.accessCounter
|
||||
< count * Math.ceil(Math.log2(count)) * 2)
|
||||
|
||||
compare(obj.model.get(0).a, 7)
|
||||
compare(obj.model.get(1).a, 6)
|
||||
compare(obj.model.get(6).a, 1)
|
||||
}
|
||||
|
||||
function test_enabled() {
|
||||
const obj = createTemporaryObject(testComponent, root,
|
||||
{ sorterEnabled: false, d: 0 })
|
||||
compare(obj.observer.accessCounter, 0)
|
||||
|
||||
compare(obj.model.get(0).a, 1)
|
||||
compare(obj.model.get(1).a, 2)
|
||||
compare(obj.model.get(6).a, 7)
|
||||
|
||||
obj.sorterEnabled = true
|
||||
|
||||
const count = obj.model.count
|
||||
verify(obj.observer.accessCounter
|
||||
< count * Math.ceil(Math.log2(count)) * 2)
|
||||
|
||||
compare(obj.model.get(0).a, 7)
|
||||
compare(obj.model.get(1).a, 6)
|
||||
compare(obj.model.get(6).a, 1)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue