This is a guest post by Commit Analytics, a Philadelphia based startup who builds software for swim coaches and athletes.

Olympics Swimming Backstory

On June 26th the fastest swimmers in the United States woke up in Omaha, Nebraska, the meet site of the US Olympic Swim Trials. Why is it in Omaha, you ask? No clue. They awoke and hopped onto swimming “blocks” equipped with the same accurate timepieces that took us to the moon and raced each other, taking out 20+ years of grueling training for a two minute display of raw speed.

In order to advance to the Olympics, you need to be in the top two in an individual event (breaststroke, butterfly, backstroke, IM) or in the top six in freestyle (to make it onto the freestyle relay).

To put this into perspective, let’s say you wanted to make the Olympics in the 200-meter Medley. This means you need to get one of the top two times at this meet in Omaha. At the Olympic Trials, Michael Phelps got first, and Ryan Lochte got second. Of the top ten times ever recorded (by any person) in that event, Michael Phelps owns five, and Ryan Lochte owns the other five. This almost goes without saying, but it is really hard to get to the Olympics.

Building a real-time results app

We had six weeks to build an app that captured the excitement of this event. It needed to report results quickly, do cool things with these results, and show news articles of interest, on top of being fast, reliable, web and mobile compatible, and generally not sucky.

We expected it to see a lot of load — NBC SN was airing the meet, and we had connections with SwimSwam, a leading Swimming News Site, to link to our app. To add to that, we needed to parse results from pdfs, and throw them into our database in near real-time, all while using Meteor’s magic to publish them down to all connected clients. Over the span of the eight day meet, we saw 1.2 million page views and 120,000 unique users.

For the rest of this article, I’m going to shy away from generic advice (“Use oplog tailing! Only publish relevant fields to the client! Set up database Indexes! Scale to more machines!”), and focus on some “boots-on-the-ground” things that made a big difference for us.

The basics

We used MeteorJS because of its awesome publish/subscribe data layer. We used React because of its performance and the ability to easily reuse widgets across web and mobile. We used Material-ui because it is fast, easy to use, and sleek. We also hosted the app on Galaxy because we wanted to be sure it wouldn’t go down. ‘Nuff said.

Parts of our app

There are really 2 parts to our app:

1. Building out the client so that it is fast, sleek, and packed full of useful features.

2. Getting the data correctly out of pdfs, into our database, and down to all connected clients.

Fast, sleek client

If you can run a marathon, then you can run a 5k. And if your app is snappy on a phone, then you’ll be good on a beefy desktop browser. So we made sure to test everything on a crappy phone during development.

Using PureRenderMixin

I know. Mixins are dead. And whether or not you use PureRenderMixin or Dan Abramov’s “shallow compare” suggestion referenced here, unnecessary rerenders will kill your performance.

We started at the lowest nodes in our react tree and worked our way up, making as many components as we could pure. How? We focused on passing props that are easily “shallow-comparable”, like strings, numbers, booleans (or arrays of strings, numbers, booleans). If you pass in functions as props (with ES6 classes), make sure to bind them beforehand. By passing in objects that are not “shallow-comparable”, or by not binding functions before passing them down, you will force a lot of rerenders, even with PureRenderMixin on the component.

Tip: We also put console.log(‘COMPONENT NAME’) in the render function of all our components. This is the ultimate truth test that checks whether the PureRenderMixin was being respected, and that our component wasn’t being rerendered. Old school, but effective.

Split out subscriptions

In many apps, a top level component will subscribe to a bunch of data and display a loading screen while it waits for all of the subscriptions to finish. In order to speed up our app, we render the screen as soon as the minimal number of subscriptions finishes.

Let’s take for example the LeftNav component in trials2016.commitswimming.com (image below). In order to render this widget, you would need a list of swimming “sessions” (things labeled “Sunday Morning”, and “Sunday Evening”), and a list of “events” (things labeled “Men 400 IM”, and “Women 100 Fly”). Now, you can only see the events once you have clicked on a single session (they expand underneath). So why wait for them? We subscribe to the sessions and events separately, and render the widget as soon as sessions subscription finishes.

Downside: More round trips to your app server. For all of our apps, we’ve found that the increase in client-side snappiness and perceived loading speed to the user outweighs the heavier load on our Galaxy machines.

Expensive DOM operations

Whenever you hit one of the result pages, we need a ton of data in order to give the user an awesome experience on that page. We need two versions (what are called ‘start lists’ and ‘results’ in swimming) of around 150 result documents for the ‘preliminaries’, sixteen for the ‘semi-finals’, and eight for the ‘finals’.

We need all the athlete documents for each of these results. Each result document is pretty complicated, with an array of splits for that swimmer along with much more information. This adds up to a decent chunk of data being shipped over the wire.

