Medir cuánto tarda en ejecutarse una operación parece uno de los problemas más sencillos de programación. Guardamos la hora antes, ejecutamos el código, volvemos a consultar la hora y restamos ambos valores. Durante años, en Swift ha sido habitual resolverlo con Date, CFAbsoluteTimeGetCurrent() o DispatchTime. El problema es que no todos esos relojes representan el mismo concepto de tiempo y elegir el incorrecto puede producir mediciones que no significan exactamente lo que creemos.
Date representa un instante del calendario. Es perfecto para saber cuándo ocurrió algo, almacenar la fecha de una publicación o calcular cuánto tiempo ha pasado entre dos acontecimientos reales. Sin embargo, el reloj del sistema puede ajustarse mientras nuestra aplicación está funcionando: el usuario puede cambiar la hora, el sistema puede sincronizarla o puede aplicarse una corrección externa. Para medir rendimiento necesitamos otra propiedad diferente: un reloj monolítico, cuya progresión no dependa de la hora configurada en el dispositivo.
// ⚠️ Funciona en muchos casos, pero Date no es la herramienta
// adecuada para medir rendimiento.
let start = Date()
let sortedValues = values.sorted()
let elapsed = Date().timeIntervalSince(start)
print("Tiempo: \(elapsed) segundos")
Durante bastante tiempo, DispatchTime fue una de las mejores alternativas para este trabajo. DispatchTime.now().uptimeNanoseconds utiliza un reloj apropiado para medir intervalos y evita los problemas del reloj de calendario. Sigue siendo una API perfectamente válida, pero obliga a trabajar con nanosegundos y realizar conversiones manuales que hoy ya no son necesarias.
let start = DispatchTime.now()
let sortedValues = values.sorted()
let end = DispatchTime.now()
let nanoseconds = end.uptimeNanoseconds - start.uptimeNanoseconds
let milliseconds = Double(nanoseconds) / 1_000_000
print("Tiempo: \(milliseconds) ms")
Swift 5.7 introdujo mediante SE-0329 un modelo específico para trabajar con tiempo basado en tres conceptos: Clock, Instant y Duration. En lugar de representar el tiempo como un Double sin contexto o como un número de nanosegundos, la librería estándar puede expresar qué reloj estamos utilizando, un punto concreto dentro de ese reloj y la duración existente entre dos puntos.
ContinuousClock: el cronómetro de Swift
Para la mayoría de mediciones de tiempo transcurrido, la opción adecuada es ContinuousClock. Apple lo describe como un reloj similar a un cronómetro: siempre avanza y continúa haciéndolo aunque el dispositivo entre en reposo. Sus instantes no representan una fecha del calendario, sino posiciones dentro de una referencia temporal local al proceso o al sistema.
La API incluye además el método measure, por lo que ni siquiera necesitamos guardar manualmente el instante inicial y final:
let clock = ContinuousClock()
let elapsed = clock.measure {
_ = records.sorted { $0.priority > $1.priority }
}
print("La ordenación tardó \(elapsed)")
El resultado no es un Double, sino un Duration. Esto elimina una ambigüedad muy habitual en APIs antiguas: ya no tenemos que recordar si un valor numérico representa segundos, milisegundos o nanosegundos. El tipo conserva esa semántica y permite construir o comparar duraciones directamente.
let timeout: Duration = .milliseconds(750)
let animationWindow: Duration = .seconds(2)
if elapsed > timeout {
print("La operación ha superado el tiempo esperado")
}
También funciona con código asíncrono
La misma idea funciona con async/await. measure dispone de una variante asíncrona que espera a que termine el bloque antes de devolver la duración completa. No importa que durante ese trabajo la tarea cambie de ejecutor o atraviese varios puntos de suspensión: lo que estamos midiendo es el tiempo transcurrido entre el inicio y el final de la operación.
let clock = ContinuousClock()
let elapsed = await clock.measure {
await thumbnailCache.preloadMissingImages()
}
print("Precarga completada en \(elapsed)")
Esto es especialmente importante porque medir únicamente el tiempo de CPU y medir el tiempo que percibe el usuario son dos cosas distintas. Una operación asíncrona puede pasar una parte importante de su vida suspendida esperando datos, disco u otro recurso. ContinuousClock mide el intervalo completo, que suele ser precisamente lo que interesa cuando queremos conocer la latencia real de una operación.
Cuando también necesitamos el resultado
measure está pensado para medir un bloque cuyo resultado no necesitamos recuperar. Si queremos ejecutar una función y obtener a la vez su valor y su duración, podemos utilizar directamente los Instant del reloj.
func timed<T>(
clock: ContinuousClock = .init(),
_ operation: () throws -> T
) rethrows -> (result: T, duration: Duration) {
let start = clock.now
let result = try operation()
let duration = start.duration(to: clock.now)
return (result, duration)
}
Podemos reutilizarlo con cualquier operación síncrona:
let measurement = timed {
try JSONDecoder().decode(Catalog.self, from: data)
}
print("Elementos: \(measurement.result.items.count)")
print("Decodificación: \(measurement.duration)")
La variante asíncrona mantiene exactamente la misma estructura:
func timed<T>(
clock: ContinuousClock = .init(),
_ operation: () async throws -> T
) async rethrows -> (result: T, duration: Duration) {
let start = clock.now
let result = try await operation()
let duration = start.duration(to: clock.now)
return (result, duration)
}
Este patrón resulta útil para instrumentación ligera, registros de diagnóstico o para conocer la latencia de una operación sin mezclar la lógica de negocio con cálculos de nanosegundos.
ContinuousClock y SuspendingClock no miden lo mismo
Swift proporciona otro reloj llamado SuspendingClock. La diferencia parece pequeña, pero cambia completamente el significado de una medición: ContinuousClock sigue avanzando mientras el sistema está suspendido, mientras que SuspendingClock deja de avanzar durante ese periodo.
Imaginemos una tarea que empieza, el dispositivo permanece bloqueado durante treinta segundos y después la tarea continúa. Un ContinuousClock incluirá esos treinta segundos en la duración. Un SuspendingClock no.
let continuous = ContinuousClock()
let suspending = SuspendingClock()
let wallElapsed = await continuous.measure {
await performLongRunningWork()
}
let activeElapsed = await suspending.measure {
await performLongRunningWork()
}
La elección depende de qué queremos representar. Si medimos cuánto ha tenido que esperar realmente una persona desde que inició una acción, ContinuousClock suele expresar mejor esa latencia. Si el tiempo debe avanzar únicamente mientras el dispositivo está activo, SuspendingClock puede representar mejor el comportamiento deseado. Apple utiliza precisamente esta distinción para explicar por qué un reloj puede ser más adecuado para tiempos relacionados con el usuario y otro para comportamientos ligados a la ejecución del dispositivo.
Duration tiene más precisión que un simple número
Duration almacena internamente componentes de segundos y attosegundos. Eso no significa que el hardware de nuestro iPhone pueda medir intervalos con una resolución de un attosegundo; describe la precisión con la que el tipo puede representar una duración. La resolución real depende del reloj y de la plataforma.
Podemos acceder a esos componentes cuando necesitamos interoperar con APIs que esperan tipos numéricos:
let components = elapsed.components
let milliseconds =
Double(components.seconds) * 1_000
+ Double(components.attoseconds) / 1_000_000_000_000_000
En código de aplicación, sin embargo, suele ser mejor mantener el valor como Duration todo el tiempo posible. Convertirlo inmediatamente a Double vuelve a introducir el problema que el nuevo modelo intenta resolver: perdemos la unidad y la semántica del dato.
Formatear una duración para mostrarla
Cuando la duración forma parte de la interfaz o de un registro legible, tampoco es necesario implementar las conversiones manualmente. Duration dispone de estilos de formato localizados que pueden mostrar unidades o un formato compacto de tiempo.
let text = elapsed.formatted(
.units(
allowed: [.seconds, .milliseconds],
width: .abbreviated,
maximumUnitCount: 2
)
)
print(text)
Para intervalos más largos podemos utilizar un formato similar al de un cronómetro:
let text = elapsed.formatted(
.time(
pattern: .hourMinuteSecond(
padHourToLength: 2,
fractionalSecondsLength: 2
)
)
)
Esta separación entre medición y presentación también es importante. El valor de dominio sigue siendo un Duration, mientras que la decisión de mostrar segundos, milisegundos o un formato hh:mm:ss queda reservada a la capa que realmente necesita presentar el dato.
Medir no es lo mismo que hacer un benchmark
ContinuousClock.measure es excelente para comprobar rápidamente cuánto tarda una operación, añadir telemetría o localizar una regresión evidente. Pero una única ejecución no constituye un benchmark fiable. El resultado puede verse afectado por caches, carga del sistema, optimizaciones del compilador, planificación de tareas y otros procesos que estén ejecutándose al mismo tiempo.
Cuando queremos proteger el rendimiento de una operación mediante tests, XCTest dispone de tests de rendimiento y de XCTClockMetric, que mide el tiempo transcurrido utilizando un reloj monótono. Además, puede combinarse con métricas de CPU, memoria, almacenamiento o regiones marcadas mediante signposts.
func testCatalogDecodingPerformance() throws {
let data = try loadFixture()
measure(metrics: [XCTClockMetric(), XCTCPUMetric()]) {
_ = try? JSONDecoder().decode(Catalog.self, from: data)
}
}
Y cuando el problema ya afecta a una aplicación real —scroll con tirones, una pantalla que tarda demasiado en aparecer o trabajo inesperado en el hilo principal— la respuesta no es llenar el código de cronómetros. Instruments permite analizar la ejecución completa y descubrir dónde se consume realmente el tiempo, en lugar de medir únicamente las funciones que sospechamos de antemano.
Una API pequeña que evita muchos errores
La principal ventaja de Clock no es que permita escribir menos líneas que DispatchTime. Su verdadero valor es proporcionar un modelo correcto para razonar sobre el tiempo. Un Date representa un momento del calendario, un Instant representa un punto dentro de un reloj y un Duration representa la distancia temporal entre dos instantes.
Para código Swift moderno, ContinuousClock().measure { ... } debería ser el punto de partida cuando necesitamos conocer cuánto tarda una operación. SuspendingClock queda disponible cuando el tiempo suspendido no debe contar, Duration evita unidades numéricas ambiguas y XCTest o Instruments cubren los escenarios donde una medición rápida deja paso a un análisis de rendimiento serio y reproducible.