Thư viện JankStats

Thư viện JankStats giúp bạn theo dõi và phân tích các vấn đề về hiệu suất của ứng dụng. Hiện tượng giật (jank) đề cập đến tình huống khi các khung hình của ứng dụng mất quá nhiều thời gian để hiển thị. Còn thư viện JankStats cung cấp các báo cáo thống kê về hiện tượng giật của ứng dụng.

Tính năng

JankStats xây dựng dựa trên các tính năng hiện có của nền tảng Android, bao gồm FrameMetrics API trên Android 7 (API cấp 24) trở lên hoặc OnPreDrawListener trên các phiên bản trước. Các cơ chế này có thể giúp ứng dụng theo dõi thời lượng hoàn tất một khung hình. Thư viện JanksStats cung cấp 2 tính năng bổ sung, giúp tăng cường độ linh động và tính dễ sử dụng: suy đoán hiện tượng giật (jank heuristics) và trạng thái giao diện người dùng.

Suy đoán hiện tượng giật

Mặc dù FrameMetrics có thể dùng để theo dõi thời lượng khung hình, nhưng FrameMetrics không hỗ trợ việc xác định hiện tượng giật trên thực tế. Tuy nhiên, JankStats có các cơ chế nội bộ có khả năng định cấu hình để xác định thời điểm xảy ra hiện tượng giật, giúp báo cáo thông tin hữu ích tức thì.

Trạng thái giao diện người dùng

Thường thì bạn cần phải biết bối cảnh xảy ra các vấn đề về hiệu suất trong ứng dụng. Ví dụ: nếu phát triển một ứng dụng phức tạp có nhiều màn hình và sử dụng FrameMetrics, rồi bạn phát hiện khung hình của ứng dụng thường bị giật nhiều, thì bạn sẽ muốn hiểu rõ thông tin về bối cảnh, chẳng hạn như nơi xảy ra vấn đề, người dùng đang làm gì vào lúc đó và cách tái hiện vấn đề.

JankStats giúp giải quyết vấn đề này bằng cách giới thiệu API state, cho phép bạn giao tiếp với thư viện để cung cấp thông tin về Hoạt động trong ứng dụng. Khi JankStats ghi lại thông tin về một khung hình có hiện tượng giật, thông tin này sẽ bao gồm trạng thái hiện tại của ứng dụng trong báo cáo về hiện tượng giật.

Cách sử dụng

Để bắt đầu sử dụng JankStats, hãy tạo thực thể và bật thư viện này cho mỗi Window. Mỗi đối tượng JankStats chỉ theo dõi dữ liệu trong một Window. Để tạo thực thể của thư viện này, bạn cần có một thực thể Window cùng với trình nghe OnFrameListener – cả hai đều dùng để gửi các chỉ số đến máy khách. Trình nghe này được gọi cùng với FrameData trên mỗi khung hình và trình bày chi tiết các thông tin sau:

  • Thời gian bắt đầu khung hình
  • Giá trị thời lượng
  • Liệu có nên xem khung hình đó có hiện tượng giật không
  • Tập hợp các cặp chuỗi chứa thông tin về trạng thái ứng dụng trong khung hình

Để JankStats cung cấp nhiều thông tin hữu ích hơn, các ứng dụng nên đưa thông tin liên quan đến trạng thái giao diện người dùng vào thư viện để báo cáo trong FrameData. Bạn có thể thực hiện việc này thông qua API PerformanceMetricsState (không sử dụng trực tiếp JankStats), trong đó tất cả API và logic quản lý trạng thái đều hoạt động.

Khởi chạy

Để bắt đầu sử dụng thư viện JankStats, trước tiên, hãy thêm phần phụ thuộc JankStats vào tệp Gradle:

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

Tiếp theo, hãy khởi chạy và bật JankStats cho mỗi Window. Bạn cũng nên tạm dừng theo dõi JankStats khi một Hoạt động (Activity) được thực hiện trong nền. Tạo và bật đối tượng JankStats trong phần ghi đè Hoạt động:

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

