VS Code Dev Container hoge IO: gedetailleerde configuratie van executeInWSL en oorzaak‑analyse

Een volledige opsporingsprocedure van het hoge IO‑probleem veroorzaakt door de VS Code Dev Container‑extensie op Windows, van symptoomidentificatie tot oorzaak‑analyse, met dev.containers.executeInWSL als kernoplossing voor de Docker‑CLI‑communicatielimiet.

In Windows, wanneer je de Dev Container‑extensie van VS Code gebruikt voor container‑gebaseerde ontwikkeling, ervaren sommige gebruikers merkbare systeemvertragingen. In Taak‑beheer is te zien dat het Extension Host‑proces continu hoge CPU‑ en schijf‑read‑IO‑waarden heeft, zelfs zonder actieve bewerkingen. Dit artikel legt stap‑voor‑stap de volledige opsporingsprocedure vast, van symptoom tot oorzaak en uiteindelijk de kernoplossing.

Symptomen

Het systeem vertraagt zodra de Dev Container‑extensie verbinding maakt met een container. In Taak‑beheer zie je dat het Extension Host‑proces continu hoge schijf‑read‑IO en CPU‑gebruik vertoont, zelfs zonder bewerkingen. In extreme gevallen wordt de algehele Windows‑desktoprespons traag en knippert de muiscursor.

Opsporingsprocedure

IO‑bron lokaliseren met Process Monitor

Sysinternals Process Monitor is de eerste tool voor dit soort problemen. Start procmon en stel een filter in op Process Name is Code.exe of Process Name is Extension Host. Je kunt nu alle ReadFile/WriteFile‑acties en hun frequentie volgen. In de resultaten zie je veel \\pipe\‑named‑pipe‑acties, tientallen per seconde. Deze pipes vormen de communicatie tussen Docker CLI en Docker daemon, wat aangeeft dat Extension Host vaak Docker CLI aanroept.

Controle met ingebouwde VS Code‑tools

Open Help > Toggle Developer Tools en neem een CPU‑profiel op in het Performance‑paneel. Je ziet dat het grootste deel van de tijd van Extension Host wordt besteed aan het starten van subprocessen en het lezen van stdout‑pipes. Zet de log‑niveau van “Dev Containers” in het Output‑paneel op “trace” om de volledige commandoreeks te zien: docker inspect --type container, docker version --format, docker exec, docker ps, enz. Uit de gegevens van issue #9194 blijkt dat één docker inspect --type container ongeveer 1800 ms duurt, en docker version ongeveer 620 ms.

Systeem‑IO analyseren met Windows Performance Analyzer

Voor diepere analyse start je wpr.exe -start GeneralProfile -filemode, reproduceer het probleem, en stop je met wpr.exe -stop capture.etl. Laad het bestand in Windows Performance Analyzer. De Disk‑I/O‑view bevestigt dat Extension Host de belangrijkste bron van schijf‑read‑IO is; de Process Life Cycle‑view toont talloze korte‑levensduur subprocessen die Docker CLI‑aanroepen vertegenwoordigen.

Oorzaakanalyse

Procescommunicatie‑architectuur van Dev Container

