# Instrumentando o CPython com DTrace e SystemTap

autor:

  David Malcolm

autor:

  Łukasz Langa

DTrace e SystemTap são ferramentas de monitoramento, cada uma fornecendo uma maneira de inspecionar o que os processos em um sistema de computador estão fazendo. Ambas usam linguagens específicas de domínio, permitindo que um usuário escreva scripts que:

- filtrar quais processos devem ser observados

- coletem dados dos processos de interesse

- gerem relatórios sobre os dados

A partir do Python 3.6, o CPython pode ser criado com “marcadores” incorporados, também conhecidos como “sondas” (probes), que podem ser observados por um script DTrace ou SystemTap, facilitando o monitoramento do que os processos CPython em um sistema estão fazendo.

Os marcadores DTrace são detalhes de implementação do interpretador CPython. Não há garantias sobre a compatibilidade de sondas entre versões do CPython. Os scripts DTrace podem parar de funcionar ou funcionar incorretamente sem aviso ao alterar as versões do CPython.

## Habilitando os marcadores estáticos

O macOS vem com suporte embutido para DTrace. No Linux, para construir o CPython com os marcadores incorporados para SystemTap, as ferramentas de desenvolvimento SystemTap devem ser instaladas.

Em uma máquina Linux, isso pode ser feito via:

    $ yum install systemtap-sdt-devel

ou:

    $ sudo apt-get install systemtap-sdt-dev

O CPython deve então ser `configurado com a opção --with-dtrace`:

    checking for --with-dtrace... yes

No macOS, você pode listar as sondas DTrace disponíveis executando um processo Python em segundo plano e listando todas as sondas disponibilizadas pelo provedor Python:

    $ python3.6 -q &
    $ sudo dtrace -l -P python$!  # or: dtrace -l -m python3.6

       ID   PROVIDER            MODULE                          FUNCTION NAME
    29564 python18035        python3.6          _PyEval_EvalFrameDefault function-entry
    29565 python18035        python3.6             dtrace_function_entry function-entry
    29566 python18035        python3.6          _PyEval_EvalFrameDefault function-return
    29567 python18035        python3.6            dtrace_function_return function-return
    29568 python18035        python3.6                           collect gc-done
    29569 python18035        python3.6                           collect gc-start
    29570 python18035        python3.6          _PyEval_EvalFrameDefault line
    29571 python18035        python3.6                 maybe_dtrace_line line

No Linux, você pode verificar se os marcadores estáticos do SystemTap estão presentes no binário compilado, observando se ele contém uma seção “.note.stapsdt”.

    $ readelf -S ./python | grep .note.stapsdt
    [30] .note.stapsdt        NOTE         0000000000000000 00308d78

Se você construiu o Python como uma biblioteca compartilhada (com a opção de configuração `--enable-shared`), você precisa procurar dentro da biblioteca compartilhada. Por exemplo:

    $ readelf -S libpython3.3dm.so.1.0 | grep .note.stapsdt
    [29] .note.stapsdt        NOTE         0000000000000000 00365b68

Um readelf moderno o suficiente pode exibir os metadados:

    $ readelf -n ./python

    Displaying notes found at file offset 0x00000254 with length 0x00000020:
        Owner                 Data size          Description
        GNU                  0x00000010          NT_GNU_ABI_TAG (ABI version tag)
            OS: Linux, ABI: 2.6.32

    Displaying notes found at file offset 0x00000274 with length 0x00000024:
        Owner                 Data size          Description
        GNU                  0x00000014          NT_GNU_BUILD_ID (unique build ID bitstring)
            Build ID: df924a2b08a7e89f6e11251d4602022977af2670

    Displaying notes found at file offset 0x002d6c30 with length 0x00000144:
        Owner                 Data size          Description
        stapsdt              0x00000031          NT_STAPSDT (SystemTap probe descriptors)
            Provider: python
            Name: gc__start
            Location: 0x00000000004371c3, Base: 0x0000000000630ce2, Semaphore: 0x00000000008d6bf6
            Arguments: -4@%ebx
        stapsdt              0x00000030          NT_STAPSDT (SystemTap probe descriptors)
            Provider: python
            Name: gc__done
            Location: 0x00000000004374e1, Base: 0x0000000000630ce2, Semaphore: 0x00000000008d6bf8
            Arguments: -8@%rax
        stapsdt              0x00000045          NT_STAPSDT (SystemTap probe descriptors)
            Provider: python
            Name: function__entry
            Location: 0x000000000053db6c, Base: 0x0000000000630ce2, Semaphore: 0x00000000008d6be8
            Arguments: 8@%rbp 8@%r12 -4@%eax
        stapsdt              0x00000046          NT_STAPSDT (SystemTap probe descriptors)
            Provider: python
            Name: function__return
            Location: 0x000000000053dba8, Base: 0x0000000000630ce2, Semaphore: 0x00000000008d6bea
            Arguments: 8@%rbp 8@%r12 -4@%eax

