wandres.dev
PROFILING · medir con Perfetto

Trazas propias: poner TU código en el timeline de Perfetto

Una traza del sistema muestra con detalle exquisito lo que hace el sistema y deja tu lógica de negocio como un bloque opaco de tiempo sin nombre. Esta lección cubre la instrumentación propia: el contrato estricto de las secciones síncronas, la variante `trace()` que garantiza el cierre, las secciones asíncronas con testigo para trabajo que cruza hilos, los contadores para magnitudes que no son duraciones, y la trazabilidad de la composición. Cierra con la disciplina de nomenclatura que separa una traza legible de un cronograma inutilizable, el coste real de la instrumentación y su conversión en métrica de regresión medida automáticamente.

⏱ 19 min

Al abrir una traza de sistema por primera vez ocurre algo desconcertante: el sistema operativo se ve con una nitidez asombrosa —cada medida, cada disposición, cada dibujo, cada transacción, cada cambio de contexto— y tu aplicación aparece como un vacío. Hay un hueco de cuarenta milisegundos en el hilo principal y nadie dice qué pasó ahí dentro. La razón es sencilla y desagradable: el marco de trabajo está instrumentado y tu código no. Todos esos segmentos con nombre que hacen legible el cronograma existen porque alguien, en el código de la plataforma, escribió una marca de inicio y una de fin alrededor de un bloque. Instrumentar el código propio consiste exactamente en hacer lo mismo, y el resultado transforma el análisis: el hueco opaco se convierte en una jerarquía anidada que dice carga de red, deserialización, escritura en base de datos, mapeo al modelo de la interfaz. Ese es el momento en que una traza deja de describir la plataforma y empieza a describir tu programa.

🎯 Al terminar esta lección sabrás
  • Emitir secciones síncronas respetando su contrato de anidamiento estricto, hilo único y límite de nombre.
  • Usar la variante con bloque que garantiza el cierre incluso ante una excepción o un retorno temprano.
  • Instrumentar trabajo asíncrono con secciones de testigo y magnitudes continuas con contadores.
  • Diseñar una nomenclatura de baja cardinalidad y convertir las secciones propias en métricas de regresión.

El contrato de una sección síncrona

Una sección de traza es un par de marcas emitidas por el mismo hilo: una de apertura con un nombre y una de cierre sin él. El sistema de captura las empareja por orden, no por nombre, y de ahí salen las tres reglas que hay que respetar sin excepción.

La primera es el anidamiento estricto: las secciones se cierran en orden inverso al de apertura, como una pila. Si dos secciones se solapan parcialmente, el cronograma resultante no es incorrecto, es incomprensible. La segunda es el hilo único: apertura y cierre deben ocurrir en el mismo hilo, porque el emparejamiento es por pila de hilo. Una sección abierta antes de una suspensión y cerrada después, posiblemente en otro hilo trabajador, produce un cronograma corrupto. La tercera es el límite de longitud del nombre, unos pocos decenas de caracteres; lo que sobra se trunca silenciosamente.

La forma cruda de emitir el par es propensa al error más caro de todos, que es olvidar el cierre en un camino de excepción o de retorno temprano. Una sección abierta y nunca cerrada no desaparece: se traga todo lo que venga después en ese hilo y contamina la traza entera. Por eso la forma correcta es siempre la que envuelve un bloque y garantiza el cierre.

import androidx.tracing.trace

suspend fun cargarPantalla(id: String): EstadoUi {
    val crudo = trace("pantalla:red") { api.obtener(id) }
    val entidad = trace("pantalla:mapeo") { mapeador.aEntidad(crudo) }
    trace("pantalla:persistencia") { dao.guardar(entidad) }
    return trace("pantalla:estado") { entidad.aEstadoUi() }
}

Cada llamada abre la sección, ejecuta el bloque y cierra en un bloque final aunque se lance una excepción. Obsérvese que las secciones envuelven fragmentos que no cruzan una suspensión: cada una empieza y termina dentro del mismo tramo de ejecución. Esa es la traducción práctica de la regla del hilo único al mundo de las corrutinas, y es el error que más trazas arruina.

