Biblioteka JankStats

Biblioteka JankStats ułatwia śledzenie i analizowanie problemów z wydajnością aplikacji. Jank odnosi się do ramek aplikacji, których renderowanie trwa zbyt długo, a biblioteka JankStats zawiera raporty o statystykach zacięć aplikacji.

Uprawnienia

JankStats wykorzystuje istniejące funkcje platformy Androida, w tym interfejs FrameMetrics API w Androidzie 7 (poziom API 24) i nowszy lub OnPreDrawListener we wcześniejszych wersjach. Mechanizmy te pomagają aplikacjom śledzić czas potrzebny na zrealizowanie klatek. Biblioteka JanksStats oferuje 2 dodatkowe funkcje, które zwiększają jej dynamikę i są łatwiejsze w użyciu: heurystyczną heurystykę i stan interfejsu użytkownika.

Heurystyka Jank

Chociaż możesz używać FrameMetrics do śledzenia czasu trwania klatki, nie zapewnia on pomocy w określaniu rzeczywistego zacięcia. JankStats ma jednak konfigurowalne, wewnętrzne mechanizmy, które określają, kiedy występuje zacinanie, dzięki czemu raporty są bardziej przydatne od razu.

Stan interfejsu

Często jest potrzebna znajomość kontekstu problemów z wydajnością aplikacji. Jeśli na przykład masz złożoną aplikację na wiele urządzeń, która korzysta z FrameMetrics, i stwierdzisz, że często zawiera ona wyjątkowo nieładne ramki, musisz umieścić te informacje w kontekście, wiedząc, gdzie wystąpił problem, co robił użytkownik i jak go odtworzyć.

JankStats rozwiązuje ten problem, wprowadzając interfejs API state, który umożliwia komunikowanie się z biblioteką w celu dostarczania informacji o aktywności w aplikacji. Gdy JankStats rejestruje informacje o nieprawidłowej ramce, w raportach o zacięciu dołączany jest bieżący stan aplikacji.

Wykorzystanie

Aby zacząć korzystać z JankStats, utwórz instancję i włącz bibliotekę w przypadku każdego obiektu Window. Każdy obiekt JankStats śledzi dane tylko w obrębie Window. Utworzenie instancji biblioteki wymaga instancji Window wraz z detektorem OnFrameListener. Oba narzędzia służą do wysyłania danych do klienta. Detektor jest wywoływany z parametrem FrameData w każdej klatce i szczegółowo podaje:

  • Czas rozpoczęcia ramki
  • Wartości czasu trwania
  • Określa, czy ramka ma być uważana za zaciętą
  • Zbiór par ciągów tekstowych zawierających informacje o stanie aplikacji w ramce

Aby JankStats były bardziej przydatne, aplikacje powinny wypełniać bibliotekę odpowiednimi informacjami o stanie interfejsu na potrzeby raportowania w FrameData. Możesz to zrobić za pomocą interfejsu API PerformanceMetricsState (nie bezpośrednio JankStats), w którym działają wszystkie interfejsy API i logika zarządzania stanem.

Inicjacja

Aby zacząć korzystać z biblioteki JankStats, najpierw dodaj zależność JankStats do pliku Gradle:

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

Następnie zainicjuj i włącz JankStats dla każdego zdarzenia Window. Należy też wstrzymać śledzenie JankStats, gdy aktywność przechodzi w tle. Utwórz i włącz obiekt JankStats w zastąpieniach aktywności:

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)
        // ...
    }

W przykładzie powyżej wstrzyknięto informacje o stanie bieżącej aktywności po utworzeniu obiektu JankStats. Wszystkie przyszłe raporty FrameData utworzone dla tego obiektu JankStats zawierają teraz także informacje o aktywności.

Metoda JankStats.createAndTrack odwołuje się do obiektu Window, który jest serwerem proxy hierarchii widoków w obrębie tego obiektu Window, a także dla samego obiektu Window. Funkcja jankFrameListener jest wywoływana w tym samym wątku, który służy do przekazywania informacji z platformy do JankStats wewnętrznie.

Aby włączyć śledzenie i raportowanie w przypadku dowolnego obiektu JankStats, wywołaj isTrackingEnabled = true. Chociaż jest ona domyślnie włączona, wstrzymanie aktywności wyłącza śledzenie. W takim przypadku, zanim przejdziesz dalej, ponownie włącz śledzenie. Aby zatrzymać śledzenie, zadzwoń pod numer isTrackingEnabled = false.

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

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

Zgłaszanie

Biblioteka JankStats przekazuje w przypadku włączonych obiektów JankStats wszystkie śledzenie danych (dla każdej klatki) w elemencie OnFrameListener. Aplikacje mogą przechowywać i agregować te dane, aby móc je później przesłać. Więcej informacji znajdziesz na przykładach w sekcji Agregacja.

