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

Loader with aggregation and visual loading state #555

Merged
merged 14 commits into from
Sep 30, 2020

Conversation

davidlatwe
Copy link
Collaborator

This PR supersede and will close #553.
The problem in #533 was, quoting from #553 (comment):

One thing to be noted, this PR lists out all subsets then fetching versions later, so if there is subset that has no versions at all, it will remain in view. Although this is a rare case in production but better to avoid that. The another approach I am currently trying will not have this issue.

And this is the PR what was promised in that comment.

What's changed

  • Subset, version fetching speed improved by involving MongoDB's aggregation.
  • The subset, version fetching is running in worker thread and is breakable.
  • Added loading spinner on subset tree view, provide visual feed while waiting results from database, although you may not even sees it in most cases. (aggregation is fast !)
  • Displaying "No data" in subset tree view if no versioned subset found.
  • Previously, toggling "Enable Grouping" check box will refresh the view and query the database again, now it will only reset the model. This was benefit from caching documents in worker fetching thread implementation.
  • Disabled QTreeView header's ResizeToContents resize mode to improve the performance of layout changing. (Inherited from Non blocking loader #553)

And here's the visual result. Note that I am not in the office at the moment, so I mocked the loading time in order to see the loading spinner in view.

a2465c4d-5e79-4992-af0d-cd52b11e3522

And sorry for Pype guys @mkolar, @iLLiCiTiT , need another review from you and test if this PR will cause any trouble to merge #550.

Please let me know what you think !

@davidlatwe
Copy link
Collaborator Author

Hmmm, just had a stab on merging #550, seems not an easy task.

painter = QtGui.QPainter(self.viewport())
rect = event.rect()
rect = QtCore.QRectF(rect.topLeft(), rect.bottomRight())
qtext_opt = QtGui.QTextOption(QtCore.Qt.AlignHCenter |
Copy link

Choose a reason for hiding this comment

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

line break after binary operator

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Either before or after line break, Hound barks. 😠

Copy link
Contributor

Choose a reason for hiding this comment

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

We use line break before so operators are on new line (both won't work with hound if hound is not set ;) ).

I would personally in this case use:

qtext_opt = QtGui.QTextOption(
    QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter
)

{"$group": {"_id": "$parent",
"_version_id": {"$last": "$_id"},
"name": {"$last": "$name"},
"data": {"$last": "$data"},
Copy link
Contributor

Choose a reason for hiding this comment

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

I would add "type" key here, but don't have explanation why :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Sure :D

_subsets.append(subset)
_ids.append(subset["_id"])

_pipeline = [
Copy link
Contributor

Choose a reason for hiding this comment

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

Would be great if there is (e.g. in avalon.lib) function for this pipeline to be usable in multiple places.

def last_version_pipeline(subset_ids):
    return [
        # Find all versions of those subsets
        {"$match": {"type": "version", "parent": {"$in": subset_ids}}},
        ...
    ]

But it is only nice to have, because it is not used in other places yet.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Maybe it could be in the avalon.io module. 🤔

def __init__(self, parent=None):
super(SubsetTreeView, self).__init__(parent=parent)
loading_gif = os.path.dirname(style.__file__) + "/svg/spinner-200.svg"
spinner = QtSvg.QSvgRenderer(loading_gif)
Copy link
Contributor

Choose a reason for hiding this comment

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

My experience with svg in Qt is that it is slow in comparison to png or gif. Recommendation is to implement rotating QLabel with png, on the other side I guess that one svg won't affect Qt much.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yep, there's only one, and it's animation playback will be stopped once the loading is completed. So yeah, I think we are safe :P

some of the first level field may not be presented.

"""
def _fetch():
Copy link
Contributor

Choose a reason for hiding this comment

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

_fetch should be class method, there is not much reason to have it inside.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I guess it's there because it acts more like a local lambda written as a function.

Also, if it were put on the class it shouldn't be a classmethod right? Because it uses variables/functions of the instance of the class inside of it (not how it refers to self). So if put on the class it would be a regular method.

Copy link
Contributor

Choose a reason for hiding this comment

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

Also, if it were put on the class it shouldn't be a classmethod right?

I meant as object method def _fetch(self):, not as class method, my fault.

it acts more like a local lambda written as a function

I see advantage of lamda but there's no advantage of that here, all _fetch need is self. Method fetch_subset_and_version has 4 lines in fact. All what these lines do, is preparation and launching another method in thread, but because the definition of launched method is inside it's harder to read (for me) what's really happening inside.

This is "formatting" comment of my preferences. I have no ambition to change mindset because I know it's hard to change mine :)

Copy link
Contributor

Choose a reason for hiding this comment

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

Looks right to me. Nesting a function tells me that it is only useful to the parent function, which seems to be the case here. If you put it in the class or instance, I would expect it to be useful standalone and by others, and require its own tests. The smaller the interface the better. Also, if Python did support multiline lambdas, this would be a good place for it.

@iLLiCiTiT
Copy link
Contributor

Logic seems to be ok for me. I'll have to modify it to be able to test it in our fork, but at this moment I don't see any possibly breaking part. Notes I have are nice to have changes, nothing will happen if they're not accepted.

for subset in _subsets]
self.doc_fetched.emit()

self._doc_payload[:] = []
Copy link
Contributor

@iLLiCiTiT iLLiCiTiT Jun 25, 2020

Choose a reason for hiding this comment

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

I am curious why this is used self._doc_payload[:] = [] and not just self._doc_payload = []?

EDITED:
Because [:] creates copy of list but assigning new value to it is strange to me.

Copy link
Collaborator

@BigRoy BigRoy Jun 26, 2020

Choose a reason for hiding this comment

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

This is basically an in-place assignment. It would be the same as doing:

my_list = ["a", "b", "c"]
my_list.clear()

And this replaces the list in place:

a = ["a", "b"]
b = a

# Show mutability
a.append("c")
print(b)
# ["a", "b", "c"]

# Assign new value to a does not change value of b, because it's assigning a new list
a = ["aa", "bb"]
print(b)
# ["a", "b", "c"]

# But when using in place assignment it would, because it's changing the same mutable list.
a = ["a", "b"]
b = a
a[:] = ["aa", "bb"]
print(b)
# ["aa", "bb"]

Edit: added example above of the in-place assignment that is not just clearing it

I believe the reason to do explicit in-place assignments is due to the "mutable list" potentially being shared to another thread and otherwise that one working on an older copy of a list that isn't actually the same list as self._doc_payload. However, because it doesn't seem to be passed around as an object I don't think that's actually the case. @davidlatwe is it redundant for this case?

There should be some informational topics online on replacing lists in-place with regards to its mutability if you're looking for more info.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

However, because it doesn't seem to be passed around as an object I don't think that's actually the case.

Yes, it's true that there's no need to use in-place assignment, just re-using the same object for no special reason. Will change it for good.

@iLLiCiTiT
Copy link
Contributor

I can confirm it works with current multiselection implementation.

@davidlatwe
Copy link
Collaborator Author

I can confirm it works with current multiselection implementation.

Nice ! 🎉

@BigRoy
Copy link
Collaborator

BigRoy commented Sep 21, 2020

Should we get this PR ready for merging? I imagine @davidlatwe you've been using this in production since.
And likely @iLLiCiTiT adopted it in the meantime too?

@iLLiCiTiT
Copy link
Contributor

And likely @iLLiCiTiT adopted it in the meantime too?

Yes, we use it.

@mottosso
Copy link
Contributor

Hope you're not waiting for me, please go ahead and merge if it's ready!

@BigRoy BigRoy mentioned this pull request Sep 30, 2020
@BigRoy
Copy link
Collaborator

BigRoy commented Sep 30, 2020

Tested this and worked flawlessly here. @davidlatwe feel free to merge - or I'll merge in a few days.

@davidlatwe
Copy link
Collaborator Author

Merging this !!

@davidlatwe davidlatwe merged commit 4f6c897 into getavalon:master Sep 30, 2020
@davidlatwe davidlatwe deleted the loader-with-aggregation branch September 30, 2020 06:57
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.

5 participants