6

So I recently migrated to navigation components (2.2.0-alpha01). As I was working on a high end device I didn't really noticed any slowdowns, but as soon as I finished, testers started reporting slugish app navigation.

In my navigation code I use calls like findNavController().navigate(CustomFragmentDirections.actionEtc()) or findNavController().popBackStack(fragmentId, false) I also use safeargs with navigation. In my navigation xml I have actions that heavily rely on popUpTo and app:launchSingleTop="true"

To investigate I made very basic profiler in my BaseFragment class:

 private var lastTimestamp = System.currentTimeMillis()

protected fun getEllapsedTime(): String {
    val currTime = System.currentTimeMillis()
    val elapsedTime = currTime - lastTimestamp
    lastTimestamp = currTime
    return "${elapsedTime}ms"
}

override fun onAttach(context: Context) {
    Timber.d("onAttach(${javaClass.simpleName})(${getEllapsedTime()})")
    super.onAttach(context)
}

override fun onCreate(savedInstanceState: Bundle?) {
    Timber.d("onCreate(${javaClass.simpleName})(${getEllapsedTime()})")
    super.onCreate(savedInstanceState)
    savedInstanceState?.let { restoreState(it) }
}

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
    Timber.d("onCreateView(${javaClass.simpleName})(${getEllapsedTime()})")
    return inflater.inflate(layoutRes, container, false)
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    Timber.d("onViewCreated(${javaClass.simpleName})(${getEllapsedTime()})")
    super.onViewCreated(view, savedInstanceState)
}

override fun onActivityCreated(savedInstanceState: Bundle?) {
    Timber.d("onActivityCreated(${javaClass.simpleName})(${getEllapsedTime()})")
    super.onActivityCreated(savedInstanceState)
}

override fun onStart() {
    Timber.d("onStart(${javaClass.simpleName})(${getEllapsedTime()})")
    super.onStart()
}

override fun onResume() {
    Timber.d("onResume(${javaClass.simpleName})(${getEllapsedTime()})")
    super.onResume()
    requireActivity().window?.decorView?.post {
        firstFrameRendered()
    }
}

private fun firstFrameRendered() {
    Timber.d("onFrameRendered(${javaClass.simpleName})(${getEllapsedTime()})")
}

override fun onPause() {
    Timber.d("onPause(${javaClass.simpleName})(${getEllapsedTime()})")
    super.onPause()
}

override fun onStop() {
    Timber.d("onStop(${javaClass.simpleName})(${getEllapsedTime()})")
    super.onStop()
}

override fun onDestroyView() {
    Timber.d("onDestroyView(${javaClass.simpleName})(${getEllapsedTime()})")
    super.onDestroyView()
}

override fun onDestroy() {
    Timber.d("onDestroy(${javaClass.simpleName})(${getEllapsedTime()})")
    super.onDestroy()
}

override fun onDetach() {
    Timber.d("onDetach(${javaClass.simpleName})(${getEllapsedTime()})")
    super.onDetach()
}

I tried profiling using android studio profiler, but didn't really noticed anything out of the ordinary. I also tried window.addOnFrameMetricsAvailableListener but it pretty much gave me same results as my profiler code. The main method of importance is onFrameRendered. Basic Idea is to let layout inflate and render and immediately after screen is rendered count how many milliseconds passed since onResume was called.

I tried different devices and timings were not very consistent, but after measuring same transitions many times I noticed some tendency, that all my layouts now take almost twice as long to load when compared to previous app navigation which was using simple supportFragmentManager transactions.

I tried isolating navigation from one fragment to the other and I would always get this poor performance.

At the moment I know it has something to do with the way navigation switches fragments, because if I mock NavController with my custom code that just directly uses FragmentManager I get the same good performance as the old code. Will update the question if I'll find the exact problem.

Meanwhile, does anyone have any ideas what might be wrong?

SMGhost
  • 3,867
  • 6
  • 38
  • 68
  • You can go look at the [`FragmentNavigator` source code](https://android.googlesource.com/platform/frameworks/support/+/androidx-master-dev/navigation/navigation-fragment/src/main/java/androidx/navigation/fragment/FragmentNavigator.java) - it just does a FragmentTransaction, just like you would. – ianhanniballake Sep 05 '19 at 20:08
  • 1
    Thanks, @ianhanniballake, I was able to better pinpoint the exact culprit, which was ft.setReorderingAllowed(true) in FragmentNavigator. Removing this line greatly increased performance. Not sure why though, as it's purpose seems to be to speed up fragment transactions, not to slow them down. – SMGhost Sep 06 '19 at 05:21
  • It interleaves the lifecycle callbacks (and makes it so that animations actually work if you're doing multiple transactions). If you're doing expensive things in your old Fragment's lifecycle methods, that would certainly slow things down more when using `setReorderingAllowed()` if you're only measuring the new Fragment's timing – ianhanniballake Sep 06 '19 at 05:36
  • 1
    @ianhanniballake, I'm only using onCreate to inject dependency and onCreateView to inflate layout. No other explicit lifecycle methods are being used. I also use viewModel which has like 6 observers on live data to know when to navigate, or to update text. Maybe that could cause some issues. Hopefully I'll find time to investigate further. – SMGhost Sep 06 '19 at 06:06
  • In My case, When I go to a one fragment and go from this fragment to another or came back from this fragment it takes time. while all other fragment do not take time. – Shubham Mogarkar May 19 '22 at 11:25

0 Answers0