Ví dụ ở trên sẽ chèn thông tin trạng thái về Hoạt động hiện tại sau khi tạo đối tượng JankStats. Bây giờ, tất cả báo cáo FrameData được tạo trong tương lai cho đối tượng JankStats này đều chứa thông tin về Hoạt động.

Phương thức JankStats.createAndTrack tham chiếu đến một đối tượng Window là proxy cho Hệ phân cấp khung hiển thị bên trong Window cũng như cho chính Window đó. jankFrameListener được gọi trên cùng một luồng dùng để phân phối thông tin đó từ nền tảng đến JankStats nội bộ.

Để bật tính năng theo dõi và báo cáo về mọi đối tượng JankStats, hãy thiết lập isTrackingEnabled = true. Mặc dù tính năng này được bật theo mặc định, nhưng khi tạm dừng một hoạt động thì tính năng theo dõi sẽ bị tắt. Trong trường hợp này, hãy nhớ bật lại tính năng theo dõi trước khi tiếp tục. Để ngừng theo dõi, hãy thiết lập isTrackingEnabled = false.

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

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

Báo cáo

Thư viện JankStats báo cáo tất cả các hoạt động theo dõi dữ liệu cho mọi khung hình đến OnFrameListener cho các đối tượng được bật JankStats. Các ứng dụng có thể lưu trữ và tổng hợp dữ liệu này để tải lên sau này. Để biết thêm thông tin, hãy tham khảo các ví dụ được cung cấp trong mục Tổng hợp.

Bạn sẽ cần tạo và cung cấp OnFrameListener cho ứng dụng của mình để nhận được báo cáo trên mỗi khung hình. Trình nghe được gọi trên mọi khung hình để cung cấp dữ liệu giật liên tục cho các ứng dụng.

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

Trình nghe cung cấp thông tin trên mỗi khung hình về hiện tượng giật với đối tượng FrameData. Tệp này chứa các thông tin sau về khung hình được yêu cầu:

  • isjank: Một cờ boolean cho biết liệu hiện tượng giật có xảy ra trong khung hình hay không.
  • frameDurationUiNanos: Thời lượng khung hình (tính bằng nano giây).
  • frameStartNanos: Thời gian bắt đầu khung hình (tính bằng nano giây).
  • states: Trạng thái của ứng dụng trong khung hình.

Nếu đang sử dụng Android 12 (API cấp 31) trở lên, bạn có thể dùng những mục sau để hiển thị thêm dữ liệu về thời lượng khung hình:

Sử dụng StateInfo trong trình nghe để lưu trữ thông tin về trạng thái của ứng dụng.

Lưu ý là OnFrameListener được gọi trên cùng một luồng dùng trong nội bộ để phân phối thông tin trên mỗi khung hình cho JankStats. Trên Android phiên bản 6 (API cấp 23) trở xuống, đó là luồng Chính (Giao diện người dùng). Trên Android phiên bản 7 (API cấp 24) trở lên, đây là luồng được tạo cho FrameMetrics và được FrameMetrics sử dụng. Trong cả hai trường hợp, quan trọng là bạn phải xử lý lệnh gọi lại và nhanh chóng trả về để ngăn các vấn đề về hiệu suất trên luồng đó.

Ngoài ra, vui lòng lưu ý đối tượng FrameData được gửi trong lệnh gọi lại sẽ được tái sử dụng trên mọi khung hình để ngăn việc phân bổ các đối tượng mới cho hoạt động báo cáo dữ liệu. Có nghĩa là bạn phải sao chép và lưu dữ liệu trên vào bộ nhớ đệm ở nơi khác, vì đối tượng đó phải được coi là có trạng thái và lỗi thời ngay khi lệnh gọi lại trở về.

Tổng hợp

Có thể bạn cần sử dụng mã ứng dụng để tổng hợp dữ liệu trên mỗi khung hình, cho phép lưu và tải thông tin lên theo quyết định riêng của mình. Mặc dù chi tiết về việc lưu và tải thông tin lên nằm ngoài phạm vi bản phát hành alpha của JankStats API, nhưng bạn có thể xem Hoạt động sơ bộ để tổng hợp dữ liệu trên mỗi khung hình thành một tập dữ liệu lớn hơn thông qua JankAggregatorActivity có sẵn trong kho lưu trữ GitHub của chúng tôi.

