VS Code Dev Container IO eccessivo: configurazione executeInWSL e analisi della causa radice

Registra il processo completo di排查 su Windows del problema di IO eccessivo causato dall’estensione VS Code Dev Container, dalla定位 del fenomeno all’analisi della causa radice, risolvendo infine il collo di bottiglia della comunicazione CLI Docker cross-boundary con dev.containers.executeInWSL come soluzione core.

Quando si utilizza l’estensione Dev Container di VS Code per lo sviluppo containerizzato su Windows, alcuni utenti riscontrano un evidente rallentamento del sistema. Nel Task Manager si può osservare che la CPU del processo Extension Host e l’IO di lettura del disco rimangono costantemente elevati, anche senza operazioni attive. Questo documento registra il processo completo di排查, partendo dal fenomeno del problema, localizzando gradualmente la causa radice e trovando la soluzione core.

Fenomeno del problema

Il rallentamento del sistema si verifica dopo la connessione dell’estensione Dev Container al container. Attraverso il Task Manager si può osservare che l’IO di lettura del disco e l’utilizzo della CPU del processo Extension Host rimangono costantemente elevati, anche quando l’utente non esegue alcuna operazione di editing o terminale, questi indicatori non scendono al livello di idle. Nei casi più gravi, la velocità di risposta dell’intero desktop Windows viene compromessa, con il cursore del mouse che presenta rallentamenti intermittenti.

Processo di排查

Utilizzo di Process Monitor per localizzare la fonte dell’IO

Sysinternals Process Monitor è il primo strumento per排查 questo tipo di problemi. Dopo aver avviato procmon, impostare le condizioni di 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 named pipe che iniziano con \\pipe\ hanno una frequenza anomala, fino a decine di volte al secondo. Queste operazioni named pipe corrispondono alla comunicazione tra Docker CLI e Docker daemon, indicando che Extension Host sta chiamando Docker CLI frequentemente.

Utilizzo degli strumenti integrati di VS Code per confermare

Attraverso Help > Toggle Developer Tools si apre Chromium DevTools, nel pannello Performance si può registrare il profilo CPU, dove si può vedere che Extension Host consuma molto tempo nello spawn di subprocessi e nella lettura del pipe stdout. Impostando il livello di log “Dev Containers” nel pannello Output su “trace”, si può vedere la sequenza completa delle chiamate: docker inspect --type container, docker version --format, docker exec, docker ps e altri comandi vengono eseguiti ripetutamente. Dai dati di log di issue #9194 si può quantificare l’overhead di una singola chiamata: docker inspect --type container richiede circa 1800ms, docker version richiede circa 620ms.

Utilizzo di Windows Performance Analyzer per analizzare l’IO a livello di sistema

Per un’analisi più approfondita, utilizzare wpr.exe -start GeneralProfile -filemode per avviare la registrazione ETW, riprodurre il problema e poi usare wpr.exe -stop capture.etl per interrompere la registrazione, caricare i risultati in Windows Performance Analyzer. La vista Disk I/O conferma che Extension Host è il principale contributore all’IO di lettura del disco, la vista Process Life Cycle mostra numerosi subprocessi a vita breve creati e distrutti ripetutamente, questi subprocessi sono proprio le istanze di chiamata di Docker CLI.

Analisi della causa radice

Architettura di comunicazione dei processi Dev Container

I risultati della排查 puntano all’architettura di comunicazione multi-processo di Dev Container. Quando un utente apre un workspace containerizzato tramite l’estensione Dev Container su Windows, in realtà avvia una catena di comunicazione multi-processo che attraversa i confini tra Windows e Linux.

flowchart LR
    subgraph Windows["Host Windows"]
        A["Client VS Code<br/>(Electron)"]
        B["Extension Host<br/>(Node.js)"]
        C["Docker CLI<br/>(sottoprocesso)"]
    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 tramite IPC con Extension Host locale (Node.js). Extension Host ha bisogno di stabilire una connessione con VS Code Server all’interno del container, e questa connessione dipende da Docker CLI come intermediario. Dopo l’avvio di VS Code Server nel container, Remote Extension Host assume l’esecuzione delle estensioni, e il processo di port forwarding fornisce un tunnel TCP per l’accesso ai servizi del container da parte del browser locale. Ogni anello di questa catena può diventare una fonte di amplificazione dell’IO.

Meccanismo di polling di Docker CLI

