
Hoje estamos compartilhando com você a tradução de um artigo do criador do FunctionTrace, um criador de perfil Python com uma interface gráfica intuitiva que pode criar o perfil de aplicativos multiprocessadores e multithread e usa uma ordem de magnitude menos recursos do que outros criadores de perfil Python. Não importa se você está apenas aprendendo desenvolvimento web em Python ou se já o usa há muito tempo - é sempre bom entender o que seu código está fazendo. Sobre como esse projeto apareceu, sobre os detalhes de seu desenvolvimento - mais abaixo do corte.
Introdução
O Firefox Profiler foi a pedra angular do Firefox durante a era do Project Quantum . Ao abrir a entrada de exemplo, você vê uma poderosa interface de análise de desempenho baseada na web que inclui árvores de chamadas, diagramas de pilha, diagramas de incêndio e muito mais. Todas as ações de filtragem, dimensionamento, corte e transformação de dados são salvas em uma URL que pode ser compartilhada. Os resultados podem ser compartilhados em um relatório de bug, suas descobertas podem ser documentadas, comparadas com outros registros ou as informações podem ser repassadas para pesquisas futuras. O Firefox DevEdition possui um thread de criação de perfil integrado. Esse fluxo facilita a comunicação. Nosso objetivo é capacitar todos os desenvolvedores, mesmo fora do Firefox, para colaborar de forma produtiva.
Anteriormente, o Firefox Profiler importava outros formatos, começando com o perf do Linux e os perfis do Chrome . Com o tempo, os desenvolvedores adicionaram mais formatos. Hoje, os primeiros projetos estão surgindo para adaptar o Firefox para ferramentas de análise. FunctionTrace é um desses projetos. Matt conta a história de como o instrumento foi feito.
FunctionTrace
Recentemente, criei uma ferramenta para ajudar os desenvolvedores a entender melhor o que está acontecendo em seu código Python. FunctionTrace é um criador de perfil sem amostragem para Python que é executado em aplicativos não modificados com sobrecarga muito baixa - menos de 5%. É importante observar que ele está integrado ao Firefox Profiler. Isso permite que você interaja com perfis graficamente, tornando mais fácil descobrir padrões e fazer alterações em sua base de código.
Vou repassar os objetivos de desenvolvimento do FunctionTrace e compartilhar os detalhes técnicos da implementação. No final jogaremos com uma pequena demonstração .

