VS Code Dev Container IO alto: configuración detallada de executeInWSL y análisis de causa raíz

Registro completo del proceso de diagnóstico del problema de IO alto causado por la extensión VS Code Dev Container en Windows, desde la identificación del síntoma hasta el análisis de causa raíz, y la solución basada en dev.containers.executeInWSL para resolver el cuello de botella de comunicación del Docker CLI entre fronteras.

En Windows, al usar la extensión Dev Container de VS Code para el desarrollo en contenedores, algunos usuarios experimentan una notable ralentización del sistema. En el Administrador de tareas se observa que el proceso Extension Host mantiene un uso elevado de CPU y de lectura de disco de forma continua, incluso sin realizar operaciones activas. Este artículo documenta el proceso completo de diagnóstico, partiendo del síntoma, localizando la causa raíz y encontrando la solución central.

Síntoma del problema

El bloqueo del sistema ocurre después de conectar la extensión Dev Container al contenedor. En el Administrador de tareas se ve que el proceso Extension Host mantiene una alta lectura de disco y uso de CPU, sin que el usuario esté editando o usando la terminal; estos indicadores no vuelven a niveles de reposo. En casos extremos, la velocidad de respuesta del escritorio de Windows se ve afectada y el cursor del ratón presenta bloqueos intermitentes.

Proceso de diagnóstico

Uso de Process Monitor para localizar la fuente de IO

Sysinternals Process Monitor es la primera herramienta para investigar este tipo de problemas. Después de iniciar procmon, configure el filtro Process Name is Code.exe o Process Name is Extension Host para observar en tiempo real todas las operaciones ReadFile/WriteFile, sus rutas y frecuencias. En los resultados filtrados, las rutas que comienzan con \\pipe\ (named pipe) aparecen con una frecuencia anormalmente alta, llegando a decenas de veces por segundo. Estas operaciones de named pipe corresponden a la comunicación entre Docker CLI y Docker daemon, lo que indica que Extension Host está llamando frecuentemente a Docker CLI.

Uso de herramientas integradas de VS Code para confirmar

Abra Help > Toggle Developer Tools y, en el panel Performance, grabe un perfil de CPU; allí se verá que gran parte del tiempo del Extension Host se consume en la creación de subprocesos y la lectura de tuberías stdout. Cambie el nivel de registro de “Dev Containers” en el panel Output a “trace” para ver la secuencia completa de comandos: docker inspect --type container, docker version --format, docker exec, docker ps, etc., que se ejecutan repetidamente. Los datos del issue #9194 cuantifican el coste de una llamada: docker inspect --type container tarda ~1800 ms y docker version ~620 ms.

Análisis de IO a nivel del sistema con Windows Performance Analyzer

Para un análisis más profundo, use wpr.exe -start GeneralProfile -filemode para iniciar la captura ETW, reproduzca el problema y luego detenga la captura con wpr.exe -stop capture.etl. Cargue el archivo resultante en Windows Performance Analyzer. La vista Disk I/O confirma que Extension Host es el principal contribuyente de lecturas de disco, y la vista Process Life Cycle muestra la creación y destrucción frecuente de subprocesos de corta vida, que son instancias de llamadas a Docker CLI.

Análisis de causa raíz

Arquitectura de comunicación de procesos de Dev Container

