Bibliothèque JankStats

La bibliothèque JankStats vous permet de suivre et d'analyser les problèmes de performances de vos applications. Le terme "à-coups" ("janks") fait référence aux frames de l'application qui mettent trop de temps à s'afficher, et la bibliothèque JankStats fournit des rapports sur les statistiques qui s'y rapportent.

Fonctionnalités

JankStats s'appuie sur les fonctionnalités existantes de la plate-forme Android, y compris les API FrameMetrics sur Android version 7 (niveau 24 d'API) ou ultérieure, ou OnPreDrawListener sur les versions antérieures. Ces mécanismes peuvent aider les applications à suivre la durée d'exécution des frames. La bibliothèque JanksStats offre deux fonctionnalités supplémentaires qui la rendent plus dynamique et plus facile à utiliser : l'heuristique des à-coups et l'état de l'interface utilisateur.

Heuristique des à-coups

Bien que FrameMetrics vous permette de suivre les durées de frames, il ne vous aide pas à déterminer les à-coups en soi. Toutefois, JankStats dispose de mécanismes internes configurables qui déterminent le moment où les à-coups se produisent. Les rapports sont ainsi exploitables de manière plus immédiate.

État de l'interface utilisateur

Il est souvent nécessaire de connaître le contexte des problèmes de performances de votre application. Par exemple, si vous développez une application multi-écran complexe utilisant FrameMetrics et que vous constatez que votre application présente souvent des frames très irréguliers, vous allez essayer de contextualiser ces informations en déterminant l'origine du problème, ce que faisait l'utilisateur et comment le reproduire.

JankStats résout ce problème en introduisant une API state qui vous permet de communiquer avec la bibliothèque pour fournir des informations sur l'activité dans l'application. Lorsque JankStats enregistre des informations sur un frame irrégulier, il inclut l'état actuel de l'application dans les rapports d'à-coups.

Utilisation

Pour commencer à utiliser JankStats, instanciez et activez la bibliothèque pour chaque Window. Chaque objet JankStats suit uniquement les données d'un élément Window. L'instanciation de la bibliothèque nécessite une instance Window ainsi qu'un écouteur OnFrameListener, qui sont tous deux utilisés pour envoyer des métriques au client. L'écouteur est appelé avec FrameData sur chaque frame et détaille les éléments suivants :

  • Début du frame
  • Valeurs de durée
  • Irrégularité ou non du frame
  • Paires de chaînes contenant des informations sur l'état de l'application pendant le frame

Pour augmenter l'intérêt de JankStats, les applications doivent remplir la bibliothèque avec les informations d'état pertinentes de l'interface utilisateur pour la création de rapports dans FrameData. Vous pouvez le faire via l'API PerformanceMetricsState (pas directement via JankStats), qui contient l'ensemble de la logique de gestion des états et les API.

Initialisation

Pour commencer à utiliser la bibliothèque JankStats, ajoutez d'abord la dépendance JankStats à votre fichier Gradle :

implementation "androidx.metrics:metrics-performance:1.0.0-beta01"

Ensuite, initialisez et activez JankStats pour chaque élément Window. Vous devriez également suspendre le suivi JankStats lorsqu'une activité passe en arrière-plan. Créez et activez l'objet JankStats dans vos remplacements d'activités :

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.getHolderForHierarchy(binding.root)

        // initialize JankStats for current window
        jankStats = JankStats.createAndTrack(window, jankFrameListener)

        // add activity name as state
        metricsStateHolder.state?.putState("Activity", javaClass.simpleName)
        // ...
    }

L'exemple précédent injecte des informations d'état sur l'activité actuelle après la construction de l'objet JankStats. Tous les futurs rapports FrameData créés pour cet objet JankStats incluent également des informations sur l'activité.

La méthode JankStats.createAndTrack utilise une référence à un objet Window, qui est un proxy pour la hiérarchie des vues dans cet élément Window, ainsi que pour l'élément Window elle-même. jankFrameListener est appelé sur le même thread que celui utilisé pour transmettre ces informations de la plate-forme à JankStats en interne.

Pour activer le suivi et la création de rapports sur n'importe quel objet JankStats, appelez isTrackingEnabled = true. Bien que l'option soit activée par défaut, l'interruption d'une activité désactive le suivi. Dans ce cas, veillez à le réactiver avant de poursuivre. Pour arrêter le suivi, appelez isTrackingEnabled = false.

override fun onResume() {
    super.onResume()
    jankStats.isTrackingEnabled = true
}

override fun onPause() {
    super.onPause()
    jankStats.isTrackingEnabled = false
}

Rapports

La bibliothèque JankStats signale l'ensemble de votre suivi de données, pour chaque frame, à OnFrameListener pour les objets JankStats activés. Les applications peuvent stocker et agréger ces données pour les importer ultérieurement. Pour en savoir plus, consultez les exemples fournis dans la section Agrégation.

