JankStats 라이브러리

JankStats 라이브러리를 사용하면 애플리케이션의 성능 문제를 추적하고 분석할 수 있습니다. 버벅거림은 렌더링하는 데 시간이 너무 오래 걸리는 애플리케이션 프레임을 의미하며 JankStats 라이브러리는 앱의 버벅거림 통계에 관한 보고서를 제공합니다.

기능

JankStats는 Android 7(API 수준 24) 이상의 FrameMetrics API 또는 이전 버전의 OnPreDrawListener를 포함한 기존 Android 플랫폼 기능을 기반으로 합니다. 이러한 메커니즘을 통해 애플리케이션은 프레임 완료에 걸리는 시간을 추적할 수 있습니다. JankStats 라이브러리는 더 동적이고 사용이 간편한 두 가지 추가 기능인 버벅거림 휴리스틱과 UI 상태를 제공합니다.

버벅거림 휴리스틱

FrameMetrics를 사용하여 프레임 지속 시간을 추적할 수 있지만 FrameMetrics는 실제 버벅거림을 파악하는 데 도움이 되지는 않습니다. 하지만 JankStats에는 구성 가능한 내부 메커니즘으로 버벅거림이 발생하는 시점을 파악하여 보고서를 보다 즉각적으로 활용할 수 있도록 합니다.

UI 상태

앱에서 성능 문제가 발생하는 상황을 알아야 할 때가 많습니다. 예를 들어 FrameMetrics를 사용하는 복잡한 멀티스크린 앱을 개발하는데 앱에서 버벅거림이 매우 심한 프레임이 종종 발견된다면 문제가 발생한 위치, 사용자가 실행한 작업, 문제 재현 방법을 확인하여 정보의 맥락을 파악하려고 할 것입니다.

JankStats는 라이브러리와의 통신을 지원하여 앱 Activity에 관한 정보를 제공하는 state API를 도입해 이 문제를 해결합니다. JankStats가 버벅거리는 프레임에 관한 정보를 기록할 때 버벅거림 보고서에 애플리케이션의 현재 상태가 포함됩니다.

사용

JankStats를 사용하려면 각 Window에 라이브러리를 인스턴스화하고 사용 설정하세요. 각 JankStats 객체는 Window 내에서만 데이터를 추적합니다. 라이브러리를 인스턴스화하려면 Window 인스턴스와 함께 OnFrameListener 리스너가 필요하며 둘 다 클라이언트로 측정항목을 전송하는 데 사용됩니다. 리스너는 모든 프레임에서 FrameData로 호출되며 다음 항목에 관한 세부정보를 제공합니다.

  • 프레임 시작 시간
  • 지속 시간 값
  • 프레임을 버벅거림으로 간주해야 하는지 여부
  • 프레임 중 애플리케이션 상태에 관한 정보가 포함된 String 쌍 세트

JankStats를 더 유용하게 만들려면 애플리케이션이 FrameData에서 보고하기 위한 관련 UI 상태 정보로 라이브러리를 채워야 합니다. 모든 상태 관리 로직과 API가 있는 PerformanceMetricsState API(JankStats를 직접 사용하지 않음)를 통해 실행하면 됩니다.

초기화

JankStats 라이브러리를 사용하려면 먼저 Gradle 파일에 JankStats 종속 항목을 추가합니다.

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

그런 다음 각 Window에 JankStats를 초기화하고 사용 설정합니다. 또한 Activity가 백그라운드로 전환될 때 JankStats 추적을 일시중지해야 합니다. Activity 재정의에서 JankStats 객체를 만들고 사용 설정합니다.

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

위의 예는 JankStats 객체를 구성한 후 현재 Activity에 관한 상태 정보를 삽입합니다. 이제 이 JankStats 객체에 관해 생성된 모든 향후 FrameData 보고서에는 Activity 정보도 포함됩니다.

JankStats.createAndTrack 메서드는 Window 객체를 참조합니다. 이는 Window 내부의 뷰 계층 구조 프록시이며 Window 자체의 프록시입니다. jankFrameListener는 이러한 정보를 플랫폼에서 JankStats로 내부적으로 전달하는 데 사용되는 것과 동일한 스레드에서 호출됩니다.

JankStats 객체에서 추적 및 보고를 사용 설정하려면 isTrackingEnabled = true를 호출합니다. 추적은 기본적으로 사용 설정되어 있지만 활동을 일시중지하면 사용 중지됩니다. 이 경우 계속 진행하기 전에 추적을 다시 사용 설정해야 합니다. 추적을 중지하려면 isTrackingEnabled = false를 호출하세요.

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

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

보고

JankStats 라이브러리는 모든 프레임의 모든 데이터 추적을 사용 설정된 JankStats 객체의 OnFrameListener에 보고합니다. 앱은 나중에 업로드할 수 있도록 이 데이터를 저장하고 집계할 수 있습니다. 자세한 내용은 집계 섹션에 제공된 예를 참고하세요.