⚠️
Nunca abras una sección alrededor de un punto de suspensión

Envolver una llamada suspendida completa parece lo natural y es justamente lo prohibido. La corrutina se suspende, el hilo queda libre y ejecuta otra cosa completamente distinta dentro de tu sección abierta; después reanuda, quizá en otro hilo, y cierra una sección que en esa pila no existía. El resultado es un cronograma donde tu operación aparece durando cuatro segundos y conteniendo trabajo ajeno, y donde otras secciones quedan huérfanas. Para medir una operación asíncrona completa existe la sección asíncrona con testigo, que está diseñada precisamente para eso porque no se apoya en la pila del hilo.

Asíncrono, contadores y composición

Cuando el trabajo empieza en un sitio y termina en otro, la sección síncrona no sirve. La sección asíncrona resuelve el problema emparejando apertura y cierre por un testigo numérico en lugar de por la pila: se abre con un nombre y un testigo, se cierra con el mismo nombre y el mismo testigo, y entre medias pueden pasar hilos, suspensiones y milisegundos. El testigo debe ser único entre las instancias vivas de esa misma operación, y ese es el matiz que se olvida: dos descargas simultáneas con el mismo testigo producen un emparejamiento arbitrario.

Los contadores cubren la otra carencia. Hay magnitudes que no son duraciones y que sin embargo explican el cronograma: elementos en cola, tamaño de una caché, peticiones en vuelo, número de recomposiciones. Un contador se dibuja como una serie temporal bajo los carriles del proceso, y correlacionar visualmente un pico de cola con un frame perdido resuelve diagnósticos que ninguna pila de llamadas resolvería.

import androidx.tracing.Trace

// Trabajo que cruza hilos: emparejamiento por testigo, no por pila.
fun iniciarDescarga(tarea: Tarea) {
    Trace.beginAsyncSection("descarga", tarea.id)
    cola.enviar(tarea) { Trace.endAsyncSection("descarga", tarea.id) }
    Trace.setCounter("descargas:en-vuelo", cola.tamano.toLong())
}

Para que nada de esto aparezca en el cronograma hace falta que la captura incluya la categoría de instrumentación de aplicación; sin ella, las marcas se emiten y se descartan, y el resultado es una traza del sistema perfecta con el mismo hueco opaco de siempre.

# La categoria de aplicacion es la que recoge TUS secciones y contadores.
adb shell perfetto -o /data/misc/perfetto-traces/traza.pftrace \
  -t 15s -b 64mb \
  sched freq gfx view app dalvik binder_driver

Queda un tercer plano, específico de la interfaz declarativa: la trazabilidad de la composición. Activada mediante las bibliotecas correspondientes y la opción adecuada en la captura, hace que cada función componible aparezca como un segmento con su propio nombre dentro del segmento de preparación de frame. Es la única forma de responder qué parte del árbol se está recomponiendo y cuánto cuesta, en vez de contemplar un bloque monolítico de composición. Su coste es alto y exige una compilación depurable, así que se enciende para investigar y se apaga para medir.

🧱

Fronteras, no interiores

Instrumenta los límites entre fases: red, mapeo, persistencia, construcción del estado. Una sección dentro de un bucle apretado destruye la señal y multiplica el coste.

🏷️

Nombres de baja cardinalidad

Nunca metas identificadores ni datos del usuario en el nombre. Un nombre por operación permite agregar, comparar entre ejecuciones y medir regresiones; un nombre por instancia impide todo eso.

🌳

Prefijos jerárquicos

Un prefijo estable por subsistema convierte la lista de segmentos en algo navegable y hace triviales las consultas por familia sobre el modelo relacional de la traza.

🔇

Coste casi nulo si nadie escucha

Cuando la captura está apagada, la emisión se reduce a comprobar una bandera. El gasto real aparece con captura activa, y es proporcional al número de secciones emitidas.

