Making Shadowfax Android App 40% faster

Making Shadowfax Android App 40% faster

Not Clapped
unlike icon
Ishaan Garg - Senior Android Developer Shadowfax

1. Setting Goals

Every millisecond counts when it comes to mobile app performance. The faster your app loads, the more likely users are to stick around.

The Shadowfax Rider App, with more than 100,000 DAUs, faced a challenge as the app had bloated to take about 3.5 seconds to start!

The goal was to trim this time down to less than 2 seconds.

2. Measuring App Start Time

According to Firebase, app start time is the duration from when the app is launched from the launcher until the first activity’s `onResume()` method is called. This duration is also reported in the logcat like so:

You can read more here. For us Firebase Startup time was the source of truth.

If you consider your app to be fully loaded at some point after the onResume is called (like after your map has been fully drawn), then you can report that point in time to the system & Firebase with Activity.reportFullyDrawn()

If you’re using Perfetto, then it also visualizes startup time, more on that later.

3. Drilling down

To break down parts of the app by their start time durations, we added @trace annotation from firebase perf library to app class onCreate() function, and onCreate() & onStart() of BaseActivity & MainActivity. Basically measuring everything on a top-level so we get to know the main culprits & drill-down from there.

Apart from Main & Base activities, the app class was taking 30% of the app startup time, so we did 2 things:

3.1 Lazy load libraries & content providers (-10% startup time)

Application class usually initialises a lot of libraries. We moved whichever libs we didn’t need immediately on app start to init in background with a coroutine. If you have any content providers you can also use the Startup Library to lazy load those.

3.2 Baseline Profiles (-7% startup time)

Google recommends setting up baseline profile to improve first app startup time. We noticed a 7% overall app start time improvement, your mileage may vary but definitely try it out.

So this was a good start but we needed to dig deeper.

4. Using Perfetto

We can measure time taken by every function by running a system trace from Android Studio, then launching the app & loading the trace into the Perfetto visualizer.

We can use Android Studio’s inbuilt Profiler, but Perfetto has better navigation & details.

Details on how to do system tracing is here, but for profiling app launch time, don’t run the app normally. Here’s what you need to do:

  1. First choose the release build variant for your app, instead of debug build, for accurate results
  2. Click the 3-dot menu near run button in Android Studio
  3. then choose “Profile App with low overhead” to launch the app
  4. Now once the app has completely loaded, stop the recording
  5. Lastly export the trace using the save icon on Profiler, then import it into Perfetto Web UI

Once you load the trace into Perfetto, don’t get scared after you encounter a gazillion colours throwing up on your screen. We don’t need to deal with all of that.

4.1 Find startup time metric in Perfetto

Ctrl/cmd+F for “startup” to see the visualisation of your app startup time. Click on that green bar then press ‘M’ on your keyboard. This will mark the start & end of that bar, this is what we’re concerned about for now, everything else is just noise.

4.2 Find the root cause(s)

Now search for your package name below the startup time bar & click to expand it. Remember to focus on just the marked area of the graph and see the main thread, it will show you the duration of each function on x-axis, nested bars on y-axis mean nested functions.

Inspect the horizontally longer bars first, as those took the most time. See which of these functions took more time than expected & list them down. Then once we have the top 4–5 culprits we can optimize them.

Remember that frames should get rendered within 16ms to achieve 60fps

Tip: Use WASD keys to move around, read more about perfetto UI here. The docs are a tad bit outdated but the core principles are same.

5. Going down the rabbit hole

There may be functions with long durations but nothing to show for it.

Look at this looong BaseActivity onResume() bar, the graph doesn’t tell us what’s taking so long.

Only at the end do we see some nested functions that account for like a fourth of its overall time.

So what do we do?

We need to add more tracing. It’s easy with the tracing library available for both Java & kotlin. Details are here but let’s look at a snippet:

Now will start showing up in Perfetto. So add tracing for all nested functions in the function you’re debugging, then re-record a trace & analyse it in Perfetto. Rinse & repeat for each lifecycle function.

6. Our Solutions

This whole exercise gave us very clear root causes & targets. Now it was time to fix each of these causes one by one. Based on the observations from Perfetto and SysTrace, multiple optimisations were executed:

6.1 Optimising Home Fragment Layout (-15% startup time)

Reduced up to 600ms by ensuring no nesting of views with Constraint Layout, and using viewstubs instead of hidden views. This approach prevents unnecessary measurement & inflation of views that are hidden by default & displayed only after user-interaction. Read more on view stubs.

The map View which took 200–300 ms was moved to init only after on Resume() was invoked so it loads after the core UI showing order count & status has been laid out.

6.2 Optimising MainActivity (-5% startup time)

Repeated trace tests showed that LinearLayout was performing better than ConstraintLayout for our MainActivity as it was just a container for Fragments with not more than 3–4 views. In fact switching to LinearLayout made the MainActivity start 2x faster, especially on warm starts.

6.3 Lazy loading MainActivity SDKs (-10% time)

Perfetto showed that a single 3rd party SDK being initialised in MainActivity was contributing to 70% of the onCreate() duration. We started lazy-loading it in the background as it was not required immediately on app start.

7. Actual performance in the wild

After releasing these solutions over several releases, we saw the 90th percentile of start-up time gradually go from 3.5 seconds to just under 2 seconds, a remarkable 42% reduction. We continue to look for more bottlenecks & work on speeding up the app to help make our partners more productive.

Finding bottlenecks with Perfetto was crucial to this project & gave us the confidence to fix them, as we knew how much of a perf gain we’d achieve from fixing each issue.

Relevant articles

Shadowfax stories, delivered fresh

plane arrow
Thank you! You have subscribed successfully!
Oops! Something went wrong while submitting the form.

Become our

right arrow black
Take on our
right arrow black
Cookie Consent

We use cookies on our website for analytics and marketing purposes which helps us to improve the site functionality and user experience. By continuing to use this site, you agree to our use of cookie policy.

Unable to find what you were looking for?

Get in Touch

Company Information

Shadowfax is India's Fastest Growing End-to-End Logistics provider that provides e-commerce, hyper-local, on-demand delivery solutions for businesses for seamless business operations and dynamic growth.

Office Address

1st floor, Appek Building, 93/A,4th B Cross Rd,
5th Block, Koramangala, Bengaluru, Karnataka - 560095