การสร้างโปรไฟล์ที่ขับเคลื่อนโดยแอป

ใน Android 15 (ระดับ API 35) ขึ้นไป การทำโปรไฟล์ที่ขับเคลื่อนโดยแอปช่วยให้แอปของคุณเริ่มและหยุดการบันทึกโปรไฟล์ที่รองรับอย่างชัดเจนในโค้ดโดยใช้ API ProfilingManager หรือ AndroidX Core (androidx.core:core 1.15.0 ขึ้นไป) ใช้การทำโปรไฟล์โดยตรงเมื่อต้องการควบคุมเวลาของโปรไฟล์หรือพารามิเตอร์เซสชัน เช่น ระยะเวลา นโยบายบัฟเฟอร์ และอัตราการสุ่มตัวอย่าง หรือกำหนดเป้าหมายเส้นทางของผู้ใช้หรือเงื่อนไขรันไทม์ที่เฉพาะเจาะจง

เพิ่มทรัพยากร Dependency

เพื่อประสบการณ์การใช้งานที่ดีที่สุดกับ ProfilingManager API ให้เพิ่มไลบรารี Jetpack ต่อไปนี้ลงในไฟล์ build.gradle.kts

Kotlin

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

ดึงดูด

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

บันทึกการติดตามของระบบ

หลังจากเพิ่มทรัพยากร Dependency ที่จำเป็นแล้ว ให้ใช้โค้ดต่อไปนี้เพื่อบันทึกการติดตามของระบบ ตัวอย่างนี้แสดงวิธีเริ่มเซสชันการสร้างโปรไฟล์ขณะจัดการการดำเนินการที่หนักหน่วงนอกเทรดหลักอย่างปลอดภัย

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

โค้ดตัวอย่างจะตั้งค่าและจัดการเซสชันการทำโปรไฟล์ตามขั้นตอนต่อไปนี้

  1. ตั้งค่า Executor: สร้าง Executor ที่ไม่ใช่ UI เพื่อรับผลการทำโปรไฟล์นอกเธรดหลัก
  2. จัดการผลลัพธ์การทำโปรไฟล์: สร้าง Callback Consumer<ProfilingResult> เพื่อรับผลลัพธ์ที่เสร็จสมบูรณ์หรือข้อผิดพลาดจาก ProfilingManager
  3. สร้างคำขอการทำโปรไฟล์: สร้าง SystemTraceRequestBuilder เพื่อปรับแต่งการตั้งค่าเซสชัน (หรือข้ามการปรับแต่งเพื่อใช้ค่าเริ่มต้นของระบบ) ดังนี้

    • setTag(): แนบแท็กที่ผู้โทรระบุซึ่งแสดงผลโดย ProfilingResult.getTag และต่อท้ายชื่อไฟล์เอาต์พุต (อักขระที่เป็นตัวอักษรและตัวเลขคละกันตัวพิมพ์เล็กและขีดกลางสูงสุด 20 ตัวแรก)
    • setDurationMs(): ตั้งค่าระยะเวลาการบันทึกสูงสุดเป็นมิลลิวินาที (เช่น 60000 สำหรับ 60 วินาที)
    • setBufferFillPolicy(): กำหนดวิธีที่บัฟเฟอร์จัดการขีดจำกัดความจุ: BufferFillPolicy.RING_BUFFER จะเขียนทับข้อมูลที่เก่าที่สุดเมื่อเต็ม ส่วน BufferFillPolicy.DISCARD จะหยุดบันทึกข้อมูลใหม่เมื่อบัฟเฟอร์เต็ม
    • setBufferSizeKb(): กำหนดขนาดบัฟเฟอร์การบันทึกเป็นกิโลไบต์
  4. ไม่บังคับ: หยุดการบันทึกก่อนกำหนด: ส่ง CancellationSignal (stopSignal) เพื่อหยุดเซสชันทันทีที่การดำเนินการเป้าหมายเสร็จสิ้น โดยบันทึกข้อมูลที่บันทึกไว้จนถึงตอนนี้และส่ง ProfilingResult ไปยัง Callback ของคุณ ดูรายละเอียดได้ที่หัวข้อหยุดการบันทึกที่ทำงานอยู่

  5. เริ่มและรับผลลัพธ์: โทรหา requestProfiling() เพื่อเริ่มบันทึกในเบื้องหลัง เมื่อเซสชันสิ้นสุดลง resultCallback.accept() จะได้รับ ProfilingResult ดังนี้

    • เมื่อดำเนินการสำเร็จ (ERROR_NONE) ให้เรียกใช้ getResultFilePath() เพื่อรับเส้นทางในอุปกรณ์ ดูวิธีการดึงไฟล์ด้วย adb หรืออัปโหลดด้วย WorkManager ได้ที่หัวข้อดึงข้อมูลและอัปโหลดโปรไฟล์
    • หากดำเนินการไม่สำเร็จ ให้ตรวจสอบ getErrorCode() และ getErrorMessage() คำขออาจดำเนินการไม่สำเร็จเนื่องจากการจำกัดอัตราคำขอ รวมถึงสาเหตุอื่นๆ อีกหลายประการ
    • หากแอปสิ้นสุดก่อนที่จะส่งผลลัพธ์ ระบบจะจัดคิวผลลัพธ์และส่งผลลัพธ์ในครั้งถัดไปที่แอปลงทะเบียน Listener ส่วนกลางด้วย registerForAllProfilingResults
  6. เพิ่มจุดติดตามที่กำหนดเอง: รวมการดำเนินการไว้ใน trace("...") { ... } (หรือ Trace.beginSection() / Trace.endSection()) เพื่อติดป้ายกำกับภูมิภาคของไทม์ไลน์

หยุดการบันทึกที่ทำงานอยู่

หากต้องการหยุดการบันทึกทันทีที่การโต้ตอบเสร็จสมบูรณ์แทนที่จะรอให้ setDurationMs() หมดอายุ ให้ส่ง CancellationSignal ไปยัง setCancellationSignal() หรือ requestProfiling() หากคุณระบุทั้ง CancellationSignal และ setDurationMs() เซสชันจะสิ้นสุดลงเมื่อถึงเงื่อนไขใดเงื่อนไขหนึ่งก่อน แต่หากไม่ได้ระบุไว้ ระบบจะใช้ระยะเวลาเริ่มต้น

การโทรหา cancel() จะไม่ทิ้งการบันทึก ระบบจะหยุดการบันทึก ลบข้อมูล และบันทึกข้อมูลที่รวบรวมไว้จนถึงจุดนั้น จากนั้นจะส่ง ProfilingResult ไปยังผู้ฟังด้วย ERROR_NONE

บันทึกการติดตามของระบบด้วยการสุ่มตัวอย่างสแต็กการเรียกใช้

ในอุปกรณ์ที่ใช้ Android 17 (ระดับ API 37) ขึ้นไป (หรือ Android 16 ที่มีโมดูลหลัก Profiling ที่อัปเดตแล้ว) ProfilingManager รองรับการรวบรวมสแต็กการเรียกใช้ที่สุ่มตัวอย่างควบคู่ไปกับการติดตามของระบบในเซสชันเดียว เพื่อให้คุณเห็นเหตุการณ์ในไทม์ไลน์ เช่น สถานะเทรด เฟรม และส่วนการติดตามที่กำหนดเอง พร้อมกับสแต็กการเรียกใช้ CPU สำหรับเมธอดที่ไม่มีการวัดคุมการติดตามที่ชัดเจน

ตัวอย่างนี้ยังแสดงวิธีเรียกใช้ ProfilingManager.requestProfiling โดยตรงโดยไม่ต้องใช้ Jetpack Builder ด้วยการระบุประเภทโปรไฟล์ที่รองรับ (PROFILING_TYPE_SYSTEM_TRACE) และส่งการกำหนดค่า 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
);

