View The Space is Hiring

Tuesday, December 10, 2013

Optimize Your Rails Fragment Cache By Lazily Loading your Objects

I was looking into the performance of our my portfolio page the other day which was supposedly cached.  Here's what I noticed within the controller.


class MyPortfoliosController < ApplicationController
before_filter :authenticate_user!
def show
@portfolio_properties = my_portfolio_filter.filtered_properties
.includes(:city, :submarket, :photos, :videos, :spaces)
.page(page)
.per(10)
.decorate
@portfolio_spaces = my_portfolio_filter.filtered_spaces(@portfolio_properties.collect(&:id))
.includes(:videos, :property => :users)
end
private
end



And here was our view:

- content_for :title, "My Portfolio - VTS"
= cache [ "v2", 'my_portfolio', current_user.all_properties_cache_key, current_user.all_spaces_cache_key ] do
.my_portfolio#main
= render partial: 'search_filters'
= render partial: 'multishare_bar'
#search-results
= render partial: 'search_results'
#footer-links
view raw show.html.haml hosted with ❤ by GitHub


This being a page that show properties and spaces, we are attempting to cache the entire page based on some keys that get updated whenever any of the spaces or properties within the user's portfolio is updated.  But here's the problem, our queries are doing some eager loading from the database.  Even though we were fragment caching the entire page, each cached hit was still hitting the database, creating a bunch of active record objects and then never actually using them. Even on a cached hit, this is what I saw in the logs:


(15.0ms) SELECT "spaces"."property_id" FROM "spaces" INNER JOIN "properties" ON "properties"."id" = "spaces"."property_id" LEFT OUTER JOIN "user_properties" ON "user_properties"."property_id" = "properties"."id" LEFT OUTER JOIN "user_spaces" ON "user_spaces"."space_id" = "spaces"."id" WHERE ((user_spaces.user_id = 2023 and spaces.lease_type = 'sublease') or (user_properties.user_id = 2023 and spaces.lease_type = 'direct')) AND (spaces.status!='removed') ORDER BY "spaces"."status", "spaces"."id"
Property Load (9.3ms) SELECT distinct properties.* FROM "properties" LEFT OUTER JOIN "user_properties" ON "user_properties"."property_id" = "properties"."id" LEFT OUTER JOIN "spaces" ON "spaces"."property_id" = "properties"."id" AND spaces.status != 'removed' LEFT OUTER JOIN "user_spaces" ON "user_spaces"."space_id" = "spaces"."id" WHERE "properties"."id" IN (142, 143, 207, 208, 214, 304, 297, 305, 349, 351, 352, 353, 453, 454, 456, 457, 459, 461, 482, 483, 490, 508, 534, 536, 567, 596, 597, 598, 622, 621, 665, 670, 707, 710, 739, 740, 797, 801, 803, 350, 531, 659, 660, 661, 662, 667, 669, 666, 668, 858, 915, 987, 982, 983, 984, 993, 994, 995, 975, 968, 990, 989, 964, 966, 967, 997, 985, 977, 971, 1002, 1003, 1004) AND (user_properties.user_id = 2023 or user_spaces.user_id = 2023) ORDER BY "properties"."status", "properties"."id" LIMIT 10 OFFSET 0
Space Load (1.7ms) SELECT "spaces".* FROM "spaces" WHERE "spaces"."property_id" IN (142, 143, 207, 208, 214, 297, 304, 305, 349, 350) AND (spaces.status != 'removed') ORDER BY position
City Load (0.8ms) SELECT "cities".* FROM "cities" WHERE "cities"."id" IN (41598, 41811, 41613, 41620, 41712)
Submarket Load (0.3ms) SELECT "submarkets".* FROM "submarkets" WHERE "submarkets"."id" IN (59, 60)
PropertyPhoto Load (0.6ms) SELECT "photos".* FROM "photos" WHERE "photos"."type" IN ('PropertyPhoto') AND "photos"."imageable_type" = 'Property' AND "photos"."imageable_id" IN (142, 143, 207, 208, 214, 297, 304, 305, 349, 350) ORDER BY "photos"."position"
PropertyVideo Load (0.6ms) SELECT "property_videos".* FROM "property_videos" WHERE "property_videos"."property_id" IN (142, 143, 207, 208, 214, 297, 304, 305, 349, 350)
Video Load (0.4ms) SELECT "videos".* FROM "videos" WHERE "videos"."id" IN (4379, 2912, 2913, 2914, 2915, 2916, 4380, 4422, 4423, 4427, 4466, 4484, 2312, 2313, 2314, 2315, 2316, 4485)
view raw gistfile1.txt hosted with ❤ by GitHub


To fix this, we moved all of the query logic out of the controller show method and into a our query object where it is safely memoized



class MyPortfoliosController < ApplicationController
before_filter :authenticate_user!
helper_method :portfolio_spaces
helper_method :portfolio_properties
def show
end
private
def portfolio_properties
my_portfolio_filter.filtered_properties
end
def portfolio_spaces
my_portfolio_filter.filtered_spaces
end
class MyPortfolioFilter
def filtered_spaces
@filtered_spaces ||= filtered_spaces_query(filtered_properties.collect(&:id)).includes(:videos, :property => :users)
end
def filtered_properties
@filtered_properties ||= user.my_properties.where(id: filtered_spaces_query.pluck(:property_id).uniq)
.includes(:city, :submarket, :photos, :videos, :spaces)
.page(page)
.per(10)
.decorate
end


Now instead of accessing instance variables created within the controller show method, our view accesses helper methods lazily from within the cache fragment.


- if portfolio_properties.any?
%fieldset#portfolio-properties
= paginate portfolio_properties
= render partial: 'portfolio_properties', collection: portfolio_properties, as: :property
= paginate portfolio_properties


Our cached my portfolio page no longer needlessly loads dozens of active record objects.








No comments:

Post a Comment