Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes slow update in field calculator #8177

Merged
merged 6 commits into from
Oct 15, 2018

Conversation

elpaso
Copy link
Contributor

@elpaso elpaso commented Oct 12, 2018

The diff is misleading, it's just a single
change in the logic: block the vector signals ...
... and emit dataChanged at
the end.

I don't really like the idea of blocking
signals (the edit buffer basically)
and I'm a bit worried of side effects,
but I can't see any other solution.

The root of the issue here is that
for each changed field/row an attribute
valueChanged signal is emitted, and the
QgsVectorLayerCache::featureAtId
loads the feature again.

Pocessing time for the test layer in the
issue tracker went down from ~20 minutes
to less than 3 seconds.

by blocking the vector signals ...
... and emitting dataChanged at
the end.

I'm a bit worried of side effects,
but I can't see any other solution.

The root of the issue here is that
for each changed field/row an attribute
valueChanged signal is emitted, and the
QgsVectorLayerCache::featureAtId
loads the feature again.
@elpaso elpaso added the Bugfix label Oct 12, 2018
@elpaso
Copy link
Contributor Author

elpaso commented Oct 12, 2018

@m-kuhn I'd appreciate a slice of your brain time here.

@nyalldawson
Copy link
Collaborator

Just a thought -- both the attribute table dialogs and field calculator live in app, so couldn't we directly block just the attribute table dialog updates from field calculator? That'd avoid any unwanted side effects which would come from the global signal blocking (which I'm also concerned about)

@nyalldawson
Copy link
Collaborator

Thanks for tackling this by the way!

much better approach ...
@elpaso
Copy link
Contributor Author

elpaso commented Oct 13, 2018

@nyalldawson I slept on it and came to a similar conclusion: blocking just the layer cache signals, from the attribute table scope: simpler, safer, better (TM)

@elpaso elpaso added this to the 3.4.0 milestone Oct 13, 2018
@nyalldawson
Copy link
Collaborator

But what about the situation where a user has an attribute table open, and then uses the field calculator from the main window toolbar? Or layer properties window?

@elpaso
Copy link
Contributor Author

elpaso commented Oct 13, 2018

I don't understand your proposal: QgsFieldCalculator is instanciated inside the field calculator, it does not live in app.

Btw, you are right and my solution only works for the attribute table that directly opened the field calculator. It does not solve the issue for other attr tables that might have been opened separately on the same vector layer or for field calculator opened from the properties dialog.

@nyalldawson
Copy link
Collaborator

What I mean is that they are both app library classes. So we could potentially call from the field calculator something like QgisApp::instance()->blockTableUpdates, which then blocks the updates of all open table dialogs.

@elpaso
Copy link
Contributor Author

elpaso commented Oct 13, 2018

Actually, the problem was not strictly related to the field calculator: rollbacks of massive changes triggered the bug as well, I've fixed that too.

*
* \since QGIS 3.4
*/
void blockAttributeTableUpdates( const QgsVectorLayer *layer );
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could use one signal and one slot here with a bool blocked argument

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I first implemented it like that. But I was in doubt, because in general we don't like bool switches, do we?
But you solved my doubt, so I'm fine with the bool now. (I've always thought that blockSignals(bool) was not very consistent with the rest of Qt API).

if ( layer == mLayer )
this->blockCacheUpdateSignals( false );
} );
// Massive rollbacks can also freeze the GUI due to the feature cache signals
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have the same issue with attribute addition/deletion too, don't we?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I you mean removing the columns as a whole, apparently not, at least I didn't observe the issue when removing the z field, I'll double check it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed: with my PR removing a colum is FAST.

@nyalldawson
Copy link
Collaborator

One thing I'm not sure of here -- do we need to do a single "update cache/model" call after the calculator finishes?

@elpaso
Copy link
Contributor Author

elpaso commented Oct 14, 2018

One thing I'm not sure of here -- do we need to do a single "update cache/model" call after the calculator finishes?

To be honest I wasn't sure either, here we have a babel of signals/slots firing. It's a spagetti-signal-mess!
Btw, I came to the conclusion that it does work as expected, but even if it didn't we cannot really emit a number of signals for each modified row!

It wouldn't be much much easier if we could have just three signals?

  • a signal for a single row change (geometry included)
  • a signal (and buffer implementations) for bulk changes, instead of emitting a signal for every row we are going to change (60K rows -> 60K signals emitted! I'm optimisitc here, we probably have more than one signal emitted for every row)
  • a generic signal for dataChanged

@luipir
Copy link
Contributor

luipir commented Oct 14, 2018

It wouldn't be much much easier if we could have just three signals?

Are you referring only to qgis implemented signals? or also to all emitted by qt infrastructure?

@elpaso
Copy link
Contributor Author

elpaso commented Oct 14, 2018

@luipir nevermind: I know that's not going to happen until QGIS 7

I was talking about the various signals emitted by QgsVectorLayer when editing.

@elpaso elpaso closed this Oct 14, 2018
@elpaso elpaso reopened this Oct 14, 2018
@nyalldawson
Copy link
Collaborator

: I know that's not going to happen until QGIS 7

First thing on the api break list for qgis 7 -- "rm -rf src/gui/attributetable" ;)

