Execução dinâmica

A execução dinâmica é um recurso do Bazel em que a execução local e remota da mesma ação é iniciada em paralelo, usando a saída da primeira ramificação que termina, cancelando a outra. Ele combina o poder de execução e/ou o grande cache compartilhado de um sistema de build remoto com a baixa latência da execução local, oferecendo o melhor dos dois mundos para builds limpos e incrementais da mesma forma.

Esta página descreve como ativar, ajustar e depurar a execução dinâmica. Se você tem a execução local e remota e está tentando ajustar as configurações do Bazel para melhorar o desempenho, esta página é para você. Se você ainda não configurou a execução remota, acesse primeiro a Visão geral da execução remota do Bazel.

Ativar a execução dinâmica?

O módulo de execução dinâmica faz parte do Bazel, mas, para usar a execução dinâmica, você já precisa compilar local e remotamente com a mesma configuração do Bazel.

Para ativar o módulo de execução dinâmica, transmita a sinalização --internal_spawn_scheduler para o Bazel. Isso adiciona uma nova estratégia de execução chamada dynamic. Agora você pode usar isso como sua estratégia para as mnemônicas que você quer executar dinamicamente, como --strategy=Javac=dynamic. Consulte a próxima seção para saber como escolher para quais mnemônicas a execução dinâmica será ativada.

Para qualquer mnemônica que use a estratégia dinâmica, as estratégias de execução remota são tiradas da flag --dynamic_remote_strategy e as estratégias locais da flag --dynamic_local_strategy. Transmitir --dynamic_local_strategy=worker,sandboxed define o padrão para a ramificação local da execução dinâmica a ser testada com workers ou execução em sandbox nessa ordem. A transmissão de --dynamic_local_strategy=Javac=worker substitui o padrão apenas para o método mnemônico Javac. A versão remota funciona da mesma maneira. As duas sinalizações podem ser especificadas várias vezes. Se uma ação não puder ser executada localmente, ela será executada remotamente como normal e vice-versa.

Se o sistema remoto tiver um cache, a sinalização --dynamic_local_execution_delay adicionará um atraso em milissegundos à execução local depois que o sistema remoto tiver indicado uma ocorrência em cache. Isso evita a execução local quando são prováveis mais ocorrências em cache. O valor padrão é 1.000 ms, mas precisa ser ajustado para ser um pouco mais longo do que as ocorrências em cache costumam levar. O tempo real depende do sistema remoto e da duração da ida e volta. Normalmente, o valor será o mesmo para todos os usuários de um determinado sistema remoto, a menos que alguns deles estejam longe o suficiente para adicionar latência de ida e volta. Use os recursos de criação de perfil do Bazel para ver quanto tempo as ocorrências em cache típicas levam.

A execução dinâmica pode ser usada com a estratégia no sandbox local, bem como com workers permanentes. Os workers permanentes serão executados automaticamente com o sandbox quando usados com a execução dinâmica e não podem usar workers multiplex. Nos sistemas Darwin e Windows, a estratégia em sandbox pode ser lenta. Você pode transmitir --reuse_sandbox_directories para reduzir a sobrecarga da criação de sandboxes nesses sistemas.

A execução dinâmica também pode ser executada com a estratégia standalone. No entanto, como a estratégia standalone precisa bloquear a saída quando começa a ser executada, ela bloqueia efetivamente a estratégia remota de terminar primeiro. A sinalização --experimental_local_lockfree_output possibilita uma solução desse problema, permitindo que a execução local grave diretamente na saída, mas que seja cancelada pela execução remota, caso isso termine primeiro.

Se uma das ramificações da execução dinâmica terminar primeiro, mas falhar, toda a ação falhará. Essa é uma escolha intencional para evitar que as diferenças entre a execução local e a remota passem despercebidas.

Para mais informações sobre como a execução dinâmica e o bloqueio funcionam, consulte as excelentes postagens do blog (em inglês) de Julio Merino.

Quando devo usar a execução dinâmica?

A execução dinâmica requer alguma forma de sistema de execução remota. No momento, não é possível usar um sistema remoto somente cache, já que uma ausência no cache seria considerada uma ação com falha.

Nem todos os tipos de ações são adequados para execução remota. Os melhores candidatos são aqueles que são inerentemente mais rápidos localmente, por exemplo, com o uso de trabalhadores persistentes, ou aqueles que são executados rápido o suficiente para que a sobrecarga da execução remota domine o tempo de execução. Como cada ação executada localmente bloqueia uma quantidade de recursos de CPU e memória, a execução de ações que não se enquadram nessas categorias apenas atrasa a execução das demais.