flowchart TD
A[Hueco opaco en el hilo principal] --> B[Instrumentar fronteras de fase]
B --> C{El trabajo cruza hilos o suspensiones}
C -->|No| D[Seccion sincrona con bloque]
C -->|Si| E[Seccion asincrona con testigo unico]
D --> F[Cronograma anidado y legible]
E --> F
F --> G[Contadores para magnitudes que no son duracion]
G --> H[La seccion se convierte en metrica de regresion]
style A fill:#f38ba8,color:#11111b
style F fill:#89b4fa,color:#11111b
style H fill:#a6e3a1,color:#11111b
💡
Instrumentar la compilación de publicación es posible y es lo que hay que hacer

La emisión de secciones desde la aplicación solo se registra si el proceso lo permite, y por defecto una compilación de publicación no lo permite. Declararla como perfilable en el manifiesto habilita la captura sin convertirla en depurable: se conservan las optimizaciones y la minificación, y aun así las secciones propias aparecen en el cronograma. Es la configuración correcta para medir de verdad, porque una traza tomada sobre una compilación depurable describe un programa que nadie ejecuta. Y es también el requisito para el paso siguiente: que una sección propia se convierta en una métrica medida automáticamente en cada integración, de modo que una regresión de rendimiento falle la construcción igual que falla una prueba.

Instrumentar es declarar cuáles son las unidades de trabajo de tu programa

Hay una lectura de la instrumentación que la reduce a un accesorio de diagnóstico, algo que se añade cuando hay un problema y se retira cuando se resuelve. Es una lectura pobre, y la evidencia de que lo es aparece en cuanto uno intenta instrumentar código ajeno o mal estructurado: resulta imposible encontrar dónde poner las marcas. No hay fronteras naturales porque el código no tiene fases, tiene una sucesión de efectos entrelazados donde la red, el mapeo, la persistencia y la construcción del estado ocurren mezclados. La dificultad de instrumentar es, en ese sentido, un diagnóstico arquitectónico antes que una molestia técnica, y su recíproco también se cumple: un código con separación de responsabilidades clara se instrumenta casi solo, porque cada frontera de sección coincide con una frontera de responsabilidad que ya existía. Lo que una traza propia hace, entonces, no es simplemente rellenar un hueco del cronograma. Es publicar el modelo mental que el equipo tiene de su propio programa, convertido en objeto observable y por tanto en objeto discutible. Los nombres de las secciones son un vocabulario compartido: cuando alguien dice que la fase de mapeo tarda doce milisegundos en el arranque en frío, esa frase solo tiene sentido si existe una sección llamada así y todo el mundo entiende qué abarca y qué no. Ese vocabulario tiene además una propiedad que ninguna documentación posee, y es que se verifica solo: si la sección de persistencia contiene una llamada de red, el cronograma lo enseña sin piedad y la mentira arquitectónica queda expuesta en el eje temporal. Por eso la instrumentación bien hecha no envejece como envejece un comentario, y por eso conviene tratarla como parte del diseño y no como andamiaje. La consecuencia final es la más valiosa: una vez que las unidades de trabajo tienen nombre estable, dejan de ser observaciones puntuales y pasan a ser magnitudes con historia, comparables entre versiones y vigilables de forma automática. El rendimiento deja de ser una impresión que alguien tiene y se convierte en un contrato que el sistema puede comprobar.

⚔️ Convierte tu hueco opaco en un cronograma
  1. Captura una traza de tu arranque en frío, localiza el bloque de tiempo sin nombre del hilo principal y mide cuántos milisegundos son completamente opacos.
  2. Instrumenta las cuatro fronteras de fase de una carga de pantalla con la variante de bloque, vuelve a capturar y explica el reparto real del tiempo.
  3. Envuelve deliberadamente una llamada suspendida completa en una sección síncrona y observa el cronograma corrupto resultante. Después arréglalo con una sección asíncrona con testigo.
  4. Publica un contador con el número de peticiones en vuelo y correlaciona visualmente sus picos con los frames marcados como perdidos.
  5. Declara la aplicación como perfilable, captura las mismas secciones sobre la compilación de publicación y compara las duraciones con las de la compilación depurable. Decide cuál de las dos usarías para vigilar regresiones.