Instrumenting Julia with DTrace, and bpftrace
DTrace y bpftrace son herramientas que permiten la instrumentación ligera de procesos. Puedes activar y desactivar la instrumentación mientras el proceso está en ejecución, y con la instrumentación desactivada, la sobrecarga es mínima.
El soporte para probes se agregó en Julia 1.8
Esta documentación ha sido escrita desde una perspectiva de Linux, la mayor parte de esto debería ser aplicable en Mac OS/Darwin y FreeBSD.
Enabling support
En Linux, instala el paquete systemtap
que tiene una versión de dtrace
y crea un archivo Make.user
que contenga
WITH_DTRACE=1
para habilitar las sondas de USDT.
Verifying
> readelf -n usr/lib/libjulia-internal.so.1
Displaying notes found in: .note.gnu.build-id
Owner Data size Description
GNU 0x00000014 NT_GNU_BUILD_ID (unique build ID bitstring)
Build ID: 57161002f35548772a87418d2385c284ceb3ead8
Displaying notes found in: .note.stapsdt
Owner Data size Description
stapsdt 0x00000029 NT_STAPSDT (SystemTap probe descriptors)
Provider: julia
Name: gc__begin
Location: 0x000000000013213e, Base: 0x00000000002bb4da, Semaphore: 0x0000000000346cac
Arguments:
stapsdt 0x00000032 NT_STAPSDT (SystemTap probe descriptors)
Provider: julia
Name: gc__stop_the_world
Location: 0x0000000000132144, Base: 0x00000000002bb4da, Semaphore: 0x0000000000346cae
Arguments:
stapsdt 0x00000027 NT_STAPSDT (SystemTap probe descriptors)
Provider: julia
Name: gc__end
Location: 0x000000000013214a, Base: 0x00000000002bb4da, Semaphore: 0x0000000000346cb0
Arguments:
stapsdt 0x0000002d NT_STAPSDT (SystemTap probe descriptors)
Provider: julia
Name: gc__finalizer
Location: 0x0000000000132150, Base: 0x00000000002bb4da, Semaphore: 0x0000000000346cb2
Arguments:
Adding probes in libjulia
Las sondas se declaran en formato dtraces en el archivo src/uprobes.d
. El archivo de encabezado generado se incluye en src/julia_internal.h
y si agregas sondas, debes proporcionar una implementación noop allí.
El encabezado contendrá un semáforo *_ENABLED
y la llamada real a la sonda. Si los argumentos de la sonda son costosos de calcular, primero debes verificar si la sonda está habilitada y luego calcular los argumentos y llamar a la sonda.
if (JL_PROBE_{PROBE}_ENABLED())
auto expensive_arg = ...;
JL_PROBE_{PROBE}(expensive_arg);
Si su sonda no tiene argumentos, se prefiere no incluir la verificación del semáforo. Con las sondas USDT habilitadas, el costo de un semáforo es una carga de memoria, independientemente de si la sonda está habilitada o no.
#define JL_PROBE_GC_BEGIN_ENABLED() __builtin_expect (julia_gc__begin_semaphore, 0)
__extension__ extern unsigned short julia_gc__begin_semaphore __attribute__ ((unused)) __attribute__ ((section (".probes")));
Mientras que la sonda en sí es un trineo noop que será conectado a una trampa para el manejador de la sonda.
Available probes
GC probes
julia:gc__begin
: El GC comienza a ejecutarse en un hilo y activa la detención del mundo.julia:gc__stop_the_world
: Todos los hilos han alcanzado un punto seguro y se ejecuta el GC.julia:gc__mark__begin
: Comenzando la fase de marcadojulia:gc__mark_end(scanned_bytes, perm_scanned)
: La fase de marcado ha terminadojulia:gc__sweep_begin(full)
: Comenzando barridojulia:gc__sweep_end
: Fase de barrido finalizadajulia:gc__end
: GC ha terminado, otros hilos continúan trabajandojulia:gc__finalizer
: El hilo inicial de GC ha terminado de ejecutar los finalizadores
Task runtime probes
julia:rt__run__task(task)
: Cambiando a la tareatask
en el hilo actual.julia:rt__pause__task(task)
: Cambiando de la tareatask
en el hilo actual.julia:rt__new__task(parent, child)
: La tareaparent
creó la tareachild
en el hilo actual.julia:rt__start__task(task)
: Tareatask
iniciada por primera vez con una nueva pila.julia:rt__finish__task(tarea)
: La tareatarea
ha finalizado y ya no se ejecutará.julia:rt__start__process__events(task)
: La tareatask
comenzó a procesar eventos de libuv.julia:rt__finish__process__events(tarea)
: La tareatarea
terminó de procesar eventos de libuv.
Task queue probes
julia:rt__taskq__insert(ptls, task)
: El hiloptls
intentó insertartask
en un PARTR multiq.julia:rt__taskq__get(ptls, task)
: El hiloptls
sacótask
de una cola multiq PARTR.
Thread sleep/wake probes
julia:rt__sleep__check__wake(ptls, old_state)
: Hilo (PTLSptls
) despertando, previamente en el estadoold_state
.julia:rt__sleep__check__wakeup(ptls)
: El hilo (PTLSptls
) se despertó a sí mismo.julia:rt__sleep__check__sleep(ptls)
: El hilo (PTLSptls
) está intentando dormir.julia:rt__sleep__check__taskq__wake(ptls)
: El hilo (PTLSptls
) no puede dormir debido a tareas en PARTR multiq.julia:rt__sleep__check__task__wake(ptls)
: El hilo (PTLSptls
) no puede dormir debido a tareas en la cola de trabajo de Base.julia:rt__sleep__check__uv__wake(ptls)
: El hilo (PTLSptls
) no puede dormir debido a la activación de libuv.
Probe usage examples
GC stop-the-world latency
Un ejemplo de script bpftrace
se encuentra en contrib/gc_stop_the_world_latency.bt
y crea un histograma de la latencia para que todos los hilos alcancen un punto seguro.
Ejecutando este código de Julia, con julia -t 2
using Base.Threads
fib(x) = x <= 1 ? 1 : fib(x-1) + fib(x-2)
beaver = @spawn begin
while true
fib(30)
# A manual safepoint is necessary since otherwise this loop
# may never yield to GC.
GC.safepoint()
end
end
allocator = @spawn begin
while true
zeros(1024)
end
end
wait(allocator)
y en una segunda terminal
> sudo contrib/bpftrace/gc_stop_the_world_latency.bt
Attaching 4 probes...
Tracing Julia GC Stop-The-World Latency... Hit Ctrl-C to end.
^C
@usecs[1743412]:
[4, 8) 971 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
[8, 16) 837 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ |
[16, 32) 129 |@@@@@@ |
[32, 64) 10 | |
[64, 128) 1 | |
Podemos ver la distribución de latencia de la fase de detención del mundo en el proceso de Julia ejecutado.
Task spawn monitor
A veces es útil saber cuándo una tarea está generando otras tareas. Esto es muy fácil de ver con rt__new__task
. El primer argumento de la sonda, parent
, es la tarea existente que está creando una nueva tarea. Esto significa que si conoces la dirección de la tarea que deseas monitorear, puedes ver fácilmente las tareas que esa tarea específica generó. Veamos cómo hacer esto; primero, comencemos una sesión de Julia y obtengamos el PID y la dirección de la tarea del REPL:
> julia
_
_ _ _(_)_ | Documentation: https://docs.julialang.org
(_) | (_) (_) |
_ _ _| |_ __ _ | Type "?" for help, "]?" for Pkg help.
| | | | | | |/ _` | |
| | |_| | | | (_| | | Version 1.6.2 (2021-07-14)
_/ |\__'_|_|_|\__'_| | Official https://julialang.org/ release
|__/ |
1> getpid()
997825
2> current_task()
Task (runnable) @0x00007f524d088010
Ahora podemos iniciar bpftrace
y hacer que monitoree rt__new__task
solo para este padre:
sudo bpftrace -p 997825 -e 'usdt:usr/lib/libjulia-internal.so:julia:rt__new__task /arg0==0x00007f524d088010/{ printf("Tarea: %x\n", arg0); }'
(Tenga en cuenta que en lo anterior, arg0
es el primer argumento, parent
).
Y si generamos una sola tarea:
@async 1+1
vemos que esta tarea está siendo creada:
Tarea: 4d088010
Sin embargo, si generamos un montón de tareas a partir de esa tarea recién generada:
@async for i in 1:10
@async 1+1
end
todavía solo vemos una tarea de bpftrace
:
Tarea: 4d088010
y sigue siendo la misma tarea que estábamos monitoreando. Por supuesto, podemos eliminar este filtro para ver todas las tareas recién creadas con la misma facilidad:
sudo bpftrace -p 997825 -e 'usdt:usr/lib/libjulia-internal.so:julia:rt__new__task { printf("Tarea: %x\n", arg0); }'
Task: 4d088010
Task: 4dc4e290
Task: 4dc4e290
Task: 4dc4e290
Task: 4dc4e290
Task: 4dc4e290
Task: 4dc4e290
Task: 4dc4e290
Task: 4dc4e290
Task: 4dc4e290
Task: 4dc4e290
Podemos ver nuestra tarea raíz y la tarea recién creada como la madre de las diez tareas aún más nuevas.
Thundering herd detection
Los tiempos de ejecución de tareas a menudo pueden sufrir del problema de "manada atronadora": cuando se agrega trabajo a un tiempo de ejecución de tareas tranquilo, todos los hilos pueden ser despertados de su letargo, incluso si no hay suficiente trabajo para que cada hilo lo procese. Esto puede causar latencia adicional y ciclos de CPU mientras todos los hilos se despiertan (y simultáneamente vuelven a dormir, sin encontrar trabajo que ejecutar).
Podemos ver este problema ilustrado con bpftrace
bastante fácilmente. Primero, en una terminal iniciamos Julia con múltiples hilos (6 en este ejemplo), y obtenemos el PID de ese proceso:
> julia -t 6
_
_ _ _(_)_ | Documentation: https://docs.julialang.org
(_) | (_) (_) |
_ _ _| |_ __ _ | Type "?" for help, "]?" for Pkg help.
| | | | | | |/ _` | |
| | |_| | | | (_| | | Version 1.6.2 (2021-07-14)
_/ |\__'_|_|_|\__'_| | Official https://julialang.org/ release
|__/ |
1> getpid()
997825
Y en otra terminal comenzamos a monitorear nuestro proceso con bpftrace
, específicamente sondeando el gancho rt__sleep__check__wake
:
sudo bpftrace -p 997825 -e 'usdt:usr/lib/libjulia-internal.so:julia:rt__sleep__check__wake { printf("¡Hilo despertado! %x\n", arg0); }'
Ahora, creamos y ejecutamos una única tarea en Julia:
Threads.@spawn 1+1
Y en bpftrace
vemos impreso algo como:
Thread wake up! 3f926100
Thread wake up! 3ebd5140
Thread wake up! 3f876130
Thread wake up! 3e2711a0
Thread wake up! 3e312190
Aunque solo generamos una única tarea (que solo un hilo podía procesar a la vez), ¡despertamos a todos nuestros otros hilos! En el futuro, un runtime de tareas más inteligente podría despertar solo un hilo (o ninguno; ¡el hilo que la generó podría ejecutar esta tarea!), y deberíamos ver que este comportamiento desaparezca.
Task Monitor with BPFnative.jl
BPFnative.jl es capaz de adjuntarse a los puntos de sonda USDT al igual que bpftrace
. Hay una demostración disponible para monitorear el tiempo de ejecución de tareas, GC y transiciones de sueño/despertar de hilos here.
Notes on using bpftrace
Un ejemplo de sonda en el formato bpftrace se ve así:
usdt:usr/lib/libjulia-internal.so:julia:gc__begin
{
@start[pid] = nsecs;
}
La declaración de la sonda toma el tipo usdt
, luego ya sea la ruta a la biblioteca o el PID, el nombre del proveedor julia
y el nombre de la sonda gc__begin
. Tenga en cuenta que estoy utilizando una ruta relativa a libjulia-internal.so
, pero esto podría necesitar ser una ruta absoluta en un sistema de producción.