Dominando Instruments (Parte 4): Flame Graphs, Swift Concurrency bajo el microscopio y Processor Trace en acción
Aprende a leer Flame Graphs, auditar tareas asíncronas con Swift Tasks y exprimir el Processor Trace con un proyecto CLI real que usa Swift Concurrency de forma intensiva.
En la Parte 1 aprendimos a usar Instruments como técnicos. En la Parte 2 nos convertimos en médicos. En la Parte 2.5 vimos la memoria en acción. En la Parte 3 dominamos el método científico del profiling, las operaciones de cirugía del Call Tree y conocimos los tres niveles de análisis — desde Time Profiler hasta Processor Trace.
Hoy cambiamos de bisturí y de paciente. Dejamos atrás la app SuperStuff y abrimos un proyecto completamente diferente: una herramienta de línea de comandos que usa Swift Concurrency de forma intensiva. Y descubriremos visualizaciones que transforman la manera en que leemos los datos de rendimiento.
No se trata de tener más datos. Se trata de verlos de una forma que revele lo que antes era invisible.
Nuevo paciente: Swift Evolution Metadata Extractor
El Swift Evolution Metadata Extractor es una herramienta oficial del equipo de Swift que genera los datos JSON para el Swift Evolution Dashboard. Es un proyecto real, de producción, mantenido por Apple.
¿Por qué es perfecto para nuestro laboratorio?
- Concurrencia masiva — Usa
withTaskGrouppara procesar más de 400 propuestas en paralelo. Cientos de tareas creándose, suspendiéndose y reanudándose. - Parsing intensivo — Cada propuesta es un archivo Markdown que se parsea con
swift-markdown(la librería oficial de Apple). El parser genera un AST completo del documento. - Networking — Hace peticiones HTTP a la API de GitHub para obtener el contenido de cada propuesta, utilizando
URLSessionconasync/await. - Sin interfaz gráfica — Al ser una CLI, no hay run loops, ni vistas, ni hilo principal compitiendo por atención. Todo el rendimiento se mide en puro procesamiento.
// El corazón del extractor: TaskGroup procesando propuestas en paraleloawait withTaskGroup(of: SortableProposalWrapper.self) { taskGroup in for spec in proposalSpecs { taskGroup.addTask { await readAndExtractProposalMetadata(for: spec, ...) } } for await result in taskGroup { proposals.append(result) }}
Flame Graphs — una nueva forma de ver los datos
El origen: de Netflix a Xcode
Los Flame Graphs nacieron en 2011, creados por Brendan Gregg mientras trabajaba en Netflix. Su problema era sencillo: los stack traces lineales eran imposibles de leer cuando tienes miles de muestras. La solución fue apilar las funciones visualmente, donde el ancho de cada caja representa el porcentaje de muestras — no el tiempo cronológico.
Esta idea fue tan transformadora que se publicó formalmente en Communications of the ACM en 2016 y fue adoptada universalmente. Apple integró los Flame Graphs en Instruments 16.3 como parte del instrumento Processor Trace, aunque la visualización también está disponible en el Time Profiler estándar.
Cómo leer un Flame Graph
A diferencia de la línea de tiempo cronológica que ya conocemos, en un Flame Graph:
- El eje X no es el tiempo. Representa el 100% de las muestras capturadas. El ancho de una caja indica qué porcentaje del tiempo total de ejecución pasó la CPU en esa función.
- El eje Y es la profundidad del stack. Las funciones más profundas están más abajo (en Instruments se muestran como “estalactitas” — de arriba hacia abajo).
- El orden es por peso. Instruments coloca las cajas más anchas (mayor porcentaje de muestras) a la izquierda.
- Los colores indican categoría: azul para tu código, morado para librerías, gris para código del sistema, magenta para el runtime de Swift.
En un Call Tree, el cuello de botella se esconde entre números. En un Flame Graph, es la meseta más ancha que salta a la vista.
Cómo acceder al Flame Graph en Instruments
- Perfila tu app con Time Profiler o Processor Trace.
- En la vista de detalle inferior, busca el botón Graph en la esquina superior derecha del Call Tree view.
- Haz clic — la vista cambia instantáneamente al Flame Graph.

Limpieza visual: Flatten to Boundary Frames
Cuando el Flame Graph está dominado por una librería externa (como swift-markdown), puedes limpiar el ruido sin perder información:
- Control-clic sobre cualquier función de la librería.
- Selecciona “Flatten ‘swift-markdown’ to Boundary Frames”.
- Todas las funciones internas de esa librería se colapsan en una sola barra, mostrando solo los puntos de entrada y salida.
Esto es conceptualmente diferente de Flatten en el Call Tree (que ya vimos en la Parte 3). Aquí no estamos eliminando una función individual — estamos colapsando toda una librería a sus fronteras, para que tu código destaque sobre el ruido.

