VS Code Dev Container IO alto: detalhamento da configuração executeInWSL e análise de causa raiz

Registro completo do processo de investigação do problema de IO alto causado pela extensão VS Code Dev Container no Windows, desde a identificação dos sintomas até a análise de causa raiz, culminando na solução baseada em dev.containers.executeInWSL para resolver o gargalo de comunicação do Docker CLI entre fronteiras.

Ao usar a extensão Dev Container do VS Code para desenvolvimento em contêineres no Windows, alguns usuários podem encontrar um sintoma de travamento perceptível do sistema. No Gerenciador de Tarefas, é possível observar que o processo Extension Host apresenta uso elevado de CPU e leitura de disco IO de forma contínua, mesmo sem operações ativas. Este artigo registra o processo completo de investigação, partindo dos sintomas, localizando a causa raiz e encontrando a solução central.

Sintoma do Problema

O travamento do sistema ocorre após conectar a extensão Dev Container ao contêiner. Pelo Gerenciador de Tarefas, o processo Extension Host mostra leitura de disco IO e uso de CPU persistentemente altos, mesmo quando o usuário não está editando ou usando o terminal; esses indicadores não retornam ao nível ocioso. Em casos extremos, a velocidade de resposta da área de trabalho do Windows é afetada, e o cursor do mouse apresenta travamentos intermitentes.

Processo de Investigação

Usando Process Monitor para localizar a origem do IO

Sysinternals Process Monitor é a primeira ferramenta para investigar esse tipo de problema. Após iniciar o procmon, configure o filtro para Process Name is Code.exe ou Process Name is Extension Host e observe em tempo real todas as operações ReadFile/WriteFile, seus caminhos e frequência. Nos resultados filtrados, as operações de named pipe que começam com \\pipe\ aparecem com frequência anormalmente alta, chegando a dezenas por segundo. Essas operações de named pipe correspondem à comunicação entre o Docker CLI e o Docker daemon, indicando que o Extension Host está chamando o Docker CLI com frequência.

Confirmando com as ferramentas internas do VS Code

Abra Help > Toggle Developer Tools para abrir o Chromium DevTools e grave um perfil de CPU no painel Performance. É possível ver que grande parte do tempo do Extension Host é gasto em spawn de subprocessos e leitura de pipes stdout. Defina o nível de log de “Dev Containers” no painel Output para “trace” e você verá a sequência completa de comandos: docker inspect --type container, docker version --format, docker exec, docker ps, entre outros, sendo executados repetidamente. Dados do issue #9194 quantificam o custo de cada chamada: docker inspect --type container leva cerca de 1800 ms, docker version cerca de 620 ms.

Analisando IO em nível de sistema com Windows Performance Analyzer

Para uma análise mais profunda, use wpr.exe -start GeneralProfile -filemode para iniciar a gravação ETW, reproduza o problema e pare a captura com wpr.exe -stop capture.etl. Carregue o resultado no Windows Performance Analyzer. A visualização Disk I/O confirma que o Extension Host é o principal contribuinte de leitura de disco IO; a visualização Process Life Cycle mostra muitos subprocessos de curta duração sendo criados e destruídos repetidamente, correspondendo às chamadas ao Docker CLI.

Análise de Causa Raiz

Arquitetura de comunicação de processos do Dev Container

Os resultados apontam para a arquitetura de comunicação de múltiplos processos do Dev Container. Quando o usuário abre um workspace de contêiner via extensão no Windows, na prática é criada uma cadeia de comunicação multiprocessos que atravessa a fronteira Windows ↔ Linux.

flowchart LR
    subgraph Windows["Windows Host"]
        A["VS Code Client<br/>(Electron)"]
        B["Extension Host<br/>(Node.js)"]
        C["Docker CLI<br/>(subprocess)"]
    end

    subgraph WSL2["WSL2 / Docker Desktop"]
        D["Docker Daemon<br/>(dockerd)"]
    end

    subgraph Container["Dentro do Contêiner"]
        E["VS Code Server"]
        F["Remote Extension Host"]
        G["Processo de encaminhamento de porta<br/>(Node.js)"]
    end

    A e1@--> B
    B e2@--> C
    C e3@--> D
    B e4@--> E
    E e5@--> F
    F e6@--> G

    classDef win fill:#E3F2FD,stroke:#1565C0,stroke-width:1px,color:#0D47A1;
    classDef wsl fill:#FFF8E1,stroke:#EF6C00,stroke-width:1px,color:#E65100;
    classDef ctr fill:#E8F5E9,stroke:#2E7D32,stroke-width:1px,color:#1B5E20;
    classDef animate stroke:#EF6C00,stroke-width:2px,stroke-dasharray: 9\,5,stroke-dashoffset: 900,animation: dash 25s linear infinite;
    class A,B,C win;
    class D wsl;
    class E,F,G ctr;
    class e1,e2,e3,e4,e5,e6 animate;

