Phân tích hiệu suất dựa trên ứng dụng

Trên Android 15 (cấp độ API 35) trở lên, tính năng phân tích do ứng dụng điều khiển cho phép ứng dụng của bạn bắt đầu và dừng bản ghi phân tích được hỗ trợ một cách rõ ràng trong mã bằng cách sử dụng API ProfilingManager hoặc trình tạo AndroidX Core (androidx.core:core 1.15.0 trở lên). Sử dụng tính năng lập hồ sơ trực tiếp khi bạn muốn kiểm soát thời gian lập hồ sơ hoặc các tham số phiên (chẳng hạn như thời lượng, chính sách vùng đệm và tốc độ lấy mẫu) hoặc nhắm đến một hành trình của người dùng cụ thể hoặc điều kiện thời gian chạy.

Thêm phần phụ thuộc

Để có trải nghiệm tốt nhất với API ProfilingManager, hãy thêm các thư viện Jetpack sau vào tệp build.gradle.kts của bạn.

Kotlin

   dependencies {
       implementation("androidx.tracing:tracing-ktx:2.0.5")
       implementation("androidx.core:core:1.19.1")
   }
   

Groovy

   dependencies {
       implementation 'androidx.tracing:tracing-ktx:2.0.5'
       implementation 'androidx.core:core:1.19.1'
   }
   

Ghi lại dấu vết hệ thống

Sau khi thêm các phần phụ thuộc bắt buộc, hãy sử dụng mã sau để ghi lại dấu vết hệ thống. Ví dụ này cho thấy cách bắt đầu một phiên phân tích tài nguyên trong khi quản lý an toàn các thao tác nặng ngoài luồng chính.

Kotlin

@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
@Composable
fun ProfiledScreen(modifier: Modifier = Modifier) {
    // Use the application context: requestProfiling resolves the ProfilingManager
    // system service from it, so there's no reason to hand it a short-lived Activity.
    val appContext = LocalContext.current.applicationContext
    val scope = rememberCoroutineScope()

    Button(
        onClick = {
            // Run the orchestration off the main thread. Profiling a heavy operation
            // on the UI thread would freeze the UI (ANR) and distort the very metrics
            // you're trying to capture.
            //
            // Note: this scope is tied to composition. If the user leaves this screen
            // mid-session, the coroutine is cancelled and stopSignal.cancel() might not
            // run, but setDurationMs() acts as a safety net and ends the profile.
            scope.launch(Dispatchers.Default) {
                val callbackExecutor = Dispatchers.IO.asExecutor()
                val resultCallback = Consumer<ProfilingResult> { profilingResult ->
                    if (profilingResult.errorCode == ProfilingResult.ERROR_NONE) {
                        Log.d("ProfileTest", "Result file: ${profilingResult.resultFilePath}")
                    } else {
                        // errorMessage explains the failure (e.g., rate limiting); keep it.
                        Log.e(
                            "ProfileTest",
                            "Profiling failed errorCode=${profilingResult.errorCode} " +
                                "errorMessage=${profilingResult.errorMessage}"
                        )
                    }
                }

                val stopSignal = CancellationSignal()
                val requestBuilder = SystemTraceRequestBuilder().apply {
                    setCancellationSignal(stopSignal)
                    setTag("FOO") // Caller-supplied tag for identification.
                    setDurationMs(60000) // Hard cap: ends the session if cancel() never fires.
                    setBufferFillPolicy(BufferFillPolicy.RING_BUFFER)
                    setBufferSizeKb(32768)
                }

                // 1. Start the session. This is asynchronous system IPC. The tracing
                //    engine takes a moment to start and allocate buffers.
                requestProfiling(appContext, requestBuilder.build(), callbackExecutor, resultCallback)

                // 2. The API exposes no "profiling started" signal, so pad with a short,
                //    best-effort delay before running the code you care about. This is
                //    approximate. Increase it on slower or heavily loaded devices.
                delay(STARTUP_PADDING_MS)

                // 3. The session is already recording every thread in your app. This slice
                //    doesn't scope what's captured. It just labels this region of the
                //    timeline so heavyOperation() is easier to find. trace { } closes the
                //    section even if the block throws.

                trace("MyApp:HeavyOperation") {
                    heavyOperation()
                }

                // 4. Stop recording. Until this fires or the setDurationMs() cap is
                //    reached (whichever comes first), the session keeps capturing app-wide
                //    activity.

                stopSignal.cancel()
            }
        }
    ) {
        Text("Run & Profile Heavy Operation")
    }
}

// Best-effort wait for the system trace engine to initialize before profiling.
// There is no deterministic start callback; tune this for your target devices.
private const val STARTUP_PADDING_MS = 100L

