VS Code Dev Container IO Excessivo: Configuração executeInWSL Explicada e Análise de Causa Raiz

Registra o processo completo de investigação do problema de IO excessivo causado pela extensão VS Code Dev Container no Windows, desde a localização do fenômeno até a análise da causa raiz, finalmente usando dev.containers.executeInWSL como solução central para resolver o gargalo de comunicação CLI Docker跨边界.

Ao usar a extensão Dev Container do VS Code para desenvolvimento em container no Windows, alguns usuários encontram lentidão明显 no sistema. No Gerenciador de Tarefas, pode-se observar que o processo Extension Host tem CPU e IO de disco de leitura persistentemente elevados, mesmo sem operação ativa. Este documento registra o processo completo de investigação do problema, desde os sintomas até a localização da causa raiz e a encontrar a solução central.

Sintomas do Problema

A lentidão do sistema ocorre após conectar-se a um container usando a extensão Dev Container. Através do Gerenciador de Tarefas, pode-se observar que o processo Extension Host tem IO de disco de leitura e uso de CPU persistentemente elevados, mesmo que o usuário não realize nenhuma edição ou operação de terminal, esses indicadores não retornam ao nível ocioso. Em casos extremos, a velocidade de resposta de toda a área de trabalho do Windows é afetada, e o cursor do mouse apresenta travamentos intermitentes.

Processo de Investigação

Usando o Process Monitor para Localizar a Origem do IO

O Sysinternals Process Monitor é a primeira ferramenta para investigar esses problemas. Após iniciar o procmon, defina a condição de filtro como Process Name is Code.exe ou Process Name is Extension Host para observar em tempo real todos os caminhos e frequências das operações ReadFile/WriteFile. Nos resultados filtrados, operações de named pipe com caminhos começando por \\pipe\ aparecem com frequência anormalmente alta, dezenas de vezes por segundo. Essas operações de named pipe correspondem à comunicação entre Docker CLI e Docker daemon, indicando que o Extension Host está chamando o Docker CLI frequentemente.

Usando Ferramentas Integradas do VS Code para Confirmar

Através de Help > Toggle Developer Tools para abrir o Chromium DevTools, no painel Performance gravando o perfil de CPU, pode-se ver que muito tempo do Extension Host é gasto no spawn de subprocessos e leitura de pipe stdout. Definindo o nível de log “Dev Containers” no painel Output como “trace”, pode-se ver a sequência completa de chamadas de comando: docker inspect --type container, docker version --format, docker exec, docker ps e outros comandos são executados repetidamente. A partir dos dados de log do issue #9194, pode-se quantificar o custo de uma única chamada: docker inspect --type container leva cerca de 1800ms, docker version leva cerca de 620ms.

Usando o Windows Performance Analyzer para Analisar IO de Nível de Sistema

Para análise mais profunda, use wpr.exe -start GeneralProfile -filemode para iniciar a gravação ETW, reproduza o problema e use wpr.exe -stop capture.etl para parar a gravação, carregue os resultados no Windows Performance Analyzer. A visualização Disk I/O confirma que o Extension Host é o principal contribuidor de IO de disco de leitura, a visualização Process Life Cycle mostra muitos subprocessos de vida curta sendo criados e destruídos repetidamente, esses subprocessos são exatamente as instâncias de chamada do Docker CLI.

Análise da Causa Raiz

Arquitetura de Comunicação de Processos do Dev Container