O cliente VS Code local (Electron) se comunica via IPC com o Extension Host local (Node.js). O Extension Host precisa conectar ao VS Code Server dentro do contêiner, e essa conexão depende do Docker CLI como intermediário. Após o VS Code Server iniciar, o Remote Extension Host assume a execução das extensões, e o processo de encaminhamento de porta fornece um túnel TCP para que o navegador local acesse os serviços do contêiner. Cada elo dessa cadeia pode amplificar o IO.

Mecanismo de polling do Docker CLI

A extensão Dev Containers chama o Docker CLI com frequência para obter e manter o estado do contêiner. Na fase de inicialização, a extensão executa sequencialmente docker inspect --type container (metadados), docker version --format (verificação de daemon), docker exec (detecção de ambiente) e docker ps (listagem). Esses comandos não são executados apenas uma vez; são repetidos periodicamente ao longo do ciclo de vida do contêiner.

Cada chamada ao Docker CLI cria um novo subprocesso, gerando overhead de criação, leitura de pipe stdout e parsing de JSON – tudo isso envolve IO. No modo padrão, esses dados atravessam a fronteira Windows/WSL, e o protocolo 9P usado pelo WSL2 para compartilhamento de arquivos tem desempenho ruim em cenários de muitos arquivos pequenos e conexões curtas. Conforme recomenda a documentação oficial da Microsoft, operações entre sistemas de arquivos devem ser evitadas, mas a arquitetura do Dev Container dificulta a eliminação completa desse custo.

sequenceDiagram
    participant EH as Extension Host
    participant CLI as Docker CLI
    participant DD as Docker Daemon

    EH->>CLI: spawn docker inspect
    CLI->>DD: named pipe request
    DD-->>CLI: JSON response
    CLI-->>EH: stdout pipe output
    EH->>CLI: spawn docker version
    CLI->>DD: named pipe request
    DD-->>CLI: version info
    CLI-->>EH: stdout pipe output
    EH->>CLI: spawn docker exec
    CLI->>DD: named pipe request
    DD-->>CLI: exec result
    CLI-->>EH: stdout pipe output

Vazamento de processos de encaminhamento de porta

O mecanismo de encaminhamento de porta do VS Code cria um subprocesso Node.js independente para cada porta encaminhada. Esses processos usam net.createConnection para conectar ao destino dentro do contêiner e encaminham dados bidirecionalmente. Quando o navegador ou outro cliente desconecta, se a lógica de limpeza não for executada rapidamente, o processo de encaminhamento permanece ativo.

Análises do issue #5767 mostram que cada processo vazado consome cerca de 26 MiB de memória. Em ambientes com várias portas encaminhadas e acesso frequente, o número de processos pode crescer de 2 para dezenas em pouco tempo. O trecho abaixo ilustra o padrão central do encaminhamento, onde o handler client.on('close') determina se o processo encerra corretamente.

const net = require('net');
process.stdin.pause();
const client = net.createConnection({ port: 36187 }, () => {
    client.pipe(process.stdout);
    process.stdin.pipe(client);
});
client.on('close', function (hadError) {
    console.error(hadError ? 'Remote close with error' : 'Remote close');
    process.exit(hadError ? 1 : 0);
});
client.on('error', function (err) {
    process.stderr.write(err && (err.stack || err.message) || String(err));
});

Quando o subprocesso docker exec termina inesperadamente enquanto o processo Node.js dentro do contêiner ainda está ativo, esses processos órfãos não são recolhidos, levando ao crescimento contínuo de memória. O problema foi parcialmente corrigido a partir da versão 1.62 do VS Code, mas ainda pode ocorrer em certas condições de rede. Vale notar que o vazamento de encaminhamento não está diretamente relacionado ao executeInWSL; é um defeito próprio do mecanismo de encaminhamento de portas.

Loop de reconexão do Extension Host

Registros do issue #6178 indicam que, ao perder a conexão com o contêiner remoto (por falha de rede ou outro motivo), a lógica de reconexão do Extension Host contém um bug: uma função async chama a si mesma recursivamente dentro do bloco catch, provocando um loop de CPU. O stack trace mostra a função sendo chamada repetidamente em processTicksAndRejections sem condição de saída.