But in all seriousness -- attribute table is crying out for a complete, ground up rework.

@m-kuhn
Copy link
Member

m-kuhn commented Oct 15, 2018

IIRC the idea was to use the beginEditCommand and endEditCommand for bulk updates. If those are properly emitted by a component (e.g. field calculator) individual row update signals should be ignored and a single refresh done only once the end.

@elpaso elpaso merged commit 096b4ce into qgis:master Oct 15, 2018
@elpaso elpaso deleted the bugfix-20094-field-calculator-slowness branch October 15, 2018 08:32
@m-kuhn
Copy link
Member

m-kuhn commented Oct 15, 2018

@elpaso did you read my comment?

@elpaso
Copy link
Contributor Author

elpaso commented Oct 15, 2018

Yes, I'll have a look to that signals, just to be sure: are you suggesting that the attribute table should listen to those signals and block feature cache signals instead of the solution I've implemented in this PR?

Something like:

connect( mLayer, &QgsVectorLayer::editCommandStarted, [ = ]( const QString *)
  {
      this->blockCacheUpdateSignals( true );
  } );
connect( mLayer, &QgsVectorLayer::editCommandEnded [ = ]( const QString *)
  {
      this->blockCacheUpdateSignals( false );
  } );

@m-kuhn
Copy link
Member

m-kuhn commented Oct 15, 2018

I would avoid to block signals if somehow possible.

The original idea was (IIRC) to set a variable inEditCommand (or similar) and then buffer all incoming change notifications but defer updating the attribute table to the end. This way a full cache invalidation and potentially expensive reload can be avoided for partial updates. I am not sure what the current problem is: either the approach explained here is not implemented properly and the buffering somehow fails, or if it is actually the signal-and-buffering code and not the attribute table update code that is too expensive and makes the calculation slow.

An alternative would be to selectively disconnect/reconnect the signals inside the attribute table instead of completely avoiding the emission of signals if it's actually the connections which are causing the troubles.

connect( QgisApp::instance(), &QgisApp::attributeTableUpdateBlocked, [ = ]( const QgsVectorLayer * layer, const bool blocked )
{
if ( layer == mLayer )
this->blockCacheUpdateSignals( blocked );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not python, we don't need this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for reminding me that it's not Python but C++, I normally use this-> to get autocompletion and I forget to remove it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ctrl+space is your friend

@elpaso
Copy link
Contributor Author

elpaso commented Oct 15, 2018

@m-kuhn the original issue was that the field calculator loops through all records and change attribute values, but the feature cache listens to that signal (among others) and called QgsVectorLayerCache::featureAtId which called mLayer->getFeatures etc. etc.

@m-kuhn
Copy link
Member

m-kuhn commented Oct 15, 2018

And in the etc. etc. part it will actually send requests to the provider? The code was likely written under the assumption that the feature should already be cached in the edit buffer at this time.

What are your thoughts on

  • using the edit command as indicator for batch updates?
  • disconnecting connections instead of blocking signals?
  • not using this ;)

elpaso added a commit to elpaso/QGIS that referenced this pull request Oct 16, 2018
…alculator-slowness"

This reverts commit 096b4ce, reversing
changes made to 87e0f69.
@gioman
Copy link
Contributor

gioman commented Oct 18, 2018

About this fix (that is MUCH appreciated): when clicking "ok" in the field calculator as also when saving edits the table is refreshed a couple of times, is this expected?

@elpaso
Copy link
Contributor Author

elpaso commented Oct 18, 2018

@gioman I assume you are testing #8200 .
Yes, it's expected (and annoying), because it is refreshed the first time when a field is created and then again when the computation has finished (so, we are talking about at least two times, but there may be other signals that are firing up and triggering a refresh, nobody knows at this point what's happening in that ugly mess of code).

But, the important thing to check is that it does not hang and it completes in a reasonable time (in my test 60k rows with a default cache size of 10k completeted in ~8 seconds).

Note that #8200 is a few seconds slower than #8177 because I'm not blocking all signals but just deferring them to a later stage (and prevent cache reloads).

@gioman
Copy link
Contributor

gioman commented Oct 18, 2018

Thanks for the explanation!

@m-kuhn
Copy link
Member

m-kuhn commented Oct 18, 2018

@elpaso I believe there are no notifications around that could be used for one single refresh even if someone came forward with the most beautiful mess of code, but I'm probably wrong.

@luipir
Copy link
Contributor

luipir commented Oct 18, 2018

I can't find a QT way to do debouncing as in JS https://davidwalsh.name/javascript-debounce-function, this could (hopefully) avoid to retrigger events in short intervals. Probably with QEvetFilter depending on running context?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants