-
Notifications
You must be signed in to change notification settings - Fork 201
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
Lazy loading and caching for attributes set in _loadData(..)
#1510
base: master
Are you sure you want to change the base?
Lazy loading and caching for attributes set in _loadData(..)
#1510
Conversation
- Cached properties are defined using a `cached_data_property` decorator - Property caches are automatically invalidated whenever the `_data` atribute is changed - The implementation uses a metaclass to collect and track all cached properties across class inheritances
These attributes include those that call `findItems` and `listAttrs`
…on to do cache invalidation
Unrelated, but the call to |
…ve) before trying again with cleaning
@JonnyWong16 the change to |
I just realized the |
Yeah, I was just thinking about it when I saw your graph. A separate PR would be good. |
… expensive) before trying again with cleaning" This reverts commit e8348df.
The cache invalidation isn't working. from plexapi.server import PlexServer
plex = PlexServer()
movie = plex.library.section("Movies").get("GoldenEye")
id(movie._data) # 2701410045600
movie.__dict__['genres'] # KeyError as expected
movie.genres # retrieved - [<Genre:Action>, <Genre:Adventure>]
movie.__dict__['genres'] # cached - [<Genre:Action>, <Genre:Adventure>]
movie.reload()
id(movie._data) # 2701409346640 - ID changed
movie.__dict__['genres'] # still cached - [<Genre:Action>, <Genre:Adventure>] |
…operties and don't call the super class' _loadData
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just realized that PlexPartialObject
inherits _loadData
from PlexObject
, so this commit doesn't really do anything 🤦.
At the very least, it makes it clear where loadData is going to.
Still a bunch of tests failing. |
I'm not really sure how to setup the testing environment myself, so this is just my understanding for why each test is failing from reading the pytest code and logs. 🚫
|
The former was fixed by modifying the test to use getattr instead of accessing attributes through The latter was due to a typo on my part, I was using I'll revisit the remaining 3 issues another day. They aren't as straightforward. |
❓
|
Makes sense thanks for clarifying, I'll merge them in my comment.
In that case it would seem that all the the renaming issues are related to edits not working. Although, edits do work at least for some attributes. So it might be local to just fields. Will investigate more tomorrow.
Not very familiar with how auto reloads works but would this work? def test_video_Movie_reload_kwargs(movie):
assert len(movie.media)
assert movie.summary is not None
movie.reload(includeFields=False, **movie._EXCLUDES)
original_auto_reload = movie._autoReload
movie._autoReload = False
assert movie.media == []
assert movie.summary is None
movie._autoReload = original_auto_reload |
Yes, that should work for the movie reload test. |
🚫
|
Alright, then it should work without inheriting PlexObject.
Done and yes, it's a lot cleaner.
The last change removed most of the |
There's also another issue of whether Putting it in I'm leaning towards leaving it in just |
if prop_name in self.__dict__: | ||
del self.__dict__[prop_name] | ||
|
||
def _invalidateCacheAndLoadData(self, data): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A bit nit-picky, but swap the order of the two methods _invalidateCachedProperties()
and _invalidateCacheAndLoadData()
.
And forgot to change the call to _loadData()
in __init__
above.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_loadData()
in __init__
still needs to be changed?
I think it's fine where you have it in |
|
Unrelated to the PR but I'm doing the development in a VSCode devcontainer. I was assuming that "features": {
"ghcr.io/devcontainers-extra/features/flake8:2": {}
} Was enough for flake8 to be installed, but apparently you still need to get the flake8 extension from the VSCode marketplace on top of that. Linting is working correctly now, so I should be good on flake8 issues now. |
Looks like this repo doesn't allow non collaborators to submit review comments, so my reply was lost. My original reply was: "Because this is the init function, there shouldn't be any cache to invalidate. So, I think it's fine as is. Let me know what you think." |
@pkkid Can you look into this?
Okay, that's what I thought as well. |
Also, this was another comment that was lost for the same reason:
I'm a little confused on why the remaining items are fetched only after a reload, why isn't it fetched in _loadData? |
@pkkid, I don't think it worked. I tried sending another comment and it's still hidden from public view. |
I found one more instance of python-plexapi/plexapi/settings.py Line 40 in 65303c2
If you do |
Yes, doesn't really matter but I'll change it to
I'm assuming this was a requirement because getting the full list was too expensive before? If that's the case, would it make sense now that the attribute is lazy loaded to always return the full hub when called? |
…uture compatability
I think that's reasonable now.
Side note (for a separate PR), I was just looking at hubs and there is a |
…al caching mechanism
I read your comment edits after pushing a change. So, I went with something a little different. Let me know what you think of it. a06a43f. I believe my change mirrors the preexisting behavior 1-to-1 without needing to deprecate the reload function (since it still serves a purpose of invalidating the cache). Although, I'm realizing I didn't update the docstring for the reload function to reflect the change. Edit: Some more logic I was missing c98a9d1 |
You're right, for some reason I thought I'm working on another PR to refactor a bunch of the reload methods to unify them so just leave it for now. Just update the doc strings. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Everything looks good now. Thanks for all your work.
I think the next follow up PR is to remove all the manual _cache
attributes now that we have better caching.
https://github.com/search?q=repo%3Apushingkarmaorg%2Fpython-plexapi+%22%23+cache%22&type=code
Description
This PR introduces lazy loading for data attributes in all Plex objects as well as a caching mechanism when loading those attributes using a new
@cached_data_property
decorator. This change significantly improves performance by deferring the loading of computationally expensive attributes until when the user requests it.The cache of properties created with
@cached_data_property
will also automatically be invalidated when_loadData(...)
is called with a new XML data object.Fixes: #1509
Type of Change
Checklist:
Additional Notes
The initial performance testing is very promising. Below is a test script that was ran against the production python-plexapi package and this PR's version:
In this testing scenario, the program does not need access to any of the expensive
Video
attributes (e.g.images
,genres
,directors
). Since those expensive queries are skipped, the lazy-loaded testing results are ~3x faster than the production python-plexapi package.# Time in seconds Lazy-loaded: 5.732901584000501 Production: 17.07976305699958
Below are the Pyspy profiler outputs between the two packages against the above script:
Lazy-loaded:
Production:
As can clearly be seen, python-plexapi (purple) is no longer the bottleneck, instead, the requests and urllib packages (which are making the HTTP requests to the Plex server) are the new bottlenecks.