flowchart TD
    A["Conexão perdida"] e1@--> B["Função async de reconexão"]
    B e2@--> C{"Conexão bem‑sucedida?"}
    C e3@-->|Sim| D["Retorna ao normal"]
    C e4@-->|Não| E["Bloco catch"]
    E e5@--> B

    classDef start fill:#FFEBEE,stroke:#C62828,stroke-width:1px,color:#B71C1C;
    classDef work fill:#E8F5E9,stroke:#2E7D32,stroke-width:1px,color:#1B5E20;
    classDef check fill:#FFF8E1,stroke:#EF6C00,stroke-width:1px,color:#E65100;
    classDef animate stroke:#EF6C00,stroke-width:2px,stroke-dasharray: 9\,5,stroke-dashoffset: 900,animation: dash 25s linear infinite;
    class A start;
    class B,D work;
    class C,E check;
    class e1,e2,e3,e4,e5 animate;

Além disso, a memória do Extension Host cresce cerca de 1 MB por minuto devido ao acúmulo de contextos não liberados a cada chamada recursiva. O bug foi corrigido na versão pre‑release 0.221.0 do Remote‑Containers, mas usuários que não atualizaram a extensão ainda podem reproduzi‑lo ao simular uma desconexão (por exemplo, desligando o Wi‑Fi). O diálogo de aviso de desconexão nunca aparece. Esse bug é independente da configuração executeInWSL, mas agrava a percepção de travamento.

Amplificação de IO ao atravessar o sistema de arquivos do WSL2

Com o Docker Desktop usando o backend WSL2, há um problema intrínseco de desempenho ao atravessar sistemas de arquivos. A extensão Windows do VS Code comunica-se via pipe com o daemon Docker dentro do WSL2; quando o contêiner acessa caminhos /mnt/c (ou seja, arquivos do Windows), a operação passa pelo protocolo 9P, que tem latência 3‑5 vezes maior que o acesso nativo.

Relatos do issue #9372 mostram que, em Macs ARM rodando containers x86 via Rosetta, a afinidade de CPU do VS Code Server pode ficar restrita a um único núcleo, fazendo nproc retornar 1. Embora esse caso seja específico do macOS, ele evidencia inconsistências no controle de parâmetros de agendamento pelo Dev Container, que também podem aparecer em ambientes WSL2. Definir executeInWSL: true move a execução do Docker CLI para dentro do WSL, reduzindo a frequência de IO entre sistemas de arquivos, mas não elimina completamente o custo quando o contêiner acessa /mnt/c.

Soluções

Simplificar a configuração de encaminhamento de portas

Reduza a lista forwardPorts no devcontainer.json mantendo apenas as portas realmente necessárias. Isso diminui o número de processos de encaminhamento e reduz o risco de vazamento descrito no issue #5767. Fechar janelas do Dev Container que não estão em uso libera imediatamente os recursos do Extension Host e dos processos de encaminhamento.

Otimizações do Docker Desktop

No painel Settings > Resources do Docker Desktop, ajuste adequadamente CPU e memória para evitar que o daemon faça coleta de lixo ou swap com frequência. Use a versão mais recente, pois cada release traz melhorias de desempenho ao backend WSL2. Em cenários de alta demanda, considere instalar o Docker Engine diretamente dentro do WSL2, contornando a camada de virtualização do Docker Desktop e eliminando o overhead adicional da fronteira Windows/WSL.

Ajustes de configuração do VS Code

Desative extensões desnecessárias que rodam dentro do contêiner, especialmente aquelas que monitoram arquivos (TypeScript language service, ESLint, etc.). No settings.json, configure files.watcherExclude para excluir node_modules, .git, dist e outros diretórios grandes, reduzindo o IO gerado por monitoramento de arquivos. Defina extensions.autoUpdate: false para impedir atualizações automáticas de extensões que poderiam gerar tráfego de rede e disco no ambiente remoto.

Estratégias alternativas

Se as otimizações acima não forem suficientes, considere usar a extensão Remote‑SSH para conectar diretamente ao WSL2 e gerenciar contêineres com o Docker CLI dentro do WSL. Essa abordagem elimina o polling do Docker CLI pela extensão Dev Container. Outra alternativa é usar Docker Compose para orquestrar o ciclo de vida dos contêineres (docker compose up -d) e, em seguida, conectar via Remote‑SSH ao contêiner para desenvolvimento, evitando completamente o mecanismo de polling da extensão.

Habilitar executeInWSL (solução central)

As medidas anteriores mitigam o problema em diferentes graus, mas não atacam a causa raiz: o overhead de comunicação via named pipe quando o Docker CLI cruza a fronteira Windows/WSL. O parâmetro dev.containers.executeInWSL resolve diretamente isso.

Conforme esclarecido por chrmarti no issue #9194, essa configuração determina onde o comando docker será executado – no Windows ou dentro do WSL. Definindo‑o como true, todas as chamadas (docker inspect, docker version, docker exec, docker ps, etc.) são executadas dentro do WSL, comunicando‑se com o daemon via socket Unix, evitando o named pipe e a conversão 9P.