Only ship the initially rendered data down and then when the user interacts with the page (selects ‘Final’ results instead of the ‘Heats’ results) ship down the necessary data. Ship all the data at once. When the user interacts with the page we won’t need to make any more trips to the DB.

Choice 1 means every time the user clicked on the select box to choose to view a different set of data, there was a decent lag on even the beefy machine I keep talking about. On an average mobile browser, the app felt unresponsive. So we went with choice 2. Of course, we used the subscription splitting method noted above to simulate the same initial load time that choice 1 would bring. But, we ran into a different problem…

Rendering too much data

Even though we had all the data, when the user interacted with the page using toggles, select boxes, and moving from tab to tab, we noticed considerable ‘jankiness’. After some digging, we realized this was simply an expensive DOM operation. In some cases we were asking React to toss 150 expandable list items with touch ripples and many nested divs into the DOM. This obviously was very slowwwww. Nothing would happen for 3–4 seconds after clicking the select box on our beefy machines :-). Imagine if we were on that Android Kitkat device! Talk about unusable.

The Solution

React-infinite! You’ve probably heard of SeatGeek. Fortunately, they built a pretty cool React component that helps combat this very problem. Basically it allows you to only render the divs that would be visible on the screen, which drastically sped up our “list widget” in the app. And we were happy to use some tech built by a fellow DreamIt alum.

The moral of the story is: be careful drawing a lot of nested divs at once. It’s expensive. Especially on mobile. And if it is not initially viewable on the screen figure out a way to not render it. React-infinite was perfect for this use case.

PDF Data Integrity

For any of you who have dealt with the horror that is scraping pdfs, I give you my respect and my blessing. Our pdfs looked like this:

We used the npm module pdf2json which output a bunch of “text objects”, including the text and the x,y position of that text on each page, among other things. In order to read a table of results, we first had to find out what “header” each bit of text was under, making sure to deal with the litany of edge cases that arise (What happens when the text is closer to the wrong header as in the case with “Rank”, or “Lane”? How do I ignore text that is clearly not in the table (like footer text)? How does one get the split times out of the text? What happens when names are too long?)

We made the decision to put a lot of the complexity into this data ingestion period so that by the time it was in the database, we were confident that it was right and correct. We used simple-schema, and collection2 to warn us during ingestion if anything went wrong. These tools were invaluable to quickly find and fix data integrity issues that arose.

This also had the benefit that our client code could make stronger assumptions about the integrity of the data it was receiving. There wasn’t that much “defensive coding” on the client, which again, helped performance. Moreover, because of Meteor’s awesome pub/sub, if there was an issue, it took all of five seconds to go to the database, change it, and watch it fly into the app.

Dangerous tip: While one can’t yet meteor shell into your live production server, the MONGO_URL flag can be abused to get you similar behavior. By running a local version of your app, setting the MONGO_URL to the production database, one can meteor shell into your local server. Any and all code you execute will hit your real database. If there was a data issue that couldn’t be fixed manually (maybe you wanted to make a small change to 100 records), you could write a one-off function to loop through and fix them. As with all things with production databases, test locally first, and then pray.

After building out and testing the client and server pieces, we had to figure out how to launch and size our app effectively on Galaxy.

Galaxy Deployment Architecture

We chose to have two projects deployed on Galaxy, one that handled web load, and one that handled mobile load (from our Android and iOS) apps. We did this because we wanted separate analytics for each app, and because if there was an issue with either the web or mobile app, we would be able to push up a new version quickly without interrupting the experience of the users on other platforms. They both pointed at the same database on Compose.

How does one choose the number of Galaxy machines? Basically you need to know two things:

How many connected clients expected to be on the app

How many clients a single container can support

After making some educated guesses, we predicted that we would see roughly 2,000 connected clients at any given moment throughout the entire eight day meet. We were also not immune to the idea that peak events (Michael Phelps swimming) would drive up the total number of users big time.

As for the number of connected clients per container, we had data from a similar, previous app that we built, which could handle 400 connected clients. To play it super safe, we assumed 200 connected clients per container for this app, which implies ten containers. Then we doubled that to be safe, and after the first day started slowly backing off the machines, checking both the memory and CPU usage of each container. One feature that we used a lot (as we were backing off the total container number) was Galaxy’s ability to load the app from a specific container. We used this to test all the remaining containers for a good user experience as we slowly dropped the total number of machines that we had up.

Who are we

We are a couple of friends from Philadelphia, who met in middle school jazz band. We now run Commit Analytics, which has created an easy way to write and analyze your swim workouts.

This app was a fun way to engage our users and make a larger impact in the swimming market. It was just a one-off app for the US Olympic swim trials though, and we have no plans to make an app for the actual Olympic Games in Rio. You’ll just have to watch it later this summer on TV.