앱이 프레임별 보고서를 수신하려면 OnFrameListener를 만들어 제공해야 합니다. 이 리스너는 모든 프레임에서 호출되어 진행 중인 버벅거림 데이터를 앱에 제공합니다.

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

리스너에서는 FrameData 객체를 사용하여 버벅거림에 관한 프레임별 정보를 제공합니다. 여기에는 요청된 프레임에 관한 다음 정보가 포함됩니다.

  • isjank: 프레임에서 버벅거림이 발생했는지 나타내는 불리언 플래그입니다.
  • frameDurationUiNanos: 프레임의 지속 시간(나노초)입니다.
  • frameStartNanos: 프레임이 시작된 시간입니다(나노초).
  • states: 프레임 중 앱의 상태입니다.

Android 12(API 수준 31) 이상을 사용하는 경우 다음을 사용하여 프레임 지속 시간에 관한 추가 데이터를 노출할 수 있습니다.

리스너에서 StateInfo를 사용하여 애플리케이션 상태에 관한 정보를 저장합니다.

OnFrameListener는 프레임당 정보를 JankStats에 전달하는 데 내부적으로 사용되는 것과 동일한 스레드에서 호출됩니다. Android 버전 6(API 수준 23) 이하에서는 기본(UI) 스레드입니다. Android 버전 7(API 수준 24) 이상에서는 FrameMetrics를 위해 생성되고 FrameMetrics에 의해 사용되는 스레드입니다. 어떤 경우든 해당 스레드의 성능 문제를 방지하기 위해 콜백을 처리하고 빠르게 반환하는 것이 중요합니다.

또한 콜백에서 전송된 FrameData 객체는 데이터 보고에 새 객체를 할당할 필요가 없도록 모든 프레임에서 재사용됩니다. 즉, 콜백이 반환되는 즉시 해당 객체는 오래되고 사용되지 않는 것으로 간주되므로 해당 데이터를 다른 위치에 복사 및 캐시해야 합니다.

집계

앱 코드로 프레임별 데이터를 집계하여 재량에 따라 정보를 저장하고 업로드할 수 있기를 원할 것입니다. 저장 및 업로드에 관한 세부정보는 알파 JankStats API 출시 범위를 벗어나지만 GitHub 저장소에서 제공하는 JankAggregatorActivity를 사용하여 프레임별 데이터를 더 큰 컬렉션으로 집계하는 예비 Activity를 확인할 수 있습니다.

JankAggregatorActivityJankStatsAggregator 클래스를 사용하여 JankStats OnFrameListener 메커니즘 위에 자체 보고 메커니즘을 쌓아 많은 프레임을 포괄하는 정보 모음만 보고하는 상위 수준 추상화를 제공합니다.

JankStats 객체를 직접 만드는 대신 JankAggregatorActivity는 자체 JankStats 객체를 내부적으로 만드는 JankStatsAggregator 객체를 만듭니다.

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

유사한 메커니즘이 JankAggregatorActivity에서 추적을 일시중지하고 다시 시작하는 데 사용되고 issueJankReport() 호출을 통해 보고서를 발행하는 신호로 pause() 이벤트가 추가됩니다. 수명 주기 변경이 애플리케이션의 버벅거림 상태를 캡처할 적절한 시점으로 보이기 때문입니다.

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
}

위의 코드 예는 앱이 JankStats를 사용 설정하고 프레임 데이터를 수신하는 데 필요한 모든 것입니다.

상태 관리

다른 API를 호출하여 JankStats를 맞춤설정하고자 할 수도 있습니다. 예를 들어 앱 상태 정보를 삽입하면 버벅거림이 발생하는 프레임의 상황 정보를 제공하여 프레임 데이터의 유용성을 높일 수 있습니다.

이 정적 메서드는 지정된 뷰 계층 구조의 현재 MetricsStateHolder 객체를 검색합니다.

PerformanceMetricsState.getHolderForHierarchy(view: View): MetricsStateHolder

활성 계층 구조의 모든 뷰를 사용할 수 있습니다. 이는 내부적으로 이 뷰 계층 구조와 연결된 기존 Holder 객체가 있는지 확인합니다. 이 정보는 이 계층 구조의 상단에 있는 뷰에 캐시됩니다. 이러한 객체가 없으면 getHolderForHierarchy()가 객체를 만듭니다.

정적 getHolderForHierarchy() 메서드를 사용하면 나중에 검색할 수 있도록 홀더 인스턴스를 어딘가에 캐시하지 않아도 되며 기존 상태 객체를 코드(또는 다른 경우라면 원래 인스턴스 액세스 권한이 없었을 라이브러리 코드)의 어디에서나 더 쉽게 검색할 수 있습니다.

