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

ב-Android בגרסה 15 (רמת API‏ 35) ומעלה, אפשר להשתמש בפרופיילינג מבוסס-אפליקציה כדי להפעיל ולהפסיק באופן מפורש הקלטות פרופיל נתמכות בקוד באמצעות ה-API‏ ProfilingManager או ה-builders של AndroidX Core (גרסה androidx.core:core 1.15.0 ומעלה). השתמשו ביצירת פרופילים ישירה כשאתם רוצים לשלוט בתזמון הפרופיל או בפרמטרים של הסשן, כמו משך הזמן, מדיניות המאגר ותדירות הדגימה, או כשאתם רוצים לטרגט התהליך שעובר המשתמש או תנאי ספציפי בזמן הריצה.

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

כדי ליהנות מחוויית השימוש הטובה ביותר ב-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-ktx: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 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 ללא ממשק משתמש כדי לקבל את תוצאות הפרופיילינג מחוץ לשרשור הראשי.
  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(). בקשות יכולות להיכשל בגלל הגבלת קצב של יצירת בקשות וגם מסיבות אחרות.
    • אם האפליקציה נסגרת לפני שהתוצאה נשלחת, המערכת מוסיפה את התוצאה לתור ושולחת אותה בפעם הבאה שהאפליקציה רושמת מאזין גלובלי באמצעות 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 ישירות בלי ה-builders של Jetpack, על ידי ציון אחד מסוגי הפרופילים הנתמכים (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, סטאקי הביצוע שנדגמו מופיעים כטראקים ייעודיים של Callstacks לצד הפרוסות הרגילות של המעקב אחרי האפליקציה. בוחרים אזור של טראק של סטאק ביצוע כדי להציג תרשים להבות של סטאקי הביצוע שנדגמו בטווח הזמן הזה, כמו שמוצג באיור 1.

ממשק המשתמש של Perfetto שמציג עקבות מערכת עם מסלולים של סטאק ביצוע שנדגמו, פרוסות של עקבות ב-RenderThread ותרשים להבות של סטאק ביצוע לאזור שנבחר.
איור 1. עקבות המערכת בשילוב עם דגימת סטאק ביצוע בממשק המשתמש של Perfetto.

יצירת תמונת מצב של הזיכרון מ-Java

אתם יכולים לבקש תמונת מצב של זיכרון ה-Java של האפליקציה באמצעות JavaHeapDumpRequestBuilder (או PROFILING_TYPE_JAVA_HEAP_DUMP). בניגוד לתמונות מצב רגילות של זיכרון .hprof שנוצרות באמצעות Debug.dumpHprofData, תמונות המצב של זיכרון ProfilingManager מיועדות לאיסוף בטוח עם תקורה נמוכה בסביבת הייצור:

  • איסוף ללא הקפאה: המערכת יוצרת עותק של תהליך האפליקציה באמצעות copy-on-write, ואוספת את קובץ ה-heap snapshot מהתהליך המשוכפל. כך האפליקציה לא קופאת בזמן שה-dump נכתב בדיסק.
  • תרשים עזר בלבד (ללא נתוני אובייקטים גולמיים): במקום להעתיק את תוכן הזיכרון הגולמי כמו קובץ .hprof, הכלי ProfilingManager מתעד תרשים protobuf של .perfetto-java-heap-dump שמכיל מחלקות, גודלי אובייקטים, שרשראות עזר ושורשי איסוף אשפה (GC), תוך השמטת ערכים פרימיטיביים, תוכן של מחרוזות ומטענים ייעודיים של מערכים. כך אפשר להגן על פרטיות המשתמשים ולהקטין את גודל הקובץ. לפירוט מלא, קראו את המאמר ההבדלים בין תמונות מצב של הזיכרון ב-ProfilingManager לבין קובצי dump מסוג 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
);

במאמר מקרה לדוגמה: ניפוי באגים של קריסה מסוג OOM מוסבר איך לנתח תמונת מצב של הזיכרון מ-Java כדי לאבחן דליפת זיכרון.

השלבים הבאים

אחרי שיוצרים פרופילים, אפשר לאחזר אותם באופן מקומי באמצעות adb או להעלות אותם לקצה העורפי באמצעות WorkManager.