เมื่อคุณเปิดโปรไฟล์ที่ได้ใน Perfetto UI สแต็กการเรียกใช้ที่สุ่มตัวอย่างจะปรากฏเป็นแทร็ก Callstacks โดยเฉพาะควบคู่ไปกับส่วนการติดตามปกติของแอป เลือกส่วนของแทร็กสแต็กการเรียกใช้เพื่อดูกราฟ Flame ของสแต็กการเรียกใช้ที่สุ่มตัวอย่างสำหรับช่วงเวลานั้น ดังที่แสดงในรูปที่ 1

UI ของ Perfetto แสดงการติดตามของระบบพร้อมแทร็กสแต็กการเรียกใช้ที่สุ่มตัวอย่าง ชิ้นส่วนการติดตามใน RenderThread และกราฟ Flame ของสแต็กการเรียกใช้สำหรับพื้นที่ที่เลือก
รูปที่ 1 การติดตามของระบบรวมกับการสุ่มตัวอย่างสแต็กการเรียกใช้ใน UI ของ Perfetto

บันทึกฮีปดัมป์ของ Java

คุณสามารถขอสแนปชอตหน่วยความจำ Java ของแอปได้โดยใช้ JavaHeapDumpRequestBuilder (หรือ PROFILING_TYPE_JAVA_HEAP_DUMP) ฮีปดัมป์ ProfilingManager ต่างจากฮีปดัมป์ .hprof มาตรฐานที่บันทึกด้วย Debug.dumpHprofData ตรงที่ออกแบบมาเพื่อการรวบรวมข้อมูลที่ปลอดภัยและมีค่าใช้จ่ายต่ำในเวอร์ชันที่ใช้งานจริง ดังนี้

  • คอลเล็กชันที่ไม่หยุดทำงาน: ระบบจะแยกโคลนแบบคัดลอกเมื่อเขียนของกระบวนการของแอปและรวบรวมสแนปชอตฮีปจากกระบวนการที่โคลน เพื่อให้แอปไม่หยุดทำงานขณะเขียนดัมพ์ลงในดิสก์
  • กราฟอ้างอิงเท่านั้น (ไม่มีข้อมูลออบเจ็กต์ดิบ): แทนที่จะดัมพ์เนื้อหาหน่วยความจำดิบเหมือนไฟล์ .hprof ProfilingManager จะบันทึกกราฟ protobuf .perfetto-java-heap-dump ที่มีคลาส ขนาดออบเจ็กต์ เชนอ้างอิง และรูท Garbage Collection (GC) โดยไม่รวมค่าพื้นฐาน เนื้อหาสตริง และเพย์โหลดอาร์เรย์ ซึ่งจะช่วยปกป้องความเป็นส่วนตัวของผู้ใช้และลดขนาดไฟล์ ดูรายละเอียดทั้งหมดได้ที่ความแตกต่างระหว่างฮีปดัมป์ของ ProfilingManager กับดัมป์ของ HPROF

ใช้การตั้งค่า Context, Executor และ resultCallback เดียวกันกับที่แสดงในบันทึกการติดตามของระบบ แล้วสร้างคำขอด้วย 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
);

ดูคำแนะนำแบบละเอียดตั้งแต่ต้นจนจบเกี่ยวกับการวิเคราะห์ฮีปดัมป์ของ Java เพื่อวินิจฉัยหน่วยความจำรั่วไหลได้ที่กรณีศึกษา: แก้ไขข้อบกพร่องของข้อขัดข้อง OOM

ขั้นตอนถัดไป

หลังจากบันทึกโปรไฟล์แล้ว ให้ดึงข้อมูลโปรไฟล์ในเครื่องด้วย adb หรืออัปโหลดไปยังแบ็กเอนด์ด้วย WorkManager