beets: Performance: Common operations are slow.
Just started using beets and I love it, but holy cats is it slow sometimes, even for very simple things like simple queries and listings. I only have ~10k songs and it’s still rather a pain.
So, I’d like to make it faster, and welcome advice on how to do so. The first suspect is database calls. Compare how long it takes to list items for even a small library, vs. just reading the information from the database:
$ time beet ls | wc
1.01user 0.12system 0:03.15elapsed 36%CPU (0avgtext+0avgdata 29912maxresident)k
0inputs+0outputs (0major+9671minor)pagefaults 0swaps
385 2725 15198
$ time sqlite3 musiclibrary.blb 'select * from items;' | wc
0.11user 0.00system 0:00.11elapsed 97%CPU (0avgtext+0avgdata 5456maxresident)k
0inputs+0outputs (0major+697minor)pagefaults 0swaps
11437 193901 3919008
(Also I’m not sure why my library of 385 songs has 11k items in it; that’s something else to investigate.)
Some profiling suggests that this is a likely candidate:
$ python3 -m cProfile -s cumtime /usr/local/bin/beet ls | less
...
703753 function calls (689872 primitive calls) in 3.696 seconds
Ordered by: internal time
ncalls tottime percall cumtime percall filename:lineno(function)
1159 2.377 0.002 2.377 0.002 {method 'execute' of 'sqlite3.Connection' objects}
770 0.083 0.000 0.297 0.000 db.py:173(_awaken)
34677 0.074 0.000 0.119 0.000 types.py:93(from_sql)
So yes, it does spend most of its time talking to the DB, and adding more items to the library seems to add 3 more sqlite3.Connection.execute()
calls per library item. So, the first goal is to do more intelligent batching of database calls. If I can make ls faster, then at best it will make a lot of other things faster as well, and at worst I’ll be able to start attacking other problems.
Other performance issues to look into: https://github.com/beetbox/beets/issues/2370 https://github.com/beetbox/beets/issues/2281 https://github.com/beetbox/beets/issues/1795
Setup
- OS: Debian Stretch
- Python version: 3.5.3
- beets version: 1.4.3 (git master, commit 272aa8870386b171c132206829509b410cd11d18)
About this issue
- Original URL
- State: open
- Created 7 years ago
- Reactions: 11
- Comments: 20 (11 by maintainers)
I do not code and so I am not the best person to enter in this discussion, but based on my experience in compiling and testing stuff I guess the main reason of the beets slowness is the fact it is fully written in python.
If beets itself was written using some compiled language (C, C++, ADA etc) and then make use of the existent python libraries (py-musicbrainzngs, py-pyacoustid etc) it certainly would be rather faster.
While comparing apples and oranges, Picard is moderately fast but slow when performing actions what depend on (more or less) the same python libraries Beets does depend.
I am using beets for a while and I really enjoy it, but I was shocked when it took more the 36 hours to import a Maria Callas box with about 1400 tracks. 😮
It take about 2 seconds just to display “beet -h” in here. 😃
Just my 2c.
I decided to look into this a bit today. Beets is not painfully slow for me, but still quite a bit slower than it should be.
@NoahTheDuke: I disagree that
N+1 SELECT
is a problem here. While it would have been a problem in a traditional server/client DBMS, it doesn’t really apply to sqlite. The latency is just much lower as sqlite does not do any network communication. In fact, the documentation even encourages theN+1
pattern [1].I imagine that the indices in the DB where created after this issue was opened, because now the sqlite calls do not take a lot of time according to the profiler.
Running
beet ls > /dev/null
on my library of 14190 songs takes about 15 seconds. Profiling it shows that most of the effort is spent formatting and printing the output. As an experiment, I applied the following patch:This brings the runtime down to 4 seconds. At this point, the output of
cProfile
looks like this:It seems like we spend 2 seconds translating db records to models (
_make_model
). That sounds like a lot, and might be possible to optimize as well.I haven’t yet looked into how much effort it would be to optimize these two functions (printing, and model creation), but it seems realistic to get
beet ls
down to under 5 seconds on a relatively large library without any major architectural changes.[1] https://sqlite.org/np1queryprob.html
The issue isn’t with Python, the issue is with beets falling prey to the classic N+1 SELECT problem:
The clever way to do such a series of queries would be to group all of the ids for each successive query and then join them together into a single
WHERE
statement and iterate over them in Python. This is what you meant by batching, right @icefoxen?So if the old way was,
selected_albums = "select * from db.albums where album_name = $foo"
and thenfor song_id in selected_albums: song = "select * from db.songs where song_id = $song_id"
and and so on, you’d callselected_albums = "select * from db.albums where album_name = $foo"
and thensong_ids = [song.id for song in selected_albums]; songs = "select * from db.songs where song_id in (" + ", ".join(song_ids) + ")"
which would grab only those song_ids you actually one, but get them all in a single call.I don’t know anything about how beets works under the hood, so I don’t know if that’s easily done with the Object model y’all are working with, but it’s a well-trod problem and can be done.
That’s a good idea, but porting some or most of the code to a compiled language would be a huge undertaking. Similar gains may be possible just by optimizing the python code.
Not really. Abstraction is good. But each database call has quite a lot of overhead compared to the amount of actual work done, and we do multiple database calls to fetch all the information required for each item. The cost of actually constructing the Item and Album objects is… well, not quite trivial, but very small in comparison. We need an easier way to handle Items and Albums in batches, so that we do the same work with fewer calls into the database.
Right now if you want to construct 1000
Item
objects based on a query, it does 1 database query to match the query, 1000 queries to get the attributes for each item, and 1000 queries to get the flexible attributes for each item. And then if you print them out, it does at least another 1000 queries in theFormatter
objects for some reason (not sure what it’s doing there yet, something about album info). Each of those database queries has a lot of overhead compared to the amount of work done; they’re basically selecting a single row. It is much much faster to be able to say “oh we’re going to be getting all of these objects at once anyway”, do two queries (one to get attributes for all the items, one to get flexible attributes; you could even combine them together into a single call with a join) and construct all yourItem
s from that info at once.Yep, that sounds right. Maybe a useful exercise would be to hand-write the queries we wish we were executing, and then design a usable Python API around that?