Os resultados da investigação apontam para a arquitetura de comunicação multiprocessos do Dev Container. Quando um usuário abre um workspace de container através da extensão Dev Container no Windows, na verdade inicia uma cadeia de comunicação de processos cruzando as fronteiras do Windows e 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["Inside Container"]
        E["VS Code Server"]
        F["Remote Extension Host"]
        G["Port Forwarding Process<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 com o Extension Host local (Node.js) via IPC. O Extension Host precisa estabelecer conexão com o VS Code Server dentro do container, e essa conexão depende do Docker CLI como intermediário. Após o VS Code Server iniciar dentro do container, o Remote Extension Host assume a execução de extensões, e o processo de port forwarding fornece túnel TCP para o navegador local acessar os serviços do container. Cada etapa nesta cadeia pode ser uma fonte de amplificação de IO.

Mecanismo de Polling do Docker CLI

A extensão Dev Containers obtém e mantém o estado do container através de chamadas frequentes de comandos Docker CLI. Na fase de inicialização do container, a extensão executa sequencialmente docker inspect --type container para obter metadados do container, docker version --format para verificar a disponibilidade do Docker daemon, docker exec para executar comandos de探测ambiente dentro do container, e docker ps para listar os containers em execução. Esses comandos não são executados apenas uma vez na inicialização, mas chamados repetidamente com uma certa frequência durante todo o ciclo de vida do container.

Cada chamada do Docker CLI inicia um novo subprocesso, envolvendo criação de processo, leitura de pipe stdout, análise de resultado JSON e uma série de operações de IO. No modo padrão, essas operações de dados precisam cruzar a fronteira Windows/WSL, e o protocolo de compartilhamento de arquivos 9P do WSL2 tem desempenho ruim ao lidar com IO de muitos arquivos pequenos e conexões curtas de alta frequência. De acordo com a documentação oficial da Microsoft, operações de sistema de arquivos cruzados devem ser evitadas sempre que possível, mas o design arquitetural do Dev Container dificulta evitar completamente esse 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: execution result
    CLI-->>EH: stdout pipe output

Vazamento de Conexões de Port Forwarding

O mecanismo de port forwarding do VS Code cria processos filhos Node.js independentes para cada porta forwardada. Esses processos usam net.createConnection para conectar-se à porta de destino dentro do container e transmitem dados bidirecionalmente entre a porta local e a porta do container. O problema é que quando o navegador ou outro cliente acessa a porta forwardada e depois desconecta, se a lógica de limpeza não for oportuna, esses processos de forwarding continuarão existindo em vez de encerrar normalmente.

De acordo com a análise do microsoft/vscode-remote-release#5767, cada processo de port forwarding vazado ocupa cerca de 26 MiB de memória. Em um ambiente de desenvolvimento configurado com múltiplas portas forwardadas e acesso frequente, o número de processos pode crescer de 2 normais para dezenas em pouco tempo. O trecho de código abaixo mostra o padrão central do processo de port forwarding, onde o evento client.on('close') é a chave para o processo encerrar normalmente.

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 processo docker exec do Docker CLI termina anormalmente mas o processo Node.js dentro do container ainda está em execução, esses processos órfãos não podem ser回收ados normalmente, causando crescimento contínuo de memória. Esse problema foi parcialmente corrigido após a versão 1.62 do VS Code, mas ainda pode ocorrer sob certas condições de rede. Vale notar que o vazamento de port forwarding não está diretamente relacionado ao executeInWSL, é um defeito de software próprio do mecanismo de port forwarding do VS Code.

Loop de Reconexão do Extension Host

De acordo com o registro do microsoft/vscode-remote-release#6178, quando a conexão com o container remoto é perdida devido a interrupção de rede ou outros motivos, existe um bug na lógica de reconexão no Extension Host: uma função async chama a si mesma recursivamente no bloco catch, causando loop vazio de CPU. A stack de chamadas mostra essa função circulando repetidamente em processTicksAndRejections, sem condição de saída.

flowchart TD
    A["Connection Lost"] e1@--> B["Async Reconnect Function"]
    B e2@--> C{"Connection Success?"}
    C e3@-->|Yes| D["Resume Normal"]
    C e4@-->|No| E["catch Block"]
    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;

Enquanto isso, a memória do Extension Host cresce continuamente a cerca de 1 MB/minuto, porque cada chamada recursiva acumula contexto não liberado na stack de chamadas. O problema foi corrigido na versão Remote-Containers 0.221.0-pre-release, mas para usuários que não atualizaram a extensão a tempo, basta simular uma desconexão de rede (por exemplo, desligar o WiFi) para acionar esse problema, e a caixa de diálogo de提示de desconexão nunca será exibida. Este é um bug de software independente, não relacionado à configuração executeInWSL, mas agravarrá a lentidão percebida pelo usuário.

Amplificação de IO de Sistema de Arquivos Cruzado do WSL2

Ao usar o backend WSL2 do Docker Desktop no Windows, existe um problema inerente de desempenho de IO de sistema de arquivos cruzado. A extensão VS Code no lado Windows se comunica com o Docker daemon no WSL2 via pipe, e se as operações do sistema de arquivos dentro do container envolverem caminhos /mnt/c (ou seja, acessar discos Windows), precisam passar pela conversão do protocolo de compartilhamento de arquivos 9P. O protocolo 9P do WSL2 tem desempenho significativamente pior que sistemas de arquivos nativos ao lidar com IO de muitos arquivos pequenos, e a latência de uma única operação pode ser 3-5 vezes maior que caminhos nativos.

Além disso, de acordo com o relatório do microsoft/vscode-remote-release#9372, ao executar containers x86 no ARM Mac via Rosetta, a máscara de afinidade de CPU do processo VS Code Server é anormalmente definida para usar apenas um único núcleo, fazendo com que nproc retorne 1 em vez do número real de núcleos físicos. Embora esse problema ocorra principalmente na plataforma macOS, ele revela que o Dev Container tem inconsistência no controle de parâmetros de agendamento de processos em cenários de plataforma cruzada, e problemas semelhantes podem aparecer em diferentes formas no ambiente WSL2 do Windows. executeInWSL: true move a execução do Docker CLI para dentro do WSL, reduzindo a frequência de IO de sistema de arquivos cruzado, mas não pode eliminar completamente o custo 9P gerado ao acessar caminhos /mnt/c dentro do container.

Soluções

Simplificar Configuração de Port Forwarding

No devcontainer.json, simplificar a configuração forwardPorts, mantendo apenas as portas realmente necessárias, pode reduzir significativamente o número de processos de port forwarding, diminuindo o risco de vazamento de processos descrito no issue #5767. Fechar janelas de Dev Container não utilizadas também libera imediatamente os recursos correspondentes do Extension Host e port forwarding.

Otimização do Docker Desktop

No painel Settings > Resources do Docker Desktop, ajustar适当mente a alocação de CPU e memória, evitando que o Docker daemon faça coleta de lixo frequente ou operações swap devido a recursos insuficientes. Certifique-se de usar a versão mais recente do Docker Desktop, pois o desempenho do backend WSL2 é melhorado em cada versão. Para cenários com requisitos de desempenho mais altos, considere instalar o Docker Engine diretamente dentro do WSL2, contornando a camada de virtualização do Docker Desktop, eliminando assim ainda mais o custo adicional da fronteira Windows/WSL.

Otimização de Configuração do VS Code

Desativar extensões desnecessárias pode reduzir a carga do Extension Host, especialmente aquelas executadas em containers remotos com funcionalidades de monitoramento de arquivos (como serviço de linguagem TypeScript, ESLint, etc.). No settings.json, configurar files.watcherExclude para excluir node_modules, .git, dist e outros diretórios grandes, pode reduzir o IO gerado pelo monitoramento do sistema de arquivos. Configurar extensions.autoUpdate: false pode evitar que atualizações de extensão em segundo plano acionem operações de rede e disco adicionais no ambiente de container.

Alternativas

Se as medidas acima ainda não atenderem aos requisitos de desempenho, considere usar a extensão VS Code Remote-SSH para conectar ao WSL2, usando diretamente o Docker CLI dentro do WSL2 para gerenciar containers. Essa abordagem muda a comunicação de chamadas Docker CLI de comunicação cruzada Windows/WSL para comunicação local dentro do WSL2. Outra forma é usar o Docker Compose para gerenciar o ciclo de vida do container, iniciar serviços com docker compose up -d e usar apenas Remote-SSH para conectar ao container para desenvolvimento, contornando completamente o mecanismo de polling da extensão Dev Container.

Ativar executeInWSL (Solução Central)

As medidas acima podem aliviar o problema de IO excessivo em graus variados, mas elas ou apenas reduzem a frequência de IO (simplificar port forwarding) ou apenas otimizam a alocação de recursos (otimização do Docker Desktop), sem tocar na causa raiz do problema: o custo de comunicação named pipe gerado quando chamadas Docker CLI cruzam a fronteira Windows/WSL. dev.containers.executeInWSL é exatamente a solução direta para essa causa raiz.

De acordo com a explicação clara do membro da equipe do VS Code, chrmarti, no microsoft/vscode-remote-release#9194, essa configuração determina se o comando docker é executado no lado Windows ou dentro do WSL. Após defini-lo como true, todas as chamadas Docker CLI (incluindo docker inspect, docker version, docker exec, docker ps, etc.) serão executadas dentro do WSL, comunicando-se diretamente com o Docker daemon via Unix socket, contornando assim o custo de conversão de named pipe e protocolo 9P na fronteira Windows/WSL.

Adicione a seguinte configuração no settings.json para ativar:

{
  "dev.containers.executeInWSL": true
}

Para entender por que essa configuração pode melhorar significativamente o desempenho de IO, é necessário comparar a diferença de caminho de comunicação nos dois modos. No modo padrão (executeInWSL: false ou não definido), o Extension Host do VS Code é executado no Windows, e cada vez que precisa interagir com o Docker daemon, inicia um subprocesso docker.exe no Windows através de spawn. Esse subprocesso se comunica com o Docker daemon cruzando a fronteira Windows/WSL via named pipe (caminho começando com \\pipe\). Esse caminho envolve três estágios: criação de processo Windows, IO named pipe cruzado e pipe stdout de retorno, cada estágio introduz latência e custo de IO adicionais. Quando executeInWSL: true, o Extension Host muda para executar o Docker CLI dentro do WSL via wsl -d <distro> -e docker. O Docker CLI é executado nativamente dentro do WSL, comunicando-se com o Docker daemon na mesma instância do WSL via Unix socket (IPC local). Esse caminho é completamente concluído dentro do espaço do kernel Linux, evitando o custo cruzado de named pipe.

flowchart TB
    subgraph Default["Modo Padrão (executeInWSL: false)"]
        direction LR
        A1["Extension Host<br/>(Windows)"]
        A2["docker.exe<br/>(Windows Subprocess)"]
        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/>(Inside WSL)"]
        B3["Unix socket<br/>(Local IPC)"]
        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;

A partir dos dados quantificados na fase de investigação, essa melhoria pode ser verificada: no modo padrão, uma única chamada docker inspect --type container leva cerca de 1800ms, a chamada docker version leva cerca de 620ms, essas latências vêm principalmente do custo de comunicação named pipe cruzado na fronteira Windows/WSL e do custo de criação de processo. Após ativar executeInWSL: true, o Docker CLI é executado dentro do WSL se comunicando com o daemon via Unix socket, a latência de uma única chamada pode ser reduzida para nível de milissegundos, e a melhoria do efeito cumulativo é particularmente significativa.

Problemas Conhecidos e Precauções

Embora dev.containers.executeInWSL possa melhorar efetivamente o desempenho de IO, os seguintes problemas conhecidos devem ser observados ao usá-lo.

Problema de inicialização automática do Docker Desktop: O issue #9695 relata que com executeInWSL: true o Docker Desktop não inicia automaticamente. Esse problema foi corrigido na versão Dev Containers 0.353.0-pre-release, usuários de versões mais recentes não devem mais encontrar esse problema.

Encaminhamento de serviços WSL: O issue #9194 relata que mesmo definindo executeInWSL como false, a extensão ainda tenta conectar ao WSL (para encaminhamento de display/ssh-agent/gpg-agent). Esse comportamento foi corrigido na versão 0.337.0-pre-release através da adição do item de configuração dev.containers.wslServiceForwarding, permitindo que os usuários desativem independentemente o encaminhamento de serviços WSL.

Compatibilidade com Rancher Desktop: O issue #10722 relata que ao usar o Rancher Desktop em vez do Docker Desktop, executeInWSL: true acionará uma mensagem de erro do WSL1. Esse problema ainda está em aberto, usuários do Rancher Desktop podem precisar desativar temporariamente essa configuração.

Ativação acidental: O issue #11005 relata que executeInWSL: true acionará acidentalmente o processo de inicialização do Dev Container em repositórios Windows locais. Esse problema também está em aberto, usuários afetados podem considerar limitar essa configuração a workspaces específicos em vez de configuração global.

Resumo

Investigar o problema de IO excessivo do VS Code Dev Container no Windows requer localizar gradualmente a causa raiz a partir dos sintomas. Através do Process Monitor pode-se confirmar que a principal origem do IO é a comunicação named pipe do Docker CLI, através das ferramentas integradas do VS Code e do Windows Performance Analyzer pode-se quantificar ainda mais a frequência de chamadas e latência. A análise da causa raiz revela quatro fatores sobrepostos: o polling de alta frequência do Docker CLI cruzando fronteira é o gargalo de desempenho principal, o vazamento de conexões de port forwarding e o loop de reconexão do Extension Host são bugs de software confirmados, e a amplificação de IO de sistema de arquivos cruzado do WSL2 é uma limitação inerente do nível da plataforma.

Nas soluções, dev.containers.executeInWSL: true é a medida mais central, que elimina diretamente o custo de comunicação named pipe do Docker CLI cruzando a fronteira Windows/WSL, movendo a execução de chamadas CLI de alta frequência do Windows para dentro do WSL, completando IPC local via Unix socket. As outras soluções (simplificar port forwarding, otimização do Docker Desktop, otimização de configuração do VS Code) servem como medidas auxiliares, aliviando em diferentes graus o impacto de outros fatores. Para usuários atormentados por esse problema, recomenda-se confirmar a causa raiz através do processo de investigação deste artigo, primeiro ativar executeInWSL: true, e então escolher estratégias de otimização auxiliar adequadas de acordo com o cenário real.