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

Provides FastExpressionFilter component compatible with
SortFilterProxyModel. In comparison to original ExpressionFilter it allows
to define which role values should be provided to the expression's
context and improves performance significantly.

Closes: #13063
This commit is contained in:
Michał Cieślak 2024-01-08 11:54:13 +01:00 committed by Michał
parent ed89ba77b8
commit a6cf37278c
8 changed files with 336 additions and 13 deletions

View File

@ -91,6 +91,7 @@ add_library(StatusQ SHARED
include/StatusQ/QClipboardProxy.h
include/StatusQ/aggregator.h
include/StatusQ/concatmodel.h
include/StatusQ/fastexpressionfilter.h
include/StatusQ/fastexpressionrole.h
include/StatusQ/leftjoinmodel.h
include/StatusQ/modelutilsinternal.h
@ -106,6 +107,7 @@ add_library(StatusQ SHARED
src/QClipboardProxy.cpp
src/aggregator.cpp
src/concatmodel.cpp
src/fastexpressionfilter.cpp
src/fastexpressionrole.cpp
src/leftjoinmodel.cpp
src/modelutilsinternal.cpp

View File

@ -0,0 +1,49 @@
#pragma once
#include <filters/filter.h>
#include <QQmlContext>
#include <QQmlExpression>
#include <QQmlScriptString>
#include <memory>
class FastExpressionFilter : public qqsfpm::Filter
{
Q_OBJECT
Q_PROPERTY(QQmlScriptString expression READ expression WRITE setExpression
NOTIFY expressionChanged)
Q_PROPERTY(QStringList expectedRoles READ expectedRoles
WRITE setExpectedRoles NOTIFY expectedRolesChanged)
public:
using Filter::Filter;
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;
protected:
bool filterRow(
const QModelIndex& sourceIndex,
const qqsfpm::QQmlSortFilterProxyModel& proxyModel) const override;
Q_SIGNALS:
void expressionChanged();
void expectedRolesChanged();
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

@ -2,9 +2,11 @@
#include <proxyroles/singlerole.h>
#include <QQmlContext>
#include <QQmlExpression>
#include <QQmlScriptString>
class QQmlExpression;
#include <memory>
class FastExpressionRole : public qqsfpm::SingleRole
{
@ -38,8 +40,8 @@ private:
void updateExpression();
QQmlScriptString m_scriptString;
QQmlExpression* m_expression = nullptr;
QQmlContext* m_context = nullptr;
std::unique_ptr<QQmlExpression> m_expression;
std::unique_ptr<QQmlContext> m_context;
QStringList m_expectedRoles;
};

View File

@ -0,0 +1,167 @@
#include "StatusQ/fastexpressionfilter.h"
#include <qqmlsortfilterproxymodel.h>
using namespace qqsfpm;
/*!
\qmltype FastExpressionFilter
\inherits Filter
\inqmlmodule StatusQ
\brief A custom filter similar to (and based on) SFPM's ExpressionFilter but
optimized to access only explicitly indicated roles.
A FastExpressionFilter, similarly as \l ExpressionFilter, is a \l Filter
allowing to implement a filtering based on a javascript expression.
However in FastExpressionFilter's expression context there are available
only roles explicitly listed in \l expectedRoles property:
\code
SortFilterProxyModel {
sourceModel: mySourceModel
filters: FastExpressionFilter {
expression: model.a < 4 && model.b < 10
expectedRoles: ["a", "b"]
}
}
\endcode
By accessing only needed roles, the performance is significantly better in
comparison to ExpressionFilter.
*/
/*!
\qmlproperty expression FastExpressionFilter::expression
See ExpressionFilter::expression for details. Unlike the original
ExpressionFilter only roles explicitly declared via expectedRoles are accessible.
*/
const QQmlScriptString& FastExpressionFilter::expression() const
{
return m_scriptString;
}
void FastExpressionFilter::setExpression(const QQmlScriptString& scriptString)
{
if (m_scriptString == scriptString)
return;
m_scriptString = scriptString;
updateExpression();
emit expressionChanged();
invalidate();
}
void FastExpressionFilter::proxyModelCompleted(const QQmlSortFilterProxyModel& proxyModel)
{
updateContext(proxyModel);
}
/*!
\qmlproperty list<string> FastExpressionFilter::expectedRoles
List of role names intended to be available in the expression's context.
*/
void FastExpressionFilter::setExpectedRoles(const QStringList& expectedRoles)
{
if (m_expectedRoles == expectedRoles)
return;
m_expectedRoles = expectedRoles;
emit expectedRolesChanged();
invalidate();
}
const QStringList &FastExpressionFilter::expectedRoles() const
{
return m_expectedRoles;
}
bool FastExpressionFilter::filterRow(const QModelIndex& sourceIndex,
const QQmlSortFilterProxyModel& proxyModel) const
{
if (m_scriptString.isEmpty())
return true;
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(it.value(), 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 true;
}
if (result.canConvert<bool>()) {
return result.toBool();
} else {
qWarning("%s:%i:%i : Can't convert result to bool",
expression.sourceFile().toUtf8().data(),
expression.lineNumber(),
expression.columnNumber());
return true;
}
}
void FastExpressionFilter::updateContext(const QQmlSortFilterProxyModel& proxyModel)
{
m_context = std::make_unique<QQmlContext>(qmlContext(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 FastExpressionFilter::updateExpression()
{
if (!m_context)
return;
m_expression = std::make_unique<QQmlExpression>(m_scriptString,
m_context.get());
connect(m_expression.get(), &QQmlExpression::valueChanged, this,
&FastExpressionFilter::invalidate);
m_expression->setNotifyOnValueChanged(true);
m_expression->evaluate();
}

View File

@ -1,9 +1,6 @@
#include "StatusQ/fastexpressionrole.h"
#include "qqmlsortfilterproxymodel.h"
#include <QQmlContext>
#include <QQmlExpression>
#include <qqmlsortfilterproxymodel.h>
using namespace qqsfpm;
@ -38,7 +35,7 @@ using namespace qqsfpm;
/*!
\qmlproperty expression FastExpressionRole::expression
See ExpressionRole::expression for details. Unline the original
See ExpressionRole::expression for details. Unlike the original
ExpressionRole only roles explicitly declared via expectedRoles are accessible.
*/
const QQmlScriptString& FastExpressionRole::expression() const
@ -123,8 +120,7 @@ QVariant FastExpressionRole::data(const QModelIndex& sourceIndex,
void FastExpressionRole::updateContext(const QQmlSortFilterProxyModel& proxyModel)
{
delete m_context;
m_context = new QQmlContext(qmlContext(this), this);
m_context = std::make_unique<QQmlContext>(qmlContext(this));
QVariantMap modelMap;
@ -155,9 +151,10 @@ 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 = std::make_unique<QQmlExpression>(m_scriptString,
m_context.get());
connect(m_expression.get(), &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/fastexpressionfilter.h"
#include "StatusQ/fastexpressionrole.h"
#include "StatusQ/leftjoinmodel.h"
#include "StatusQ/modelutilsinternal.h"
@ -39,6 +40,7 @@ public:
qmlRegisterType<SourceModel>("StatusQ", 0, 1, "SourceModel");
qmlRegisterType<ConcatModel>("StatusQ", 0, 1, "ConcatModel");
qmlRegisterType<FastExpressionFilter>("StatusQ", 0, 1, "FastExpressionFilter");
qmlRegisterType<FastExpressionRole>("StatusQ", 0, 1, "FastExpressionRole");
qmlRegisterType<LeftJoinModel>("StatusQ", 0, 1, "LeftJoinModel");

View File

@ -0,0 +1,96 @@
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: 0
property alias filterEnabled: filter.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
filters: FastExpressionFilter {
id: filter
expression: a > d && a < 5
expectedRoles: ["a"]
}
}
readonly property SignalSpy rowsRemovedSpy: SignalSpy {
target: testModel
signalName: "rowsRemoved"
}
}
}
TestCase {
name: "FastExpressionFilter"
function test_basicFiltering() {
const obj = createTemporaryObject(testComponent, root)
compare(obj.model.count, 4)
compare(obj.observer.accessCounter, 7)
}
function test_filteringAfterContextChange() {
const obj = createTemporaryObject(testComponent, root)
compare(obj.rowsRemovedSpy.count, 0)
obj.d = 1
compare(obj.rowsRemovedSpy.count, 1)
compare(obj.observer.accessCounter, 14)
}
function test_enabled() {
const obj = createTemporaryObject(testComponent, root,
{ filterEnabled: false })
compare(obj.model.count, 7)
compare(obj.observer.accessCounter, 0)
obj.filterEnabled = true
compare(obj.model.count, 4)
compare(obj.observer.accessCounter, 7)
}
}
}

View File

@ -64,6 +64,11 @@ Item {
property string expressionRole: model.expressionRole
}
}
readonly property SignalSpy modelSignalSpy: SignalSpy {
target: testModel
signalName: "dataChanged"
}
}
}
@ -107,8 +112,11 @@ Item {
const instantiator = obj.instantiator
observerProxy.accessCounter = 0
compare(obj.modelSignalSpy.count, 0)
obj.d = 1
compare(obj.modelSignalSpy.count, 1)
compare(observerProxy.accessCounter, 4)
}