During the pre-construction phase of our Move & Copy functionality, we noticed that the Project page (where this feature can be accessed) could have benefited from performance improvements.
At the core of the Wistia application is the
Media object. Every video, image, or audio file that anyone uploads is represented as a media instance. A
Project is an organizational collection of
MediaGroup (a.k.a. "section") objects, each of which is an organizational collection of media.
In this context, the term organizational means that the order in which sections appear within a project and the order in which media appear in a section is persisted and saved to the database. When a video is uploaded, it is automatically slotted into the top of its section. Users can drag and drop media and order them however they want.
We use acts_as_list to keep track of the order in which media appear in sections, and thus within a project. It sets a
position attribute on each object with
n for the object in the nth position where
n >= 1. It also has few special helpers, like
remove_from_list. This gem works well for maintaining lists of objects at small scale.
But at Wistia's scale, where some projects have tens of thousands of media, we needed a better way to maintain ordering of media.
When a new video is uploaded, it is inserted into the top of its section by calling
video.insert_at(1). Behind the scenes, this method increments the position of all objects that have position greater than or equal to 1, and updates
video's position to 1.
In the common scenario where you have a big list of videos, each new video uploaded to that list will force an update for the entire list of videos. This results in a potentially frustrating experience when uploading to a large project.
Solutions we considered
- Get rid of the
positionattribute and maintain a list of media IDs (in the correct order) on the list itself. For example, each
MediaGroupobject records the order in which its media appear in with a column called
ordered_media_ids. The downside of this method is that we would have had to run a huge migration for existing data, and make a lot of changes since media have to be sorted in memory.
- Spread the position of media objects across the entire integer spectrum using ranked_model. Now,
insert_at(n), inspects the position of objects at cardinal positions
n+1. If there is sufficient space between the two, use their midpoint. If not, rebalance the position of all objects in the list and then use their new midpoint. The downside of this approach is that after 46 inserts (integer spectrum is from -8388607 to 8388607, the MEDIUMINT range in MySQL), you'd have to rebalance the entire list completely. Note that the concept of "position" is now split between raw position (value in the database) versus cardinal position (the 1-based index) of the object.
What we implemented
We noticed that simply by using the negative integer space, acts_as_list can be improved significantly in our 80% use case, where videos are uploaded into the first position. By default, acts_as_list is great for appending to the end of the list.
Wistia's primary use case, however, is prepending to the beginning of the list. Using a special prepend strategy,
insert_at(1) finds the position of the object currently at the beginning of the list, subtracts 1 from it, and sets the result as the position of the object newly prepended to the list. For instance, newly uploaded videos have position values of 0, -1, -2, -3, etc. and existing videos in the list don't need to be updated at all. The user's uploading experience is the same regardless of how large their project is.
What turned out to be the small code change also had a big impact. Data from New Relic shows that the weekly average response time for the saving a new upload decreased from 925ms to 697ms following the change.