os_signpost: instrumentar lo que de verdad importa
Cuando el muestreo no basta porque el coste está en la espera y no en la CPU. La API de OSSignposter, intervalos y eventos con metadatos, el instrumento Points of Interest, plantillas propias en Instruments y el cálculo honesto del coste de observar.
Los instrumentos que vienen de fábrica miden magnitudes universales: ciclos, bytes, fotogramas. Ninguno sabe qué es cargar el feed, sincronizar el borrador o descodificar la miniatura, que son exactamente las unidades en las que tu equipo razona y en las que están escritos los objetivos de rendimiento. os_signpost cierra ese hueco: te permite marcar el principio y el final de una operación con nombre propio, adjuntarle metadatos y verla dibujada en la misma línea de tiempo que la CPU, la memoria y el trabajo de la GPU. Es la diferencia entre saber que la app tarda y saber en qué tarda, y es además la única forma razonable de medir lo que ningún muestreador ve: el tiempo que se va esperando.
- Justificar cuándo la instrumentación explícita supera al muestreo estadístico.
- Emitir intervalos y eventos con
OSSignposterusando identificadores y metadatos correctos. - Leer los signposts en
Instrumentsy agregarlos por categoría, nombre y argumento. - Estimar el coste de observar y decidir qué queda activo en una build de producción.
Por qué instrumentar cuando ya tienes un perfilador
Un muestreador responde dónde se gastan los ciclos. Hay tres preguntas frecuentes que no puede responder y que resuelven las trazas propias.
La primera es el tiempo de espera. Una descarga, un bloqueo en contención o una consulta a base de datos no producen muestras de CPU; para el Time Profiler ese intervalo sencillamente no existe, aunque sea justo el que el usuario percibe.
La segunda es la atribución semántica. Cien milisegundos repartidos entre JSONDecoder y Data no dicen si el coste fue del feed o del perfil, porque la misma función sirve a diez casos de uso distintos. Un signpost sí lo dice, porque el nombre lo pones tú.
La tercera es la correlación. Al vivir en la misma línea de tiempo unificada, un intervalo tuyo se alinea visualmente con los picos de memoria, con las transacciones de Core Animation y con los fallos de fotograma, y esa coincidencia temporal es a menudo el diagnóstico entero.
El nombre de un signpost debe ser un sustantivo de dominio, estable entre versiones y comparable en el tiempo. CargaDelFeed sirve durante años; decodificarRespuestaV2 deja de tener sentido en el siguiente refactor y arruina cualquier comparación histórica.
La API y sus contratos
La forma moderna es OSSignposter, construido sobre un OSLog con un subsistema y una categoría. La categoría es la que agrupa después en la herramienta, así que merece pensarse.
import os
let registro = OSSignposter(
subsystem: "com.ejemplo.app",
category: "Rendimiento"
)
func cargarFeed(pagina: Int) async throws -> [Publicacion] {
let id = registro.makeSignpostID()
let estado = registro.beginInterval("CargaDelFeed", id: id, "pagina=\(pagina, privacy: .public)")
defer { registro.endInterval("CargaDelFeed", estado) }
let datos = try await red.obtener(pagina: pagina)
registro.emitEvent("RespuestaRecibida", id: id, "bytes=\(datos.count, privacy: .public)")
return try decodificador.decode([Publicacion].self, from: datos)
}
Tres contratos hay que respetar para que la traza sea legible y no una maraña.
El identificador distingue instancias concurrentes de la misma operación. Si diez descargas del feed se solapan y todas usan el mismo signpostID, Instruments no puede emparejar cada inicio con su final y el resultado son intervalos absurdos. Un identificador nuevo por operación, o derivado de un objeto con makeSignpostID(from:), resuelve el problema de raíz.
El emparejamiento es estricto: todo beginInterval necesita exactamente un endInterval con su estado. Un camino de error o una cancelación que se salte el cierre deja un intervalo abierto que contamina la agregación. Por eso el defer de la primera línea no es estilo, es corrección.
Los metadatos deben ser explícitos en su privacidad. Los argumentos que quieras ver en la herramienta se marcan como públicos; los demás quedan redactados. Es la misma disciplina de os_log, y aquí tiene además una consecuencia práctica: solo los argumentos públicos pueden usarse para agrupar y filtrar.
sequenceDiagram participant App as Codigo de la app participant Log as Subsistema os_signpost participant Ins as Instruments App->>Log: beginInterval CargaDelFeed id 7 Log-->>Ins: marca de inicio con marca de tiempo App->>Log: emitEvent RespuestaRecibida id 7 Log-->>Ins: punto en la linea de tiempo App->>Log: endInterval CargaDelFeed id 7 Log-->>Ins: marca de fin y duracion calculada Ins->>Ins: agrupa por nombre categoria y argumento
Verlo y explotarlo en Instruments
Con la app instrumentada, el instrumento os_signpost muestra un carril por categoría y una barra por intervalo. La pestaña de resumen es donde está el valor real: agrupa por nombre y da recuento, mínimo, máximo, media y desviación. Esa tabla convierte una impresión —a veces va lento— en una distribución, y una distribución permite hablar de percentiles en lugar de anécdotas.
El instrumento Points of Interest merece mención aparte. Usa la categoría reservada .pointsOfInterest y aparece en la parte alta de la línea de tiempo junto a los eventos del sistema, lo que lo convierte en el sitio ideal para hitos escasos y significativos: fin del arranque, primera pantalla útil, sesión iniciada. No conviene abarrotarlo, precisamente porque su utilidad viene de ser corto.
let hitos = OSSignposter(
subsystem: "com.ejemplo.app",
category: .pointsOfInterest
)
hitos.emitEvent("PrimeraPantallaUtil")
Intervalos con jerarquía
Anida intervalos para que el resumen muestre en qué subfase se va el tiempo: red, decodificación y presentación como hijos de una misma carga.
Argumentos que agrupan
Un argumento público como el tamaño de página o el tipo de caché convierte una media plana en una comparación entre casos.
Plantilla propia
Instruments permite guardar una plantilla con tus carriles y tus agregaciones. Compartirla con el equipo hace que todos midan lo mismo de la misma forma.
Sobre el coste: emitir un signpost cuesta decenas de nanosegundos cuando nadie escucha, porque el subsistema comprueba primero si hay un consumidor activo y sale. Aun así, la interpolación de argumentos puede ejecutarse aunque el registro esté inactivo si no se toman precauciones, así que para caminos calientes existe signpostsEnabled y la posibilidad de usar OSSignposter.disabled, que compila a un objeto inerte. La regla práctica es dejar activos los intervalos de grano grueso —operaciones de más de un milisegundo— y proteger explícitamente los de grano fino.
De la sesión suelta al régimen de medida
Una traza aislada resuelve un bug. Un conjunto estable de signposts resuelve una categoría entera de bugs futuros, y para eso hay que tratarlos como una API interna con sus reglas.
Un vocabulario cerrado. Los nombres no se inventan en cada llamada: viven en un solo sitio, se revisan como se revisa cualquier contrato y cambian con la misma cautela. Una enumeración de operaciones evita que dos personas midan lo mismo con dos nombres distintos.
enum Operacion: String {
case arranqueFrio = "ArranqueFrio"
case cargaDelFeed = "CargaDelFeed"
case sincronizacion = "Sincronizacion"
}
extension OSSignposter {
func medir<T>(_ op: Operacion, _ cuerpo: () throws -> T) rethrows -> T {
let id = makeSignpostID()
let estado = beginInterval(op.rawValue, id: id)
defer { endInterval(op.rawValue, estado) }
return try cuerpo()
}
}
Un nivel de detalle por defecto. Los intervalos de grano grueso quedan activos siempre; los de grano fino se protegen tras una comprobación explícita para que ni siquiera se interpolen sus argumentos cuando nadie escucha.
if registro.signpostsEnabled {
registro.emitEvent("FilaDecodificada", "indice=\(i, privacy: .public)")
}
Un puente hacia producción. MetricKit entrega en cada sesión los intervalos que hayas marcado con la categoría adecuada, de modo que la misma operación que mediste en tu mesa se convierte en una distribución sobre toda la base de usuarios. Ese es el momento en que la instrumentación deja de ser una herramienta de depuración y pasa a ser un objetivo defendible: no el feed va rápido, sino el percentil noventa y cinco de CargaDelFeed por debajo de ochocientos milisegundos en la versión actual.
Un signpost alrededor de una operación de microsegundos añade más ruido que información y ensucia la línea de tiempo hasta hacerla ilegible. La frontera práctica está en el milisegundo: por debajo, el muestreo sigue siendo mejor herramienta.
Detrás de la elección entre Time Profiler y os_signpost hay una división que atraviesa toda la observabilidad, y verla completa cambia la forma de trabajar. El muestreo es un método no supervisado: no requiere que sepas nada del programa, produce una estimación de la distribución del coste sobre todo el binario y su error es estadístico, es decir, decrece con la raíz del número de muestras y desaparece si mides el tiempo suficiente. La instrumentación es un método supervisado: exige una hipótesis previa —decidir qué merece un nombre—, no ve absolutamente nada de lo que no marcaste, y su error no es estadístico sino sistemático, porque el propio acto de medir añade trabajo dentro de la región medida. Esta asimetría explica sus dominios naturales. Cuando no sabes dónde está el problema, muestrear es lo único que escala, porque una hipótesis equivocada en instrumentación no da un resultado peor sino ningún resultado. Cuando sí sabes qué operación te importa y necesitas su distribución, la latencia de cola o la correlación con otros subsistemas, muestrear es inútil, porque una media de muestras no reconstruye un percentil noventa y nueve de una operación que ocurre tres veces por sesión. Hay una tercera consecuencia que suele pasar desapercibida y es la que más valor produce a largo plazo: los signposts son el único de los dos métodos que sobrevive fuera del laboratorio. Un trace de muestreo es una sesión aislada en un dispositivo concreto con un estado irrepetible; un intervalo con nombre estable puede emitirse también en producción a través de MetricKit y de la telemetría propia, agregarse sobre millones de sesiones y convertirse en un objetivo de servicio que el equipo defiende versión tras versión. La consecuencia estratégica es clara: el muestreo es una herramienta de investigación y la instrumentación es una herramienta de régimen. Un equipo que solo perfila descubre problemas; un equipo que además instrumenta se entera de las regresiones antes que sus usuarios, y esa diferencia no la da ninguna optimización concreta sino la decisión, tomada temprano, de que las operaciones importantes del dominio tengan nombre.
- Instrumenta la operación más lenta de tu app con un intervalo y compárala con lo que muestra
Time Profiler. - Añade intervalos anidados para red, decodificación y presentación, y reparte el total entre las tres fases.
- Emite un evento con un argumento público y agrupa por él en la pestaña de resumen.
- Provoca una cancelación que se salte el
endIntervaly observa cómo se corrompe la agregación. - Guarda una plantilla de
Instrumentscon tus carriles y documenta el percentil noventa y cinco de la operación medida.