VS Code Dev Container IO élevé : configuration détaillée de executeInWSL et analyse des causes profondes

Enregistrement complet du processus de diagnostic du problème d’IO élevé causé par l’extension VS Code Dev Container sous Windows, depuis la localisation du symptôme jusqu’à l’analyse des causes profondes, avec comme solution principale dev.containers.executeInWSL pour résoudre le goulot d’étranglement de la communication Docker CLI entre les frontières.

Sur Windows, lors de l’utilisation de l’extension Dev Container de VS Code pour le développement conteneurisé, certains utilisateurs rencontrent des ralentissements notables du système. Le Gestionnaire des tâches montre une utilisation élevée du CPU et des lectures d’IO disque par le processus Extension Host, même en l’absence d’opérations actives. Cet article consigne le processus complet de diagnostic, depuis le symptôme jusqu’à l’identification de la cause racine et la mise en œuvre de la solution principale.

Symptômes du problème

Le ralentissement du système survient après la connexion de l’extension Dev Container au conteneur. Le Gestionnaire des tâches révèle que le processus Extension Host maintient des lectures d’IO disque et une utilisation CPU élevées, même sans aucune édition ou interaction terminal. Dans les cas extrêmes, la réactivité du bureau Windows entier est affectée, le curseur de la souris présentant des saccades intermittentes.

Processus de diagnostic

Utilisation de Process Monitor pour localiser la source d’IO

Sysinternals Process Monitor est l’outil de première étape pour ce type de problème. Après le lancement de procmon, définissez le filtre sur Process Name is Code.exe ou Process Name is Extension Host afin d’observer en temps réel tous les chemins et fréquences des opérations ReadFile/WriteFile. Dans les résultats filtrés, les opérations de named pipe commençant par \\pipe\ apparaissent avec une fréquence anormalement élevée, atteignant plusieurs dizaines par seconde. Ces opérations de named pipe correspondent aux communications entre Docker CLI et le daemon Docker, indiquant que l’Extension Host invoque fréquemment Docker CLI.

Confirmation avec les outils intégrés de VS Code

En ouvrant Help > Toggle Developer Tools et en enregistrant un profil CPU dans le panneau Performance, on constate que l’Extension Host consomme beaucoup de temps dans le spawn de sous‑processus et la lecture des tubes stdout. En réglant le niveau de journalisation de “Dev Containers” sur “trace” dans le panneau Output, on voit la séquence complète des appels : docker inspect --type container, docker version --format, docker exec, docker ps, etc., exécutés de façon répétée. Les données du ticket #9194 quantifient le coût d’un appel unique : docker inspect --type container dure environ 1800 ms, docker version environ 620 ms.

Analyse système avec Windows Performance Analyzer

Pour une analyse plus profonde, utilisez wpr.exe -start GeneralProfile -filemode pour démarrer l’enregistrement ETW, reproduisez le problème, puis arrêtez l’enregistrement avec wpr.exe -stop capture.etl. Chargez le résultat dans Windows Performance Analyzer. La vue Disk I/O confirme que l’Extension Host est le principal contributeur aux lectures d’IO disque, tandis que la vue Process Life Cycle montre de nombreux sous‑processus à courte durée de vie créés et détruits de façon répétée ; ces sous‑processus sont les appels Docker CLI.

Analyse des causes profondes

Architecture de communication des processus du Dev Container

Les résultats pointent vers l’architecture de communication multi‑processus du Dev Container. Lorsqu’un utilisateur ouvre un espace de travail conteneurisé sous Windows via l’extension, une chaîne de communication multi‑processus traversant la frontière Windows/Linux est mise en place.