fun heavyOperation() {
    // Background computations to profile.
}

Java

void sampleRecordSystemTrace(Context context) {
  // Use the application context: requestProfiling resolves the ProfilingManager
  // system service from it, so there's no reason to hand it a short-lived Activity.
  Context appContext = context.getApplicationContext();

  // Run the orchestration off the main thread. Profiling a heavy operation
  // on the UI thread would freeze the UI (ANR) and distort the very metrics
  // you're trying to capture.
  profilingExecutor.execute(() -> {
    Executor callbackExecutor = Executors.newSingleThreadExecutor();
    Consumer<ProfilingResult> resultCallback = profilingResult -> {
      if (profilingResult.getErrorCode() == ProfilingResult.ERROR_NONE) {
        Log.d("ProfileTest", "Result file: " + profilingResult.getResultFilePath());
      } else {
        // errorMessage explains the failure (e.g., rate limiting); keep it.
        Log.e(
            "ProfileTest",
            "Profiling failed errorCode=" + profilingResult.getErrorCode()
                + " errorMessage=" + profilingResult.getErrorMessage());
      }
    };

    CancellationSignal stopSignal = new CancellationSignal();
    SystemTraceRequestBuilder requestBuilder = new SystemTraceRequestBuilder();
    requestBuilder.setCancellationSignal(stopSignal);
    requestBuilder.setTag("FOO"); // Caller-supplied tag for identification.
    requestBuilder.setDurationMs(60000); // Hard cap: ends the session if cancel() never fires.
    requestBuilder.setBufferFillPolicy(BufferFillPolicy.RING_BUFFER);
    requestBuilder.setBufferSizeKb(32768);

    // 1. Start the session. This is asynchronous system IPC. The tracing
    //    engine takes a moment to start and allocate buffers.
    Profiling.requestProfiling(
        appContext, requestBuilder.build(), callbackExecutor, resultCallback);

    // 2. The API exposes no "profiling started" signal, so pad with a short,
    //    best-effort delay before running the code you care about. This is
    //    approximate. Increase it on slower or heavily loaded devices.
    try {
      Thread.sleep(STARTUP_PADDING_MS);
    } catch (InterruptedException e) {
      Thread.currentThread().interrupt();
      stopSignal.cancel();
      return;
    }

    // 3. The session is already recording every thread in your app. This slice
    //    doesn't scope what's captured. It just labels this region of the
    //    timeline so heavyOperation() is easier to find. try/finally closes the
    //    section even if heavyOperation() throws.
    Trace.beginSection("MyApp:HeavyOperation");
    try {
      heavyOperation();
    } finally {
      Trace.endSection();
    }

    // 4. Stop recording. Until this fires or the setDurationMs() cap is
    //    reached (whichever comes first), the session keeps capturing app-wide
    //    activity.
    stopSignal.cancel();
  });
}

// Best-effort wait for the system trace engine to initialize before profiling.
// There is no deterministic start callback; tune this for your target devices.
private static final long STARTUP_PADDING_MS = 100L;

// Single background thread that runs the profiling orchestration.
private final ExecutorService profilingExecutor = Executors.newSingleThreadExecutor();

void heavyOperation() {
  // Background computations to profile.
}