L’estensione Dev Containers ottiene e mantiene lo stato del container chiamando frequentemente i comandi Docker CLI. Nella fase di avvio del container, l’estensione esegue in sequenza docker inspect --type container per ottenere i metadati del container, docker version --format per verificare la disponibilità di Docker daemon, docker exec per eseguire comandi di探测 ambientale all’interno del container, e docker ps per elencare i container in esecuzione. Questi comandi non vengono eseguiti solo una volta all’avvio, ma vengono chiamati ripetutamente a una certa frequenza durante l’intero ciclo di vita del container.

Ogni chiamata a Docker CLI avvia un nuovo sottoprocesso, coinvolgendo una serie di operazioni IO come overhead della creazione del processo, lettura del pipe stdout, parsing del risultato JSON. Nella modalità predefinita, questi dati devono attraversare il confine Windows/WSL, e il protocollo di condivisione file 9P di WSL2 ha prestazioni scarse quando gestisce molte operazioni IO su piccoli file e connessioni brevi ad alta frequenza. Secondo la documentazione ufficiale Microsoft, le operazioni cross-filesystem dovrebbero essere evitate, ma l’architettura di Dev Container rende difficile evitare completamente questo overhead.

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 stdout

Perdite di connessione nel port forwarding

Il meccanismo di port forwarding di VS Code crea sottoprocessi Node.js indipendenti per ogni porta inoltrata. Questi processi si connettono alla porta di destinazione nel container tramite net.createConnection e inoltrano bidirezionalmente i dati tra la porta locale e la porta del container. Il problema è che quando il browser o altri client accedono alla porta inoltrata e poi si disconnettono, se la logica di pulizia non è tempestiva, questi processi di forwarding persistono invece di uscire normalmente.

Secondo l’analisi di microsoft/vscode-remote-release#5767, ogni processo di port forwarding perduto occupa circa 26 MiB di memoria. In un ambiente di sviluppo configurato con più porte inoltrate e accessi frequenti, il numero di processi può crescere da 2 normali a decine in poco tempo. Il seguente frammento di codice mostra il pattern core del processo di port forwarding, dove l’evento client.on('close') è la chiave per determinare se il processo può uscire 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 il processo docker exec di Docker CLI termina in modo anomalo ma il processo Node.js all’interno del container è ancora in esecuzione, questi processi orfani non possono essere回收ati normalmente, causando una crescita continua della memoria. Questo problema è stato parzialmente risolto dopo la versione 1.62 di VS Code, ma può ancora verificarsi in condizioni di rete specifiche. È importante notare che la perdita di port forwarding non è direttamente correlata a executeInWSL, è un difetto software del meccanismo di port forwarding di VS Code stesso.

Loop di riconnessione di Extension Host

Secondo i record di microsoft/vscode-remote-release#6178, quando la connessione al container remoto viene persa per interruzione di rete o altri motivi, c’è un bug nella logica di riconnessione in Extension Host: una funzione async richiama se stessa ricorsivamente nel blocco catch, causando un ciclo di CPU vuoto. Lo stack delle chiamate mostra che questa funzione cicla ripetutamente in processTicksAndRejections, senza condizione di uscita.