Os metadados acima contêm informações sobre o SystemTap descrevendo como ele pode corrigir instruções de código de máquina estrategicamente posicionadas para habilitar os ganchos de rastreamento usados por um script do SystemTap.

## Sondas estáticas do DTtrace

O script DTrace de exemplo a seguir pode ser usado para mostrar a hierarquia de chamada/retorno de um script Python, rastreando apenas dentro da invocação de uma função chamada “start”. Em outras palavras, invocações de função em tempo de importação não serão listadas:

    self int indent;

    python$target:::function-entry
    /copyinstr(arg1) == "start"/
    {
            self->trace = 1;
    }

    python$target:::function-entry
    /self->trace/
    {
            printf("%d\t%*s:", timestamp, 15, probename);
            printf("%*s", self->indent, "");
            printf("%s:%s:%d\n", basename(copyinstr(arg0)), copyinstr(arg1), arg2);
            self->indent++;
    }

    python$target:::function-return
    /self->trace/
    {
            self->indent--;
            printf("%d\t%*s:", timestamp, 15, probename);
            printf("%*s", self->indent, "");
            printf("%s:%s:%d\n", basename(copyinstr(arg0)), copyinstr(arg1), arg2);
    }

    python$target:::function-return
    /copyinstr(arg1) == "start"/
    {
            self->trace = 0;
    }

Pode ser invocado assim:

    $ sudo dtrace -q -s call_stack.d -c "python3.6 script.py"

O resultado deve ser algo assim:

    156641360502280  function-entry:call_stack.py:start:23
    156641360518804  function-entry: call_stack.py:function_1:1
    156641360532797  function-entry:  call_stack.py:function_3:9
    156641360546807 function-return:  call_stack.py:function_3:10
    156641360563367 function-return: call_stack.py:function_1:2
    156641360578365  function-entry: call_stack.py:function_2:5
    156641360591757  function-entry:  call_stack.py:function_1:1
    156641360605556  function-entry:   call_stack.py:function_3:9
    156641360617482 function-return:   call_stack.py:function_3:10
    156641360629814 function-return:  call_stack.py:function_1:2
    156641360642285 function-return: call_stack.py:function_2:6
    156641360656770  function-entry: call_stack.py:function_3:9
    156641360669707 function-return: call_stack.py:function_3:10
    156641360687853  function-entry: call_stack.py:function_4:13
    156641360700719 function-return: call_stack.py:function_4:14
    156641360719640  function-entry: call_stack.py:function_5:18
    156641360732567 function-return: call_stack.py:function_5:21
    156641360747370 function-return:call_stack.py:start:28

## Marcadores estáticos do SystemTap

A maneira de baixo nível de usar a integração do SystemTap é usar os marcadores estáticos diretamente. Isso requer que você declare explicitamente o arquivo binário que os contém.

Por exemplo, este script SystemTap pode ser usado para mostrar a hierarquia de chamada/retorno de um script Python:

    probe process("python").mark("function__entry") {
         filename = user_string($arg1);
         funcname = user_string($arg2);
         lineno = $arg3;

         printf("%s => %s in %s:%d\\n",
                thread_indent(1), funcname, filename, lineno);
    }

    probe process("python").mark("function__return") {
        filename = user_string($arg1);
        funcname = user_string($arg2);
        lineno = $arg3;

        printf("%s <= %s in %s:%d\\n",
               thread_indent(-1), funcname, filename, lineno);
    }

Pode ser invocado assim:

    $ stap \
      show-call-hierarchy.stp \
      -c "./python test.py"