Los resultados apuntan a la arquitectura de comunicación multiproceso de Dev Container. Cuando un usuario abre un espacio de trabajo en contenedor desde Windows, se establece una cadena de comunicación multiproceso que cruza la frontera Windows‑Linux.

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

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

    subgraph Container["Dentro del contenedor"]
        E["VS Code Server"]
        F["Remote Extension Host"]
        G["Proceso de reenvío de puertos<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;

El cliente local de VS Code (Electron) se comunica mediante IPC con el Extension Host local (Node.js). El Extension Host necesita conectar con el VS Code Server dentro del contenedor, y esa conexión depende de Docker CLI como intermediario. Una vez iniciado el VS Code Server, el Remote Extension Host toma el control de la ejecución de extensiones y el proceso de reenvío de puertos crea un túnel TCP para que el navegador local acceda a los servicios del contenedor. Cada eslabón de esta cadena puede amplificar el IO.

Mecanismo de sondeo de Docker CLI

La extensión Dev Containers llama frecuentemente a Docker CLI para obtener y mantener el estado del contenedor. Durante el arranque, ejecuta docker inspect --type container para obtener metadatos, docker version --format para comprobar la disponibilidad del daemon, docker exec para ejecutar comandos de detección dentro del contenedor y docker ps para listar contenedores activos. Estas llamadas no se hacen solo una vez; se repiten a intervalos regulares durante todo el ciclo de vida del contenedor.

Cada invocación de Docker CLI crea un nuevo subproceso, lo que implica sobrecarga de creación, lectura de tuberías stdout y análisis de resultados JSON, todo ello generando operaciones de IO. En modo predeterminado, estos datos deben cruzar la frontera Windows/WSL, y el protocolo 9P de WSL2 para compartir archivos muestra un rendimiento pobre con muchos archivos pequeños y conexiones breves. Según la documentación oficial de Microsoft, se recomienda evitar operaciones entre sistemas de archivos siempre que sea posible, pero la arquitectura de Dev Container dificulta eliminar completamente este coste.

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

    EH->>CLI: spawn docker inspect
    CLI->>DD: solicitud named pipe
    DD-->>CLI: respuesta JSON
    CLI-->>EH: salida stdout
    EH->>CLI: spawn docker version
    CLI->>DD: solicitud named pipe
    DD-->>CLI: información de versión
    CLI-->>EH: salida stdout
    EH->>CLI: spawn docker exec
    CLI->>DD: solicitud named pipe
    DD-->>CLI: resultado de ejecución
    CLI-->>EH: salida stdout

Fugas en los procesos de reenvío de puertos

El mecanismo de reenvío de puertos de VS Code crea un subproceso Node.js independiente por cada puerto reenviado. Estos procesos usan net.createConnection para conectar al puerto objetivo dentro del contenedor y reenviar datos bidireccionalmente entre el puerto local y el del contenedor. Cuando el navegador o cualquier cliente cierra la conexión, si la lógica de limpieza no se ejecuta a tiempo, el proceso de reenvío permanece activo en lugar de terminar.

Según el análisis del issue #5767, cada proceso de reenvío filtrado consume ~26 MiB de memoria. En entornos con varios puertos reenviados y accesos frecuentes, el número de procesos puede crecer de 2 a decenas en poco tiempo. El fragmento de código siguiente muestra el patrón central del proceso de reenvío; el manejador client.on('close') es clave para que el proceso finalice correctamente.

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));
});

Cuando el proceso docker exec termina de forma anómala mientras el proceso Node.js dentro del contenedor sigue activo, estos procesos huérfanos no se recogen, provocando un crecimiento continuo de la memoria. El problema se mitigó parcialmente a partir de VS Code 1.62, pero puede reaparecer bajo ciertas condiciones de red. No está directamente relacionado con executeInWSL; es un defecto propio del mecanismo de reenvío de puertos.

Bucle de reconexión del Extension Host

Según el registro del issue #6178, cuando la conexión al contenedor remoto se pierde (por ejemplo, por una interrupción de red), la lógica de reconexión del Extension Host contiene un bug: una función async se llama recursivamente dentro del bloque catch, provocando un bucle de CPU sin salida. El stack muestra que la función se repite en processTicksAndRejections sin una condición de terminación.