Mã mẫu thiết lập và quản lý phiên phân tích tài nguyên theo các bước sau:

  1. Thiết lập trình thực thi: Tạo một Executor không phải giao diện người dùng để nhận kết quả phân tích tài nguyên ngoài luồng chính.
  2. Xử lý kết quả phân tích tài nguyên: Tạo lệnh gọi lại Consumer<ProfilingResult> để nhận kết quả hoàn tất hoặc lỗi từ ProfilingManager.
  3. Tạo yêu cầu lập hồ sơ: Tạo một SystemTraceRequestBuilder để tuỳ chỉnh chế độ cài đặt phiên (hoặc bỏ qua các tuỳ chỉnh để sử dụng chế độ mặc định của hệ thống):

    • setTag(): Đính kèm thẻ do phương thức gọi cung cấp, được trả về bằng ProfilingResult.getTag và được thêm vào tên tệp đầu ra (tối đa 20 ký tự chữ-số viết thường và dấu gạch nối đầu tiên).
    • setDurationMs(): Đặt thời lượng ghi hình tối đa tính bằng mili giây (ví dụ: 60000 cho 60 giây).
    • setBufferFillPolicy(): Xác định cách vùng đệm xử lý giới hạn dung lượng: BufferFillPolicy.RING_BUFFER ghi đè dữ liệu cũ nhất khi đầy, trong khi BufferFillPolicy.DISCARD ngừng ghi dữ liệu mới sau khi vùng đệm đầy.
    • setBufferSizeKb(): Đặt dung lượng bộ nhớ đệm ghi theo kilobyte.
  4. Không bắt buộc: Dừng ghi sớm: Truyền CancellationSignal (stopSignal) để dừng phiên ngay khi thao tác mục tiêu kết thúc, lưu dữ liệu đã ghi cho đến nay và phân phối ProfilingResult đến lệnh gọi lại của bạn. Hãy xem phần Dừng một bản ghi đang hoạt động để biết thông tin chi tiết.

  5. Bắt đầu và nhận kết quả: Gọi requestProfiling() để bắt đầu quay video trong nền. Khi phiên kết thúc, resultCallback.accept() sẽ nhận được một ProfilingResult:

    • Khi thành công (ERROR_NONE), hãy gọi getResultFilePath() để lấy đường dẫn trên thiết bị. Để biết hướng dẫn về cách lấy tệp bằng adb hoặc tải tệp lên bằng WorkManager, hãy xem phần Truy xuất và tải hồ sơ lên.
    • Nếu không thành công, hãy kiểm tra getErrorCode() và getErrorMessage(). Yêu cầu có thể không thành công do giới hạn tốc độ cũng như một số lý do khác.
    • Nếu ứng dụng của bạn chấm dứt trước khi kết quả được phân phối, hệ thống sẽ đưa kết quả vào hàng đợi và phân phối kết quả vào lần tiếp theo ứng dụng của bạn đăng ký trình nghe chung bằng registerForAllProfilingResults.
  6. Thêm điểm theo dõi tuỳ chỉnh: Gói các thao tác trong trace("...") { ... } (hoặc Trace.beginSection()/Trace.endSection()) để gắn nhãn các vùng trên dòng thời gian.

Dừng quá trình quay video đang hoạt động

Để dừng quá trình chụp ngay khi một lượt tương tác hoàn tất thay vì chờ setDurationMs() hết hạn, hãy truyền CancellationSignal đến setCancellationSignal() hoặc requestProfiling(). Nếu bạn chỉ định cả CancellationSignal và setDurationMs(), thì sự kiện nào xảy ra trước sẽ kết thúc phiên; nếu bạn không chỉ định cả hai, hệ thống sẽ áp dụng thời lượng mặc định.

Việc gọi cancel() không loại bỏ bản ghi. Hệ thống sẽ dừng quá trình thu thập dữ liệu, loại bỏ và lưu dữ liệu đã thu thập được cho đến thời điểm đó, đồng thời phân phối ProfilingResult đến trình nghe của bạn bằng ERROR_NONE.

Ghi lại dấu vết hệ thống bằng tính năng lấy mẫu ngăn xếp lệnh gọi

Trên các thiết bị chạy Android 17 (cấp độ API 37) trở lên (hoặc Android 16 có mô-đun chính Profiling đã cập nhật), ProfilingManager hỗ trợ thu thập các ngăn xếp lệnh gọi được lấy mẫu cùng với dấu vết hệ thống trong một phiên duy nhất để bạn có thể xem các sự kiện trên dòng thời gian, chẳng hạn như trạng thái luồng, khung hình và các lát cắt dấu vết tuỳ chỉnh, cùng với ngăn xếp lệnh gọi CPU cho các phương thức không có khả năng đo lường dấu vết rõ ràng.

Ví dụ này cũng cho thấy cách gọi trực tiếp ProfilingManager.requestProfiling mà không cần trình tạo Jetpack bằng cách chỉ định một trong các loại hồ sơ được hỗ trợ (PROFILING_TYPE_SYSTEM_TRACE) và truyền cấu hình Bundle:

Kotlin

val profilingManager = context.getSystemService(ProfilingManager::class.java)

// Pass a Bundle to customize the capture, or pass null for default settings.
val config = Bundle().apply {
    putInt("KEY_DURATION_MS", 15000) // 15 seconds
    putInt("KEY_SIZE_KB", 20480) // 20 MB buffer size

    // Collect sampled CPU call stacks alongside the system trace.
    putBoolean("KEY_COLLECT_STACK_SAMPLING", true)
    putInt("KEY_FREQUENCY_HZ", 100) // Sampling frequency in Hz
    // Optional: Set KEY_SAMPLE_BINDER_ONLY to true to sample only binder calls.
}
profilingManager?.requestProfiling(
    ProfilingManager.PROFILING_TYPE_SYSTEM_TRACE,
    config,
    "trace-with-callstacks",
    null,
    callbackExecutor,
    resultCallback
)

Java

ProfilingManager profilingManager = context.getSystemService(ProfilingManager.class);
if (profilingManager == null) return;

