Pembuatan profil yang didorong aplikasi

Di Android 15 (API level 35) dan yang lebih tinggi, pembuatan profil berbasis aplikasi memungkinkan aplikasi Anda memulai dan menghentikan perekaman profil yang didukung secara eksplisit dalam kode menggunakan builder ProfilingManager API atau AndroidX Core (androidx.core:core 1.15.0 atau yang lebih tinggi). Gunakan pembuatan profil langsung jika Anda ingin mengontrol waktu profil atau parameter sesi—seperti durasi, kebijakan buffering, dan frekuensi sampling—atau menargetkan perjalanan pengguna atau kondisi runtime tertentu.

Menambahkan dependensi

Untuk pengalaman terbaik dengan ProfilingManager API, tambahkan library Jetpack berikut ke file build.gradle.kts Anda.

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

Merekam pelacakan sistem

Setelah menambahkan dependensi yang diperlukan, gunakan kode berikut untuk merekam pelacakan sistem. Contoh ini menunjukkan cara memulai sesi pembuatan profil sambil mengelola operasi berat dengan aman di luar thread utama.

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

Kode contoh menyiapkan dan mengelola sesi pembuatan profil dalam langkah-langkah berikut:

  1. Siapkan eksekutor: Buat Executor non-UI untuk menerima hasil pembuatan profil di luar thread utama.
  2. Menangani hasil pembuatan profil: Buat callback Consumer<ProfilingResult> untuk menerima hasil yang telah selesai atau error dari ProfilingManager.
  3. Buat permintaan pembuatan profil: Buat SystemTraceRequestBuilder untuk menyesuaikan setelan sesi (atau hilangkan penyesuaian untuk menggunakan setelan default sistem):

    • setTag(): Melampirkan tag yang disediakan pemanggil yang ditampilkan oleh ProfilingResult.getTag dan ditambahkan ke nama file output (hingga 20 karakter alfanumerik huruf kecil dan tanda hubung pertama).
    • setDurationMs(): Menetapkan durasi perekaman maksimum dalam milidetik (misalnya, 60000 untuk 60 detik).
    • setBufferFillPolicy(): Menentukan cara buffer menangani batas kapasitas: BufferFillPolicy.RING_BUFFER menimpa data terlama saat penuh, sedangkan BufferFillPolicy.DISCARD berhenti merekam data baru setelah buffer penuh.
    • setBufferSizeKb(): Menetapkan ukuran buffering perekaman dalam kilobyte.
  4. Opsional: Hentikan perekaman lebih awal: Teruskan CancellationSignal (stopSignal) untuk menghentikan sesi segera setelah operasi target selesai, menyimpan data yang direkam sejauh ini, dan mengirimkan ProfilingResult ke callback Anda. Lihat Menghentikan perekaman aktif untuk mengetahui detailnya.

  5. Memulai dan menerima hasil: Panggil requestProfiling() untuk mulai merekam di latar belakang. Saat sesi berakhir, resultCallback.accept() akan menerima ProfilingResult:

    • Jika berhasil (ERROR_NONE), panggil getResultFilePath() untuk mendapatkan jalur di perangkat. Untuk mengetahui petunjuk tentang cara mengambil file dengan adb atau menguploadnya dengan WorkManager, lihat Mengambil dan mengupload profil.
    • Jika terjadi kegagalan, periksa getErrorCode() dan getErrorMessage(). Permintaan dapat gagal karena pembatasan kapasitas serta beberapa alasan lainnya.
    • Jika aplikasi Anda dihentikan sebelum hasilnya dikirim, sistem akan mengantrekan hasilnya dan mengirimkannya saat aplikasi Anda mendaftarkan pemroses global dengan registerForAllProfilingResults pada lain waktu.
  6. Menambahkan titik rekaman aktivitas kustom: Gabungkan operasi dalam trace("...") { ... } (atau Trace.beginSection() / Trace.endSection()) untuk memberi label pada region linimasa.

Menghentikan perekaman aktif

