The JankStats library helps you track and analyze performance problems in your applications. Jank refers to application frames that take too long to render, and the JankStats library provides reports on the jank statistics of your app.
Capabilities
JankStats builds on top of the existing Android platform capabilities, including the FrameMetrics API on Android 7 (API level 24) and higher, which tracks how long frames take to complete. The JanksStats library offers two additional capabilities that make it more dynamic and easier to use: jank heuristics and UI state.
Jank heuristics
While you can use FrameMetrics to track frame durations, FrameMetrics doesn't offer any assistance in determining actual jank. JankStats, however, has configurable, internal mechanisms to determine when jank occurs, making the reports more immediately useful.
UI state
It's often necessary to know the context of performance problems in your app. For example, if you develop a complex, multi-screen app that uses FrameMetrics and you discover that your app often has extremely janky frames, you’ll want to contextualize that information by knowing where the problem occured, what the user was doing, and how to replicate it.
JankStats solves this problem by introducing a state
API that lets you
communicate with the library to provide information about app Activity. When
JankStats logs information about a janky frame, it includes the current state of
the application in jank reports.
Usage
To begin using JankStats, instantiate and enable the library for each Window
.
Each JankStats object tracks data only within a Window
. Instantiating the
library requires a Window
instance along with an Executor and an
OnFrameListener
listener, both of which are used to send metrics to the client
on the Executor thread.
The listener is called with FrameData
on every frame and details the:
- Frame start time
- Duration values
- Whether or not the frame should be considered jank
- A set of String pairs containing information about the application state during the frame
To make JankStats more useful, applications should populate the library with
relevant UI state information for reporting in the FrameData. You can do this
through the PerformanceMetricsState
API (not JankStats directly), where all
of the state management logic and APIs live.
Initialization
To begin using the JankStats library, first add the JankStats dependency to your Gradle file:
implementation "androidx.metrics:metrics-performance:1.0.0-alpha01"
Next, initialize and enable JankStats for each Window
. You should also pause
JankStats tracking when an Activity goes into the background. Create and enable
the JankStats object in your Activity overrides:
class JankLoggingActivity : AppCompatActivity() {
private lateinit var jankStats: JankStats
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// ...
// metrics state holder can be retrieved regardless of JankStats initialization
val metricsStateHolder = PerformanceMetricsState.getForHierarchy(binding.root)
// initialize JankStats for current window
jankStats = JankStats.createAndTrack(
window,
Dispatchers.Default.asExecutor(),
jankFrameListener,
)
// add activity name as state
metricsStateHolder.state?.addState("Activity", javaClass.simpleName)
// ...
}
The example above injects state information about the current Activity after it constructs the JankStats object. All future FrameData reports created for this JankStats object now also includes Activity information.
The JankStats.createAndTrack
method takes a reference to a Window
object, which is a proxy for the View hierarchy inside that Window
as well as
for the Window
itself. jankFrameListener
is called on the thread determined
by Executor
on every frame while the JankStats object is enabled.
To enable tracking and reporting on any JankStats object,
call isTrackingEnabled = true
. Although it's enabled by default,
pausing an activity disables tracking. In this case, make sure to re-enable
tracking before proceeding. To stop tracking, call isTrackingEnabled = false
.
override fun onResume() {
super.onResume()
jankStats.isTrackingEnabled = true
}
override fun onPause() {
super.onPause()
jankStats.isTrackingEnabled = false
}
Reporting
The JankStats library reports all of your data tracking, for every frame, to the
OnFrameListener
for enabled JankStats objects. Apps can store and aggregate this
data for uploading at a later time. For more information, take
a look at the examples provided in the Aggregation section.
To create the jankFrameListener
object for initializing JankStats with an
OnFrameListener, do the following:
private val jankFrameListener = JankStats.OnFrameListener { frameData ->
// A real app could do something more interesting, like writing the info to local storage and later on report it.
Log.v("JankStatsSample", frameData.toString())
}
You'll need to create and supply an OnFrameListener for your app to receive the per-frame reports;
fun interface OnFrameListener {
fun onFrame(frameData: FrameData)
}
The FrameData structures used in the listener are as follows:
class FrameData(
/**
* The time at which this frame began (in nanoseconds)
*/
val frameStartNanos: Long,
/**
* The duration of this frame (in nanoseconds)
*/
val frameDurationNanos: Long,
/**
* Whether this frame was determined to be janky, meaning that its
* duration exceeds the duration determined by the system to indicate jank (@see
* [JankStats.jankHeuristicMultiplier])
*/
val isJank: Boolean,
/**
* The UI/app state during this frame. This is the information set by the app, or by
* other library code, that can be used later, during analysis, to determine what
* UI state was current when jank occurred.
*
* @see PerformanceMetricsState.addState
*/
val states: List<StateInfo>
)
Android 12 (API level 31) and higher exposes more data about frame durations in the FrameMetrics mechanism. JankStats provides more information on those versions, accordingly:
class FrameDataApi24 : FrameData {
/**
* The time spent in the non-GPU portions of this frame (in nanoseconds).
*
* This includes the time spent on the UI thread [frameDurationUiNanos] plus time
* spent on the RenderThread.
*/
val frameDurationCpuNanos: Long
}
class FrameDataApi31 : FrameDataApi24 {
/**
* The amount of time past the frame deadline that this frame took to complete.
*
* A positive value indicates some jank, a negative value indicates that the frame was
* complete within the given deadline
*/
val frameOverrunNanos: Long,
}
StateInfo
in the listener looks like this:
class StateInfo(
val stateName: String,
val state: String
)
Aggregating
You'll likely want your app code to aggregate the per-frame data, which allows you to
save and upload the information at your own discretion. Although details around
saving and uploading are beyond the scope of the alpha JankStats API release, you
can view a preliminary Activity for aggregating per-frame data into a larger
collection using JankAggregatorActivity
available in our GitHub repository.
JankAggregatorActivity
uses the JankStatsAggregator
class to layer its own reporting
mechanism on top of the JankStats OnFrameListener
mechanism to provide a higher
level abstraction for reporting only a collection of information that spans many
frames.
Instead of creating a JankStats object directly, JankAggregatorActivity
creates
a JankStatsAggregator
object, which creates its own JankStats object internally:
class JankAggregatorActivity : AppCompatActivity() {
private lateinit var jankStatsAggregator: JankStatsAggregator
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// ...
// Metrics state holder can be retrieved regardless of JankStats initialization.
val metricsStateHolder = PerformanceMetricsState.getForHierarchy(binding.root)
// Initialize JankStats with an aggregator for the current window.
jankStatsAggregator = JankStatsAggregator(
window,
Dispatchers.Default.asExecutor(),
jankReportListener
)
// Add the Activity name as state.
metricsStateHolder.state?.addState("Activity", javaClass.simpleName)
}
A similar mechanism is used in JankAggregatorActivity
to pause and
resume tracking, with the addition of the pause()
event as a signal to issue
a report with a call to issueJankReport()
, as lifecycle changes seem like an
appropriate time to capture the state of jank in the application:
override fun onResume() {
super.onResume()
jankStatsAggregator.jankStats.isTrackingEnabled = true
}
override fun onPause() {
super.onPause()
// Before disabling tracking, issue the report with (optionally) specified reason.
jankStatsAggregator.issueJankReport("Activity paused")
jankStatsAggregator.jankStats.isTrackingEnabled = false
}
The example code above is all that an app needs to enable JankStats and receive frame data.
Manage the state
It's possible that you may want to call other APIs to customize JankStats, for instance injecting app state information to make frame data more helpful.
This static method retrieves the current [PerformanceMetricsState
] object for a
given View hierarchy.
PerformanceMetricsState.getForHierarchy(view: View): MetricsStateHolder
Any view in an active hierarchy may be used. Internally, this checks to see
whether there's an existing MetricsStateHolder
object associated with that
view hierarchy. This information is cached in a view at the top of that
hierarchy. If no such object exists, getForHierarchy()
creates one.
The static getForHierarchy()
method allows you to avoid having to cache the instance
somewhere for later retrieval, and makes it easier to retrieve an existing state
object from anywhere in the code (or even library code, which would not
otherwise have access to the original instance).
Note that the return value is a holder object, not the state object itself. The value of the state object inside of the holder is set only by JankStats. That is, if an application creates a JankStats object, then the state object is created and set. Otherwise, without JankStats tracking the information, there is no need for the state object, and it's not necessary for app or library code to inject state.
This approach makes it possible to retrieve a holder that JankStats can then populate. External code can ask for the holder at any time. Callers can then cache this lightweight object and use it at any time to set state, depending on the value of its internal state property, as in the example code below, where state is only set when the holder’s internal state property is non-null:
val metricsStateHolder = PerformanceMetricsState.getForHierarchy(binding.root)
// ...
metricsStateHolder.state?.addState("Activity", javaClass.simpleName)
To control the UI/app state, an app can inject (or remove) a state
with the addState
and removeState
methods. JankStats logs the timestamp for
any adds or removals. If a frame overlaps the start and end time of the state,
JankStats reports that information along with the timing data for the frame.
For any state, add two pieces of information: stateName
(a category of state, such as “RecyclerView”) and state
(information about
what was happening at the time, such as “scrolling”).
Remove states using the removeState()
method when they are no
longer valid, to ensure that wrong or misleading information is not reported
with frame data.
The addSingleFrameState()
version of the state API adds a state which is
logged only once, on the next reported frame. The system automatically
removes it after that, ensuring you don't accidentally have obsolete state in
your code. Note that there is no singleFrame equivalent of
removeState()
, since JankStats removes these states automatically on the
next frame.
private val scrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
// check if JankStats is initialized and skip adding state if not
val metricsState = metricsStateHolder?.state ?: return
when (newState) {
RecyclerView.SCROLL_STATE_DRAGGING -> {
metricsState.addState("RecyclerView", "Dragging")
}
RecyclerView.SCROLL_STATE_SETTLING -> {
metricsState.addState("RecyclerView", "Settling")
}
else -> {
metricsState.removeState("RecyclerView")
}
}
}
}
Jank heuristics
To adjust the internal algorithm for determining what is considered jank, use
the jankHeuristicMultiplier
property.
By default, the system defines jank as a frame taking twice as long to render as the current refresh rate. It doesn't treat jank as anything over the refresh rate because the information around app rendering time isn't entirely clear. Therefore, it’s considered better to add a buffer and only report problems when they cause noticable performance issues.
Both of these values can be changed through these methods to suit the situation of the app more closely, or in testing to force jank to occur or not occur, as necessary for the test.
Usage in Jetpack Compose
Currently there is very little setup required to use JankStats in Compose.
To hold on to the PerformanceMetricsState
across configuration changes,
remember it like so:
/**
* Retrieve MetricsStateHolder from compose and remember until the current view changes.
*/
@Composable
fun rememberMetricsStateHolder(): PerformanceMetricsState.MetricsStateHolder {
val view = LocalView.current
return remember(view) { PerformanceMetricsState.getForHierarchy(view) }
}
And to use JankStats, add the current state to the stateHolder
as shown here:
val metricsStateHolder = rememberMetricsStateHolder()
// Reporting scrolling state from compose should be done from side effect to prevent recomposition.
LaunchedEffect(metricsStateHolder, listState) {
snapshotFlow { listState.isScrollInProgress }.collect { isScrolling ->
if (isScrolling) {
metricsStateHolder.state?.addState("LazyList", "Scrolling")
} else {
metricsStateHolder.state?.removeState("LazyList")
}
}
}
For full details on using JankStats in your Jetpack Compose application, check our performance sample app.
Provide feedback
Share your feedback and ideas with us through these resources:
- Issue tracker
- Report issues so we can fix bugs.