JankAggregatorActivity sử dụng lớp JankStatsAggregator để phân lớp cơ chế báo cáo của chính nó ở phía trên cơ chế OnFrameListener JankStats nhằm cung cấp bản tóm tắt ở cấp độ cao hơn để chỉ báo cáo tập hợp thông tin trải rộng trên nhiều khung hình.

Thay vì tạo trực tiếp đối tượng JankStats, JankAggregatorActivity tạo đối tượng JankStatsAggregator. Đối tượng này lại tạo đối tượng JankStats nội bộ riêng cho mình:

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 sử dụng cơ chế tương tự để tạm dừng và tiếp tục theo dõi, đồng thời thêm sự kiện pause() làm tín hiệu để xuất báo cáo thông qua lệnh gọi đến issueJankReport(), vì các thay đổi trong vòng đời thường là thời điểm thích hợp để nắm bắt thông tin về trạng thái giật của ứng dụng.

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
}

Mã ví dụ ở trên là tất cả những gì ứng dụng cần để bật JankStats và nhận dữ liệu khung hình.

Quản lý trạng thái

Có thể bạn muốn gọi các API khác để tuỳ chỉnh JankStats. Ví dụ: tính năng chèn thông tin trạng thái ứng dụng giúp dữ liệu khung hình trở nên hữu ích hơn nhờ cung cấp ngữ cảnh cho những khung hình có hiện tượng giật.

Phương thức tĩnh này truy xuất đối tượng MetricsStateHolder hiện tại cho Hệ phân cấp khung hiển thị hiện có.

PerformanceMetricsState.getHolderForHierarchy(view: View): MetricsStateHolder

Bạn có thể sử dụng bất cứ khung hiển thị nào trong hệ thống phân cấp đang hoạt động. Về phía nội bộ, khung hiển thị này sẽ kiểm tra xem có tồn tại đối tượng Holder nào liên kết với hệ phân cấp khung hiển thị đó hay không. Thông tin này được lưu vào bộ nhớ đệm trong một khung hiển thị ở trên đỉnh của hệ phân cấp đó. Nếu không tìm thấy đối tượng nào như vậy, getHolderForHierarchy() sẽ tạo đối tượng đó.

Với phương thức getHolderForHierarchy() tĩnh, bạn có thể tránh phải lưu thực thể phần tử giữ vào bộ nhớ đệm ở nơi nào đó cho mục đích truy xuất sau này, đồng thời dễ dàng truy xuất đối tượng trạng thái hiện có từ mọi nơi trong mã (hoặc thậm chí cả mã thư viện, nếu không thì sẽ không có quyền truy cập vào thực thể gốc).

Lưu ý rằng giá trị trả về là một đối tượng phần tử giữ (holder object), không phải là đối tượng trạng thái. Chỉ JankStats mới có thể đặt giá trị của đối tượng trạng thái bên trong phần tử giữ này. Nghĩa là nếu ứng dụng tạo một đối tượng JankStats cho cửa sổ chứa hệ phân cấp khung hiển thị đó, thì đối tượng trạng thái sẽ được tạo và đặt. Ngược lại, nếu không có JankStats theo dõi thông tin thì sẽ không cần đến đối tượng trạng thái và cũng không cần mã ứng dụng hay mã thư viện chèn trạng thái.

Cách tiếp cận này sẽ cho phép bạn truy xuất phần tử giữ mà JankStats có thể đưa vào sau đó. Mã bên ngoài có thể yêu cầu phần tử giữ này bất cứ lúc nào. Các phương thức gọi có thể lưu đối tượng Holder ít quan trọng (lightweight) này vào bộ nhớ đệm và sử dụng đối tượng đó bất cứ lúc nào để đặt trạng thái, tuỳ thuộc vào giá trị thuộc tính state nội bộ của đối tượng. Trong mã ví dụ bên dưới, trạng thái chỉ được thiết lập khi thuộc tính trạng thái nội bộ của phần tử giữ có giá trị không rỗng:

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

