ใน 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. }
โค้ดตัวอย่างจะตั้งค่าและจัดการเซสชันการทำโปรไฟล์ตามขั้นตอนต่อไปนี้
- ตั้งค่า Executor: สร้าง
Executorที่ไม่ใช่ UI เพื่อรับผลการทำโปรไฟล์นอกเธรดหลัก - จัดการผลลัพธ์การทำโปรไฟล์: สร้าง Callback
Consumer<ProfilingResult>เพื่อรับผลลัพธ์ที่เสร็จสมบูรณ์หรือข้อผิดพลาดจากProfilingManager สร้างคำขอการทำโปรไฟล์: สร้าง
SystemTraceRequestBuilderเพื่อปรับแต่งการตั้งค่าเซสชัน (หรือข้ามการปรับแต่งเพื่อใช้ค่าเริ่มต้นของระบบ) ดังนี้setTag(): แนบแท็กที่ผู้โทรระบุซึ่งแสดงผลโดยProfilingResult.getTagและต่อท้ายชื่อไฟล์เอาต์พุต (อักขระที่เป็นตัวอักษรและตัวเลขคละกันตัวพิมพ์เล็กและขีดกลางสูงสุด 20 ตัวแรก)setDurationMs(): ตั้งค่าระยะเวลาการบันทึกสูงสุดเป็นมิลลิวินาที (เช่น60000สำหรับ 60 วินาที)setBufferFillPolicy(): กำหนดวิธีที่บัฟเฟอร์จัดการขีดจำกัดความจุ:BufferFillPolicy.RING_BUFFERจะเขียนทับข้อมูลที่เก่าที่สุดเมื่อเต็ม ส่วนBufferFillPolicy.DISCARDจะหยุดบันทึกข้อมูลใหม่เมื่อบัฟเฟอร์เต็มsetBufferSizeKb(): กำหนดขนาดบัฟเฟอร์การบันทึกเป็นกิโลไบต์
ไม่บังคับ: หยุดการบันทึกก่อนกำหนด: ส่ง
CancellationSignal(stopSignal) เพื่อหยุดเซสชันทันทีที่การดำเนินการเป้าหมายเสร็จสิ้น โดยบันทึกข้อมูลที่บันทึกไว้จนถึงตอนนี้และส่งProfilingResultไปยัง Callback ของคุณ ดูรายละเอียดได้ที่หัวข้อหยุดการบันทึกที่ทำงานอยู่เริ่มและรับผลลัพธ์: โทรหา
requestProfiling()เพื่อเริ่มบันทึกในเบื้องหลัง เมื่อเซสชันสิ้นสุดลงresultCallback.accept()จะได้รับProfilingResultดังนี้- เมื่อดำเนินการสำเร็จ (
ERROR_NONE) ให้เรียกใช้getResultFilePath()เพื่อรับเส้นทางในอุปกรณ์ ดูวิธีการดึงไฟล์ด้วยadbหรืออัปโหลดด้วยWorkManagerได้ที่หัวข้อดึงข้อมูลและอัปโหลดโปรไฟล์ - หากดำเนินการไม่สำเร็จ ให้ตรวจสอบ
getErrorCode()และgetErrorMessage()คำขออาจดำเนินการไม่สำเร็จเนื่องจากการจำกัดอัตราคำขอ รวมถึงสาเหตุอื่นๆ อีกหลายประการ - หากแอปสิ้นสุดก่อนที่จะส่งผลลัพธ์ ระบบจะจัดคิวผลลัพธ์และส่งผลลัพธ์ในครั้งถัดไปที่แอปลงทะเบียน Listener ส่วนกลางด้วย
registerForAllProfilingResults
- เมื่อดำเนินการสำเร็จ (
เพิ่มจุดติดตามที่กำหนดเอง: รวมการดำเนินการไว้ใน
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
บันทึกฮีปดัมป์ของ Java
คุณสามารถขอสแนปชอตหน่วยความจำ Java ของแอปได้โดยใช้ JavaHeapDumpRequestBuilder (หรือ PROFILING_TYPE_JAVA_HEAP_DUMP)
ฮีปดัมป์ ProfilingManager ต่างจากฮีปดัมป์ .hprof มาตรฐานที่บันทึกด้วย Debug.dumpHprofData ตรงที่ออกแบบมาเพื่อการรวบรวมข้อมูลที่ปลอดภัยและมีค่าใช้จ่ายต่ำในเวอร์ชันที่ใช้งานจริง ดังนี้
- คอลเล็กชันที่ไม่หยุดทำงาน: ระบบจะแยกโคลนแบบคัดลอกเมื่อเขียนของกระบวนการของแอปและรวบรวมสแนปชอตฮีปจากกระบวนการที่โคลน เพื่อให้แอปไม่หยุดทำงานขณะเขียนดัมพ์ลงในดิสก์
- กราฟอ้างอิงเท่านั้น (ไม่มีข้อมูลออบเจ็กต์ดิบ): แทนที่จะดัมพ์เนื้อหาหน่วยความจำดิบเหมือนไฟล์
.hprofProfilingManagerจะบันทึกกราฟ 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