Adicione ao settings.json:

{
  "dev.containers.executeInWSL": true
}

Para entender por que isso melhora drasticamente o desempenho de IO, compare os caminhos de comunicação:

  • Modo padrão (executeInWSL: false ou não definido)

    1. Extension Host (Windows) → spawn docker.exe (processo Windows)
    2. docker.exe → comunica via named pipe (\\pipe\\) com o Docker daemon (WSL2)
    3. Resultado retorna por stdout pipe

    Cada etapa envolve criação de processo, IO de pipe entre fronteiras e latência adicional.

  • Modo otimizado (executeInWSL: true)

    1. Extension Host (Windows) → executa wsl -d <distro> -e docker (Docker CLI nativo no WSL)
    2. Docker CLI comunica via socket Unix (IPC local) com o daemon Docker (WSL2)

    Todo o caminho permanece dentro do kernel Linux, eliminando o overhead do named pipe.

flowchart TB
    subgraph Default["Modo padrão (executeInWSL: false)"]
        direction LR
        A1["Extension Host<br/>(Windows)"]
        A2["docker.exe<br/>(subprocess Windows)"]
        A3["named pipe<br/>(\\\\pipe\\\\)"]
        A4["Docker Daemon<br/>(WSL2)"]
        A1 e1@--> A2
        A2 e2@--> A3
        A3 e3@--> A4
    end

    subgraph Optimized["Modo otimizado (executeInWSL: true)"]
        direction LR
        B1["Extension Host<br/>(Windows)"]
        B2["wsl -e docker<br/>(WSL interno)"]
        B3["Unix socket<br/>(IPC local)"]
        B4["Docker Daemon<br/>(WSL2)"]
        B1 e4@--> B2
        B2 e5@--> B3
        B3 e6@--> B4
    end

    Default ~~~ Optimized

    classDef win fill:#E3F2FD,stroke:#1565C0,stroke-width:1px,color:#0D47A1;
    classDef wsl fill:#FFF8E1,stroke:#EF6C00,stroke-width:1px,color:#E65100;
    classDef fast fill:#E8F5E9,stroke:#2E7D32,stroke-width:1px,color:#1B5E20;
    classDef animate stroke:#EF6C00,stroke-width:2px,stroke-dasharray: 9\,5,stroke-dashoffset: 900,animation: dash 25s linear infinite;

    class A1,A2 win;
    class A3,A4 wsl;
    class B1 win;
    class B2,B3,B4 fast;
    class e1,e2,e3,e4,e5,e6 animate;

Medições mostram que, no modo padrão, uma chamada docker inspect --type container leva ~1800 ms e docker version ~620 ms, principalmente devido ao overhead do named pipe e criação de processos. Com executeInWSL: true, a mesma chamada cai para poucos milissegundos, resultando em melhorias acumulativas significativas.

Problemas Conhecidos e Atenções

  • Docker Desktop não inicia automaticamente: o issue #9695 relata que, com executeInWSL: true, o Docker Desktop pode não iniciar sozinho. Foi corrigido na versão pre‑release 0.353.0 do Remote‑Containers.
  • Encaminhamento de serviços WSL: mesmo com executeInWSL: false, a extensão tenta conectar ao WSL para encaminhamento de display/ssh‑agent/gpg‑agent. A partir da versão 0.337.0, isso pode ser controlado via dev.containers.wslServiceForwarding.
  • Compatibilidade com Rancher Desktop: o issue #10722 indica que, ao usar Rancher Desktop em vez do Docker Desktop, executeInWSL: true pode gerar erro no WSL1. O problema ainda está aberto.
  • Ativação inesperada: o issue #11005 descreve que executeInWSL: true pode disparar a inicialização do Dev Container em repositórios locais do Windows. Usuários podem limitar a configuração ao workspace ao invés de defini‑la globalmente.

Conclusão

Investigar o alto consumo de IO do VS Code Dev Container no Windows requer partir dos sintomas, localizar a origem (Docker CLI via named pipe) e analisar fatores adicionais (vazamento de processos de encaminhamento, loop de reconexão, overhead do WSL2). A causa principal é o polling frequente do Docker CLI atravessando a fronteira Windows/WSL. A solução central é habilitar dev.containers.executeInWSL: true, que move a execução do Docker CLI para dentro do WSL, usando sockets Unix e eliminando o overhead de named pipe e 9P. As demais medidas (simplificar encaminhamento, otimizar Docker Desktop e VS Code) complementam a mitigação, mas não substituem a correção fundamental. Recomenda‑se seguir o fluxo de investigação descrito, aplicar executeInWSL: true e, conforme necessário, combinar as otimizações auxiliares para obter a melhor experiência de desenvolvimento.