Để kiểm soát trạng thái giao diện người dùng/ứng dụng, ứng dụng có thể chèn (hoặc xoá) trạng thái bằng các phương thức putStateremoveState. JankStats ghi lại dấu thời gian cho các lệnh gọi này. Nếu một khung hình trùng với thời điểm bắt đầu và kết thúc trạng thái thì JankStats sẽ báo cáo thông tin đó cùng với dữ liệu thời gian của khung hình.

Với bất kỳ trạng thái nào, hãy thêm hai thông tin: key(danh mục trạng thái, chẳng hạn như “RecyclerView”) và value (thông tin về những gì đang xảy ra vào thời điểm đó, chẳng hạn như “đang cuộn").

Hãy xoá trạng thái bằng phương thức removeState() nếu trạng thái đó không còn hợp lệ. Điều này giúp loại ra những thông tin không chính xác hoặc gây hiểu lầm khi báo cáo cùng dữ liệu của khung hình.

Việc gọi putState() bằng key đã được thêm trước đó sẽ thay thế value hiện có của trạng thái đó bằng một trạng thái mới.

Phiên bản putSingleFrameState() của API trạng thái này sẽ thêm một trạng thái chỉ cho phép ghi lại thông tin một lần trên khung hình được báo cáo tiếp theo. Sau đó, hệ thống sẽ tự động xoá trạng thái đó, đảm bảo mã của bạn không vô tình chứa bất cứ trạng thái đã lỗi thời nào. Lưu ý là không có singleFrame nào tương đương với removeState(), vì JankStats tự động xoá các trạng thái khung hình đơn.

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

Lưu ý rằng khoá dùng cho các trạng thái phải đủ ý nghĩa để sau này có thể phân tích. Cụ thể, vì trạng thái có cùng key với mục từng được thêm vào sẽ thay thế giá trị trước đó, nên bạn cần cố gắng sử dụng tên key duy nhất cho các đối tượng có thể có nhiều thực thể trong ứng dụng hoặc thư viện của mình. Chẳng hạn, một ứng dụng có 5 RecyclerView cần cung cấp các khoá có thể nhận dạng từng RecyclerView, thay vì chỉ sử dụng RecyclerView cho mỗi khoá để rồi sau đó thật khó mà biết được dữ liệu khung hình đề cập đến thực thể nào trong dữ liệu kết quả.

Suy đoán hiện tượng giật

Để điều chỉnh thuật toán nội bộ nhằm xác định khung hình nào có hiện tượng giật, hãy sử dụng thuộc tính jankHeuristicMultiplier.

Theo mặc định, hệ thống định nghĩa một khung hình có hiện tượng giật là khi tốc độ hiển thị khung hình gấp đôi tốc độ làm mới khung hình đó ở thời điểm hiện tại. Không phải bất cứ những gì vượt quá tốc độ làm mới đều được xem là hiện tượng giật vì thông tin về thời gian kết xuất ứng dụng không hoàn toàn rõ ràng. Do đó, bạn nên thêm một vùng đệm và chỉ báo cáo các sự cố khi chúng gây ra các vấn đề đáng chú ý về hiệu suất.

Cả hai giá trị này đều có thể thay đổi thông qua các phương thức để phù hợp hơn với tình huống của ứng dụng. Ngoài ra, trong quá trình kiểm thử, các giá trị này có thể được thay đổi để kiểm tra lúc nào hiện tượng giật xảy ra và không xảy ra nếu cần thiết.

Cách sử dụng trong Jetpack Compose

Hiện tại, bạn không cần phải thiết lập gì nhiều để sử dụng JankStats trong Compose. Để duy trì PerformanceMetricsState khi có các thay đổi về cấu hình, vui lòng lưu ý:

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

Và để sử dụng JankStats, hãy thêm trạng thái hiện tại vào stateHolder như minh hoạ bên dưới:

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

Để biết toàn bộ thông tin chi tiết về cách sử dụng JankStats trong ứng dụng Jetpack Compose, vui lòng tham khảo ứng dụng mẫu hiệu suất của chúng tôi.

Gửi phản hồi

Hãy chia sẻ phản hồi và ý kiến của bạn với chúng tôi thông qua các tài nguyên sau:

Công cụ theo dõi lỗi
Báo cáo sự cố để chúng tôi có thể sửa lỗi.