VS Code Dev Container IO alto: configuración detallada de executeInWSL y análisis de causa raíz
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 stdoutFugas 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: trueDocker 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óndev.containers.wslServiceForwarding. - Compatibilidad con Rancher Desktop: el issue #10722 reporta que usar Rancher Desktop en lugar de Docker Desktop con
executeInWSL: truegenera 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: truepuede 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.