Pour recevoir les rapports par frame, vous devez créer et fournir l'élément OnFrameListener. Cet écouteur est appelé sur chaque frame afin de fournir aux applications des données d'à-coups en cours.

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())
}

L'écouteur fournit des informations concernant les à-coups par frame avec l'objet FrameData. Il contient les informations suivantes sur le frame demandé :

  • isjank : indicateur booléen indiquant si un à-coup s'est produit dans le frame.
  • frameDurationUiNanos : durée du frame (en nanosecondes).
  • frameStartNanos : moment auquel le frame a débuté (en nanosecondes).
  • states : état de votre application pendant le frame.

Si vous utilisez Android version 12 (niveau 31 d'API) ou ultérieure, vous pouvez utiliser les commandes suivantes pour fournir plus de données sur les durées de frames :

Utilisez StateInfo dans l'écouteur pour stocker des informations sur l'état de l'application.

Notez que OnFrameListener est appelé sur le même thread utilisé en interne pour fournir les informations par frame à JankStats. Sur Android version 6 (niveau 23 d'API) ou antérieure, il s'agit du thread principal (interface utilisateur). Sur Android niveau 7 (niveau 24 d'API) ou ultérieure, il s'agit du thread créé pour et utilisé par FrameMetrics. Dans les deux cas, il est important de gérer le rappel et de rendre rapidement pour éviter les problèmes de performances sur ce thread.

Notez également que l'objet FrameData envoyé dans le rappel est réutilisé sur chaque frame pour éviter d'allouer de nouveaux objets pour les rapports de données. Cela signifie que vous devez copier et mettre en cache ces données ailleurs, car cet objet doit être considéré comme périmé et obsolète dès que le rappel est de retour.

Agrégation

Vous souhaiterez probablement que le code de votre application regroupe les données par frame, ce qui vous permet d'enregistrer et d'importer les informations comme vous l'entendez. Bien que les détails de l'enregistrement et de l'importation dépassent le champ d'application de la version alpha de l'API JankStats, vous pouvez afficher une activité préliminaire pour rassembler les données par frame dans un ensemble plus vaste à l'aide d'une JankAggregatorActivity disponible dans notre dépôt GitHub.

Un élément JankAggregatorActivity utilise la classe JankStatsAggregator pour superposer son propre mécanisme de rapport au mécanisme OnFrameListener de JankStats afin de fournir une abstraction de niveau supérieur pour rapporter uniquement un ensemble d'informations qui recouvrent de nombreux frames.

Au lieu de créer directement un objet JankStats, JankAggregatorActivity crée un objet JankStatsAggregator, qui crée son propre objet JankStats en interne :

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.getHolderForHierarchy(binding.root)

        // Initialize JankStats with an aggregator for the current window.
        jankStatsAggregator = JankStatsAggregator(window, jankReportListener)

        // Add the Activity name as state.
        metricsStateHolder.state?.putState("Activity", javaClass.simpleName)
    }

Un mécanisme similaire est utilisé par JankAggregatorActivity pour suspendre et reprendre le suivi, avec l'ajout de l'événement pause() comme signal pour émettre un rapport avec un appel à issueJankReport(), tandis que les modifications de cycle de vie semblent être un bon moment pour capturer l'état d'à-coups dans l'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
}

L'exemple de code ci-dessus est tout ce dont une application a besoin pour activer JankStats et recevoir des données de frame.

Gérer l'état

Vous pouvez appeler d'autres API pour personnaliser JankStats. Par exemple, injecter des informations sur l'état de l'application améliore l'utilité des données de frame en fournissant du contexte pour les frames dans lesquels les à-coups se produisent.

Cette méthode statique récupère l'objet MetricsStateHolder actuel pour une certaine hiérarchie des vues.

PerformanceMetricsState.getHolderForHierarchy(view: View): MetricsStateHolder

N'importe quelle vue d'une hiérarchie active peut être utilisée. En interne, cette opération vérifie si un objet Holder existant est associé à cette hiérarchie des vues. Ces informations sont mises en cache dans une vue située au-dessus de cette hiérarchie. Si aucun objet de ce type n'existe, getHolderForHierarchy() en crée un.

La méthode statique getHolderForHierarchy() vous évite d'avoir à mettre en cache l'instance du conteneur quelque part pour une récupération ultérieure et facilite la récupération d'un objet d'état existant depuis n'importe quel point du code (ou même du code de bibliothèque, qui n'aurait sinon pas accès à l'instance d'origine).

Notez que la valeur renvoyée est un objet conteneur et non l'objet état lui-même. La valeur de l'objet état dans le conteneur est définie uniquement par JankStats. Autrement dit, si une application crée un objet JankStats pour la fenêtre contenant cette hiérarchie des vues, l'objet état est créé et défini. Sinon, si JankStats n'effectue pas le suivi des informations, l'objet état n'est pas nécessaire et le code d'application ou de bibliothèque n'est pas nécessaire pour injecter un état.

