En Android 15 (nivel de API 35) y versiones posteriores, la generación de perfiles controlada por la app permite que esta inicie y detenga de forma explícita las grabaciones de perfiles compatibles en el código con la API de ProfilingManager o los compiladores de AndroidX Core (androidx.core:core 1.15.0 o versiones posteriores). Usa la generación de perfiles directa cuando quieras controlar los tiempos de generación de perfiles
o los parámetros de sesión, como las duraciones, las políticas de búfer y las tasas de muestreo, o
cuando quieras orientar el proceso a un recorrido del usuario o una condición de tiempo de ejecución específicos.
Cómo agregar dependencias
Para obtener la mejor experiencia con la API de ProfilingManager, agrega las siguientes bibliotecas de Jetpack a tu archivo build.gradle.kts.
Kotlin
dependencies { implementation("androidx.tracing:tracing-ktx:2.0.5") implementation("androidx.core:core:1.19.1") }
Groovy
dependencies { implementation 'androidx.tracing:tracing-ktx:2.0.5' implementation 'androidx.core:core:1.19.1' }
Cómo registrar un seguimiento del sistema
Después de agregar las dependencias requeridas, usa el siguiente código para registrar un seguimiento del sistema. En este ejemplo, se muestra cómo iniciar una sesión de generación de perfiles mientras se administran de forma segura las operaciones pesadas fuera del subproceso principal.
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. }
El código de muestra configura y administra la sesión de generación de perfiles en los siguientes pasos:
- Configura el ejecutor: Crea un
Executorque no sea de IU para recibir los resultados de la generación de perfiles fuera del subproceso principal. - Controla los resultados de la generación de perfiles: Crea una devolución de llamada
Consumer<ProfilingResult>para recibir el resultado completado o el error deProfilingManager. Compila la solicitud de generación de perfiles: Crea un
SystemTraceRequestBuilderpara personalizar la configuración de la sesión (u omite las personalizaciones para usar los valores predeterminados del sistema):setTag(): Adjunta una etiqueta proporcionada por el emisor que devuelveProfilingResult.getTagy se agrega al nombre del archivo de salida (hasta los primeros 20 caracteres alfanuméricos en minúscula y guiones).setDurationMs(): Establece la duración máxima de la grabación en milisegundos (por ejemplo,60000para 60 segundos).setBufferFillPolicy(): Define cómo el búfer controla los límites de capacidad:BufferFillPolicy.RING_BUFFERreemplaza los datos más antiguos cuando está lleno, mientras queBufferFillPolicy.DISCARDdeja de registrar datos nuevos una vez que el búfer está lleno.setBufferSizeKb(): Establece el tamaño del búfer de grabación en kilobytes.
Opcional: Detén la grabación antes de tiempo: Pasa un
CancellationSignal(stopSignal) para detener la sesión en cuanto finalice la operación de destino, guardar los datos grabados hasta el momento y entregar elProfilingResulta tu devolución de llamada. Consulta Cómo detener una grabación activa para obtener más detalles.Inicia y recibe resultados: Llama a
requestProfiling()para comenzar a grabar en segundo plano. Cuando finaliza la sesión,resultCallback.accept()recibe unProfilingResult:- Si se realiza correctamente (
ERROR_NONE), llama agetResultFilePath()para obtener la ruta de acceso integrada en el dispositivo. Para obtener instrucciones sobre cómo extraer archivos conadbo subirlos conWorkManager, consulta Recupera y sube perfiles. - Si se produce un error, inspecciona
getErrorCode()ygetErrorMessage(). Las solicitudes pueden fallar debido a la limitación de frecuencia y a varios otros motivos. - Si tu app finaliza antes de que se entregue el resultado, el sistema lo pone en cola y lo entrega la próxima vez que tu app registre un objeto de escucha global con
registerForAllProfilingResults.
- Si se realiza correctamente (
Agrega puntos de seguimiento personalizados: Une las operaciones en
trace("...") { ... }(oTrace.beginSection()/Trace.endSection()) para etiquetar regiones del cronograma.
Cómo detener una grabación activa
Para detener una captura en cuanto se complete una interacción en lugar de esperar a que venza setDurationMs(), pasa un CancellationSignal a setCancellationSignal() o requestProfiling(). Si especificas
CancellationSignal y setDurationMs(), la sesión finalizará con la que ocurra primero.
Si no especificas ninguna, el sistema aplicará una duración predeterminada.
Si llamas a cancel(), no se descartará la grabación. El sistema detiene la captura, oculta y guarda los datos recopilados hasta ese momento, y entrega el ProfilingResult a tu objeto de escucha con ERROR_NONE.
Cómo registrar seguimientos del sistema con muestreo de pila de llamadas
En dispositivos que ejecutan Android 17 (nivel de API 37) y versiones posteriores (o Android 16 con un módulo principal de Profiling actualizado), ProfilingManager admite la recopilación de pilas de llamadas muestreadas junto con un registro del sistema en una sola sesión para que puedas ver los eventos de la línea de tiempo, como los estados de los subprocesos, los fotogramas y los segmentos de registro personalizados, junto con las pilas de llamadas de CPU para los métodos sin instrumentación de registro explícita.
En este ejemplo, también se muestra cómo llamar a ProfilingManager.requestProfiling directamente sin los compiladores de Jetpack especificando uno de los tipos de perfiles admitidos (PROFILING_TYPE_SYSTEM_TRACE) y pasando una configuración 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 );
Cuando abres el perfil resultante en la IU de Perfetto, las pilas de llamadas muestreadas aparecen como pistas de Callstacks dedicadas junto con los segmentos de seguimiento habituales de tu app. Selecciona una región de una pista de pila de llamadas para ver un gráfico tipo llama de las pilas de llamadas muestreadas para ese período, como se muestra en la Figura 1.
Cómo capturar un volcado de montón de Java
Puedes solicitar una instantánea de la memoria de Java de tu app con JavaHeapDumpRequestBuilder (o PROFILING_TYPE_JAVA_HEAP_DUMP). A diferencia de los volcados de montón estándar de .hprof capturados con Debug.dumpHprofData, los volcados de montón de ProfilingManager están diseñados para una recopilación segura y de baja sobrecarga en producción:
- Recopilación sin congelamiento: El sistema bifurca un clon de copia en escritura del proceso de tu app y recopila la instantánea del montón del proceso clonado, por lo que tu app no se congela mientras el volcado se escribe en el disco.
- Solo gráfico de referencia (sin datos de objetos sin procesar): En lugar de volcar el contenido de la memoria sin procesar como un archivo
.hprof,ProfilingManagerregistra un gráfico de protobuf.perfetto-java-heap-dumpque contiene clases, tamaños de objetos, cadenas de referencia y raíces de recolección de elementos no utilizados (GC), mientras que omite valores primitivos, contenido de cadenas y cargas útiles de arrays. Esto protege la privacidad del usuario y reduce el tamaño del archivo. Para obtener un desglose completo, consulta En qué se diferencian los volcados de montón de ProfilingManager de los volcados de HPROF.
Con la misma configuración de Context, Executor y resultCallback que se muestra en Cómo registrar un seguimiento del sistema, compila una solicitud con 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 );
Para obtener una explicación completa de extremo a extremo sobre cómo analizar un volcado de montón de Java para diagnosticar una fuga de memoria, consulta Caso de éxito: Cómo depurar una falla de OOM.
Próximos pasos
Después de capturar los perfiles, recupéralos de forma local con adb o súbelos a
tu backend con WorkManager.