VS Code Dev Container IO elevato: dettagli della configurazione executeInWSL e analisi della causa radice
Su Windows, quando si utilizza l’estensione Dev Container di VS Code per lo sviluppo containerizzato, alcuni utenti riscontrano un evidente rallentamento del sistema. Nel Task Manager è possibile osservare che il processo Extension Host mantiene costantemente alti valori di CPU e di I/O di lettura del disco, anche in assenza di operazioni attive. Questo articolo documenta l’intero processo di indagine, partendo dal sintomo, passando per l’individuazione della causa radice e arrivando alla soluzione centrale.
Sintomo
Il rallentamento del sistema si verifica dopo aver collegato l’estensione Dev Container al container. Nel Task Manager si nota che il processo Extension Host mantiene costantemente alti valori di I/O di lettura del disco e di utilizzo CPU, anche quando l’utente non sta modificando file o usando il terminale. In casi estremi, l’intera interfaccia di Windows risponde più lentamente, con il cursore del mouse che si blocca occasionalmente.
Processo di indagine
Individuazione della fonte di I/O con Process Monitor
Sysinternals Process Monitor è lo strumento di partenza per questo tipo di problemi. Dopo aver avviato Procmon, impostare il filtro su Process Name is Code.exe o Process Name is Extension Host per osservare in tempo reale tutti i percorsi e le frequenze delle operazioni ReadFile/WriteFile. Nei risultati filtrati, le operazioni su named pipe che iniziano con \\pipe\ compaiono con una frequenza anormalmente alta, raggiungendo decine di volte al secondo. Queste operazioni corrispondono alla comunicazione tra Docker CLI e Docker daemon, indicando che Extension Host chiama frequentemente Docker CLI.
Conferma con gli strumenti integrati di VS Code
Aprendo Help > Toggle Developer Tools e registrando un profilo CPU nel pannello Performance, è possibile vedere che gran parte del tempo di Extension Host è speso nello spawn di processi figlio e nella lettura dei loro pipe stdout. Impostando il livello di log di “Dev Containers” su “trace” nel pannello Output, si ottiene la sequenza completa dei comandi: docker inspect --type container, docker version --format, docker exec, docker ps e così via. Dal log dell’issue #9194 si può quantificare il costo di una singola chiamata: docker inspect --type container richiede circa 1800 ms, docker version circa 620 ms.
Analisi del I/O a livello di sistema con Windows Performance Analyzer
Per un’analisi più profonda, utilizzare wpr.exe -start GeneralProfile -filemode per avviare la registrazione ETW, riprodurre il problema, quindi fermare la registrazione con wpr.exe -stop capture.etl. Caricando il risultato in Windows Performance Analyzer, la vista Disk I/O conferma che Extension Host è il principale contributore al I/O di lettura, mentre la vista Process Life Cycle mostra numerosi processi figlio a vita breve creati e distrutti ripetutamente, corrispondenti alle chiamate di Docker CLI.
Analisi della causa radice
Architettura di comunicazione dei processi di Dev Container
I risultati dell’indagine puntano all’architettura di comunicazione multi‑processo di Dev Container. Quando l’utente apre un workspace containerizzato su Windows, si avvia una catena di comunicazione multi‑processo che attraversa il confine Windows‑Linux.
flowchart LR
subgraph Windows["Windows Host"]
A["VS Code Client<br/>(Electron)"]
B["Extension Host<br/>(Node.js)"]
C["Docker CLI<br/>(processo figlio)"]
end
subgraph WSL2["WSL2 / Docker Desktop"]
D["Docker Daemon<br/>(dockerd)"]
end
subgraph Container["All'interno del container"]
E["VS Code Server"]
F["Remote Extension Host"]
G["Processo di port forwarding<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;Il client VS Code locale (Electron) comunica via IPC con l’Extension Host locale (Node.js). L’Extension Host deve stabilire una connessione con il VS Code Server all’interno del container, dipendente da Docker CLI come intermediario. Dopo l’avvio del VS Code Server, il Remote Extension Host prende in carico l’esecuzione delle estensioni, mentre il processo di port forwarding fornisce un tunnel TCP per l’accesso del browser locale ai servizi del container. Ogni anello di questa catena può amplificare l’I/O.
Meccanismo di polling di Docker CLI
L’estensione Dev Containers chiama frequentemente Docker CLI per ottenere e mantenere lo stato del container. Durante l’avvio, esegue sequenzialmente docker inspect --type container, docker version --format, docker exec (per eseguire comandi di rilevamento) e docker ps. Questi comandi non vengono eseguiti una sola volta, ma vengono ripetuti periodicamente per tutta la durata del container.
Ogni chiamata avvia un nuovo processo figlio, con costi di creazione, lettura del pipe stdout e parsing del risultato JSON, tutti operazioni I/O. In modalità predefinita, questi dati attraversano il confine Windows/WSL, e il protocollo 9P di condivisione file di WSL2 è inefficiente per numerosi piccoli file e connessioni brevi. La documentazione di Microsoft consiglia di minimizzare le operazioni cross‑filesystem, ma l’architettura di Dev Container rende difficile evitarle completamente.
sequenceDiagram
participant EH as Extension Host
participant CLI as Docker CLI
participant DD as Docker Daemon
EH->>CLI: spawn docker inspect
CLI->>DD: richiesta named pipe
DD-->>CLI: risposta JSON
CLI-->>EH: output pipe stdout
EH->>CLI: spawn docker version
CLI->>DD: richiesta named pipe
DD-->>CLI: informazioni versione
CLI-->>EH: output pipe stdout
EH->>CLI: spawn docker exec
CLI->>DD: richiesta named pipe
DD-->>CLI: risultato esecuzione
CLI-->>EH: output pipe stdoutPerdita di processi di port forwarding
Il meccanismo di port forwarding di VS Code crea un processo Node.js separato per ogni porta inoltrata. Questi processi usano net.createConnection per collegarsi alla porta di destinazione nel container e inoltrare i dati bidirezionalmente. Se il client (browser o altro) chiude la connessione senza che il processo di forwarding venga pulito, il processo rimane attivo.
Secondo l’analisi di microsoft/vscode-remote-release#5767, ogni processo di forwarding perduto consuma circa 26 MiB di RAM. In un ambiente con molte porte inoltrate e accessi frequenti, il numero di processi può passare da 2 a decine in pochi minuti. Il frammento di codice seguente mostra il modello chiave, dove la gestione dell’evento client.on('close') determina se il processo termina correttamente.
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 il processo docker exec termina in modo anomalo ma il processo Node.js all’interno del container rimane attivo, questi processi orfani non vengono riciclati, causando una crescita della memoria. Il problema è stato parzialmente risolto a partire da VS Code 1.62, ma può ancora verificarsi in condizioni di rete particolari. Non è direttamente correlato a executeInWSL.
Loop di riconnessione dell’Extension Host
Come documentato in microsoft/vscode-remote-release#6178, quando la connessione al container remoto viene persa, la logica di riconnessione dell’Extension Host contiene un bug: una funzione async si richiama ricorsivamente nel blocco catch, creando un ciclo CPU. Lo stack mostra la funzione ripetuta in processTicksAndRejections senza una condizione di uscita.
flowchart TD
A["Connessione persa"] e1@--> B["Funzione async di riconnessione"]
B e2@--> C{"Connessione riuscita?"}
C e3@-->|Sì| D["Ripristino normale"]
C e4@-->|No| E["Blocco 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;Durante questo loop, la memoria dell’Extension Host cresce di circa 1 MiB al minuto a causa dell’accumulo di contesti non rilasciati. Il bug è stato corretto nella versione pre‑release 0.221.0 di Remote‑Containers, ma gli utenti che non aggiornano l’estensione possono ancora riprodurlo disattivando temporaneamente la rete (ad esempio spegnendo il Wi‑Fi). Il problema è indipendente da executeInWSL, ma peggiora la percezione di rallentamento.
Amplificazione dell’I/O cross‑filesystem in WSL2
Con Docker Desktop su WSL2, le operazioni di I/O che attraversano il filesystem di Windows subiscono un notevole overhead. Quando l’estensione VS Code su Windows comunica con il daemon Docker in WSL2 tramite pipe, ogni accesso a percorsi /mnt/c richiede la traduzione tramite il protocollo 9P, molto più lento rispetto a un filesystem nativo. La latenza di una singola operazione può essere 3‑5 volte superiore.
Un altro problema segnalato in microsoft/vscode-remote-release#9372 riguarda l’affinità della CPU del processo VS Code Server su Mac ARM con Rosetta: il processo viene vincolato a un solo core, facendo sì che nproc restituisca 1. Sebbene questo sia specifico per macOS, evidenzia l’incoerenza nella gestione dei parametri di scheduling nei diversi ambienti. In WSL2, executeInWSL: true sposta l’esecuzione di Docker CLI all’interno di WSL, riducendo la frequenza delle operazioni cross‑filesystem, ma non elimina completamente il costo quando il container accede a /mnt/c.
Soluzioni
Ridurre la configurazione di port forwarding
Nel file devcontainer.json, limitare la proprietà forwardPorts ai soli porti realmente necessari riduce il numero di processi di forwarding e il rischio di perdita descritta nell’issue #5767. Chiudere le finestre Dev Container non utilizzate libera immediatamente le risorse di Extension Host e dei processi di forwarding.
Ottimizzare Docker Desktop
Nel pannello Settings > Resources di Docker Desktop, regolare CPU e memoria per evitare che il daemon Docker esegua frequenti operazioni di garbage collection o swap. Utilizzare sempre l’ultima versione di Docker Desktop, poiché le ottimizzazioni per WSL2 sono introdotte ad ogni rilascio. Per carichi particolarmente esigenti, considerare l’installazione di Docker Engine direttamente in WSL2, bypassando lo strato di virtualizzazione di Docker Desktop e eliminando l’overhead del confine Windows/WSL.
Ottimizzare la configurazione di VS Code
Disabilitare le estensioni non necessarie riduce il carico dell’Extension Host, soprattutto quelle che eseguono il monitoraggio dei file all’interno del container (ad es. TypeScript language service, ESLint). Nel file settings.json, impostare files.watcherExclude per escludere node_modules, .git, dist e altre directory di grandi dimensioni, diminuendo l’I/O generato dal file watcher. Disattivare extensions.autoUpdate evita aggiornamenti automatici che generano I/O aggiuntivo in ambiente container.
Approccio alternativo
Se le ottimizzazioni sopra non bastano, è possibile utilizzare l’estensione Remote‑SSH per connettersi direttamente a WSL2 e gestire i container con Docker CLI all’interno di WSL. In questo modo, le chiamate a Docker CLI avvengono localmente in WSL, evitando il polling cross‑boundary. Un’altra opzione è gestire i container con Docker Compose (docker compose up -d) e poi utilizzare Remote‑SSH per lo sviluppo, bypassando completamente l’estensione Dev Container.
Abilitare executeInWSL (soluzione centrale)
Le misure precedenti alleviano l’I/O elevato, ma non affrontano la causa principale: il costo di comunicazione via named pipe quando Docker CLI attraversa il confine Windows/WSL. L’impostazione dev.containers.executeInWSL sposta l’esecuzione di Docker CLI all’interno di WSL, facendo sì che tutti i comandi (docker inspect, docker version, docker exec, docker ps, ecc.) vengano eseguiti in WSL e comunichino con il daemon Docker tramite socket Unix, eliminando il named pipe e la conversione 9P.
Come spiegato da chrmarti in microsoft/vscode-remote-release#9194, questa impostazione determina se il comando docker viene eseguito sul lato Windows o all’interno di WSL. Impostandola a true, tutti i comandi Docker vengono lanciati con wsl -d <distro> -e docker, usando il socket Unix locale per comunicare con il daemon Docker, evitando così i tre passaggi (creazione processo Windows, named pipe cross‑boundary, pipe stdout) che introducono latenza.
{
"dev.containers.executeInWSL": true
}
Per comprendere perché questa configurazione migliora drasticamente le prestazioni I/O, confrontiamo i due percorsi di comunicazione. In modalità predefinita (executeInWSL: false o non impostata):
- Extension Host (Windows) →
docker.exe(processo Windows) → named pipe (\\pipe\\) → Docker Daemon (WSL2)
Questo percorso implica creazione di processo, I/O cross‑boundary e ritorno del pipe stdout, ognuno con latenza aggiuntiva. Con executeInWSL: true:
- Extension Host (Windows) →
wsl -e docker(eseguito dentro WSL) → socket Unix (IPC locale) → Docker Daemon (WSL2)
Tutto avviene all’interno del kernel Linux, eliminando il named pipe e riducendo la latenza a pochi millisecondi.
flowchart TB
subgraph Default["Modalità predefinita (executeInWSL: false)"]
direction LR
A1["Extension Host<br/>(Windows)"]
A2["docker.exe<br/>(processo Windows)"]
A3["named pipe<br/>(\\\\pipe\\\\)"]
A4["Docker Daemon<br/>(WSL2)"]
A1 e1@--> A2
A2 e2@--> A3
A3 e3@--> A4
end
subgraph Optimized["Modalità ottimizzata (executeInWSL: true)"]
direction LR
B1["Extension Host<br/>(Windows)"]
B2["wsl -e docker<br/>(interno WSL)"]
B3["Unix socket<br/>(IPC locale)"]
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;I dati di indagine mostrano che, in modalità predefinita, una singola chiamata docker inspect --type container richiede circa 1800 ms, mentre docker version ne richiede 620 ms, principalmente a causa del named pipe e della creazione del processo. Con executeInWSL: true, le chiamate avvengono tramite socket Unix e la latenza scende a pochi millisecondi, con un impatto cumulativo notevole.
Problemi noti e avvertenze
- Avvio automatico di Docker Desktop: l’issue #9695 segnala che con
executeInWSL: trueDocker Desktop non si avvia automaticamente. Il problema è stato risolto nella versione pre‑release 0.353.0 di Dev Containers; gli utenti con versioni più recenti non dovrebbero più incontrarlo. - Forwarding dei servizi WSL: l’issue #9194 indica che, anche con
executeInWSL: false, l’estensione tenta comunque di connettersi a WSL per forwarding di display/ssh‑agent/gpg‑agent. È possibile disabilitare questo comportamento con la nuova impostazionedev.containers.wslServiceForwardingintrodotta nella versione 0.337.0. - Compatibilità con Rancher Desktop: l’issue #10722 riporta che, usando Rancher Desktop al posto di Docker Desktop,
executeInWSL: truegenera errori WSL1. Il problema è ancora aperto; gli utenti di Rancher Desktop potrebbero dover disabilitare temporaneamente l’impostazione. - Attivazione inattesa: l’issue #11005 descrive come
executeInWSL: truepossa innescare l’inizializzazione di Dev Container anche in repository Windows locali. È consigliabile applicare l’impostazione solo a workspace specifici anziché a livello globale.
Conclusione
Indagare l’elevato I/O di VS Code Dev Container su Windows richiede un approccio step‑by‑step, partendo dal sintomo, passando per l’analisi con Process Monitor, gli strumenti integrati di VS Code e Windows Performance Analyzer, fino a identificare le quattro cause principali: polling frequente di Docker CLI, perdita di processi di port forwarding, loop di riconnessione dell’Extension Host e amplificazione dell’I/O cross‑filesystem in WSL2.
Tra le soluzioni, dev.containers.executeInWSL: true è la più efficace perché elimina il costo di comunicazione via named pipe tra Windows e WSL, spostando l’esecuzione di Docker CLI all’interno di WSL e usando socket Unix. Le altre misure (riduzione del port forwarding, ottimizzazioni di Docker Desktop e configurazione di VS Code) agiscono come mitigazioni aggiuntive. Per gli utenti colpiti, si consiglia di seguire il percorso di indagine descritto, abilitare executeInWSL: true e, se necessario, applicare le ottimizzazioni complementari.