flowchart TD
    A["Connessione persa"] e1@--> B["Funzione async 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;

Nel frattempo, la memoria di Extension Host cresce costantemente a circa 1 MB/minuto, perché ogni chiamata ricorsiva accumula contesti non rilasciati nello stack delle chiamate. Questo problema è stato risolto nella versione pre-release 0.221.0 di Remote-Containers, ma per gli utenti che non aggiornano l’estensione in tempo, basta simulare una disconnessione di rete (ad esempio disattivare il WiFi) per attivare questo problema, e la finestra di dialogo che indica la disconnessione non verrà mai visualizzata. Questo è un bug software indipendente, non correlato alla configurazione executeInWSL, ma aggrava il rallentamento percepito dall’utente.

Amplificazione IO cross-filesystem di WSL2

Quando si utilizza il backend WSL2 di Docker Desktop su Windows, esiste un problema intrinseco di prestazioni IO cross-filesystem. L’estensione VS Code sul lato Windows comunica con Docker daemon in WSL2 tramite pipe, e se le operazioni sul filesystem del container coinvolgono percorsi /mnt/c (cioè l’accesso ai dischi Windows), richiedono la conversione tramite il protocollo di condivisione file 9P. Il protocollo 9P di WSL2 ha prestazioni significativamente inferiori rispetto al filesystem nativo negli scenari IO con molti piccoli file, con latenza per singola operazione che può essere 3-5 volte quella dei percorsi nativi.

Inoltre, secondo il report di microsoft/vscode-remote-release#9372, su ARM Mac quando si eseguono container x86 tramite Rosetta, la maschera CPU affinity del processo VS Code Server viene impostata in modo anomalo per usare un solo core, causando nproc che restituisce 1 invece del numero effettivo di core fisici. Sebbene questo problema si manifesti principalmente sulla piattaforma macOS, rivela che Dev Container ha un’incoerenza nel controllo dei parametri di scheduling dei processi su piattaforme diverse, e problemi simili possono manifestarsi in forme diverse anche nell’ambiente WSL2 di Windows. executeInWSL: true sposta l’esecuzione di Docker CLI all’interno di WSL, riducendo la frequenza dell’IO cross-filesystem, ma non può eliminare completamente l’overhead 9P generato quando il container accede ai percorsi /mnt/c.

Soluzioni

Semplificare la configurazione del port forwarding

In devcontainer.json, semplificare la configurazione forwardPorts mantenendo solo le porte effettivamente necessarie, può ridurre significativamente il numero di processi di port forwarding, abbassando il rischio di perdita di processi descritto in issue #5767. Chiudere le finestre Dev Container non utilizzate rilascia anche immediatamente le risorse corrispondenti di Extension Host e port forwarding.

Ottimizzazione di Docker Desktop

Nel pannello Settings > Resources di Docker Desktop, regolare adeguatamente la allocazione di CPU e memoria, per evitare che Docker daemon esegua frequentemente garbage collection o operazioni swap per mancanza di risorse. Assicurarsi di utilizzare l’ultima versione di Docker Desktop, perché le prestazioni del backend WSL2 vengono migliorate in ogni versione. Per scenari con requisiti di prestazioni più elevati, si può considerare di installare direttamente Docker Engine all’interno di WSL2, bypassando il livello di virtualizzazione di Docker Desktop, eliminando così ulteriormente l’overhead aggiuntivo del confine Windows/WSL.

Ottimizzazione della configurazione di VS Code

Disabilitare le estensioni non necessarie può ridurre il carico di Extension Host, specialmente quelle che eseguono all’interno del container remoto e hanno funzionalità di file watching (come il servizio linguistico TypeScript, ESLint, ecc.). In settings.json, impostare files.watcherExclude per escludere directory grandi come node_modules, .git, dist, può ridurre l’IO generato dal file system watching. Impostare extensions.autoUpdate: false può evitare che gli aggiornamenti delle estensioni in background attivino operazioni di rete e disco aggiuntive nell’ambiente container.

Alternative

Se le misure di cui sopra non soddisfano ancora i requisiti di prestazioni, si può considerare l’utilizzo dell’estensione VS Code Remote-SSH per connettersi a WSL2, usando direttamente Docker CLI all’interno di WSL2 per gestire i container. Questo metodo trasforma la chiamata Docker CLI da comunicazione cross-boundary Windows/WSL a comunicazione locale all’interno di WSL2. Un altro modo è utilizzare Docker Compose per gestire il ciclo di vita del container, avviare i servizi con docker compose up -d e poi utilizzare solo Remote-SSH per connettersi al container per lo sviluppo, bypassando completamente il meccanismo di polling dell’estensione Dev Container.

Abilitare executeInWSL (soluzione core)

Le misure di cui sopra possono alleviare il problema di IO eccessivo in vari gradi, ma riducono solo la frequenza dell’IO (semplificazione port forwarding) o ottimizzano solo l’allocazione delle risorse (ottimizzazione Docker Desktop), senza toccare la causa fondamentale del problema: l’overhead di comunicazione named pipe quando le chiamate Docker CLI attraversano il confine Windows/WSL. dev.containers.executeInWSL è la soluzione diretta proprio a questa causa fondamentale.

Secondo la chiara spiegazione del membro del team VS Code chrmarti in microsoft/vscode-remote-release#9194, questa impostazione determina se il comando docker viene eseguito sul lato Windows o all’interno di WSL. Dopo averla impostata su true, tutte le chiamate Docker CLI (incluse docker inspect, docker version, docker exec, docker ps, ecc.) verranno eseguite all’interno di WSL, comunicando direttamente con Docker daemon tramite Unix socket, bypassando così l’overhead di conversione named pipe e protocollo 9P al confine Windows/WSL.

Aggiungere la seguente configurazione in settings.json per abilitarla:

{
  "dev.containers.executeInWSL": true
}

Per capire perché questa configurazione può migliorare significativamente le prestazioni IO, è necessario confrontare la differenza nel percorso di comunicazione tra le due modalità. Nella modalità predefinita (executeInWSL: false o non impostato), Extension Host di VS Code viene eseguito su Windows, ogni volta che ha bisogno di interagire con Docker daemon, avvia un sottoprocesso docker.exe su Windows tramite spawn. Questo sottoprocesso comunica con Docker daemon attraverso named pipe (percorso che inizia con \\pipe\) attraversando il confine Windows/WSL. Questo percorso coinvolge tre fasi: creazione del processo Windows, IO named pipe cross-boundary e ritorno del pipe stdout, ogni fase introduce latenza e overhead IO aggiuntivi. Quando executeInWSL: true, Extension Host改为tramite wsl -d <distro> -e docker per eseguire Docker CLI all’interno di WSL. Docker CLI viene eseguito nativamente all’interno di WSL, comunicando con Docker daemon nella stessa istanza WSL tramite socket Unix (IPC locale). Questo percorso è completamente completato all’interno dello spazio kernel Linux, evitando l’overhead cross-boundary dei named pipe.

flowchart TB
    subgraph Default["Modalità predefinita (executeInWSL: false)"]
        direction LR
        A1["Extension Host<br/>(Windows)"]
        A2["docker.exe<br/(sottoprocesso 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/(dentro 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;

Dai dati quantificati nella fase di排查 si può verificare questo miglioramento: nella modalità predefinita, una singola chiamata docker inspect --type container richiede circa 1800ms, la chiamata docker version richiede circa 620ms, questi ritardi derivano principalmente dall’overhead di comunicazione named pipe cross-boundary Windows/WSL e dall’overhead della creazione del processo. Dopo aver abilitato executeInWSL: true, Docker CLI comunica con daemon tramite Unix socket all’interno di WSL, la latenza di una singola chiamata può scendere a livello di millisecondi, e il miglioramento dell’effetto cumulativo è particolarmente significativo.

Problemi noti e precauzioni

dev.containers.executeInWSL sebbene possa migliorare efficacemente le prestazioni IO, presenta i seguenti problemi noti da considerare durante l’uso.

Problema di avvio automatico di Docker Desktop: issue #9695 riporta che con executeInWSL: true Docker Desktop non si avvia automaticamente. Questo problema è stato risolto nella versione pre-release 0.353.0 di Dev Containers, gli utenti che utilizzano versioni più recenti non dovrebbero più incontrare questo problema.

Inoltro servizi WSL: issue #9194 riporta che anche impostando executeInWSL su false, l’estensione tenta ancora di connettersi a WSL (per inoltro display/ssh-agent/gpg-agent). Questo comportamento è stato risolto nella versione pre-release 0.337.0 tramite l’aggiunta dell’impostazione dev.containers.wslServiceForwarding, che gli utenti possono controllare per disattivare indipendentemente l’inoltro dei servizi WSL.

Compatibilità con Rancher Desktop: issue #10722 riporta che quando si usa Rancher Desktop al posto di Docker Desktop, executeInWSL: true attiva un messaggio di errore WSL1. Questo problema è attualmente ancora aperto, gli utenti di Rancher Desktop potrebbero dover disattivare temporaneamente questa impostazione.

Attivazione involontaria: issue #11005 riporta che executeInWSL: true attiva involontariamente il processo di inizializzazione Dev Container nei repository Windows locali. Questo problema è anch’esso aperto, gli utenti interessati possono considerare di limitare questa impostazione a workspace specifici invece che alla configurazione globale.

La排查 del problema di IO eccessivo di VS Code Dev Container su Windows richiede di partire dal fenomeno e localizzare gradualmente la causa radice. Attraverso Process Monitor si può confermare che la fonte principale dell’IO è la comunicazione named pipe di Docker CLI, attraverso gli strumenti integrati di VS Code e Windows Performance Analyzer si può ulteriormente quantificare la frequenza delle chiamate e la latenza. L’analisi della causa radice rivela quattro fattori sovrapposti: il polling cross-boundary ad alta frequenza di Docker CLI è il principale collo di bottiglia delle prestazioni, la perdita di connessione dei processi di port forwarding e il loop di riconnessione di Extension Host sono bug software confermati, e l’amplificazione IO cross-filesystem di WSL2 è una limitazione intrinseca a livello di piattaforma.

Tra le soluzioni, dev.containers.executeInWSL: true è la misura più core, elimina direttamente l’overhead di comunicazione named pipe di Docker CLI che attraversa il confine Windows/WSL, spostando l’esecuzione delle chiamate CLI ad alta frequenza da Windows all’interno di WSL, completando IPC locale tramite Unix socket. Le altre soluzioni (semplificazione port forwarding, ottimizzazione Docker Desktop, ottimizzazione configurazione VS Code) come misure ausiliarie, alleviano in vari gradi l’impatto di altri fattori. Per gli utenti affetti da questo problema, si consiglia di confermare la causa radice seguendo il processo di排查 di questo documento, poi abilitare prioritariamente executeInWSL: true, e poi scegliere strategie di ottimizzazione ausiliarie adeguate in base allo scenario effettivo.