Aby aplikacja otrzymywała raporty o klatce, musisz utworzyć i podać parametr OnFrameListener. Ten detektor jest wywoływany przy każdej ramce, aby dostarczyć aplikacjom dane o bieżącym zacięciu.

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

Detektor dostarcza informacje z poszczególnych klatek o zacięciu z obiektem FrameData. Zawiera on te informacje o żądanej ramce:

  • isjank: flaga wartości logicznej wskazująca, czy w klatce wystąpiło zacięcie.
  • frameDurationUiNanos: Czas trwania klatki (w nanosekundach).
  • frameStartNanos: czas rozpoczęcia klatki (w nanosekundach).
  • states: stan aplikacji w klatce.

Jeśli używasz Androida w wersji 12 (poziom interfejsu API 31) lub nowszej, możesz skorzystać z tych opcji, aby udostępnić więcej danych o czasie trwania klatek:

Użyj StateInfo w detektorze, aby przechowywać informacje o stanie aplikacji.

Pamiętaj, że funkcja OnFrameListener jest wywoływana w tym samym wątku używanym wewnętrznie do przesyłania informacji o ramkach do JankStats. W Androidzie w wersji 6 (poziom interfejsu API 23) lub starszych jest to wątek główny (UI). W Androidzie w wersji 7 (poziom interfejsu API 24) i nowszych jest to wątek utworzony i używany przez FrameMetrics. W obu przypadkach ważne jest, by wykonać wywołanie zwrotne i szybko wrócić, by uniknąć problemów z wydajnością w danym wątku.

Obiekt FrameData wysłany w wywołaniu zwrotnym jest używany ponownie w każdej ramce, aby uniknąć konieczności przydzielania nowych obiektów na potrzeby raportowania danych. Oznacza to, że musisz skopiować te dane i zachować je w pamięci podręcznej w innym miejscu, ponieważ po przywróceniu wywołania zwrotnego obiekt powinien zostać uznany za statyczny i nieaktualny.

Zbieram

Kod aplikacji powinien agregować dane z każdej ramki, co pozwala zapisywać i przesyłać informacje według własnego uznania. Chociaż szczegóły dotyczące zapisywania i przesyłania wykraczają poza zakres wersji alfa interfejsu JankStats API, ale możesz wyświetlić działanie wstępne dotyczące agregacji danych z poszczególnych ramek w większej kolekcji za pomocą usługi JankAggregatorActivity dostępnej w naszym repozytorium GitHub.

JankAggregatorActivity używa klasy JankStatsAggregator do nałożenia własnego mechanizmu raportowania na mechanizm JankStats OnFrameListener, aby zapewnić wyższy poziom abstrakcji w przypadku raportowania tylko zbioru informacji, który obejmuje wiele ramek.

Zamiast bezpośrednio tworzyć obiekt JankStats, JankAggregatorActivity tworzy obiekt JankStatsAggregator, który wewnętrznie tworzy własny obiekt JankStats:

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

Podobny mechanizm jest używany w JankAggregatorActivity do wstrzymywania i wznawiania śledzenia. Dodanie zdarzenia pause() jako sygnału do wygenerowania raportu z wywołaniem metody issueJankReport(), ponieważ zmiany w cyklu życia wydają się odpowiednim momentem do rejestrowania stanu zacięcia w aplikacji:

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
}

Przykładowy kod powyżej to wszystko, czego potrzebuje aplikacja, aby włączyć JankStats i otrzymywać dane ramek.

Zarządzaj stanem

Możesz wywołać inne interfejsy API, aby dostosować JankStats. Na przykład wstrzykiwanie informacji o stanie aplikacji zwiększa przydatność danych ramek, ponieważ dostarcza kontekst dla ramek, w których dochodzi do zacinania.

Ta metoda statyczna pobiera bieżący obiekt MetricsStateHolder z danej hierarchii widoków.

PerformanceMetricsState.getHolderForHierarchy(view: View): MetricsStateHolder

Można użyć dowolnego widoku w aktywnej hierarchii. Wewnętrznie sprawdza to, czy z hierarchią widoku jest powiązany istniejący obiekt Holder. Informacje te są przechowywane w pamięci podręcznej w widoku u góry tej hierarchii. Jeśli taki obiekt nie istnieje, getHolderForHierarchy() go utworzy.

Metoda statycznej getHolderForHierarchy() pozwala uniknąć konieczności zapisywania instancji instancji w pamięci podręcznej w celu późniejszego pobrania danych i ułatwia pobieranie istniejących obiektów stanu z dowolnego miejsca w kodzie (a nawet kodu biblioteki, który w innym przypadku nie miałby dostępu do pierwotnej instancji).