O resultado deve ser algo assim:

    11408 python(8274):        => __contains__ in Lib/_abcoll.py:362
    11414 python(8274):         => __getitem__ in Lib/os.py:425
    11418 python(8274):          => encode in Lib/os.py:490
    11424 python(8274):          <= encode in Lib/os.py:493
    11428 python(8274):         <= __getitem__ in Lib/os.py:426
    11433 python(8274):        <= __contains__ in Lib/_abcoll.py:366

sendo as colunas:

- tempo em microssegundos desde o início do script

- nome do executável

- PID do processo

e o restante indica a hierarquia de chamada/retorno conforme o script é executado.

Para uma construção com `--enable-shared` do CPython, os marcadores estão contidos na biblioteca compartilhada libpython, e o caminho pontilhado da sonda precisa refletir isso. Por exemplo, esta linha do exemplo acima:

    probe process("python").mark("function__entry") {

deve ler-se em vez disso:

    probe process("python").library("libpython3.6dm.so.1.0").mark("function__entry") {

(presumindo uma construção de depuração do CPython 3.6)

## Marcadores estáticos disponíveis

function__entry(str filename, str funcname, int lineno)

  Este marcador indica que a execução de uma função Python começou. Ele é acionado somente para funções Python puro (bytecode).

  O nome do arquivo, o nome da função e o número da linha são fornecidos de volta ao script de rastreamento como argumentos posicionais, que devem ser acessados usando `$arg1`, `$arg2`, `$arg3`:

  > - `$arg1` : nome de arquivo como `(const char *)`, acessível usando `user_string($arg1)`

  > - `$arg2` : nome da função como `(const char *)`, acessível usando `user_string($arg2)`

  > - `$arg3` : número da linha como `int`

function__return(str filename, str funcname, int lineno)

  Este marcador é o inverso de `function__entry()`, e indica que a execução de uma função Python terminou (seja via `return`, ou via uma exceção). Ele é acionado somente para funções Python puro (bytecode).

  Os argumentos são os mesmos de `function__entry()`

line(str filename, str funcname, int lineno)

  Este marcador indica que uma linha Python está prestes a ser executada. É o equivalente ao rastreamento linha por linha com um perfilador Python. Ele não é acionado dentro de funções C.

  Os argumentos são os mesmos de `function__entry()`.

gc__start(int generation)

  Dispara quando o interpretador Python inicia um ciclo de coleta de lixo. `arg0` é a geração a ser percurrida, como `gc.collect()`.

gc__done(long collected)

  Dispara quando o interpretador Python termina um ciclo de coleta de lixo. `arg0` é o número de objetos coletados.

import__find__load__start(str modulename)

  Dispara antes de `importlib` tentar encontrar e carregar o módulo. `arg0` é o nome do módulo.

  Adicionado na versão 3.7.

import__find__load__done(str modulename, int found)

  Dispara após a função find_and_load do `importlib` ser chamada. `arg0` é o nome do módulo, `arg1` indica se o módulo foi carregado com sucesso.

  Adicionado na versão 3.7.

audit(str event, void *tuple)

  Dispara quando `sys.audit()` ou `PySys_Audit()` é chamada. `arg0` é o nome do evento como string C, `arg1` é um ponteiro `PyObject` para um objeto tupla.

  Adicionado na versão 3.8.

### C Entry Points

To simplify triggering of DTrace markers, Python’s C API comes with a number of helper functions that mirror each static marker. On builds of Python without DTrace enabled, these do nothing.

In general, it is not necessary to call these yourself, as Python will do it for you.

C API Function | Static Marker | Notas
--- | --- | ---
void PyDTrace_LINE(const char *arg0, const char *arg1, int arg2) | `line()` |
void PyDTrace_FUNCTION_ENTRY(const char *arg0, const char *arg1, int arg2) | `function__entry()` |
void PyDTrace_FUNCTION_RETURN(const char *arg0, const char *arg1, int arg2) | `function__return()` |
void PyDTrace_GC_START(int arg0) | `gc__start()` |
void PyDTrace_GC_DONE(Py_ssize_t arg0) | `gc__done()` |
void PyDTrace_INSTANCE_NEW_START(int arg0) | `instance__new__start()` | Not used by Python
void PyDTrace_INSTANCE_NEW_DONE(int arg0) | `instance__new__done()` | Not used by Python
void PyDTrace_INSTANCE_DELETE_START(int arg0) | `instance__delete__start()` | Not used by Python
void PyDTrace_INSTANCE_DELETE_DONE(int arg0) | `instance__delete__done()` | Not used by Python
void PyDTrace_IMPORT_FIND_LOAD_START(const char *arg0) | `import__find__load__start()` |
void PyDTrace_IMPORT_FIND_LOAD_DONE(const char *arg0, int arg1) | `import__find__load__done()` |
void PyDTrace_AUDIT(const char *arg0, void *arg1) | `audit()` |

### C Probing Checks

int PyDTrace_LINE_ENABLED(void)

int PyDTrace_FUNCTION_ENTRY_ENABLED(void)

int PyDTrace_FUNCTION_RETURN_ENABLED(void)

int PyDTrace_GC_START_ENABLED(void)

int PyDTrace_GC_DONE_ENABLED(void)

int PyDTrace_INSTANCE_NEW_START_ENABLED(void)

int PyDTrace_INSTANCE_NEW_DONE_ENABLED(void)

int PyDTrace_INSTANCE_DELETE_START_ENABLED(void)

int PyDTrace_INSTANCE_DELETE_DONE_ENABLED(void)

int PyDTrace_IMPORT_FIND_LOAD_START_ENABLED(void)

int PyDTrace_IMPORT_FIND_LOAD_DONE_ENABLED(void)

int PyDTrace_AUDIT_ENABLED(void)

  All calls to `PyDTrace` functions must be guarded by a call to one of these functions. This allows Python to minimize performance impact when probing is disabled.

  On builds without DTrace enabled, these functions do nothing and return `0`.

## Tapsets de SystemTap

A maneira mais avançada de usar a integração do SystemTap é usar um “tapset”: o equivalente do SystemTap a uma biblioteca, que oculta alguns dos detalhes de nível inferior dos marcadores estáticos.

Aqui está um arquivo tapset, baseado em uma construção não compartilhada do CPython:

    /*
       Fornece um envólucro de mais alto nível em volta dos marcadores
       function__entry e function__return:
     \*/
    probe python.function.entry = process("python").mark("function__entry")
    {
        filename = user_string($arg1);
        funcname = user_string($arg2);
        lineno = $arg3;
        frameptr = $arg4
    }
    probe python.function.return = process("python").mark("function__return")
    {
        filename = user_string($arg1);
        funcname = user_string($arg2);
        lineno = $arg3;
        frameptr = $arg4
    }

Se este arquivo for instalado no diretório de tapsets do SystemTap (por exemplo, `/usr/share/systemtap/tapset`), estes pontos de sondagem adicionais ficarão disponíveis:

python.function.entry(str filename, str funcname, int lineno, frameptr)

  Este ponto de sondagem indica que a execução de uma função Python começou. Ele é acionado somente para funções Python puro (bytecode).

python.function.return(str filename, str funcname, int lineno, frameptr)

  Este ponto de sondagem é o inverso de `python.function.return`, e indica que a execução de uma função Python terminou (seja via `return`, ou via uma exceção). Ele é acionado somente para funções Python puro (bytecode).

## Exemplos

Este script SystemTap usa o tapset acima para implementar de forma mais limpa o exemplo dado acima de rastreamento da hierarquia de chamada de função Python, sem precisar na diretamente

    probe python.function.entry
    {
      printf("%s => %s in %s:%d\n",
             thread_indent(1), funcname, filename, lineno);
    }

    probe python.function.return
    {
      printf("%s <= %s in %s:%d\n",
             thread_indent(-1), funcname, filename, lineno);
    }

O script a seguir usa o tapset acima para fornecer uma visão geral de todo o código CPython em execução, mostrando os 20 quadros de bytecode mais frequentemente inseridos, a cada segundo, em todo o sistema:

    global fn_calls;

    probe python.function.entry
    {
        fn_calls[pid(), filename, funcname, lineno] += 1;
    }

    probe timer.ms(1000) {
        printf("\033[2J\033[1;1H") /* clear screen \*/
        printf("%6s %80s %6s %30s %6s\n",
               "PID", "FILENAME", "LINE", "FUNCTION", "CALLS")
        foreach ([pid, filename, funcname, lineno] in fn_calls- limit 20) {
            printf("%6d %80s %6d %30s %6d\n",
                pid, filename, lineno, funcname,
                fn_calls[pid, filename, funcname, lineno]);
        }
        delete fn_calls;
    }