flowchart TD
    A["Pérdida de conexión"] e1@--> B["Función async de reconexión"]
    B e2@--> C{"¿Conexión exitosa?"}
    C e3@-->|Sí| D["Recuperación normal"]
    C e4@-->|No| E["Bloque 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 mismo tiempo, la memoria del Extension Host crece ~1 MiB por minuto debido a la acumulación de contextos no liberados en cada llamada recursiva. Este bug se solucionó en Remote‑Containers 0.221.0‑pre‑release, pero los usuarios que no actualicen la extensión pueden reproducirlo simplemente desconectando la red (por ejemplo, desactivando Wi‑Fi); el cuadro de diálogo de desconexión nunca aparece. Es un bug independiente de la configuración executeInWSL, pero agrava la percepción de bloqueo del sistema.

Amplificación de IO por sistemas de archivos cruzados en WSL2

Con Docker Desktop usando el backend WSL2, existe un problema inherente de rendimiento al cruzar sistemas de archivos. La extensión de VS Code en Windows comunica con el daemon Docker en WSL2 mediante pipes; si dentro del contenedor se accede a rutas bajo /mnt/c (es decir, al disco de Windows), la operación pasa por el protocolo 9P, que es mucho más lento que el acceso nativo. En escenarios con muchos archivos pequeños, la latencia puede ser 3‑5 veces mayor que en rutas nativas.

Además, según el informe del issue #9372, en Macs ARM que ejecutan contenedores x86 mediante Rosetta, el proceso VS Code Server recibe una máscara de afinidad de CPU que lo limita a un solo núcleo, haciendo que nproc devuelva 1 en lugar del número real de núcleos. Aunque este problema es mayormente de macOS, muestra que Dev Container maneja de forma inconsistente los parámetros de planificación de procesos en entornos multiplataforma; situaciones similares pueden aparecer en WSL2. Configurar executeInWSL: true mueve la ejecución de Docker CLI al interior de WSL, reduciendo la frecuencia de IO cruzado, pero no elimina por completo el coste cuando se accede a /mnt/c.

Soluciones

Simplificar la configuración de reenvío de puertos

En devcontainer.json, reduzca la lista forwardPorts a solo los puertos realmente necesarios; esto disminuye considerablemente la cantidad de procesos de reenvío y el riesgo descrito en issue #5767. Cerrar ventanas de Dev Container que no se usen también libera inmediatamente los recursos del Extension Host y los procesos de reenvío.

Optimizar Docker Desktop

En Settings > Resources de Docker Desktop, ajuste adecuadamente la asignación de CPU y memoria para evitar que el daemon realice recolección de basura o swapping con frecuencia. Mantenga Docker Desktop actualizado, ya que cada versión mejora el rendimiento del backend WSL2. Para cargas de trabajo muy exigentes, considere instalar Docker Engine directamente dentro de WSL2, evitando la capa de virtualización de Docker Desktop y eliminando el coste adicional de la frontera Windows/WSL.

Optimizar la configuración de VS Code

Desactive extensiones innecesarias para reducir la carga del Extension Host, especialmente aquellas que se ejecutan dentro del contenedor y realizan vigilancia de archivos (por ejemplo, TypeScript language service, ESLint). En settings.json, configure files.watcherExclude para excluir node_modules, .git, dist y otras carpetas grandes, disminuyendo el IO generado por la vigilancia del sistema de archivos. Establecer extensions.autoUpdate: false evita actualizaciones automáticas que podrían generar tráfico de red y operaciones de disco en el contenedor.

Alternativas

Si las medidas anteriores no son suficientes, considere usar la extensión Remote‑SSH para conectar directamente a WSL2 y gestionar contenedores con Docker CLI dentro de WSL. Esta estrategia elimina el sondeo de Docker CLI entre Windows y WSL. Otra opción es usar Docker Compose para gestionar el ciclo de vida de los contenedores (docker compose up -d) y luego conectar con Remote‑SSH, evitando por completo el mecanismo de sondeo de Dev Container.

Activar executeInWSL (solución central)

Las medidas anteriores alivian el problema en distintos grados, pero ninguna aborda la causa raíz: el coste de comunicación por named pipe al cruzar la frontera Windows/WSL cuando Docker CLI se ejecuta en Windows. La configuración dev.containers.executeInWSL está diseñada precisamente para eso.

Según la aclaración de chrmarti en issue #9194, esta opción determina si el comando docker se ejecuta en el lado de Windows o dentro de WSL. Al establecerla en true, todas las llamadas a Docker CLI (docker inspect, docker version, docker exec, docker ps, etc.) se ejecutan dentro de WSL, comunicándose con el daemon mediante un socket Unix, evitando así los named pipes y la conversión 9P.

Añada la siguiente configuración en settings.json:

{
  "dev.containers.executeInWSL": true
}

Para comprender por qué mejora tanto el rendimiento de IO, compare los dos caminos de comunicación. En modo predeterminado (executeInWSL: false o sin definir):

  • Extension Host (Windows) → docker.exe (subproceso Windows) → named pipe (\\pipe\\) → Docker Daemon (WSL2)

En modo optimizado (executeInWSL: true):

  • Extension Host (Windows) → wsl -e docker (ejecución dentro de WSL) → socket Unix (IPC local) → Docker Daemon (WSL2)
flowchart TB
    subgraph Default["Modo predeterminado (executeInWSL: false)"]
        direction LR
        A1["Extension Host<br/>(Windows)"]
        A2["docker.exe<br/>(subproceso Windows)"]
        A3["named pipe<br/>(\\\\pipe\\\\)"]
        A4["Docker Daemon<br/>(WSL2)"]
        A1 e1@--> A2
        A2 e2@--> A3
        A3 e3@--> A4
    end

    subgraph Optimized["Modo optimizado (executeInWSL: true)"]
        direction LR
        B1["Extension Host<br/>(Windows)"]
        B2["wsl -e docker<br/>(dentro de WSL)"]
        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;

Los datos cuantitativos del proceso de diagnóstico confirman la mejora: en modo predeterminado, una llamada docker inspect --type container tarda ~1800 ms y docker version ~620 ms, principalmente por la comunicación de named pipe y la creación de procesos. Con executeInWSL: true, Docker CLI se comunica mediante socket Unix dentro de WSL, reduciendo la latencia a unos pocos milisegundos; el efecto acumulado es notable.

Problemas conocidos y consideraciones

  • Docker Desktop no se inicia automáticamente: el issue #9695 indica que con executeInWSL: true Docker Desktop no arranca automáticamente. Este problema se solucionó en Dev Containers 0.353.0‑pre‑release; los usuarios con versiones más recientes no lo experimentarán.
  • Reenvío de servicios WSL: el issue #9194 muestra que, incluso con executeInWSL: false, la extensión intenta conectar a WSL para reenvío de display/ssh‑agent/gpg‑agent. Desde la versión 0.337.0‑pre‑release se puede controlar mediante la nueva opción dev.containers.wslServiceForwarding.
  • Compatibilidad con Rancher Desktop: el issue #10722 reporta que usar Rancher Desktop en lugar de Docker Desktop con executeInWSL: true genera errores en WSL1. El problema sigue abierto; los usuarios de Rancher Desktop pueden necesitar desactivar la opción.
  • Activación inesperada: el issue #11005 indica que executeInWSL: true puede iniciar inadvertidamente el flujo de Dev Container en repositorios locales de Windows. La solución es aplicar la configuración solo a los espacios de trabajo que lo requieran, no a nivel global.

Conclusión

Diagnosticar el alto consumo de IO de VS Code Dev Container en Windows requiere partir del síntoma, localizar la causa raíz y aplicar la solución adecuada. Process Monitor confirma que la mayor parte del IO proviene de la comunicación de named pipe entre Docker CLI y Docker daemon; las herramientas internas de VS Code y Windows Performance Analyzer cuantifican la frecuencia y latencia de esas llamadas. El análisis revela cuatro factores superpuestos: el sondeo frecuente de Docker CLI (principal cuello de botella), fugas en procesos de reenvío de puertos, un bucle de reconexión del Extension Host y la amplificación de IO por sistemas de archivos cruzados en WSL2.

Entre las soluciones, dev.containers.executeInWSL: true es la medida central, ya que elimina el coste de comunicación entre Windows y WSL al ejecutar Docker CLI dentro de WSL y usar sockets Unix. Las demás medidas (simplificar reenvío de puertos, optimizar Docker Desktop y la configuración de VS Code) actúan como complementos que mitigan los factores secundarios. Para los usuarios afectados, se recomienda seguir el proceso de diagnóstico descrito, habilitar executeInWSL: true y, según el caso, aplicar las optimizaciones auxiliares.