feat(StatusQ): Faster version of SFPM's ExpressionRole

Provides FastExpressionRole component compatible with
SortFilterProxyModel. In comparison to original ExpressionRole it allows
to define which role values should be provided to the expression's
context. This approach improves performance significantly in most cases.

Closes: #13047
This commit is contained in:
Michał Cieślak 2023-12-19 16:26:03 +01:00 committed by Michał
parent 480673d8be
commit 1c4a936833
5 changed files with 349 additions and 5 deletions

View File

@ -89,33 +89,35 @@ endif()
add_library(StatusQ SHARED
${STATUSQ_QRC_COMPILED}
include/StatusQ/QClipboardProxy.h
include/StatusQ/aggregator.h
include/StatusQ/concatmodel.h
include/StatusQ/fastexpressionrole.h
include/StatusQ/leftjoinmodel.h
include/StatusQ/modelutilsinternal.h
include/StatusQ/permissionutilsinternal.h
include/StatusQ/rolesrenamingmodel.h
include/StatusQ/rxvalidator.h
include/StatusQ/singleroleaggregator.h
include/StatusQ/statussyntaxhighlighter.h
include/StatusQ/statuswindow.h
include/StatusQ/stringutilsinternal.h
include/StatusQ/submodelproxymodel.h
include/StatusQ/aggregator.h
include/StatusQ/singleroleaggregator.h
include/StatusQ/sumaggregator.h
src/QClipboardProxy.cpp
src/aggregator.cpp
src/concatmodel.cpp
src/fastexpressionrole.cpp
src/leftjoinmodel.cpp
src/modelutilsinternal.cpp
src/permissionutilsinternal.cpp
src/plugin.cpp
src/rolesrenamingmodel.cpp
src/rxvalidator.cpp
src/singleroleaggregator.cpp
src/statussyntaxhighlighter.cpp
src/statuswindow.cpp
src/stringutilsinternal.cpp
src/submodelproxymodel.cpp
src/aggregator.cpp
src/singleroleaggregator.cpp
src/sumaggregator.cpp
# wallet

View File

@ -0,0 +1,45 @@
#pragma once
#include <proxyroles/singlerole.h>
#include <QQmlScriptString>
class QQmlExpression;
class FastExpressionRole : public qqsfpm::SingleRole
{
Q_OBJECT
Q_PROPERTY(QQmlScriptString expression READ expression WRITE setExpression
NOTIFY expressionChanged)
Q_PROPERTY(QStringList expectedRoles READ expectedRoles
WRITE setExpectedRoles NOTIFY expectedRolesChanged)
public:
using SingleRole::SingleRole;
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();
private:
QVariant data(const QModelIndex& sourceIndex,
const qqsfpm::QQmlSortFilterProxyModel& proxyModel) override;
void updateContext(const qqsfpm::QQmlSortFilterProxyModel& proxyModel);
void updateExpression();
QQmlScriptString m_scriptString;
QQmlExpression* m_expression = nullptr;
QQmlContext* m_context = nullptr;
QStringList m_expectedRoles;
};

View File

@ -0,0 +1,164 @@
#include "StatusQ/fastexpressionrole.h"
#include "qqmlsortfilterproxymodel.h"
#include <QQmlContext>
#include <QQmlExpression>
using namespace qqsfpm;
/*!
\qmltype FastExpressionRole
\inherits SingleRole
\inqmlmodule StatusQ
\brief A custom role similar to (and based on) SFPM's ExpressionRole but
optimized to access only explicitly indicated roles.
A FastExpressionRole, similarly as \l ExpressionRole, is a \l ProxyRole
allowing to implement a custom role based on a javascript expression.
However in FastExpressionRole's expression context there are available only
roles explicitly listed in \l expectedRoles property:
\code
SortFilterProxyModel {
sourceModel: numberModel
proxyRoles: FastExpressionRole {
name: "c"
expression: model.a + model.b
expectedRoles: ["a", "b"]
}
}
\endcode
By accessing only needed roles, the performance is significantly better in
comparison to ExpressionRole, especially when the model has multiple
FastExpressionRole's.
*/
/*!
\qmlproperty expression FastExpressionRole::expression
See ExpressionRole::expression for details. Unline the original
ExpressionRole only roles explicitly declared via expectedRoles are accessible.
*/
const QQmlScriptString& FastExpressionRole::expression() const
{
return m_scriptString;
}
void FastExpressionRole::setExpression(const QQmlScriptString& scriptString)
{
if (m_scriptString == scriptString)
return;
m_scriptString = scriptString;
updateExpression();
emit expressionChanged();
invalidate();
}
void FastExpressionRole::proxyModelCompleted(const QQmlSortFilterProxyModel& proxyModel)
{
updateContext(proxyModel);
}
void FastExpressionRole::setExpectedRoles(const QStringList& expectedRoles)
{
if (m_expectedRoles == expectedRoles)
return;
m_expectedRoles = expectedRoles;
emit expectedRolesChanged();
invalidate();
}
/*!
\qmlproperty list<string> FastExpressionRole::expectedRoles
List of role names intended to be available in the expression's context.
*/
const QStringList& FastExpressionRole::expectedRoles() const
{
return m_expectedRoles;
}
QVariant FastExpressionRole::data(const QModelIndex& sourceIndex,
const QQmlSortFilterProxyModel& proxyModel)
{
if (m_scriptString.isEmpty())
return {};
QVariantMap modelMap;
auto roles = proxyModel.roleNames();
QQmlContext context(qmlContext(this));
auto addToContext = [&] (const QString &name, const QVariant& value) {
context.setContextProperty(name, value);
modelMap.insert(name, value);
};
for (auto it = roles.cbegin(); it != roles.cend(); ++it) {
auto name = it.value();
if (!m_expectedRoles.contains(name))
continue;
addToContext(name, proxyModel.sourceData(sourceIndex, it.key()));
}
addToContext(QStringLiteral("index"), sourceIndex.row());
context.setContextProperty(QStringLiteral("model"), modelMap);
QQmlExpression expression(m_scriptString, &context);
QVariant result = expression.evaluate();
if (expression.hasError())
qWarning() << expression.error();
return result;
}
void FastExpressionRole::updateContext(const QQmlSortFilterProxyModel& proxyModel)
{
delete m_context;
m_context = new QQmlContext(qmlContext(this), this);
QVariantMap modelMap;
auto addToContext = [&] (const QString &name, const QVariant& value) {
m_context->setContextProperty(name, value);
modelMap.insert(name, value);
};
const auto roles = proxyModel.roleNames();
for (auto it = roles.cbegin(); it != roles.cend(); ++it) {
auto name = it.value();
if (!m_expectedRoles.contains(name))
continue;
addToContext(name, {});
}
addToContext(QStringLiteral("index"), -1);
m_context->setContextProperty(QStringLiteral("model"), modelMap);
updateExpression();
}
void FastExpressionRole::updateExpression()
{
if (!m_context)
return;
delete m_expression;
m_expression = new QQmlExpression(m_scriptString, m_context, nullptr, this);
connect(m_expression, &QQmlExpression::valueChanged, this, &FastExpressionRole::invalidate);
m_expression->setNotifyOnValueChanged(true);
m_expression->evaluate();
}

View File

@ -5,6 +5,7 @@
#include "StatusQ/QClipboardProxy.h"
#include "StatusQ/concatmodel.h"
#include "StatusQ/fastexpressionrole.h"
#include "StatusQ/leftjoinmodel.h"
#include "StatusQ/modelutilsinternal.h"
#include "StatusQ/permissionutilsinternal.h"
@ -16,7 +17,6 @@
#include "StatusQ/submodelproxymodel.h"
#include "StatusQ/sumaggregator.h"
#include "wallet/managetokenscontroller.h"
#include "wallet/managetokensmodel.h"
@ -38,6 +38,9 @@ public:
qmlRegisterType<SourceModel>("StatusQ", 0, 1, "SourceModel");
qmlRegisterType<ConcatModel>("StatusQ", 0, 1, "ConcatModel");
qmlRegisterType<FastExpressionRole>("StatusQ", 0, 1, "FastExpressionRole");
qmlRegisterType<LeftJoinModel>("StatusQ", 0, 1, "LeftJoinModel");
qmlRegisterType<SubmodelProxyModel>("StatusQ", 0, 1, "SubmodelProxyModel");
qmlRegisterType<RoleRename>("StatusQ", 0, 1, "RoleRename");

View File

@ -0,0 +1,130 @@
import QtQml 2.15
import QtQuick 2.15
import QtTest 1.15
import SortFilterProxyModel 0.2
import StatusQ 0.1
import StatusQ.Core.Utils 0.1
import StatusQ.TestHelpers 0.1
Item {
id: root
Component {
id: testComponent
QtObject {
property int d: 0
readonly property ListModel source: ListModel {
id: listModel
ListElement { a: 1; b: 2; c: 3 }
}
readonly property ModelAccessObserverProxy observer: ModelAccessObserverProxy {
id: observerProxy
property int accessCounter: 0
sourceModel: listModel
onDataAccessed: accessCounter++
}
readonly property FastExpressionRole expressionRole: expressionRole
readonly property SortFilterProxyModel model: SortFilterProxyModel {
id: testModel
sourceModel: observerProxy
proxyRoles: [
FastExpressionRole {
id: expressionRole
name: "expressionRole"
expression: a + model.b + (model.c ?? 0) + d + index
expectedRoles: ["a", "b"]
},
FastExpressionRole {
name: "expressionRole2"
expression: "staticRole"
}
]
}
readonly property Instantiator instantiator: Instantiator {
model: testModel
QtObject {
property string expressionRole: model.expressionRole
}
}
}
}
TestCase {
name: "FastExpressionRole"
function test_expressionRoleValue() {
const obj = createTemporaryObject(testComponent, root)
const instantiator = obj.instantiator
const listModel = obj.source
fuzzyCompare(instantiator.object.expressionRole, 3, 1e-7)
listModel.setProperty(0, "b", 9)
fuzzyCompare(instantiator.object.expressionRole, 10, 1e-7)
obj.d = 42
fuzzyCompare(instantiator.object.expressionRole, 52, 1e-7)
}
function test_expressionRoleAccessToSource() {
const obj = createTemporaryObject(testComponent, root)
const testModel = obj.model
const observerProxy = obj.observer
observerProxy.accessCounter = 0
ModelUtils.get(testModel, 0, "expressionRole")
compare(observerProxy.accessCounter, 2)
ModelUtils.get(testModel, 0, "expressionRole2")
compare(observerProxy.accessCounter, 2)
}
function test_expressionRoleAccessToSourceViaContextChange() {
const obj = createTemporaryObject(testComponent, root)
const testModel = obj.model
const observerProxy = obj.observer
const instantiator = obj.instantiator
observerProxy.accessCounter = 0
obj.d = 1
compare(observerProxy.accessCounter, 4)
}
function test_expressionRoleChangeExpectedRoles() {
const obj = createTemporaryObject(testComponent, root)
const instantiator = obj.instantiator
const expressionRole = obj.expressionRole
fuzzyCompare(instantiator.object.expressionRole, 3, 1e-7)
expressionRole.expectedRoles = ["a", "b", "c"]
fuzzyCompare(instantiator.object.expressionRole, 6, 1e-7)
expressionRole.expectedRoles = ["a", "b"]
fuzzyCompare(instantiator.object.expressionRole, 3, 1e-7)
}
}
}