Experimenta con el Flame Graph interactivo basado en datos del extractor:
Explorador de Flame Graph
Visualiza los datos de profiling del Swift Evolution Metadata Extractor. Alterna entre vistas y aplica Flatten.
- Ancho de barra = porcentaje de muestras (no tiempo cronológico)
- Profundidad = posición en el call stack (más profundo = más abajo)
- Barra ancha inesperada = posible cuello de botella
- Colores: Azul = tu código | Morado = librerías | Gris = sistema | Magenta = runtime
- Flatten to Boundary Frames = colapsar librería a sus fronteras (limpiar ruido)
- Acceso: Botón “Graph” en la esquina superior derecha del Call Tree
Swift Concurrency bajo el microscopio
El Call Tree y los Flame Graphs nos muestran dónde se gasta la CPU. Pero cuando tu app usa Swift Concurrency, hay una pregunta igual de importante: ¿qué están haciendo tus tareas? ¿Cuántas están vivas? ¿Cuántas están realmente ejecutándose? ¿Cuántas están suspendidas esperando algo?
La plantilla Swift Concurrency
Instruments incluye una plantilla dedicada: Swift Concurrency. Al seleccionarla, obtienes dos instrumentos principales:
- Swift Tasks — Rastrea el ciclo de vida de cada tarea asíncrona.
- Swift Actors — Monitorea el acceso exclusivo a los actores y las colas de espera.
Para el extractor, Swift Tasks es nuestro protagonista. Al perfilar la extracción de metadatos, el instrumento captura cada taskGroup.addTask como una nueva tarea con un identificador único.
Los tres contadores mágicos
En la parte superior del track de Swift Tasks, Instruments muestra tres histogramas:
- Running Tasks — Cuántas tareas se están ejecutando simultáneamente en un momento dado. En nuestro extractor, verás picos cuando el
TaskGrouplanza las tareas de extracción. - Alive Tasks — Cuántas tareas existen (creadas pero no finalizadas). La diferencia entre Alive y Running revela cuántas tareas están suspendidas o en cola.
- Total Tasks — Acumulado de tareas creadas hasta ese punto. Útil para detectar si se están creando más tareas de las necesarias.
Running te dice cuántas tareas trabajan. Alive te dice cuántas existen. La diferencia entre ambas es tiempo que tus tareas pasan esperando — y eso es lo que debes investigar.


Task Summary y Task Forest
Debajo de los histogramas, el panel de detalle ofrece dos vistas clave:
- Task Summary — Una tabla que muestra cuánto tiempo cada tarea pasó en cada estado: ejecutándose, suspendida, esperando acceso a un actor. Si ves una tarea con mucho tiempo “Enqueued”, significa que está bloqueada esperando acceso exclusivo a un actor.
- Task Forest — Representación gráfica de las relaciones padre-hijo entre tareas. En nuestro extractor, verás la tarea principal (
ExtractionJob.run) como raíz, con cientos de tareas hijas (una por propuesta) organizadas bajo elTaskGroup.
Narrative View: la biografía de una tarea
Selecciona cualquier tarea en el Task Summary y haz clic derecho → Pin Track. Instruments añade un track dedicado a esa tarea en la línea de tiempo, y en la parte inferior aparece la Narrative View.
La Narrative View es como leer la biografía de una tarea:
- En qué hilo empezó a ejecutarse.
- Por qué se suspendió (esperando una continuación, esperando acceso a un actor, etc.).
- Cuánto tiempo pasó en cada estado.
- Si esperaba otra tarea, cuál era esa tarea.
Para nuestro extractor, esto revela patrones fascinantes: cada tarea de extracción empieza ejecutándose brevemente para parsear el Markdown, se suspende esperando I/O si necesita datos de la red, y se reanuda para escribir el resultado.

- Plantilla: Swift Concurrency (incluye Swift Tasks + Swift Actors)
- Running Tasks = tareas ejecutándose ahora (limitado por cores)
- Alive Tasks = tareas creadas pero no finalizadas
- Total Tasks = acumulado histórico
- Task Summary = tiempo por estado (running, suspended, enqueued)
- Task Forest = relaciones padre-hijo (structured concurrency)
- Narrative View = biografía completa de una tarea individual
- Pin Track = clic derecho en Task Summary para fijar una tarea a la línea de tiempo
Processor Trace — profundizando