Cette approche permet de récupérer un conteneur que JankStats peut ensuite remplir. Le code externe peut demander le titulaire à tout moment. Les appelants peuvent mettre en cache l'objet Holder léger et l'utiliser à tout moment pour définir un état, en fonction de la valeur de sa propriété state interne, comme dans l'exemple de code ci-dessous, où l'état n'est défini que lorsque la propriété d'état interne du conteneur n'est pas nulle :

val metricsStateHolder = PerformanceMetricsState.getHolderForHierarchy(binding.root)
// ...
metricsStateHolder.state?.putState("Activity", javaClass.simpleName)

Pour contrôler l'état de l'interface utilisateur ou de l'application, une application peut injecter (ou supprimer) un état à l'aide des méthodes putState et removeState. JankStats enregistre le code temporel de ces appels. Si un frame dépasse les temps de début et de fin de l'état, JankStats indique ces informations d'état ainsi que les données temporelles du frame.

Pour chaque état, ajoutez deux informations : key (catégorie d'état, comme "RecyclerView") et value (informations sur ce qui se passait sur le moment, comme "défilement").

Supprimez les états à l'aide de la méthode removeState() lorsque cet état n'est plus valide afin d'éviter que des informations erronées ou trompeuses ne soient signalées avec les données de frame.

Le fait d'appeler putState() avec une key ajoutée précédemment remplace la value existante de cet état par la nouvelle.

La version putSingleFrameState() de l'API d'état ajoute un état qui n'est enregistré qu'une seule fois, sur le prochain frame rapporté. Le système le supprime ensuite automatiquement, ce qui vous évite d'avoir par accident un état obsolète dans votre code. Notez qu'il n'existe pas d'équivalent singleFrame de removeState(), car JankStats supprime automatiquement les états à frame unique.

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.putState("RecyclerView", "Dragging")
            }
            RecyclerView.SCROLL_STATE_SETTLING -> {
                metricsState.putState("RecyclerView", "Settling")
            }
            else -> {
                metricsState.removeState("RecyclerView")
            }
        }
    }
}

Notez que la clé utilisée pour les états doit avoir assez de sens pour permettre une analyse ultérieure. En particulier, étant donné qu'un état ayant la même key qu'un attribut ajouté précédemment remplace cette valeur antérieure, vous devez essayer d'utiliser des noms de key uniques pour les objets dont les instances peuvent être différentes dans votre application ou votre bibliothèque. Par exemple, une application avec cinq RecyclerViews différentes peut vouloir fournir des clés identifiables pour chacune d'elles au lieu d'utiliser simplement RecyclerView pour chacune, sans pouvoir ensuite déterminer facilement les instances auxquelles les données de frame font référence.

Heuristique des à-coups

Pour ajuster l'algorithme interne et déterminer ce qui est considéré comme un à-coup, utilisez la propriété jankHeuristicMultiplier.

Par défaut, le système définit un à-coup comme un frame qui met deux fois plus de temps à s'afficher que la fréquence d'actualisation normale. Elle ne considère pas les à-coups comme un élément supérieur à la fréquence d'actualisation, car les informations sur le délai d'affichage de l'application ne sont pas totalement claires. Il est donc préférable d'ajouter un tampon et de signaler seulement les problèmes qui entraînent des problèmes de performances notables.

Ces deux valeurs peuvent être modifiées via ces méthodes pour s'adapter plus étroitement à la situation de l'application, ou lors de tests visant à forcer ou non les à-coups, selon les besoins.

Utilisation dans Jetpack Compose

Il n'y a actuellement aucune configuration nécessaire pour utiliser JankStats dans Compose. Pour conserver le PerformanceMetricsState en cas de modifications de configuration, retenez-le comme suit :

/**
 * Retrieve MetricsStateHolder from compose and remember until the current view changes.
 */
@Composable
fun rememberMetricsStateHolder(): PerformanceMetricsState.Holder {
    val view = LocalView.current
    return remember(view) { PerformanceMetricsState.getHolderForHierarchy(view) }
}

Pour utiliser JankStats, ajoutez l'état actuel à stateHolder, comme indiqué ici :

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?.putState("LazyList", "Scrolling")
        } else {
            metricsStateHolder.state?.removeState("LazyList")
        }
    }
}

Pour plus de détails sur l'utilisation de JankStats dans votre application Jetpack Compose, consultez notre application exemple des performances.

Envoyer un commentaire

Faites-nous part de vos commentaires et de vos idées via les ressources suivantes :

Issue Tracker
Signalez les problèmes pour que nous puissions corriger les bugs.