نمایه‌سازی با هدایت برنامه

در Android 15 (سطح میانای برنامه‌سازی کاربردی ۳۵) و بالاتر، نمایه‌سازی مبتنی بر برنامه به برنامه شما اجازه می‌دهد به‌طور صریح ضبط‌های نمایه پشتیبانی‌شده را در کد بااستفاده از میانای برنامه‌سازی کاربردی ProfilingManager یا سازندگان AndroidX Core (androidx.core:core ۱.۱۵.۰ یا بالاتر) شروع و متوقف کند. وقتی می‌خواهید زمان‌بندی نمایه یا پارامترهای جلسه (مانند مدت، خط‌مشی‌های میان‌گیر، و نرخ‌های نمونه‌برداری) را کنترل کنید یا سفر کاربر یا شرایط زمان اجرای خاصی را هدف قرار دهید، از نمایه‌سازی مستقیم استفاده کنید.

افزودن وابستگی‌ها

برای داشتن بهترین تجربه با ProfilingManager API، کتابخانه‌های Jetpack زیر را به فایل build.gradle.kts خود اضافه کنید.

کاتلین

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

ضبط ردیابی سیستم

پس‌از افزودن وابستگی‌های لازم، از کد زیر برای ضبط ردگیری سیستم استفاده کنید. این مثال نشان می‌دهد که چگونه می‌توان یک جلسه نمایه‌سازی را شروع کرد و درعین‌حال عملیات سنگین را به‌طور ایمن خارج از رشته اصلی مدیریت کرد.

کاتلین

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

جاوا

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 غیررابط کاربری ایجاد کنید تا نتایج نمایه‌سازی را خارج از رشته اصلی دریافت کنید.
  2. مدیریت نتایج نمایه‌سازی: یک Consumer<ProfilingResult> callback ایجاد کنید تا نتیجه تکمیل‌شده یا خطا را از ProfilingManager دریافت کنید.
  3. ساختن درخواست نمایه‌سازی: یک SystemTraceRequestBuilder ایجاد کنید تا تنظیمات جلسه را سفارشی‌سازی کنید (یا سفارشی‌سازی‌ها را حذف کنید تا از پیش‌فرض‌های سیستم استفاده شود):

    • setTag(): برچسبی را که تماس‌گیرنده ارائه کرده است پیوست می‌کند که توسط ProfilingResult.getTag برگردانده می‌شود و به نام فایل خروجی اضافه می‌شود (تا اولین ۲۰ نویسه الفباعددی کوچک و خط تیره).
    • setDurationMs(): حداکثر مدت ضبط را برحسب میلی‌ثانیه تنظیم می‌کند (برای مثال، 60000 برای ۶۰ ثانیه).
    • setBufferFillPolicy(): نحوه مدیریت محدودیت‌های ظرفیت توسط میان‌گیر را تعریف می‌کند: BufferFillPolicy.RING_BUFFER درصورت پر شدن، قدیمی‌ترین داده‌ها را رونویسی می‌کند، درحالی‌که BufferFillPolicy.DISCARD پس‌از پر شدن میان‌گیر، ضبط داده‌های جدید را متوقف می‌کند.
    • ‫setBufferSizeKb(): اندازه میان‌گیر ضبط را به کیلوبایت تنظیم می‌کند.
  4. اختیاری: توقف زودهنگام ضبط: CancellationSignal (stopSignal) را ارسال کنید تا جلسه به‌محض اتمام عملیات هدف متوقف شود، داده‌های ضبط‌شده تا آن لحظه ذخیره شود، و ProfilingResult به فراخوان برگشتی شما ارسال شود. برای جزئیات، به توقف ضبط فعال مراجعه کنید.

  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 (سطح میانای برنامه‌سازی کاربردی ۳۷) و بالاتر (یا Android 16 با Profiling به‌روزشده) را اجرا می‌کنند، ProfilingManager از جمع‌آوری پشته‌های فراخوانی نمونه‌برداری‌شده در کنار ردگیری سیستم در یک جلسه پشتیبانی می‌کند تا بتوانید رویدادهای خط زمان، مانند وضعیت‌های رشته، فریم‌ها، و برش‌های ردگیری سفارشی را همراه با پشته‌های فراخوانی CPU برای روش‌های بدون ابزار دقیق ردگیری صریح ببینید.

