Closed rosshamish closed 9 years ago
Gotta see how fast this bb can go
Oh boy, check this out. Big improvement with the threading. This benchmark assesses total time taken to process 5 schedule generation requests:
time: 3.754
(classtime)ross(master)*$ nosetests tests/test_schedule_generator.py --nologcapture --nocapture --pdb
INFO: Making schedules for courses ['002896', '001341'] 11:08:47 [classtime.scheduling.schedule_generator]
.INFO: Making schedules for courses ['001343', '004093', '004096', '006768', '009019'] 11:08:47 [classtime.scheduling.schedule_generator]
.INFO: Making schedules for courses ['010344', '105014', '105006', '105471', '006708', '010812'] 11:08:48 [classtime.scheduling.schedule_generator]
.INFO: Making schedules for courses ['006973', '006790', '006974', '098325', '001607', 04104'] 11:08:49 [classtime.scheduling.schedule_generator]
.INFO: Making schedules for courses ['001343', '004093', '004096', '006768', '009019'] 11:08:49 [classtime.scheduling.schedule_generator]
.
----------------------------------------------------------------------
Ran 5 tests in 3.754s
OK
time:: 1.279s
(classtime)ross(i36-multithread-schedule-generation)*$ nosetests tests/test_schedule_generator.py --nologcapture --nocapture --pdb
INFO: Making schedules for courses ['002896', '001341'] 11:10:44 [classtime.scheduling.schedule_generator]
.INFO: Making schedules for courses ['001343', '004093', '004096', '006768', '009019'] 11:10:44 [classtime.scheduling.schedule_generator]
.INFO: Making schedules for courses ['010344', '105014', '105006', '105471', '006708', '010812'] 11:10:44 [classtime.scheduling.schedule_generator]
.INFO: Making schedules for courses ['006973', '006790', '006974', '098325', '001607', '004104'] 11:10:44 [classtime.scheduling.schedule_generator]
.INFO: Making schedules for courses ['001343', '004093', '004096', '006768', '009019'] 11:10:45 [classtime.scheduling.schedule_generator]
.
----------------------------------------------------------------------
Ran 5 tests in 1.279s
OK
Threaded version distributes the schedule search among 10 worker threads. At each iteration, it breaks up the work into 10 sections, distributes it, then aggregates the results as they come back from each worker.
Damn, that's fast. Is there any potential downside of threading? Like is there a reason in the future why synch might be a good thing? (just being the devil's advocate)
Haha, turns out it was a false positive. There was a bug which caused only one thread to ever spawn, and since each thread was assigned about 1/5 of the workload, only about 1/5 of the search space was being processed :-1: (this made it very fast though :laughing: )
I fixed the bug, and got real multithreading going, but the multithreaded schedule generation is actually slower - upwards of 6 or 7 seconds using the same 5-request benchmark, compared to less than 4s for the synchronous version.
After googling around, the reason seems to be the Global Interpreter Lock that Python has. Aka: only one thread can execute at a time in python - they aren't actually executing concurrently. For IO bound tasks like waiting for user input or redrawing a GUI, this works. For CPU bound tasks like our schedule generation, this is bad: extra CPU time is required to manage threads, but available CPU time remains the same.
I'm going to look into multiprocessing instead.
And to answer your question: multithreading/multiprocessing is inherently more complex - race conditions, shared resources, etc. are all problems that don't exist in a synchronous environment. However, Heroku supports up to 255 processes/threads on a single dyno, and PostgreSQL (which we'll use as a database in production) supports concurrency.
So yes, there are potential issues but we'll keep the synchronous version around (in git history) and we can revert back if problems become unmanageable.
So, the multiprocessing approach seems to affect performance based on two factors: the working set size and the number of worker processes used. I did some good 'ol research to look for an optimal combination of the two.
working set size: the number of "best" schedules which are kept around at any one time. A larger working set means less chance of mistakenly discarding a potentially great schedule.
For each working set size, I varied the number of workers used, ran the benchmark (tests/test_schedule_generator.py
), and compared those times to the synchronous approach.
Synchronous approach: 3.8 seconds with a working set of 50
Synchronous approach: 12 seconds with a working set of 200
Interestingly, the second chart shows that it's possible to quadruple the working set size and still improve performance by about 25%. This requires at least 20 processes, and at least 32 for optimal performance. That's a lot, but we can potentially pull it off in production:
Interesting. Love the graphs. So it seems really worth it to multi-process then.
The size of the working set seems very important, especially since the raw set is so large sometime. There are many thousand (or million?) possible schedules for first year engineering? I forget the number you told me earlier.
I sat in the car from Cochrane and back today (and still am) so I was reading about heroku and dyno's. Turns out it is true that if you only have one web dyno it will shut off after inactivity. So I wonder if using only one dyno in production is realistic...
Lol you closed the issue in the time I was typing my message on my iPhone.
Haha, comments still work though, all good. I wrote "Closes #36" in one of those commits, so when I pushed them GitHub closed it for me.
I think the number was something like 3 million first year engineering schedules? I could find out again, but it's probably not important - it's somewhere up there. For smaller / more restrictive requests, the number is a lot smaller.
I was also reading about that, and found some services that will ping your site every X minutes to prevent it from idling/sleeping. That's kind of cheating I think - Heroku offers the free tier as a "development" tier, not a production tier. However, we could probably do that initially and see what happens from there.
The nice thing though is that we'll be able to scale very easily if need be - our dynos won't depend on each other at all and Heroku uses a load balancer to send requests equally between them.
We'll need to switch over to a concurrency-safe database though - Heroku offers PostgreSQL for a certain $fee per month I believe. SQLite is filesystem based, which won't work on more than one process/thread/dyno.
Right now is it SQLLite behind SQLAlchemy? it's SQLAlchemy as the ORM. I didn't know SQLLite was currently involved also
As far as I can tell, SQLAlchemy is a layer which abstracts the server's database implementation, similar to how jQuery abstracts the client's browser implementation.
Ya, it's SQLite right now (sqllite:////
). The database URI is defined in angular_flask/settings.py
. To migrate, we change the prefix to postgres://
, as well as other minor changes.
Big Boi.