Pamiętaj, że zwracana wartość to obiekt zastępczy, a nie sam obiekt stanu. Wartość obiektu stanu w elemencie jest ustawiana tylko przez JankStats. Oznacza to, że jeśli aplikacja tworzy obiekt JankStats dla okna zawierającego hierarchię widoku, obiekt stanu jest tworzony i ustawiany. W przeciwnym razie bez śledzenia informacji przez JankStats nie jest potrzebny obiekt State ani wstrzykiwany kod aplikacji czy biblioteki.

Takie podejście umożliwia uzyskanie właściciela, który może następnie wypełnić JankStats. Kod zewnętrzny może poprosić o właściciela w każdej chwili. Wywołujący mogą buforować uproszczony obiekt Holder i używać go w dowolnym momencie do ustawienia stanu, w zależności od wartości jego wewnętrznej właściwości state, jak w przykładowym kodzie poniżej, gdzie stan jest ustawiany tylko wtedy, gdy właściwość wewnętrznego stanu właściciela nie ma wartości null:

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

Aby kontrolować stan UI/aplikacji, aplikacja może wstrzykiwać (lub usuwać) stan za pomocą metod putState i removeState. JankStats rejestruje sygnaturę czasową tych wywołań. Jeśli ramka pokrywa się z czasem rozpoczęcia i zakończenia stanu, JankStats podaje w raportach informacje oraz dane o czasie wyświetlania ramki.

Dla każdego stanu dodaj 2 informacje: key (kategorię stanu, np. „RecyclerView”) i value (informację o tym, co działo się w danym momencie, np. „scrolling”).

Usuwaj stany za pomocą metody removeState(), gdy są one już nieważne, aby mieć pewność, że nieprawidłowe lub wprowadzające w błąd informacje nie są zgłaszane razem z danymi ramek.

Wywołanie elementu putState() za pomocą dodanego wcześniej elementu key zastępuje dotychczasowy element value w danym stanie nowym.

Wersja interfejsu putSingleFrameState() State API dodaje stan, który jest rejestrowany tylko raz, w przypadku następnej raportowanej klatki. Następnie system automatycznie go usuwa, dzięki czemu kod nie zawiera przypadkiem przestarzałego stanu. Pamiętaj, że nie ma odpowiednika elementu removeState() w pojedynczej klatce, bo JankStats automatycznie usuwa stany jednoklatkowe.

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

Pamiętaj, że klucz używany w przypadku stanów powinien być na tyle istotny, by umożliwić późniejszą analizę. W szczególności stan z tą samą wartością key co ten, który został dodany wcześniej, zastąpi tę wcześniejszą wartość, dlatego zalecamy używanie unikalnych nazw key w przypadku obiektów, które mogą mieć inne instancje w aplikacji lub bibliotece. Na przykład aplikacja z 5 różnymi obiektami RecyclerView może chcieć udostępnić możliwe do zidentyfikowania klucze w przypadku każdego z nich, zamiast korzystać z funkcji RecyclerView w każdym z nich. W takiej sytuacji trudno jest stwierdzić w wynikowych danych, do której instancji odnoszą się dane ramki.

Heurystyka Jank

Aby dostosować wewnętrzny algorytm określający, co jest uważane za zacinanie, użyj właściwości jankHeuristicMultiplier.

Domyślnie system definiuje zacinanie jako klatki, której renderowanie trwa 2 razy dłużej niż obecnie. Nie traktuje zacięcia jako nic związanego z częstotliwością odświeżania, ponieważ informacje o czasie renderowania aplikacji nie są do końca jasne. Dlatego warto dodać bufor i zgłaszać problemy tylko wtedy, gdy powodują one zauważalne problemy z wydajnością.

Obie te wartości można zmienić za pomocą tych metod, aby lepiej dopasować się do sytuacji w aplikacji, lub podczas testowania w celu wymuszenia lub niezatrzymania zacięcia, co jest konieczne do przeprowadzenia testu.

Wykorzystanie w Jetpack Compose

Aby korzystać z JankStats w narzędziu do tworzenia wiadomości, nie musisz jeszcze przeprowadzać konfiguracji. Aby zachować PerformanceMetricsState po wprowadzeniu zmian w konfiguracji, zapamiętaj je w ten sposób:

/**
 * 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) }
}

Aby korzystać z JankStats, dodaj do parametru stateHolder bieżący stan w ten sposób:

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

Szczegółowe informacje o korzystaniu z JankStats w aplikacji Jetpack Compose znajdziesz w naszej przykładowej aplikacji wydajności.

Prześlij opinię

Podziel się z nami swoją opinią i pomysłami, korzystając z tych zasobów:

Śledzenie problemów
Zgłoś problemy, żebyśmy mogli naprawić błędy.