I have been trying to understand SABnzbd in order to find ways to reduce the CPU consumption and if possible make it run smoother. I haven't done any Python stuff before, but I've done lots of programming in other languages. My setup:
4x core i5
SSD drives
Windows 10
60 Mbit broadband
SABnzbd from the git Py3 branch
yappi for profiling
What I found was that NzbQueue.get_article is using a lot of or almost all the CPU time in all high load cases. First I tried letting a single nzb run for 1 minute with no bandwidth limit. get_article was called 1.74M times and the CPU consumption was 15-20%.
I then added a check for if an article was found for any of the servers in the loop starting at
https://github.com/sabnzbd/sabnzbd/blob ... er.py#L424
If it wasn't I did a time.sleep(0.001) after the loop. With this change get_article was only called 31193 times and the download speed seemingly remained the same. The CPU consumption was reduced to ~3%.
I then loaded up about 350 nzbs totaling 400 GB, let it do it's indexing and stuff, and ran the same test. The cpu usage was now spread over more functions:
Code: Select all
name ncall tsub ttot tavg
downloader.py:407 Downloader.run 1 0.656250 61.68750 61.68750
nzbqueue.py:661 NzbQueue.get_article 32636 21.45312 56.34375 0.001726
config.py:80 OptionStr.__call__ 8656608 15.06250 22.68750 0.000003
nzbstuff.py:74 NzbObject.server_in_try_list 9010168 11.09375 11.09375 0.000001
config.py:84 OptionStr.get 8656616 7.625000 7.625000 0.000001
get_article is called about the same number of times with and without sleep. To avoid too much sleeping it could be changed to only sleep if no article is found several times in a row. It would still lead to a significant reduction in CPU usage with few NZBs.
The OptionStr calls create a lot of overhead with lots of nzbs so I added
propagation_delay = float(cfg.propagation_delay() * 60)
before https://github.com/sabnzbd/sabnzbd/blob ... ue.py#L665 and replaced it in the loop accordingly. Result after:
Code: Select all
name ncall tsub ttot tavg
downloader.py:407 Downloader.run 1 0.578125 54.48438 54.48438
nzbqueue.py:661 NzbQueue.get_article 81198 26.56250 52.85938 0.000651
nzbstuff.py:74 NzbObject.server_in_try_list 21624185 25.14062 25.14062 0.000001
config.py:80 OptionStr.__call__ 81217 0.109375 0.171875 0.000002
config.py:84 OptionStr.get 81222 0.062500 0.062500 0.000001
I am not sure where to go from here. As you can see, those two functions occupy the CPU almost all the time. Because only one thread can run at any time in Python unless it's doing I/O, very little CPU time is available for the other threads. This makes SABnzbd very sluggish if the nzb queue is big. It would be an advantage if it could be sped up further.
I have been thinking about trying to create an article list for each server which the loop would refill regularly. Idle threads could then take the first article in the list. When the queue needs refilling several could be fetched and tested against the try_list in one batch. My hope is that this would make it more efficient than getting one at a time. Unfortunately I don't know enough Python to actually implement it, but I am trying to learn.
Any thoughts about this?