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:
Michał Cieślak 2024-01-08 12:16:16 +01:00 committed by Michał
parent a6cf37278c
commit 56493fa5e1
5 changed files with 349 additions and 0 deletions

View File

@ -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

View File

@ -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;
};

View File

@ -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();
}

View File

@ -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");

View File

@ -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)
}
}
}