De resultaten wijzen op de multi‑process‑communicatie‑architectuur van Dev Container. Wanneer een gebruiker een container‑werkruimte opent via de extensie, wordt een keten van processen over Windows‑ en Linux‑grenzen heen opgezet.

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["Binnen de 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;

De lokale VS Code‑client (Electron) communiceert via IPC met de lokale Extension Host (Node.js). De Extension Host moet verbinding maken met de VS Code Server in de container, en die verbinding maakt via Docker CLI. Na het starten van de server neemt de Remote Extension Host de extensie‑uitvoering over, en het port‑forwarding‑proces biedt een TCP‑tunnel voor de lokale browser. Elk onderdeel kan een bron van IO‑vergroting zijn.

Docker CLI‑polling‑mechanisme

De Dev Containers‑extensie roept Docker CLI vaak aan om de containerstatus te verkrijgen en te onderhouden. Bij het starten van een container voert de extensie opeenvolgend docker inspect --type container, docker version --format, docker exec (voor environment‑detectie) en docker ps uit. Deze commando’s worden niet alleen bij opstarten één keer uitgevoerd, maar gedurende de hele levensduur van de container periodiek.

Elke CLI‑aanroep start een nieuw subprocess, wat proces‑creatie‑overhead, stdout‑pipe‑lezen en JSON‑parsing met zich meebrengt. Standaard moeten deze gegevens de Windows/WSL‑grens oversteken, en de 9P‑bestandsdeling van WSL2 presteert slecht bij veel kleine bestanden en korte verbindingen. Volgens de Microsoft‑documentatie moet cross‑filesystem‑toegang zoveel mogelijk vermeden worden, maar de architectuur van Dev Container maakt dit lastig.

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

Port‑forwarding‑lekkage

De port‑forwarding‑mechanisme van VS Code maakt voor elke doorgestuurde poort een apart Node.js‑subprocess aan. Deze processen verbinden via net.createConnection met de doelpoort in de container en sturen data bidirectioneel door. Als een client (bijv. een browser) de poort verlaat zonder dat de cleanup‑logica wordt uitgevoerd, blijven deze processen bestaan.

Uit de analyse van microsoft/vscode-remote-release#5767 blijkt dat elk lekproces ongeveer 26 MiB geheugen gebruikt. In een omgeving met meerdere poorten en frequente toegang kan het aantal processen snel groeien van 2 naar tientallen. De volgende code toont het kernpatroon, waarbij de client.on('close')‑handler bepaalt of het proces correct afsluit.

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

Wanneer een docker exec‑subprocess onverwacht eindigt terwijl de Node.js‑processen in de container nog draaien, blijven deze “orphaned” processen bestaan, wat leidt tot geheugen‑groei. Het probleem is gedeeltelijk opgelost sinds VS Code 1.62, maar kan nog optreden onder bepaalde netwerkomstandigheden. Het is belangrijk te weten dat deze lekken niet direct gerelateerd zijn aan executeInWSL; ze zijn een eigen defect in de port‑forwarding‑implementatie.

Extension Host‑herconnectie‑lus

Volgens microsoft/vscode-remote-release#6178 bestaat er een bug in de herconnectielogica van Extension Host: een async‑functie roept zichzelf recursief aan vanuit een catch‑blok, waardoor de CPU blijft draaien. De stack‑trace toont herhaalde aanroepen van processTicksAndRejections zonder exit‑conditie.

flowchart TD
    A["Verbinding verbroken"] e1@--> B["async herconnectie‑functie"]
    B e2@--> C{"Verbonden?"}
    C e3@-->|Ja| D["Terug naar normaal"]
    C e4@-->|Nee| E["catch‑blok"]
    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;

Tegelijkertijd groeit het geheugen van Extension Host met ongeveer 1 MiB per minuut door de opeenstapeling van niet‑gereleasede contexten. Het probleem is verholpen in Remote‑Containers 0.221.0‑pre‑release, maar gebruikers die de extensie niet updaten kunnen het nog steeds reproduceren door bijvoorbeeld Wi‑Fi uit te schakelen; er verschijnt geen dialoogvenster bij het verbreken van de verbinding. Dit is een onafhankelijk software‑bug, los van executeInWSL.

WSL2‑cross‑filesystem‑IO‑vergroting

Met Docker Desktop’s WSL2‑backend onder Windows bestaat er een inherente prestatie‑penalty bij cross‑filesystem‑IO. De VS Code‑extensie communiceert via pipes met de Docker daemon in WSL2; wanneer bestanden in de container /mnt/c (Windows‑schijf) benaderen, moet de 9P‑share‑protocol de data vertalen. 9P presteert veel slechter bij veel kleine bestanden, met latenties 3‑5 × hoger dan native paden.

Daarnaast meldt microsoft/vscode-remote-release#9372 dat op ARM‑Macs via Rosetta de CPU‑affinity van de VS Code Server soms op één core wordt beperkt, waardoor nproc 1 teruggeeft. Hoewel dit primair een macOS‑probleem is, toont het de inconsistentie in proces‑scheduling tussen platformen; vergelijkbare effecten kunnen in Windows‑WSL2 voorkomen. executeInWSL: true verplaatst de Docker CLI‑uitvoering naar WSL, waardoor cross‑filesystem‑IO minder vaak voorkomt, maar het elimineert de 9P‑overhead bij toegang tot /mnt/c niet volledig.

Oplossingen

Port‑forwarding‑configuratie minimaliseren

Verwijder onnodige poorten uit devcontainer.jsonforwardPorts. Dit vermindert het aantal port‑forwarding‑processen en verkleint het risico op lekken (zie issue #5767). Sluit ongebruikte Dev Container‑vensters om de bijbehorende Extension Host‑ en port‑forwarding‑resources direct vrij te geven.

Docker Desktop‑optimalisatie

Pas in Docker Desktop → Settings → Resources de CPU‑ en geheugen‑toewijzing aan om te voorkomen dat de daemon vaak moet garbage‑collecten of swappen. Houd Docker Desktop up‑to‑date; elke release verbetert de WSL2‑prestaties. Voor zeer veeleisende scenario’s kan je Docker Engine direct in WSL2 installeren en de Docker Desktop‑virtualisatielaag omzeilen, waardoor de extra Windows/WSL‑overhead verdwijnt.

VS Code‑configuratie‑optimalisatie

Schakel onnodige extensies uit, vooral die met file‑watchers (bijv. TypeScript, ESLint). Voeg in settings.json files.watcherExclude toe voor node_modules, .git, dist enz. Zet extensions.autoUpdate: false om te voorkomen dat achtergrond‑updates extra IO veroorzaken.

Alternatieve benadering

Als bovenstaande maatregelen onvoldoende zijn, overweeg dan VS Code Remote‑SSH te gebruiken om direct naar WSL2 te verbinden en Docker CLI daar te beheren. Hierdoor blijft de CLI‑communicatie binnen WSL2 en wordt de poll‑mechanisme van Dev Containers vermeden. Een andere optie is Docker Compose te gebruiken voor container‑levenscyclusbeheer (docker compose up -d) en vervolgens Remote‑SSH voor ontwikkeling, waardoor de Dev Container‑extensie volledig wordt omzeild.

executeInWSL inschakelen (kernoplossing)

De eerder genoemde maatregelen verlichten de symptomen, maar raken de fundamentele oorzaak niet: de named‑pipe‑communicatie tussen Docker CLI en Docker daemon over de Windows/WSL‑grens. De instelling dev.containers.executeInWSL verplaatst alle Docker‑CLI‑aanroepen naar WSL, waardoor ze via een Unix‑socket direct met de daemon communiceren.

Zoals chrmarti aangeeft in microsoft/vscode-remote-release#9194, bepaalt deze instelling of docker in Windows of in WSL wordt uitgevoerd. Voeg de volgende configuratie toe aan settings.json:

{
  "dev.containers.executeInWSL": true
}

Waarom dit de IO‑prestaties verbetert: in de standaardmodus (executeInWSL: false of niet ingesteld) draait Extension Host op Windows; elke Docker‑interactie start docker.exe als Windows‑subprocess, die via een named pipe (\\pipe\) met de Docker daemon in WSL2 communiceert. Deze route omvat proces‑creatie, cross‑boundary pipe‑IO en stdout‑pipe‑terugvoer, elk met extra latency en IO‑kosten. Met executeInWSL: true wordt Docker CLI via wsl -d <distro> -e docker direct in WSL uitgevoerd; de CLI gebruikt een Unix‑socket (lokale IPC) om met de daemon te praten, volledig binnen de Linux‑kernel, zonder de named‑pipe‑overhead.

flowchart TB
    subgraph Default["Standaardmodus (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["Geoptimaliseerde modus (executeInWSL: true)"]
        direction LR
        B1["Extension Host<br/>(Windows)"]
        B2["wsl -e docker<br/>(WSL intern)"]
        B3["Unix socket<br/>(lokale 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;

Metingen tonen dat een docker inspect --type container in de standaardmodus ~1800 ms duurt, terwijl dezelfde oproep met executeInWSL: true in enkele milliseconden voltooid is. Het cumulatieve effect is aanzienlijk.

Bekende problemen en aandachtspunten

  • Docker Desktop auto‑start: issue #9695 meldt dat Docker Desktop niet automatisch start wanneer executeInWSL: true is ingeschakeld. Dit is opgelost in Dev Containers 0.353.0‑pre‑release.
  • WSL‑service‑forwarding: Zelfs met executeInWSL: false probeert de extensie soms WSL te benaderen voor display/ssh‑agent/gpg‑agent‑forwarding. Sinds versie 0.337.0‑pre‑release kan dit worden uitgeschakeld via dev.containers.wslServiceForwarding.
  • Rancher Desktop‑compatibiliteit: Bij gebruik van Rancher Desktop in plaats van Docker Desktop veroorzaakt executeInWSL: true een WSL1‑fout. Het probleem staat nog open; Rancher‑gebruikers moeten de instelling mogelijk tijdelijk uitschakelen.
  • Onverwachte activering: issue #11005 beschrijft dat executeInWSL: true onbedoeld Dev Container‑initialisatie triggert in lokale Windows‑repositories. Dit blijft een open bug; een mogelijke workaround is de instelling alleen per werkruimte toe te passen.

Conclusie

Het opsporen van hoge IO‑belasting door VS Code Dev Container op Windows vereist een systematische aanpak: start met Process Monitor om de bron te identificeren, gebruik VS Code‑devtools en Windows Performance Analyzer voor kwantificering, en analyseer vervolgens de architectuur. Vier factoren dragen bij aan de problematiek:

  1. Frequent Docker CLI‑polling (primaire bottleneck)
  2. Port‑forwarding‑lekkage (software‑bug)
  3. Extension Host‑herconnectie‑lus (software‑bug)
  4. WSL2 cross‑filesystem IO‑vergroting (platform‑limiet)

De kernoplossing is dev.containers.executeInWSL: true, die de named‑pipe‑overhead elimineert door Docker CLI binnen WSL uit te voeren. Aanvullende maatregelen – port‑forwarding minimaliseren, Docker Desktop optimaliseren en VS Code‑instellingen verfijnen – verlichten de overige factoren. Gebruikers die dit probleem ondervinden, worden aangeraden eerst de oorzaak te bevestigen via de beschreven opsporingsstappen, vervolgens executeInWSL in te schakelen en tenslotte de aanvullende optimalisaties toe te passen.