Um exemplo de um perfil FunctionTrace aberto no Firefox Profiler.
Dívida de tecnologia como motivação
As bases de código tendem a ficar maiores com o tempo. Especialmente quando se trabalha em projetos complexos com muitas pessoas. Alguns idiomas lidam melhor com esse problema. Por exemplo, os recursos do Java IDE já existem há décadas. Ou Rust e sua forte tipagem, o que torna a refatoração muito fácil. Às vezes, parece que, à medida que as bases de código em outras linguagens aumentam, fica mais difícil de manter. Isso é especialmente verdadeiro para o código Python mais antigo. Pelo menos somos todos Python 3 agora, certo?
Fazer alterações em grande escala ou refatorar código desconhecido pode ser extremamente difícil. É muito mais fácil alterar o código corretamente quando vejo todas as interações do programa e o que ele faz. Freqüentemente, me pego reescrevendo trechos de código que nunca tive a intenção de tocar: a ineficiência é óbvia quando a vejo na visualização.
Eu queria entender o que estava acontecendo no código sem ter que ler centenas de arquivos. Mas não encontrei as ferramentas que atendiam às minhas necessidades. Além disso, perdi o interesse em construir essa ferramenta sozinho devido à quantidade de trabalho de IU envolvido. E a interface era necessária. Minhas esperanças de uma compreensão rápida da execução do programa reacenderam quando me deparei com o criador de perfil do Firefox.
O criador de perfil forneceu todos os elementos difíceis de implementar - uma interface de usuário de código aberto intuitiva que exibe gráficos de pilha, marcadores de log com limite de tempo, gráficos de fogo e oferece estabilidade cuja natureza está vinculada a um navegador da web famoso. Qualquer ferramenta que pode escrever um perfil JSON formatado corretamente pode reutilizar todos os recursos de análise gráfica mencionados anteriormente.
Design FunctionTrace
Felizmente, eu já tinha uma semana de férias planejada depois que descobri o perfilador do Firefox. E eu tinha um amigo que queria desenvolver um instrumento comigo. Ele também tirou um dia de folga naquela semana.
Objetivos
Tínhamos vários objetivos quando começamos a desenvolver o FunctionTrace:
- A capacidade de ver tudo o que acontece no programa.
- .
- , .
O primeiro objetivo teve um impacto significativo no design. Os dois últimos adicionaram complexidade de engenharia. Nós dois sabíamos, por experiência anterior com ferramentas semelhantes, que a frustração é que não veremos chamadas de função muito curtas. Quando você grava um registro de rastreamento de 1 ms, mas tem funções importantes e mais rápidas, está perdendo muito do que está acontecendo dentro do seu programa.
Também sabíamos que precisávamos rastrear todas as chamadas de função. Portanto, não podemos usar o criador de perfil de amostragem. Além disso, recentemente passei algum tempo com código em que funções Python executam outro código Python, geralmente por meio de um script intermediário de shell. Com base nisso, queríamos ser capazes de rastrear os processos filhos.
Implementação inicial
Para oferecer suporte a vários processos e descendentes, optamos por um modelo cliente-servidor. Os clientes Python enviam dados de rastreamento para o servidor Rust. O servidor agrega e compacta os dados antes de gerar um perfil, que pode ser consumido pelo criador de perfil do Firefox. Escolhemos o Rust por vários motivos, incluindo tipagem forte, esforço para desempenho consistente e uso de memória previsível e facilidade de prototipagem e refatoração.
Nós prototipamos o cliente como um módulo Python chamado
python -m functiontrace code.py. Isso facilitou o uso dos ganchos de rastreamento integrados para registrar a execução. A implementação original era assim:
def profile_func(frame, event, arg):
if event == "call" or event == "return" or event == "c_call" or event == "c_return":
data = (event, time.time())
server.sendall(json.dumps(data))
sys.setprofile(profile_func)
O servidor está escutando em um soquete de domínio Unix . Os dados são então lidos do cliente e convertidos em JSON pelo criador de perfil do Firefox .
O profiler oferece suporte a vários tipos de perfis, como logs de desempenho . Mas decidimos gerar JSON do formato interno do profiler. Requer menos espaço e manutenção do que adicionar um novo formato compatível. É importante observar que o criador de perfil mantém compatibilidade com versões anteriores entre as versões do perfil. Isso significa que qualquer perfil projetado para a versão atual do formato é automaticamente convertido para a versão mais recente quando baixado no futuro. O profiler também se refere a strings com identificadores inteiros. Isso permite uma economia significativa de espaço usando a desduplicação (embora seja trivial usarindexmap ).
Várias otimizações
Principalmente o código original funcionou. Em cada chamada e retorno de função, o Python chamou o gancho. O gancho enviou uma mensagem JSON ao servidor por meio de um soquete para convertê-la no formato desejado. Mas foi incrivelmente lento. Mesmo depois de agrupar as chamadas de soquete, vimos pelo menos oito vezes a sobrecarga de alguns programas de teste.
Depois de ver esses custos, descemos para o nível C usando a API C para Python . E eles obtiveram um coeficiente de sobrecarga de 1,1 nos mesmos programas. Depois disso, pudemos realizar outra otimização de chave, substituindo chamadas
time.time()para operações rdtsc viaclock_gettime()... Reduzimos a sobrecarga de desempenho de funções de chamada para instruções múltiplas e gerando 64 bits de dados. Isso é muito mais eficiente do que encadear chamadas Python e aritmética complexa em um caminho de missão crítica.
Mencionei que o rastreamento de vários threads e processos filho é compatível. Esta é uma das partes mais difíceis do cliente, portanto, vale a pena discutir alguns detalhes de nível inferior.
Suporte para vários fluxos
O manipulador para todos os threads é instalado via
threading.setprofile(). Registramos por meio de um manipulador como este quando configuramos o estado do thread. Isso garante que o Python esteja em execução e o GIL sendo mantido. Isso simplifica algumas das suposições:
// This is installed as the setprofile() handler for new threads by
// threading.setprofile(). On its first execution, it initializes tracing for
// the thread, including creating the thread state, before replacing itself with
// the normal Fprofile_FunctionTrace handler.
static PyObject* Fprofile_ThreadFunctionTrace(..args..) {
Fprofile_CreateThreadState();
// Replace our setprofile() handler with the real one, then manually call
// it to ensure this call is recorded.
PyEval_SetProfile(Fprofile_FunctionTrace);
Fprofile_FunctionTrace(..args..);
Py_RETURN_NONE;
}
Quando o gancho é chamado
Fprofile_ThreadFunctionTrace(), ele aloca a estrutura ThreadState. Essa estrutura contém informações necessárias para o thread registrar eventos e se comunicar com o servidor. Em seguida, enviamos uma mensagem init para o servidor de perfil. Aqui, notificamos o servidor para iniciar um novo fluxo e fornecemos algumas informações iniciais: hora, PID, etc. Após a inicialização, substituímos o gancho por Fprofile_FunctionTrace()um que faz o rastreamento real.
Suporte para processos filho
Ao trabalhar com vários processos, assumimos que os processos filhos são iniciados por meio do interpretador Python. Infelizmente, os processos filhos não são chamados com
-m functiontrace, portanto, não sabemos como rastreá-los. Para garantir que os processos filhos sejam monitorados, a variável de ambiente $ PATH é alterada na inicialização . Isso garante que o Python esteja apontando para um executável que conhece functiontrace:
# Generate a temp directory to store our wrappers in. We'll temporarily
# add this directory to our path.
tempdir = tempfile.mkdtemp(prefix="py-functiontrace")
os.environ["PATH"] = tempdir + os.pathsep + os.environ["PATH"]
# Generate wrappers for the various Python versions we support to ensure
# they're included in our PATH.
wrap_pythons = ["python", "python3", "python3.6", "python3.7", "python3.8"]
for python in wrap_pythons:
with open(os.path.join(tempdir, python), "w") as f:
f.write(PYTHON_TEMPLATE.format(python=python))
os.chmod(f.name, 0o755)
Um interpretador com um argumento
-m functiontraceé chamado dentro do invólucro. Finalmente, uma variável de ambiente é adicionada na inicialização. A variável indica qual soquete é usado para se comunicar com o servidor de perfil. Se o cliente inicializar e vir uma variável de ambiente já definida, ele reconhecerá o processo filho. Em seguida, ele se conecta à instância do servidor existente, permitindo que seu rastreio seja correlacionado ao do cliente original.
FunctionTrace agora
A implementação de FunctionTrace hoje tem muito em comum com a implementação descrita acima. Em um nível alto o cliente é monitorado através FunctionTrace uma chamada como esta:
python -m functiontrace code.py. Essa linha carrega um módulo Python para alguma personalização e, em seguida, chama o módulo C para definir vários ganchos de rastreamento. Esses ganchos incluem os sys.setprofileganchos de alocação de memória mencionados acima , bem como ganchos personalizados com recursos interessantes como builtins.printou builtins.__import__. Além disso, uma instância functiontrace-serveré gerada, um soquete é configurado para se comunicar com ela e é garantido que as futuras threads e processos filhos se comuniquem com o mesmo servidor.
Em cada evento de rastreamento, o cliente Python envia uma entrada MessagePack... Ele contém informações mínimas de eventos e um carimbo de data / hora no buffer de memória de fluxo. Quando o buffer está cheio (a cada 128 KB), ele é liberado para o servidor por meio do soquete compartilhado e o cliente continua a fazer seu trabalho. O servidor escuta cada cliente de forma assíncrona, consumindo rastreios rapidamente em um buffer separado para evitar bloqueá-los. O thread correspondente a cada cliente pode então analisar cada evento de rastreamento e convertê-lo no formato final apropriado. Depois que todos os clientes conectados saem, os logs de cada tópico são combinados em um log de perfil completo. Por fim, tudo isso é enviado para um arquivo, que pode ser usado com o criador de perfil do Firefox.
Lições aprendidas
Um módulo Python C oferece muito mais potência e desempenho, mas ao mesmo tempo tem um custo alto. Mais código é necessário, boa documentação é mais difícil de encontrar, poucos recursos estão disponíveis. Módulos C parecem ser uma ferramenta subutilizada para escrever módulos Python de alto desempenho. Digo isso com base em alguns dos perfis FunctionTrace que vi. Recomendamos um equilíbrio. Escreva a maior parte do código de missão crítica sem desempenho em Python e chame loops internos ou código de configuração C para partes de seu programa onde Python não brilha.
A codificação e decodificação JSON podem ser incrivelmente lentas quando não há necessidade de legibilidade. Nós mudamos para
MessagePackpara comunicação cliente-servidor e descobri que é tão fácil de trabalhar, enquanto corta o tempo de alguns benchmarks pela metade!
O suporte à criação de perfis multi-thread em Python é bastante difícil. É compreensível por que não era um recurso importante nos criadores de perfis anteriores do Python. Foram necessárias várias abordagens diferentes e muitos erros de segmentação antes de termos uma boa ideia de como trabalhar com o GIL e, ao mesmo tempo, manter o alto desempenho.
Você pode obter uma profissão exigida do zero ou Subir de nível em habilidades e salário fazendo cursos online SkillFactory:
- Curso de Python for Web Development (9 meses)
- Profissão Web developer (8 meses)
- Treinando a profissão de Ciência de Dados do zero (12 meses)
- - Data Science (14 )
- - Data Analytics (5 )
- (18 )
E
- Machine Learning (12 )
- « Machine Learning Data Science» (20 )
- «Machine Learning Pro + Deep Learning» (20 )
- (6 )
- DevOps (12 )
- iOS- (12 )
- Android- (18 )
- Java- (18 )
- JavaScript (12 )
- UX- (9 )
- Web- (7 )
