יצירת פרופילים מבוססת-אפליקציה

בדף הזה מוסבר איך לתעד את עקבות המערכת באמצעות ProfilingManager API.

ProfilingManager יכול גם להקליט סוגים אחרים של פרופילים. התהליך הזה דומה להקלטה של עקבות המערכת, אבל כל סוג משתמש ב-builder אחר. הפרופילים הנתמכים והכלים ליצירת פרופילים הם:

  • עקבות המערכת: מתועדים באמצעות SystemTraceRequestBuilder, ומועילים לניתוח זמן האחזור ולניפוי באגים כללי בביצועים.

  • תמונות מצב של הזיכרון: מתועדות באמצעות JavaHeapDumpRequestBuilder, ועוזרות לזהות דליפות זיכרון ולבצע אופטימיזציה.

  • תמונות מצב של ערימה: מתועדות באמצעות HeapProfileRequestBuilder, ומועילות לאופטימיזציה של הזיכרון.

  • פרופילים של סטאק ביצוע: מתועדים באמצעות StackSamplingRequestBuilder, ושימושיים להבנת הרצת הקוד ולניתוח זמן האחזור.

הוספת יחסי תלות

כדי ליהנות מחוויית השימוש הטובה ביותר ב-API של ProfilingManager, צריך להוסיף את ספריות 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:2.0.5'
       implementation 'androidx.core:core:1.19.1'
   }
   

תיעוד עקבות המערכת

אחרי שמוסיפים את יחסי התלות הנדרשים, משתמשים בקוד הבא כדי לתעד את עקבות המערכת. בדוגמה הזו מוסבר איך להתחיל סשן פרופיילינג מ-פונקציה קומפוזבילית תוך ניהול בטוח של פעולות כבדות מחוץ ל-Thread הראשי.

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 trace.
            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 heavyOperation() {
  // Computations you want to profile
}

void sampleRecordSystemTrace() {
  Executor mainExecutor = Executors.newSingleThreadExecutor();
  Consumer<ProfilingResult> resultCallback =
      new Consumer<ProfilingResult>() {
        @Override
        public void accept(ProfilingResult profilingResult) {
          if (profilingResult.getErrorCode() == ProfilingResult.ERROR_NONE) {
            Log.d(
                "ProfileTest",
                "Received profiling result file=" + profilingResult.getResultFilePath());
            setupProfileUploadWorker(profilingResult.getResultFilePath());
          } else {
            Log.e(
                "ProfileTest",
                "Profiling failed errorcode="

                    + profilingResult.getErrorCode()
                    + " errormsg="
                    + profilingResult.getErrorMessage());
          }
        }
      };
  CancellationSignal stopSignal = new CancellationSignal();

  SystemTraceRequestBuilder requestBuilder = new SystemTraceRequestBuilder();
  requestBuilder.setCancellationSignal(stopSignal);
  requestBuilder.setTag("FOO");
  requestBuilder.setDurationMs(60000);
  requestBuilder.setBufferFillPolicy(BufferFillPolicy.RING_BUFFER);
  requestBuilder.setBufferSizeKb(32768);
  Profiling.requestProfiling(getApplicationContext(), requestBuilder.build(), mainExecutor,
      resultCallback);

  // Wait some time for profiling to start.

  Trace.beginSection("MyApp:HeavyOperation");
  heavyOperation();
  Trace.endSection();

  // Once the interesting code section is profiled, stop profile
  stopSignal.cancel();
}

הקוד לדוגמה מגדיר ומנהל את סשן יצירת הפרופיל באמצעות השלבים הבאים:

  1. מגדירים את מנהל העיזבון. יוצרים Executor כדי להגדיר את ה-thread שיקבל את תוצאות יצירת הפרופיל. הפרופיילינג מתבצע ברקע. שימוש ב-executor של שרשור שאינו UI עוזר למנוע שגיאות מסוג "האפליקציה לא מגיבה" (ANR) אם מוסיפים עוד עיבוד לקריאה החוזרת (callback) בשלב מאוחר יותר.

  2. טיפול בתוצאות של יצירת הפרופילים. יוצרים אובייקט Consumer<ProfilingResult>. המערכת משתמשת באובייקט הזה כדי לשלוח את תוצאות יצירת הפרופיל מ-ProfilingManager בחזרה לאפליקציה.

  3. יוצרים את בקשת הפרופיילינג. יוצרים SystemTraceRequestBuilder כדי להגדיר את סשן יצירת הפרופיל. הכלי הזה מאפשר להתאים אישית את הגדרות המעקב של ProfilingManager. ההתאמה האישית של ה-builder היא אופציונלית. אם לא תבצעו התאמה אישית, המערכת תשתמש בהגדרות ברירת המחדל.

    • מגדירים תג. אפשר להשתמש ב-setTag() כדי להוסיף תג לשם המעקב. התג הזה עוזר לזהות את המעקב.
    • אופציונלי: מגדירים את משך הזמן. משתמשים ב-setDurationMs() כדי לציין את משך הזמן של יצירת הפרופיל באלפיות השנייה. לדוגמה, 60000 מגדיר מעקב של 60 שניות. המעקב מסתיים באופן אוטומטי אחרי משך הזמן שצוין, אם לא מפעילים את CancellationSignal לפני כן.
    • בוחרים מדיניות מאגר נתונים זמני. משתמשים ב-setBufferFillPolicy() כדי להגדיר איך נתוני המעקב נשמרים. BufferFillPolicy.RING_BUFFER פירושו שכאשר המאגר מלא, נתונים חדשים מחליפים את הנתונים הישנים ביותר, וכך נשמר תיעוד רציף של הפעילות האחרונה.
    • מגדירים את גודל המאגר. אפשר להשתמש ב-setBufferSizeKb() כדי לציין את גודל המאגר למעקב, שבאמצעותו אפשר לשלוט בגודל של קובץ המעקב שנוצר.
  4. אופציונלי: ניהול מחזור החיים של הסשן. יוצרים CancellationSignal. האובייקט הזה מאפשר לכם לעצור את סשן הפרופיילינג מתי שתרצו, וכך לשלוט באופן מדויק במשך שלו.

  5. מתחילים ומקבלים תוצאות. כשקוראים לפונקציה requestProfiling(), ProfilingManager מתחיל סשן של יצירת פרופיל ברקע. בסיום יצירת הפרופיל, הוא שולח את ה-ProfilingResult ל-method‏ resultCallback#accept. אם יצירת הפרופיל מסתיימת בהצלחה, ProfilingResult מספק את הנתיב שבו נשמר המעקב במכשיר באמצעות ProfilingResult#getResultFilePath. אפשר לקבל את הקובץ הזה באופן פרוגרמטי, או על ידי הפעלת adb pull <trace_path> מהמחשב, כדי ליצור פרופיל מקומי.

  6. הוספת נקודות מעקב בהתאמה אישית אתם יכולים להוסיף נקודות מעקב בהתאמה אישית בקוד של האפליקציה. בדוגמת הקוד הקודמת, הבלוק trace("MyApp:HeavyOperation") { ... } יוצר פרוסה בהתאמה אישית בפרופיל שנוצר.