Perfiles en Go: cómo el runtime captura tu programa en ejecución

Fuentes: Profiling | Internals for Interns

El runtime de Go ofrece cinco tipos de perfiles —CPU, heap, block, mutex y goroutine— que, pese a parecer cinco subsistemas independientes, comparten un mismo esqueleto: el formato pprof, un búfer de protocolo comprimido con gzip. Cada muestra almacena un identificador de pila (location_id) y un conjunto de valores numéricos cuyo significado declara el campo sample_type; las cadenas se deduplican en una única string_table y las muestras se enlazan por referencia, de modo que el nombre de una función recorrida por diez mil muestras se guarda una sola vez.

La diferencia entre los cinco perfiles no es un simple ajuste: es el modelo de recolección. El perfil de CPU opera de forma asíncrona: una señal del sistema interrumpe un hilo en ejecución, el manejador captura la pila con el unwinder y la deposita en un anillo sin bloqueos; una goroutine en segundo plano drena ese anillo. Si se llena, se descartan muestras y se cuentan las pérdidas, porque congelar un hilo sería peor que perder datos. Heap, block y mutex registran in situ: cuando ocurre el evento, se calcula un hash de la pila, se busca un registro previo en una tabla persistente y se incrementan sus contadores en el momento, sin streaming ni drenaje. El perfil de goroutine no captura nada durante la ejecución: cuando se solicita, el runtime recorre todas las goroutines vivas y toma una instantánea única de sus pilas.

El artículo forma parte de la serie Internals for Interns y cierra explicando cómo, en el caso del perfil de CPU, el manejador de señal, al no poder asignar memoria ni tomar bloqueos, usa precisamente ese anillo sin cerrojos como destino exclusivo, lo que hace de la recolección asíncrona la pieza más singular del conjunto.