این مثال همچنین نشان می‌دهد که چگونه می‌توان ProfilingManager.requestProfiling را مستقیماً بدون سازندگان Jetpack با مشخص کردن یکی از انواع نمایه پشتیبانی‌شده (PROFILING_TYPE_SYSTEM_TRACE) و ارسال پیکربندی Bundle فراخوانی کرد:

کاتلین

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
)

جاوا

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 باز می‌کنید، پشته‌های فراخوانی نمونه‌برداری‌شده به‌عنوان قطعه‌های اختصاصی پشته‌های فراخوانی در کنار برش‌های ردیابی معمولی برنامه‌تان ظاهر می‌شوند. منطقه‌ای از قطعه پشته فراخوانی را انتخاب کنید تا نمودار شعله‌ای از پشته‌های فراخوانی نمونه‌برداری‌شده برای آن محدوده زمانی را مشاهده کنید، همان‌طور که در شکل ۱ نشان داده شده است.

Perfetto UI showing a system trace with sampled call stack tracks, trace
  slices on the RenderThread, and a call stack flame graph for the selected
  area.
شکل ۱. ردیابی سیستم همراه با نمونه‌برداری پشته فراخوانی در واسط کاربر Perfetto.

ضبط رونوشت پشته جاوا

می‌توانید بااستفاده از JavaHeapDumpRequestBuilder (یا PROFILING_TYPE_JAVA_HEAP_DUMP) رونوشتی از حافظه جاوا برنامه‌تان درخواست کنید. برخلاف رونوشت‌های پشته استاندارد .hprof که با Debug.dumpHprofData ضبط می‌شوند، رونوشت‌های پشته ProfilingManager برای جمع‌آوری ایمن و با سربار کم در محیط تولید طراحی شده‌اند:

  • مجموعه غیرانجمادی: سیستم یک کلون کپی-در-نوشتن از فرایند برنامه شما ایجاد می‌کند و تصویر کلی پشته را از فرایند کلون‌شده جمع‌آوری می‌کند، بنابراین برنامه شما درحالی‌که تخلیه در دیسک نوشته می‌شود، منجمد نمی‌شود.
  • فقط نمودار مرجع (بدون داده‌های شیء خام): به‌جای تخلیه محتویات حافظه خام مانند فایل .hprof، ProfilingManager یک .perfetto-java-heap-dump نمودار protobuf حاوی کلاس‌ها، اندازه‌های شیء، زنجیره‌های مرجع، و ریشه‌های جمع‌آوری زباله (GC) را ثبت می‌کند، درحالی‌که مقادیر اولیه، محتویات رشته، و محموله‌های آرایه را حذف می‌کند. این کار از حریم خصوصی کاربر محافظت می‌کند و اندازه فایل را کاهش می‌دهد. برای مشاهده تفکیک کامل، به تفاوت رونوشت‌های پشته ProfilingManager با رونوشت‌های HPROF مراجعه کنید.

بااستفاده از همان راه‌اندازی Context، Executor، و resultCallback نشان‌داده‌شده در ضبط ردیابی سیستم، درخواستی با JavaHeapDumpRequestBuilder بسازید:

کاتلین

val heapDumpRequest = JavaHeapDumpRequestBuilder()
    .setTag("memory-leak-check")
    .build()

requestProfiling(
    appContext,
    heapDumpRequest,
    callbackExecutor,
    resultCallback
)

جاوا

ProfilingRequest heapDumpRequest = new JavaHeapDumpRequestBuilder()
    .setTag("memory-leak-check")
    .build();

Profiling.requestProfiling(
    getApplicationContext(),
    heapDumpRequest,
    callbackExecutor,
    resultCallback
);

برای راهنمای گام‌به‌گام کامل و جامع درباره تحلیل رونوشت پشته جاوا برای تشخیص نشت حافظه، به مطالعه موردی: اشکال‌زدایی ازکارافتادن به‌دلیل کمبود حافظه مراجعه کنید.

مراحل بعدی

پس‌از ضبط نمایه‌ها، آن‌ها را به‌صورت محلی با adb بازیابی کنید یا با WorkManager در پسای خود بارگذاری کنید.