A partir da versão 5.0.0-pre.20210708.4, a criação de perfil de desempenho contém dados sobre a execução do worker, incluindo o tempo gasto para concluir uma solicitação de trabalho após perder uma corrida de execução dinâmica. Se as linhas de execução do worker de execução dinâmica passarem muito tempo adquirindo recursos ou muito no async-worker-finish, é possível que algumas ações locais lentas atrasem as linhas de execução do worker.

Criação de perfil de dados com baixo desempenho de execução dinâmica

No perfil acima, que usa oito workers Javac, vemos muitos deles perdendo as corridas e terminando o trabalho nas linhas de execução async-worker-finish. Isso foi causado por uma mnemônica que não era worker, consumiu recursos suficientes para atrasar os workers.

Como criar perfis de dados com melhor desempenho de execução dinâmica

Quando apenas o Javac é executado com execução dinâmica, apenas cerca da metade dos workers iniciados acabam perdendo a corrida depois de iniciar o trabalho.

A sinalização --experimental_spawn_scheduler recomendada anteriormente foi descontinuada. Ela ativa a execução dinâmica e define dynamic como a estratégia padrão para todas as mnemônicas, o que geralmente levaria a esses tipos de problemas.

Desempenho

A abordagem de execução dinâmica pressupõe que há recursos suficientes disponíveis local e remotamente que vale a pena gastar alguns recursos extras para melhorar o desempenho geral. No entanto, o uso excessivo de recursos pode atrasar o próprio Bazel ou a máquina em que ele é executado ou sobrecarregar um sistema remoto de maneira inesperada. Há várias opções para mudar o comportamento da execução dinâmica:

--dynamic_local_execution_delay atrasa o início de uma ramificação local em alguns milissegundos após o início da ramificação remota, mas somente se houver uma ocorrência em cache remoto durante a compilação atual. Isso faz com que as versões que se beneficiem do armazenamento em cache remoto não desperdice recursos locais quando é provável que a maioria das saídas possa ser encontrada no cache. Dependendo da qualidade do cache, a redução pode melhorar as velocidades de build, ao custo de usar mais recursos locais.

--experimental_dynamic_local_load_factor é uma opção de gerenciamento de recursos avançado experimental. É preciso um valor de 0 a 1, 0 para desativar esse recurso. Quando definido como um valor acima de 0, o Bazel ajusta o número de ações programadas localmente quando muitas ações aguardam ser programadas. Definir como 1 permite que o máximo de ações seja programado conforme o número de CPUs disponíveis (de acordo com --local_cpu_resources). Valores mais baixos definem o número de ações programadas para um número menor, já que há um número maior de ações disponíveis para execução. Isso pode parecer pouco intuitivo, mas, com um bom sistema remoto, a execução local não ajuda muito quando muitas ações estão sendo executadas, e é melhor a CPU local gerenciar ações remotas.

--experimental_dynamic_slow_remote_time prioriza a inicialização de ramificações locais quando a ramificação remota está em execução há pelo menos esse período. Normalmente, a ação programada mais recentemente tem prioridade, já que tem maior chance de vencer a corrida. No entanto, se o sistema remoto às vezes travar ou demorar muito, o build poderá continuar. Isso não é ativado por padrão, porque pode ocultar problemas com o sistema remoto que precisam ser corrigidos. Monitore o desempenho do sistema remoto se você ativar essa opção.

--experimental_dynamic_ignore_local_signals pode ser usado para permitir que a ramificação remota assuma o controle quando uma geração local é encerrada devido a um determinado sinal. Isso é útil principalmente com os limites de recursos do worker (consulte --experimental_worker_memory_limit_mb, --experimental_worker_sandbox_hardening e --experimental_sandbox_memory_limit_mb)), em que os processos do worker podem ser encerrados quando usam muitos recursos.

O perfil de trace JSON contém vários gráficos relacionados ao desempenho que podem ajudar a identificar maneiras de melhorar a compensação entre desempenho e uso de recursos.

Solução de problemas

Os problemas com a execução dinâmica podem ser sutis e difíceis de depurar, já que podem se manifestar apenas sob algumas combinações específicas de execução local e remota. O --debug_spawn_scheduler adiciona uma saída extra do sistema de execução dinâmica que pode ajudar a depurar esses problemas. Também é possível ajustar a sinalização --dynamic_local_execution_delay e o número de jobs remotos versus locais para facilitar a reprodução dos problemas.

Se você tiver problemas com a execução dinâmica usando a estratégia standalone, tente executar sem --experimental_local_lockfree_output ou execute as ações locais no sandbox. Isso pode deixar o build um pouco mais lento (veja acima se você estiver usando um Mac ou Windows), mas remove algumas possíveis causas de falhas.