Performance/Fenix/Best Practices

< Performance‎ | Fenix

This guide will help Fenix developers working on front-end code (i.e. Kotlin/Java) produce code which is as performant as possible – not just on its own, but in terms of its impact on other parts of Firefox. Always keep in mind the side effects your changes may have, from blocking other tasks, to interfering with other user interface elements.

Contents

Avoid blocking on the main thread

TODO

Think carefully before optimizations using background threads

Concurrency is complex so be careful before introducing it! Even choosing a threading strategy that does not have side effects on the system can be challenging. TODO

Dispatchers.Default is not a good default

When a task needs to be done on a background thread but it's not an IO operation, it frequently gets added to the default coroutine dispatcher, Dispatchers.Default. However, this is an anti-pattern: if the default dispatcher's task queue is full, newly added tasks must wait for earlier tasks to finish before executing. This is a problem if your task needs to return a result (to the user/UI) quickly and the running tasks are slow: for example, if the user clicked a button and you want to show them content in response, they may end up waiting on unrelated tasks. Note: this problem is exacerbated on 2-core devices where only two tasks can execute simultaneously.

In general, Dispatchers.Default should only be used for computationally intensive tasks where tasks are FIFO priority or equal priority (this doesn't come up much in Android). It'd be much less misleading if it were named Dispatchers.Computation.

As for what to do if the default dispatcher is not a good fit, there's no one-size-fits-all solution. To choose the right threading strategy, it's important to understand your options: learn what each of the Dispatchers are intended for, what mechanisms they use to manage that, and their implications. Here are some additional considerations:

  • Dispatchers.IO is often a much better default choice than the default dispatcher because it will usually re-use threads and generally execute tasks immediately. However, if your code produces many simultaneous tasks, the IO dispatcher may not be a good fit because it could create numerous threads and cause the dispatcher to hit its cap, causing new tasks to wait on earlier tasks
  • Avoid creating a dedicated thread or thread pool unless it's strictly necessary: each new thread costs the system resources

If you have questions about what the appropriate threading strategy is, please ask the perf team!

Use the profiler is to understand problems, not assert their absence

The profiler is useful for understanding what might cause a perf problem but it's imperfect for understanding if a perf problem exists or not. For example, if you've made a code change with the intention of improving performance, you may notice that the problem point is gone in your profile. Success, right? Maybe not: the code change may have moved the performance problem elsewhere and it's easy to overlook this in the profiler view. For example, perhaps you removed a long call to load SharedPreferences but the next call to SharedPreferences increases in duration to compensate and start up is just as slow.

To see if a code change creates a perf regression or improvement, you should ideally run a known benchmark – i.e. a duration measurement from a start point to a stop point – and see how performance changes before and after your code change. With benchmarks, there's less opportunity to overlook "the potential fix moved the perf problem elsewhere" like in the profiler. If you don't have a benchmark, you can create your own with timestamp logs, though it should be done carefully to ensure the measurements are consistent – writing good benchmarks is hard.

Don't guess, measure.

Coroutines, posted events, & delaying UI events

How problematic is this in practice? Worth writing this section?

Also the UI churn associated with it? (less impactful except time to fully drawn?)