Untuk menghentikan perekaman segera setelah interaksi selesai, bukan menunggu setDurationMs() berakhir, teruskan CancellationSignal ke setCancellationSignal() atau requestProfiling(). Jika Anda menentukan CancellationSignal dan setDurationMs(), mana pun yang terjadi lebih dulu akan mengakhiri sesi; jika Anda tidak menentukan keduanya, sistem akan menerapkan durasi default.

Memanggil cancel() tidak akan menghapus rekaman. Sistem akan menghentikan perekaman, menyunting dan menyimpan data yang dikumpulkan hingga saat itu, serta mengirimkan ProfilingResult ke pemroses Anda dengan ERROR_NONE.

Merekam pelacakan sistem dengan pengambilan sampel stack panggilan

Pada perangkat yang menjalankan Android 17 (API level 37) dan yang lebih tinggi (atau Android 16 dengan modul mainline Profiling yang diupdate), ProfilingManager mendukung pengumpulan stack panggilan sampel bersama dengan pelacakan sistem dalam satu sesi sehingga Anda dapat melihat peristiwa linimasa, seperti status thread, frame, dan slice rekaman aktivitas kustom, bersama dengan stack panggilan CPU untuk metode tanpa instrumentasi rekaman aktivitas eksplisit.

Contoh ini juga menunjukkan cara memanggil ProfilingManager.requestProfiling secara langsung tanpa builder Jetpack dengan menentukan salah satu jenis profil yang didukung (PROFILING_TYPE_SYSTEM_TRACE) dan meneruskan konfigurasi 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
);

Saat Anda membuka profil yang dihasilkan di UI Perfetto, stack panggilan yang diambil sampelnya akan muncul sebagai trek Callstacks khusus bersama dengan slice rekaman aktivitas reguler aplikasi Anda. Pilih region trek stack panggilan untuk melihat flame graph stack panggilan yang dijadikan sampel untuk rentang waktu tersebut, seperti yang ditunjukkan pada Gambar 1.

UI Perfetto yang menampilkan pelacakan sistem dengan trek stack panggilan yang diambil sampelnya, slice pelacakan di RenderThread, dan flame graph stack panggilan untuk area yang dipilih.
Gambar 1. Pelacakan sistem yang digabungkan dengan pengambilan sampel stack panggilan di UI Perfetto.

Merekam heap dump Java

Anda dapat meminta snapshot memori Java aplikasi menggunakan JavaHeapDumpRequestBuilder (atau PROFILING_TYPE_JAVA_HEAP_DUMP). Tidak seperti heap dump .hprof standar yang diambil dengan Debug.dumpHprofData, heap dump ProfilingManager dirancang untuk pengumpulan yang aman dan beroverhead rendah dalam produksi:

  • Pengumpulan tanpa pembekuan: Sistem melakukan fork clone copy-on-write dari proses aplikasi Anda dan mengumpulkan cuplikan heap dari proses yang di-clone, sehingga aplikasi Anda tidak berhenti saat dump ditulis ke disk.
  • Hanya grafik referensi (tanpa data objek mentah): Alih-alih membuang konten memori mentah seperti file .hprof, ProfilingManager mencatat grafik protobuf .perfetto-java-heap-dump yang berisi class, ukuran objek, rantai referensi, dan root pembersihan sampah memori (GC) sekaligus menghilangkan nilai primitif, konten string, dan payload array. Tindakan ini melindungi privasi pengguna dan mengurangi ukuran file. Untuk perincian lengkap, lihat Perbedaan heap dump ProfilingManager dengan dump HPROF.

Dengan menggunakan penyiapan Context, Executor, dan resultCallback yang sama seperti yang ditunjukkan dalam Merekam pelacakan sistem, buat permintaan dengan 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
);

Untuk panduan menyeluruh yang lengkap tentang cara menganalisis heap dump Java untuk mendiagnosis kebocoran memori, lihat Studi kasus: Men-debug error OOM.

Langkah berikutnya

Setelah mengambil profil, ambil profil secara lokal dengan adb atau upload ke backend Anda dengan WorkManager.