// Pass a Bundle to customize the capture, or pass null for default settings.
Bundle config = new Bundle();
config.putInt("KEY_DURATION_MS", 15000); // 15 seconds
config.putInt("KEY_SIZE_KB", 20480); // 20 MB buffer size

// Collect sampled CPU call stacks alongside the system trace.
config.putBoolean("KEY_COLLECT_STACK_SAMPLING", true);
config.putInt("KEY_FREQUENCY_HZ", 100); // Sampling frequency in Hz
// Optional: Set KEY_SAMPLE_BINDER_ONLY to true to sample only binder calls.

profilingManager.requestProfiling(
    ProfilingManager.PROFILING_TYPE_SYSTEM_TRACE,
    config,
    "trace-with-callstacks",
    null,
    callbackExecutor,
    resultCallback
);

Khi bạn mở hồ sơ kết quả trong giao diện người dùng Perfetto, các ngăn xếp lệnh gọi được lấy mẫu sẽ xuất hiện dưới dạng các bản nhạc Ngăn xếp lệnh gọi chuyên dụng cùng với các lát cắt theo dõi thông thường của ứng dụng. Chọn một vùng của kênh ngăn xếp lệnh gọi để xem biểu đồ hình ngọn lửa của các ngăn xếp lệnh gọi được lấy mẫu cho phạm vi thời gian đó, như trong Hình 1.

Giao diện người dùng Perfetto cho thấy dấu vết hệ thống với các bản nhạc ngăn xếp lệnh gọi được lấy mẫu, các lát cắt dấu vết trên RenderThread và biểu đồ ngọn lửa ngăn xếp lệnh gọi cho khu vực đã chọn.
Hình 1. Dấu vết hệ thống kết hợp với tính năng lấy mẫu ngăn xếp lệnh gọi trong giao diện người dùng Perfetto.

Ghi tệp báo lỗi Java

Bạn có thể yêu cầu ảnh chụp nhanh bộ nhớ Java của ứng dụng bằng cách sử dụng JavaHeapDumpRequestBuilder (hoặc PROFILING_TYPE_JAVA_HEAP_DUMP). Không giống như tệp báo lỗi vùng nhớ .hprof tiêu chuẩn được ghi lại bằng Debug.dumpHprofData, tệp báo lỗi vùng nhớ ProfilingManager được thiết kế để thu thập an toàn, ít hao tổn tài nguyên trong môi trường thực tế:

  • Thu thập không bị treo: Hệ thống phân nhánh một bản sao chép khi ghi (copy-on-write) của quy trình ứng dụng và thu thập snapshot của vùng nhớ heap từ quy trình được sao chép, nhờ đó ứng dụng không bị treo trong khi tệp báo lỗi được ghi vào ổ đĩa.
  • Chỉ biểu đồ tham chiếu (không có dữ liệu đối tượng thô): Thay vì kết xuất nội dung bộ nhớ thô như tệp .hprof, ProfilingManager sẽ ghi lại biểu đồ protobuf .perfetto-java-heap-dump chứa các lớp, kích thước đối tượng, chuỗi tham chiếu và gốc thu gom rác (GC) trong khi bỏ qua các giá trị gốc, nội dung chuỗi và tải trọng mảng. Điều này giúp bảo vệ quyền riêng tư của người dùng và giảm kích thước tệp. Để biết thông tin chi tiết đầy đủ, hãy xem phần Sự khác biệt giữa tệp báo lỗi vùng nhớ khối xếp của ProfilingManager và tệp báo lỗi HPROF.

Sử dụng cùng một chế độ thiết lập Context, Executor và resultCallback như trong phần Ghi lại dấu vết hệ thống, hãy tạo một yêu cầu bằng JavaHeapDumpRequestBuilder:

Kotlin

val heapDumpRequest = JavaHeapDumpRequestBuilder()
    .setTag("memory-leak-check")
    .build()

requestProfiling(
    appContext,
    heapDumpRequest,
    callbackExecutor,
    resultCallback
)

Java

ProfilingRequest heapDumpRequest = new JavaHeapDumpRequestBuilder()
    .setTag("memory-leak-check")
    .build();

Profiling.requestProfiling(
    getApplicationContext(),
    heapDumpRequest,
    callbackExecutor,
    resultCallback
);

Để xem hướng dẫn đầy đủ từ đầu đến cuối về cách phân tích tệp báo lỗi Java để chẩn đoán lỗi rò rỉ bộ nhớ, hãy xem phần Nghiên cứu điển hình: Gỡ lỗi sự cố OOM.

Các bước tiếp theo

Sau khi thu thập hồ sơ, hãy truy xuất hồ sơ trên thiết bị bằng adb hoặc tải hồ sơ lên phần phụ trợ bằng WorkManager.