As part of my rails project, I have a feature that allows a user to issue invites to their FB friends. I'm using fb_graph for the API calls, and the below is a sampling of the code from the controller when the user hits the invite page.
This operation gets really expensive. I've seen it take longer than 30 seconds for users with upwards of 1000 friends. Also, this code re-executes each time a user hits the invite page. While a user's FB friends list isn't exactly static, it should be okay not recalculating this on every request.
So what I want to do is improve on this code and make it more efficient. I can think of a few different potential ways to potentially do this, but what makes the most sense in this case? This is a bit more open-ended than I usually like to ask on SO, but as I'm still relatively new to programming, I'm curious just as much on what you would/wouldn't do as much as how to do it.
Here are some ideas on optimizations I could make:
1) Only optimize within a session. The code executes the first time the page is hit and will persist for the rest of the session. I'm actually not sure how to do this.
2) Persist to the database. Add a column to the user table that will hold the friends hash. I could refresh this data periodically using a background job (perhaps once a week?)
3) Persist with caching. I'm not even exactly sure what's involved with this or if this is an appropriate use-case. My feeling is that option 2 requires a lot of manual maintenance, and that perhaps there's a nice caching solution for this that handles expirations, etc., but not sure
Other ideas? Appreciate your thoughts on the options.
# fetch full array of facebook friends
@fb_friends = current_user.facebook.fetch.friends
# strip out only id, name, and photo for each friend
@fb_friends.map! { |f| { identifier: f.identifier, name: f.name, picture: f.picture }}
# sort alphabetically by first name
@fb_friends.sort! { |a,b| a[:name].downcase <=> b[:name].downcase }
# split into two lists. those already on vs not on network
@fb_friends_on_network = Array.new
@fb_friends.each do |friend|
friend_find = Authorization.find_by_uid_and_provider(friend[:identifier], 'facebook')
if friend_find
@fb_friends_on_network << friend_find.user_id
@fb_friends.delete(friend)
end
end
UPDATE # 1
Adding a bit more on an initial experiment I conducted. I added a column to my user table that holds the @fb_friends array (post-processing the transformations shown above). Basically the controller code above is replaced with simply @fb_friends = current_user.fbfriends. I thought this would cut the load significantly as there is no more call to Facebook, not to mention all the processing done above. This did save some time but not as much as expected. My own friends list took about 6 secs to load on my local machine, after these changes its down to 4 secs. I must be missing something bigger here on the load issue.
UPDATE #2
Upon further investigation, I learned that almost half of data transfer was attributed to the form I was using for the "Invite" buttons. The form would load once for each friend and looked like this:
<%= form_for([@group, @invitation], :remote => true, :html => { :'data-type' => 'html', :class => 'fbinvite_form', :id => friend[:identifier]}) do |f| %>
<%= f.hidden_field :recipient_email, :value => "facebook@meetcody.com" %>
<div class = "fbinvite btn_list_right" id = "<%= friend[:identifier] %>">
<%= f.submit "Invite", :class => "btn btn-medium btn-primary", :name => "fb" %>
</div>
<% end %>
I decided to remove the form and inside place a simple button:
<div class = "fbinvite_form" id = "<%= friend[:identifier] %>" name = "fb">
<div class = "btn btn-small">
Invite
</div>
</div>
I then used ajax to detect a click and take the appropriate actions. This change literally cut the data transfer in half. Before loading about 500 friends took ~650kb, now its down to ~330kb.
I'm thinking I will go back and try what I tried in Update # 1, to do pre-processing. Combined I'm hoping I can get this down to a ~2 sec operation.
UPDATE #3
I ended up installing Miniprofiler to learn more what could be slowing this operation down and learned that my for loop above is terrible inefficient as it makes a trip to the DB on every friend. I posted in a separate question and got help to reduce the trips down to just one. I then went ahead and implemented the pre-processing I mentioned in Update #1. With all these changes, I have it down to ~700ms which is remarkable considering it was taking +20 secs plus before going down this path!