반환 값은 상태 객체 자체가 아니라 홀더 객체입니다. 홀더 내부의 상태 객체 값은 JankStats에서만 설정합니다. 즉, 애플리케이션이 해당 뷰 계층 구조를 포함하는 창의 JankStats 객체를 만들면 상태 객체가 만들어지고 설정됩니다. 그 외의 경우 JankStats가 정보를 추적하지 않으면 상태 객체가 필요하지 않으며 앱이나 라이브러리 코드가 상태를 삽입할 필요가 없습니다.

이 접근 방식을 사용하면 JankStats가 채울 수 있는 홀더를 검색할 수 있습니다. 외부 코드는 언제든지 홀더를 요청할 수 있습니다. 호출자는 경량 객체 Holder를 캐시하고, 아래 코드 예와 같이 홀더의 내부 상태 속성이 null이 아닐 때만 상태가 설정되는 내부 state 속성 값에 따라 언제든지 상태를 설정하는 데 해당 객체를 사용할 수 있습니다.

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

UI/앱 상태를 제어하기 위해 앱은 putStateremoveState 메서드로 상태를 삽입(또는 삭제)할 수 있습니다. JankStats는 이러한 호출의 타임스탬프를 기록합니다. 프레임이 상태의 시작 및 종료 시간과 겹치는 경우 JankStats는 이 상태 정보와 프레임의 타이밍 데이터를 함께 보고합니다.

어떤 상태든 다음 두 가지 정보를 추가합니다. 하나는 'RecyclerView'와 같은 상태 카테고리인 key이고 또 하나는 'scrolling'과 같은 당시 발생한 일에 관한 정보인 value입니다.

상태가 더 이상 유효하지 않을 경우 removeState() 메서드로 상태를 삭제하여 잘못되거나 오해의 소지가 있는 정보가 프레임 데이터와 함께 보고되지 않도록 합니다.

이전에 추가된 keyputState()를 호출하면 해당 상태의 기존 value가 새 상태로 대체됩니다.

State API의 putSingleFrameState() 버전은 다음 보고된 프레임에 한 번만 기록되는 상태를 추가합니다. 그 후에는 시스템에서 자동으로 상태를 삭제하므로 더 이상 사용되지 않는 상태가 실수로 코드에 포함되는 일이 생기지 않습니다. JankStats는 단일 프레임 상태를 자동으로 삭제하므로 removeState()에 상응하는 singleFrame은 없습니다.

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

상태에 사용되는 키는 나중에 분석할 수 있을 만큼 유의미해야 합니다. 특히, 이전에 추가된 상태와 동일한 key를 가진 상태가 이전 값을 대체하므로, 앱이나 라이브러리에 다른 인스턴스를 가질 수 있는 객체에 고유한 key 이름을 사용하도록 해야 합니다. 예를 들어 5개의 서로 다른 RecyclerView가 포함된 앱은 단순히 RecyclerView마다 RecyclerView를 사용하여 프레임 데이터가 참조하는 인스턴스를 결과 데이터에 쉽게 알릴 수 없는 대신 각 RecyclerView에 식별 가능한 키를 제공하는 것이 좋습니다.

버벅거림 휴리스틱

버벅거림으로 간주되는 요소를 파악하기 위해 내부 알고리즘을 조정하려면 jankHeuristicMultiplier 속성을 사용합니다.

기본적으로 시스템은 현재 새로고침 빈도보다 렌더링하는 데 두 배가 걸리는 프레임으로 버벅거림을 정의합니다. 앱 렌더링 시간에 관한 정보가 완전히 명확하지는 않기 때문에, 새로고침 빈도를 넘는 모든 경우를 버벅거림으로 간주하지는 않습니다. 따라서 버퍼를 추가하고 눈에 띄는 성능 문제를 일으킬 때만 문제를 보고하는 것이 좋습니다.

이 두 가지 값은 모두 앱의 상황에 더 정확히 맞추거나 테스트에서 필요에 따라 버벅거림을 강제로 발생 또는 발생시키지 않으려 할 때 이러한 메서드를 통해 변경할 수 있습니다.

Jetpack Compose의 사용

현재 Compose에서 JankStats를 사용하는 데 필요한 설정은 거의 없습니다. 구성 변경에도 PerformanceMetricsState를 유지하려면 다음과 같이 합니다.

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

JankStats를 사용하려면 다음과 같이 현재 상태를 stateHolder에 추가합니다.

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

Jetpack Compose 애플리케이션에서 JankStats를 사용하는 방법에 관한 자세한 내용은 성능 샘플 앱을 확인하세요.

의견 보내기

다음 리소스를 통해 의견을 보내고 아이디어를 공유해 주세요.

Issue Tracker
버그를 수정할 수 있도록 문제를 신고해 주세요.