Advanced caching in Rails
broadcastingadam.com
broadcastingadam.com
My Rails sites spend 85% of the time generating HTML, the other 15% of the time is spent communicating with the db and other external services. And it's not easy to figure out where the slowness is. When I've ran performance profiles in the past, something like 50% of the time was spent in GC.
One thing worth mentioning: anecdotally I've found that ActiveRecord can have an impact on performance far beyond the cost of the database queries, both by directly adding a reasonable amount of overhead and by instantiating a lot of objects internally which in turn triggers a lot of garbage collection. I wouldn't be surprised if that accounts for a fair chunk of what you're seeing.
I've found that database views and functions are one of the easiest ways to improve performance in Rails.
FOR EXAMPLE
To generate user's information shown above each comment for https://img.skitch.com/20120220-pununfygpsaw1cw5gmjin8e95i.p..., I have to get the user's username, the total amount of "points" the user has, the user's profile image, if the user is an admin (admins are formatted differently), etc. This information is stored across around 5 or 6 tables. Enter this view (which calls a few db functions):
CREATE VIEW user_profile_info
AS
SELECT users.id AS user_id,
users.slug AS user_slug,
users.username,
user_stats.total_points,
user_profile_image(users.id) AS profile_image,
is_user_admin(users.id) AS admin
FROM (users
JOIN user_stats
ON (( user_stats.user_id = users.id )));
Now, I can have a simple UserProfileInfo ActiveRecord class that wraps this user_profile_info database view.Then I can do:
@object.comments.includes(:user_profile_info)
and, very efficiently, I get a list of comments and all the user's information.If I didn't use this approach, I would have to have a complex caching scheme to avoid the multiple sql queries. The goal is to minimize the amount of data that comes over the wire via sql queries (which also reduces the amount of work ActiveRecord has to do to construct these objects in memory).
BTW, I'm starting to write a book about using postgresql effectively with web applications. I've found that there are tons of web developers (especially in Rails) that don't use the full-power of postgresql correctly which leads to slow and buggy code.
That is, incorporating `updated_at` into the cache key, and using `touch: true` on your AR relations to make sure that caches of affected parent objects get expired too?
Are there other complications I may not have run into yet?
However, ActiveRecord doesn't support touch on has_many relations. (It probably shouldn't, as updating the username would mean updating/touching thousands (or more) comment rows).
Also, if you have to update the database outside of ActiveRecord for any reason, you could be screwed - the cache would become out of sync with the database.
I believe this is the first time it has made it to HN. I've always gotten very good feedback on it. Thanks for your comments.