flowchart LR
    subgraph Windows["Windows hôte"]
        A["VS Code Client<br/>(Electron)"]
        B["Extension Host<br/>(Node.js)"]
        C["Docker CLI<br/>(sous‑processus)"]
    end

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

    subgraph Container["À l'intérieur du conteneur"]
        E["VS Code Server"]
        F["Remote Extension Host"]
        G["Processus de transfert de port<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;

Le client VS Code local (Electron) communique via IPC avec l’Extension Host local (Node.js). L’Extension Host doit établir une connexion avec le VS Code Server à l’intérieur du conteneur, connexion qui dépend de Docker CLI comme intermédiaire. Une fois le VS Code Server démarré, le Remote Extension Host prend le relais, et le processus de transfert de port fournit un tunnel TCP pour l’accès du navigateur local aux services du conteneur. Chaque maillon de cette chaîne peut amplifier l’IO.

Mécanisme de sondage de Docker CLI

L’extension Dev Containers invoque fréquemment Docker CLI pour obtenir et maintenir l’état du conteneur. Au démarrage, elle exécute successivement docker inspect --type container (récupération des métadonnées), docker version --format (vérification du daemon), docker exec (détection d’environnement) et docker ps (liste des conteneurs actifs). Ces commandes ne sont pas exécutées une seule fois, mais sont rappelées périodiquement pendant tout le cycle de vie du conteneur.

Chaque appel démarre un nouveau sous‑processus, engendrant des coûts de création, la lecture du tube stdout et l’analyse du JSON retourné. En mode par défaut, ces opérations traversent la frontière Windows/WSL, et le protocole 9P de partage de fichiers de WSL2 montre de mauvaises performances pour de nombreux petits fichiers et connexions fréquentes. Selon les recommandations de la documentation officielle de Microsoft, les accès inter‑systèmes de fichiers doivent être minimisés, mais l’architecture du Dev Container rend difficile leur évitement complet.

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

    EH->>CLI: spawn docker inspect
    CLI->>DD: requête named pipe
    DD-->>CLI: réponse JSON
    CLI-->>EH: sortie stdout
    EH->>CLI: spawn docker version
    CLI->>DD: requête named pipe
    DD-->>CLI: informations de version
    CLI-->>EH: sortie stdout
    EH->>CLI: spawn docker exec
    CLI->>DD: requête named pipe
    DD-->>CLI: résultat d'exécution
    CLI-->>EH: sortie stdout

Fuite des processus de transfert de port

Le mécanisme de transfert de port de VS Code crée un sous‑processus Node.js distinct pour chaque port transféré. Ces processus utilisent net.createConnection pour se connecter au port cible du conteneur et transfèrent les données dans les deux sens. Le problème survient lorsque le client (navigateur ou autre) ferme la connexion ; si la logique de nettoyage n’est pas exécutée rapidement, le processus de transfert reste actif au lieu de se terminer.

Selon l’analyse du ticket #5767, chaque processus de transfert fuité consomme environ 26 MiB de mémoire. Dans un environnement de développement avec de nombreux ports transférés et des accès fréquents, le nombre de processus peut passer de 2 à plusieurs dizaines en très peu de temps. Le fragment de code suivant montre le modèle central, où le gestionnaire d’événement client.on('close') détermine si le processus peut se terminer correctement.

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

Lorsque le processus docker exec se termine de façon anormale alors que le processus Node.js à l’intérieur du conteneur continue de fonctionner, ces processus orphelins ne sont pas récupérés, entraînant une croissance continue de la mémoire. Le problème a été partiellement corrigé à partir de VS Code 1.62, mais il peut encore se reproduire dans certaines conditions réseau. Il est important de noter que cette fuite n’est pas directement liée à executeInWSL; il s’agit d’un défaut du mécanisme de transfert de port lui‑même.

Boucle de reconnexion de l’Extension Host

Le ticket #6178 décrit un bug dans la logique de reconnexion de l’Extension Host lorsqu’une connexion distante est perdue : une fonction async s’appelle récursivement dans le bloc catch, provoquant une boucle CPU infinie. La pile d’appels montre la fonction se répéter dans processTicksAndRejections sans condition d’arrêt.

flowchart TD
    A["Perte de connexion"] e1@--> B["Fonction async de reconnexion"]
    B e2@--> C{"Connexion réussie ?"}
    C e3@-->|Oui| D["Retour à la normale"]
    C e4@-->|Non| E["Bloc 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;

Parallèlement, la mémoire de l’Extension Host augmente d’environ 1 MiB/minute à cause de l’accumulation de contextes non libérés dans la pile d’appels. Ce bug a été corrigé dans Remote‑Containers 0.221.0‑pre‑release, mais les utilisateurs qui n’ont pas mis à jour l’extension peuvent encore le reproduire en simulant une perte de réseau (par ex. désactivation du Wi‑Fi). Une boîte de dialogue d’avertissement n’apparaît jamais, ce qui rend le problème difficile à diagnostiquer. Il s’agit d’un bug logiciel indépendant du paramètre executeInWSL.

Amplification d’IO due au système de fichiers inter‑WSL2

Avec Docker Desktop utilisant le backend WSL2, les opérations de fichiers qui traversent la frontière Windows/WSL2 subissent une amplification d’IO. L’extension VS Code sur Windows communique via pipe avec le daemon Docker dans WSL2 ; lorsqu’un fichier du conteneur implique un chemin /mnt/c (accès au disque Windows), les opérations passent par le protocole 9P, qui est nettement plus lent que les accès natifs. Les latences d’opération peuvent être 3 à 5 fois supérieures à celles d’un chemin natif.

De plus, le ticket #9372 signale que sur les Mac ARM exécutant des conteneurs x86 via Rosetta, le masque d’affinité CPU du serveur VS Code est limité à un seul cœur, faisant que nproc renvoie 1 au lieu du nombre réel de cœurs. Bien que ce problème soit principalement macOS, il montre une incohérence dans la gestion des paramètres de planification des processus par le Dev Container, qui peut également se manifester différemment sous Windows WSL2. Le paramètre executeInWSL: true déplace l’exécution de Docker CLI à l’intérieur de WSL, réduisant la fréquence des traversées de système de fichiers, mais il ne supprime pas complètement le coût lorsqu’un conteneur accède à /mnt/c.

Solutions

Simplifier la configuration de transfert de ports

Réduire la liste forwardPorts dans devcontainer.json aux seuls ports réellement nécessaires diminue considérablement le nombre de processus de transfert et le risque de fuite décrit dans le ticket #5767. Fermer les fenêtres Dev Container inutilisées libère immédiatement les ressources d’Extension Host et de transfert de ports associées.

Optimisation de Docker Desktop

Dans Settings > Resources de Docker Desktop, ajustez correctement les allocations CPU et mémoire afin d’éviter que le daemon Docker ne déclenche fréquemment du garbage collection ou du swap. Utilisez la version la plus récente, car chaque mise à jour améliore les performances du backend WSL2. Pour les charges très exigeantes, envisagez d’installer Docker Engine directement dans WSL2 et de contourner la couche de virtualisation de Docker Desktop, éliminant ainsi le surcoût de la frontière Windows/WSL.

Optimisation de la configuration VS Code

Désactivez les extensions inutiles afin de réduire la charge de l’Extension Host, en particulier celles qui s’exécutent dans le conteneur et surveillent les fichiers (TypeScript language service, ESLint, etc.). Dans settings.json, définissez files.watcherExclude pour exclure node_modules, .git, dist et autres grands répertoires, limitant ainsi l’IO générée par la surveillance du système de fichiers. Mettre extensions.autoUpdate: false évite les mises à jour d’extensions en arrière‑plan qui déclencheraient des opérations réseau et disque supplémentaires.

Alternatives

Si les mesures ci‑dessus ne suffisent pas, envisagez d’utiliser l’extension Remote‑SSH pour se connecter directement à WSL2 et gérer les conteneurs avec Docker CLI à l’intérieur de WSL. Cette approche transforme les appels Docker CLI en communications locales WSL, contournant le mécanisme de sondage du Dev Container. Une autre option consiste à gérer le cycle de vie des conteneurs avec Docker Compose (docker compose up -d) et à n’utiliser Remote‑SSH que pour le développement, éliminant ainsi le mécanisme de sondage du Dev Container.

Activation de executeInWSL (solution principale)

Les mesures précédentes atténuent partiellement le problème d’IO élevé, mais aucune ne traite la cause fondamentale : le coût de communication du named pipe entre Docker CLI et le daemon lorsqu’ils traversent la frontière Windows/WSL. Le paramètre dev.containers.executeInWSL cible directement cette cause.

Comme l’explique clairement le contributeur chrmarti dans le ticket #9194, ce paramètre détermine si la commande docker s’exécute du côté Windows ou à l’intérieur de WSL. Une fois réglé sur true, tous les appels Docker CLI (docker inspect, docker version, docker exec, docker ps, etc.) s’exécutent dans WSL, communiquant avec le daemon via un socket Unix, contournant ainsi le named pipe et la conversion 9P.

Ajoutez simplement la configuration suivante dans settings.json :

{
  "dev.containers.executeInWSL": true
}

Pourquoi cela améliore tant les performances d’IO

En mode par défaut (executeInWSL: false ou non défini) :

  • L’Extension Host s’exécute sous Windows.
  • Chaque interaction avec le daemon Docker lance docker.exe sous Windows.
  • docker.exe utilise un named pipe (\\pipe\\) pour communiquer avec le daemon Docker dans WSL2.
  • Cette chaîne implique la création de processus Windows, le passage du pipe inter‑système et la lecture du tube stdout, chacun ajoutant latence et surcharge d’IO.

En mode optimisé (executeInWSL: true) :

  • L’Extension Host reste sous Windows, mais invoque wsl -d <distro> -e docker pour exécuter Docker CLI directement dans WSL.
  • Docker CLI s’exécute nativement dans WSL et utilise un socket Unix (IPC local) pour parler au daemon Docker.
  • Toute la communication reste dans l’espace noyau Linux, éliminant le named pipe et la conversion 9P.
flowchart TB
    subgraph Default["Mode par défaut (executeInWSL: false)"]
        direction LR
        A1["Extension Host<br/>(Windows)"]
        A2["docker.exe<br/>(processus Windows)"]
        A3["named pipe<br/>(\\\\pipe\\\\)"]
        A4["Docker Daemon<br/>(WSL2)"]
        A1 e1@--> A2
        A2 e2@--> A3
        A3 e3@--> A4
    end

    subgraph Optimized["Mode optimisé (executeInWSL: true)"]
        direction LR
        B1["Extension Host<br/>(Windows)"]
        B2["wsl -e docker<br/>(interne 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;

Les mesures quantitatives recueillies lors du diagnostic montrent : en mode par défaut, un appel docker inspect --type container dure ~1800 ms, docker version ~620 ms, principalement à cause du named pipe et de la création de processus. En activant executeInWSL: true, les appels passent à l’échelle de quelques millisecondes, et l’effet cumulé sur la performance globale est très significatif.

Problèmes connus et précautions

  • Problème d’auto‑démarrage de Docker Desktop : le ticket #9695 signale que Docker Desktop ne démarre pas automatiquement avec executeInWSL: true. Ce bug a été corrigé dans Dev Containers 0.353.0‑pre‑release; les utilisateurs de versions plus récentes ne devraient plus le rencontrer.
  • Transfert de services WSL : même avec executeInWSL: false, l’extension tente de se connecter à WSL pour le transfert d’affichage/ssh‑agent/gpg‑agent. Ce comportement est contrôlable depuis la version 0.337.0‑pre‑release via le paramètre dev.containers.wslServiceForwarding.
  • Compatibilité Rancher Desktop : le ticket #10722 indique que executeInWSL: true déclenche une erreur WSL1 lorsqu’on utilise Rancher Desktop à la place de Docker Desktop. Le problème reste ouvert.
  • Activation accidentelle : le ticket #11005 décrit une activation involontaire du processus Dev Container dans un dépôt Windows local. Les utilisateurs concernés peuvent restreindre le paramètre à des espaces de travail spécifiques plutôt qu’à la configuration globale.

Conclusion

Diagnostiquer un problème d’IO élevé avec VS Code Dev Container sous Windows nécessite de partir du symptôme, de localiser la source avec Process Monitor, d’affiner l’analyse via les outils intégrés de VS Code et Windows Performance Analyzer, puis d’identifier les causes : sondage fréquent de Docker CLI, fuites de processus de transfert de port, boucle de reconnexion de l’Extension Host et amplification d’IO liée à WSL2.

Parmi les solutions, dev.containers.executeInWSL: true constitue la mesure la plus impactante, éliminant le coût du named pipe entre Windows et WSL. Les autres actions (simplification des transferts de ports, optimisation de Docker Desktop, réglages de VS Code) complètent cette approche en atténuant les facteurs secondaires. Pour les utilisateurs affectés, il est recommandé de suivre le processus de diagnostic présenté, d’activer d’abord executeInWSL: true, puis d’appliquer les optimisations complémentaires selon le contexte.