En la Parte 3 conocimos los tres niveles de profiling: Time Profiler (estadístico, ~1kHz), CPU Profiler (contadores de hardware) y Processor Trace (cada instrucción). Sabemos qué es Processor Trace. Ahora vamos a usarlo.
Requisitos de hardware
Processor Trace necesita chips de última generación:
- Mac con M4 o posterior
- iPad Pro con M4 o posterior
- iPhone 16 / iPhone 16 Pro o posterior
Si no tienes este hardware, no te preocupes — puedes analizar traces guardados por alguien de tu equipo que sí lo tenga, en cualquier Mac con Instruments 16.3+.
La sorpresa del overhead
Quizás lo más contra-intuitivo de Processor Trace es su overhead. Cuando grabas cada instrucción ejecutada por cada core, esperarías un impacto brutal en rendimiento. Pero Apple reporta un overhead de apenas ~1%. El truco es que el hardware almacena la información en un buffer dedicado y la vuelca al disco de forma asíncrona — sin interferir con la ejecución normal.
El precio real no es el overhead de CPU, sino el volumen de datos: una grabación de pocos segundos en una app multi-hilo puede generar gigabytes de información. Por eso Apple recomienda mantener las grabaciones cortas y dirigidas.
Processor Trace en acción con el extractor
- Abre Instruments y selecciona la plantilla Processor Trace.
- Graba 3-5 segundos durante la extracción del metadatos.
- Haz zoom extremo (Option-drag) en la línea de tiempo.
Lo que verás es revelador: donde Time Profiler mostraba barras gruesas, Processor Trace revela un mosaico de funciones diminutas. Puedes ver literalmente cada llamada a swift_retain y swift_release — las operaciones de reference counting que ARC ejecuta detrás de escena (las que estudiamos en la Parte 2.5).
Flame Graph determinístico
Con Processor Trace activo, cambia a la vista Flame Graph (botón Graph en Call Tree). Ahora cada barra refleja el conteo exacto de instrucciones y ciclos — no una estimación estadística. La diferencia es sutil pero fundamental:
- En el Flame Graph de Time Profiler, una función rápida que siempre se ejecuta entre dos muestras podría no aparecer nunca.
- En el Flame Graph de Processor Trace, aparece todo. Nada se escapa.
- Hardware: M4 / A18 o posterior
- Overhead: ~1% (el costo real es el volumen de datos, no el rendimiento)
- Recomendación: Grabaciones cortas (3-5 segundos), dirigidas al momento de interés
- Flame Graph: Determinístico — cada barra refleja instrucciones reales ejecutadas
- Capacidad única: Ver funciones de nanosegundos (retain/release, destructores, thunks)
- Análisis remoto: Puedes abrir traces grabados en cualquier Mac con Instruments 16.3+
Conectando los puntos
Empezamos esta serie con botones y plantillas. Hoy analizamos una herramienta CLI real con cientos de tareas concurrentes, leímos sus Flame Graphs, auditamos el ciclo de vida de sus tareas asíncronas y vimos operaciones de nanosegundos con Processor Trace.
El arco ha sido deliberado: de la interfaz al modelo mental, del modelo mental a la anatomía, de la anatomía al método científico, y del método científico a las herramientas de visualización más avanzadas. Cada parte construye sobre la anterior.
Las herramientas cambian, las plantillas se actualizan, los instrumentos evolucionan. Pero la habilidad de observar, hipotetizar, medir e interpretar es permanente. Eso es lo que esta serie intenta cultivar.
Referencias
- Analyzing CPU usage with the Processor Trace instrument — Apple Documentation — Documentación oficial de Apple sobre el Processor Trace, incluyendo Flame Graphs y operaciones de Charge/Prune/Flatten.
- Visualize and optimize Swift concurrency — WWDC22 — Sesión donde Apple introduce el instrumento Swift Tasks y la Narrative View.
- Optimize CPU performance with Instruments — WWDC25 — Sesión más reciente sobre Processor Trace, Flame Graphs y CPU Counters.
- Analyze hangs with Instruments — WWDC23 — Cómo usar Instruments para diagnosticar hangs en todas las plataformas Apple.
- Flame Graphs — Brendan Gregg — Página oficial del creador de los Flame Graphs con filosofía, variantes y herramientas.
- The Flame Graph — Communications of the ACM — Artículo formal de Brendan Gregg en ACM sobre la visualización.
- How to find and fix slow code using Instruments — Paul Hudson (Hacking with Swift) — Guía práctica de Paul Hudson sobre optimización con Instruments.
- Using Instruments to profile a SwiftUI app — Donny Wals — Tutorial de Donny Wals sobre profiling con Instruments.
- Xcode Instruments Time Profiler — Antoine van der Lee (AvanderLee) — Tutorial de Antoine van der Lee sobre el uso efectivo del Time Profiler.
Relacionados
-
- swift
- swift-cero-experto
- swift-fundamentals
Swift de Cero a Experto #5: Funciones — ciudadanos de primera clase
Parámetros, labels, inout, function types y funciones como valores. La puerta de entrada a los closures y la programación funcional.
-
- swift
- ios
- performance
Dominando Instruments (Parte 3): método científico, Time Profiler avanzado y profiling a escala
Aprende a diagnosticar problemas de rendimiento como un proceso científico. Domina Weight vs Self-Weight, Charge/Prune/Flatten, y escala tu profiling con xctrace.
-
- swift
- swift-cero-experto
- swift-fundamentals
Swift de Cero a Experto #4: Control de flujo — de if/else a pattern matching
if/else, switch exhaustivo con pattern matching, guard como filosofía, y cómo el compilador optimiza tus decisiones a jump tables.