VS Code Dev Container IO excessif : Configuration executeInWSL détaillée et analyse de la cause racine
Lors de l'utilisation de l'extension Dev Container de VS Code pour le développement en conteneur sur Windows, certains utilisateurs rencontrent des ralentissements système évidents. Dans le Gestionnaire de tâches, on peut observer que le processus Extension Host présente une utilisation CPU et des IO de lecture disque constamment élevées, qui ne diminuent pas même sans opération active. Ce document retrace le processus complet d'investigation, depuis l'observation du problème jusqu'à l'identification de la cause racine et la découverte de la solution principale.
## Phénomène observé
Le ralentissement du système se produit après la connexion de l'extension Dev Container au conteneur. Via le Gestionnaire de tâches, on peut observer que les IO de lecture disque et le taux d'utilisation CPU du processus `Extension Host` restent élevés, même lorsque l'utilisateur n'effectue aucune opération d'édition ou de terminal. Dans les cas extrêmes, la réactivité de l'ensemble du bureau Windows est affectée, avec des saccades intermittentes du curseur de la souris.
## Processus d'investigation
### Utilisation de Process Monitor pour localiser la source des IO
[Sysinternals Process Monitor](https://learn.microsoft.com/en-us/sysinternals/downloads/procmon) est le premier outil pour investiguer ce type de problème. Après avoir lancé procmon, configurez les filtres sur `Process Name is Code.exe` ou `Process Name is Extension Host` pour observer en temps réel toutes les opérations ReadFile/WriteFile et leur fréquence. Dans les résultats filtrés, les opérations sur les named pipes commençant par `\\pipe\` apparaissent avec une fréquence anormale, pouvant atteindre plusieurs dizaines par seconde. Ces opérations sur named pipes correspondent à la communication entre Docker CLI et Docker daemon, indiquant que l'Extension Host appelle fréquemment Docker CLI.
### Utilisation des outils intégrés de VS Code
Via `Help > Toggle Developer Tools`, ouvrez les Chromium DevTools. Dans le panneau Performance, enregistrez un profil CPU : vous pouvez voir que l'Extension Host passe beaucoup de temps à spawner des processus enfants et à lire les tubes stdout. En définissant le niveau de journalisation "Dev Containers" sur "trace" dans le panneau Output, vous pouvez voir la séquence complète des appels de commandes : `docker inspect --type container`, `docker version --format`, `docker exec`, `docker exec` et d'autres commandes sont exécutées de manière répétée. Selon les données de journal de l'issue #9194, le coût d'un seul appel peut être quantifié : `docker inspect --type container` prend environ 1800ms, et `docker version` prend environ 620ms.
### Utilisation de Windows Performance Analyzer pour analyser les IO au niveau système
Pour une analyse plus approfondie, utilisez `wpr.exe -start GeneralProfile -filemode` pour démarrer l'enregistrement ETW, reproduisez le problème, puis utilisez `wpr.exe -stop capture.etl` pour arrêter l'enregistrement. Chargez les résultats dans Windows Performance Analyzer. La vue Disk I/O confirme que l'Extension Host est le principal contributeur aux IO de lecture disque. La vue Process Life Cycle montre un grand nombre de processus enfants à courte durée de vie créés et détruits de manière répétée : ces processus enfants sont précisément les instances d'appel de Docker CLI.
## Analyse de la cause racine
### Architecture de communication des processus Dev Container
Les résultats de l'investigation pointent vers l'architecture de communication multi-processus de Dev Container. Lorsqu'un utilisateur ouvre un espace de travail conteneur via l'extension Dev Container sur Windows, il lance en réalité une chaîne de communication multi-processus traversant les frontières Windows et Linux.
```mermaid
flowchart LR
subgraph Windows["Hôte Windows"]
A["Client VS Code<br/>(Electron)"]
B["Extension Host<br/>(Node.js)"]
C["Docker CLI<br/>(Processus enfant)"]
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 redirection de ports<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 avec l’Extension Host local (Node.js) via IPC. L’Extension Host doit établir une connexion avec le VS Code Server à l’intérieur du conteneur, et cette connexion repose sur Docker CLI comme intermédiaire. Une fois le VS Code Server démarré à l’intérieur du conteneur, le Remote Extension Host prend en charge l’exécution des extensions, et le processus de redirection de ports fournit un tunnel TCP pour que le navigateur local accède aux services du conteneur. Chaque maillon de cette chaîne peut devenir une source d’amplification des IO.
Mécanisme de polling Docker CLI
L’extension Dev Containers appelle fréquemment les commandes Docker CLI pour obtenir et maintenir l’état du conteneur. Pendant la phase de démarrage du conteneur, l’extension exécute successivement docker inspect --type container pour obtenir les métadonnées du conteneur, docker version --format pour vérifier la disponibilité du Docker daemon, docker exec pour exécuter des commandes de détection d’environnement à l’intérieur du conteneur, et docker ps pour lister les conteneurs en cours d’exécution. Ces commandes ne sont pas exécutées une seule fois au démarrage, mais sont appelées de manière répétée à une certaine fréquence pendant tout le cycle de vie du conteneur.
Chaque appel de Docker CLI démarre un nouveau processus enfant, impliquant une série d’opérations IO comme la création du processus, la lecture du tube stdout, et l’analyse du résultat JSON. En mode par défaut, toutes ces données doivent traverser la frontière Windows/WSL, et le protocole de partage de fichiers 9P de WSL2 présente de mauvaises performances lors de la gestion d’un grand nombre de petites opérations IO et de connexions courtes fréquentes. Selon la documentation officielle Microsoft, les opérations de fichiers cross-filesystem doivent être évitées autant que possible, mais l’architecture de Dev Container rend difficile l’éviter complètement de cette surcharge.
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 tube stdout
EH->>CLI: spawn docker version
CLI->>DD: Requête named pipe
DD-->>CLI: Informations de version
CLI-->>EH: Sortie tube stdout
EH->>CLI: spawn docker exec
CLI->>DD: Requête named pipe
DD-->>CLI: Résultat d'exécution
CLI-->>EH: Sortie tube stdoutFuite de connexions de redirection de ports
Le mécanisme de redirection de ports de VS Code crée un processus enfant Node.js indépendant pour chaque port redirigé. Ces processus se connectent via net.createConnection au port cible à l’intérieur du conteneur, et redirigent bidirectionnellement les données entre le port local et le port du conteneur. Le problème est que lorsque le navigateur ou un autre client accède à un port redirigé puis se déconnecte, si la logique de nettoyage n’est pas exécutée à temps, ces processus de redirection persistent au lieu de se terminer normalement.
Selon l’analyse de l’issue #5767, chaque processus de redirection de port fuites occupe environ 26 MiB de mémoire. Dans un environnement de développement configuré avec plusieurs ports redirigés et des accès fréquents, le nombre de processus peut passer de 2 normaux à des dizaines en peu de temps. L’extrait de code suivant montre le modèle central du processus de redirection de ports, où l’événement client.on('close') est crucial pour la terminaison normale du processus.
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 de Docker CLI se termine de manière anormale mais que le processus Node.js à l’intérieur du conteneur continue de tourner, ces processus orphelins ne peuvent pas être回收 normalement, entraînant une croissance continue de la mémoire. Ce problème a été partiellement corrigé après la version 1.62 de VS Code, mais peut toujours se reproduire dans certaines conditions réseau. Il est important de noter que la fuite de redirection de ports n’est pas directement liée à executeInWSL : c’est un défaut logiciel propre au mécanisme de redirection de ports de VS Code.
Boucle de reconnexion de l’Extension Host
Selon l’issue #6178, lorsque la connexion au conteneur distant est perdue pour une raison réseau ou autre, il existe un bug dans la logique de reconnexion de l’Extension Host : une fonction async s’appelle récursivement dans le bloc catch, provoquant une consommation CPU à vide. La pile d’appels montre cette fonction tourner en boucle dans processTicksAndRejections, sans condition de sortie.
flowchart TD
A["Connexion perdue"] e1@--> B["Fonction de reconnexion async"]
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 croît à environ 1 Mo/minute car chaque appel récursif accumule des contextes non libérés sur la pile d’appels. Ce problème a été corrigé dans la version Remote-Containers 0.221.0-pre-release, mais pour les utilisateurs qui n’ont pas mis à jour l’extension à temps, il suffit de simuler une coupure réseau (par exemple désactiver le WiFi) pour déclencher ce problème, et le dialogue d提示 de déconnexion ne s’affichera jamais. C’est un bug logiciel indépendant, non lié à la configuration executeInWSL, mais il aggrave le ralentissement perçu par l’utilisateur.
Amplification des IO cross-filesystem WSL2
Lorsque Docker Desktop avec le backend WSL2 est utilisé sur Windows, il existe un problème de performance IO cross-filesystem inhérent. L’extension VS Code côté Windows communique avec le Docker daemon dans WSL2 via des pipes, et si les opérations sur le système de fichiers à l’intérieur du conteneur impliquent des chemins /mnt/c (c’est-à-dire l’accès aux disques Windows), elles nécessitent la conversion via le protocole de partage de fichiers 9P. Le protocole 9P de WSL2 présente des performances significativement inférieures aux systèmes de fichiers natifs pour les scénarios avec un grand nombre de petites opérations IO, avec une latence par opération pouvant atteindre 3 à 5 fois celle des chemins natifs.
De plus, selon le rapport de l’issue #9372, sur les Mac ARM exécutant des conteneurs x86 via Rosetta, le masque CPU affinity du processus VS Code Server est anormalement défini pour utiliser un seul cœur uniquement,,使得 nproc retourne 1 au lieu du nombre réel de cœurs physiques. Bien que ce problème apparaisse principalement sur macOS, il révèle une incohérence dans le contrôle de Dev Container sur les paramètres d’ordonnancement des processus à travers les plates-formes. Des problèmes similaires peuvent également apparaître sous différentes formes dans l’environnement WSL2 de Windows. executeInWSL: true déplace l’exécution de Docker CLI à l’intérieur de WSL, réduisant la fréquence des IO cross-filesystem, mais ne peut pas éliminer complètement la surcharge 9P générée lors de l’accès aux chemins /mnt/c à l’intérieur du conteneur.
Solutions
###精简 de la configuration des ports redirigés
Dans devcontainer.json, la精简 de la configuration forwardPorts pour ne garder que les ports réellement nécessaires peut réduire considérablement le nombre de processus de redirection de ports, diminuant ainsi le risque de fuite de processus décrit dans l’issue #5767. Fermer les fenêtres Dev Container inutilisées libère également immédiatement les ressources correspondantes de l’Extension Host et de redirection de ports.
Optimisation de Docker Desktop
Dans le panneau Settings > Resources de Docker Desktop, ajustez适当 l’allocation CPU et mémoire pour éviter que le Docker daemon ne effectue fréquemment des opérations de garbage collection ou de swap par manque de ressources. Assurez-vous d’utiliser la dernière version de Docker Desktop, car les performances du backend WSL2 sont améliorées à chaque version. Pour les scénarios exigeants en performances, envisagez d’installer directement Docker Engine à l’intérieur de WSL2 pour contourner la couche de virtualisation de Docker Desktop, éliminant ainsi davantage la surcharge supplémentaire de la frontière Windows/WSL.
Optimisation de la configuration VS Code
Désactiver les extensions inutiles peut réduire la charge de l’Extension Host, en particulier celles qui s’exécutent dans le conteneur distant et ont des fonctionnalités de surveillance de fichiers (comme le service de langage TypeScript, ESLint, etc.). Dans settings.json, définissez files.watcherExclude pour exclure les grands répertoires comme node_modules, .git, dist, ce qui réduit les IO générées par la surveillance du système de fichiers. Définir extensions.autoUpdate: false évite les opérations réseau et disque supplémentaires déclenchées par les mises à jour d’extensions en arrière-plan dans l’environnement conteneur.
Solutions alternatives
Si les mesures ci-dessus ne répondent toujours pas aux besoins de performances, envisagez d’utiliser l’extension VS Code Remote-SSH pour vous connecter à WSL2 et utiliser directement Docker CLI à l’intérieur de WSL2 pour gérer les conteneurs. Cette approche transforme les appels Docker CLI d’une communication cross-border Windows/WSL en communication locale à l’intérieur de WSL2. Une autre approche consiste à utiliser Docker Compose pour gérer le cycle de vie des conteneurs : démarrez les services avec docker compose up -d, puis utilisez uniquement Remote-SSH pour vous connecter au conteneur pour le développement, contournant complètement le mécanisme de polling de l’extension Dev Container.
Activation de executeInWSL (Solution principale)
Les mesures ci-dessus peuvent atténuer le problème d’IO excessif à des degrés divers, mais elles ne font que réduire la fréquence des IO (精简 des ports redirigés) ou optimiser l’allocation des ressources (optimisation Docker Desktop), sans toucher à la cause racine : la surcharge de communication named pipe lors des appels Docker CLI traversant la frontière Windows/WSL. dev.containers.executeInWSL est précisément la solution directe针对 cette cause racine.
Selon l’explication claire de chrmarti, membre de l’équipe VS Code, dans l’issue #9194, ce paramètre détermine si la commande docker s’exécute côté Windows ou à l’intérieur de WSL. Après l’avoir défini sur true, tous les appels Docker CLI (y compris docker inspect, docker version, docker exec, docker ps, etc.) seront exécutés à l’intérieur de WSL, communiquant directement avec Docker daemon via Unix socket, contournant ainsi la surcharge de conversion named pipe et protocole 9P à la frontière Windows/WSL.
Ajoutez la configuration suivante dans settings.json pour l’activer :
{
"dev.containers.executeInWSL": true
}
Pour comprendre pourquoi cette configuration améliore significativement les performances IO, il faut comparer la différence de chemin de communication entre les deux modes. En mode par défaut (executeInWSL: false ou non défini), l’Extension Host VS Code s’exécute sur Windows. Chaque fois qu’il doit interagir avec Docker daemon, il démarre un processus enfant docker.exe sur Windows via spawn. Ce processus enfant communique avec Docker daemon via des named pipes (chemins commençant par \\pipe\) traversant la frontière Windows/WSL. Ce chemin implique trois étapes : création de processus Windows, IO named pipe cross-border, et renvoi de données via le tube stdout, chaque étape introduisant une latence et une surcharge IO supplémentaires. Lorsque executeInWSL: true, l’Extension Host exécute Docker CLI à l’intérieur de WSL via wsl -d <distro> -e docker. Docker CLI s’exécute nativement à l’intérieur de WSL et communique avec Docker daemon via Unix socket (IPC local) dans la même instance WSL. Ce chemin se déroule entièrement dans l’espace kernel Linux, évitant la surcharge cross-border des named pipes.
flowchart TB
subgraph Default["Mode par défaut (executeInWSL: false)"]
direction LR
A1["Extension Host<br/>(Windows)"]
A2["docker.exe<br/>(Processus enfant 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/>(À l'intérieur 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;Les données quantifiées de la phase d’investigation permettent de vérifier cette amélioration : en mode par défaut, un seul appel docker inspect --type container prend environ 1800ms, et l’appel docker version prend environ 620ms, ces latences provenant principalement de la communication named pipe cross-border Windows/WSL et de la surcharge de création de processus. Après l’activation de executeInWSL: true, Docker CLI communique avec le daemon via Unix socket à l’intérieur de WSL, la latence d’un seul appel peut chuter au niveau milliseconde, et l’amélioration de l’effet cumulatif est particulièrement significative.
Problèmes connus et précautions
Bien que dev.containers.executeInWSL soit efficace pour améliorer les performances IO, les problèmes connus suivants doivent être pris en compte lors de son utilisation.
Problème de démarrage automatique de Docker Desktop : L’issue #9695 rapporte qu’avec executeInWSL: true, Docker Desktop ne démarre pas automatiquement. Ce problème a été corrigé dans la version Dev Containers 0.353.0-pre-release, et les utilisateurs de versions plus récentes ne devraient plus le rencontrer.
Redirection de services WSL : L’issue #9194 rapporte que même avec executeInWSL défini sur false, l’extension essaie toujours de se connecter à WSL (pour la redirection display/ssh-agent/gpg-agent). Ce comportement a été contrôlé dans la version 0.337.0-pre-release via l’ajout du paramètre dev.containers.wslServiceForwarding, permettant aux utilisateurs de désactiver indépendamment la redirection des services WSL.
Compatibilité avec Rancher Desktop : L’issue #10722 rapporte que lors de l’utilisation de Rancher Desktop au lieu de Docker Desktop, executeInWSL: true déclenche une erreur WSL1. Ce problème est actuellement toujours ouvert, et les utilisateurs de Rancher Desktop peuvent avoir besoin de désactiver temporairement ce paramètre.
Activation inattendue : L’issue #11005 rapporte que executeInWSL: true déclenche accidentellement le processus d’initialisation Dev Container dans les dépôts Windows locaux. Ce problème est également ouvert, et les utilisateurs concernés peuvent envisager de limiter ce paramètre à des espaces de travail spécifiques plutôt que de le configurer globalement.
Résumé
L’investigation du problème d’IO excessif de VS Code Dev Container sur Windows nécessite de partir du phénomène et de localiser progressivement la cause racine. Via Process Monitor, on peut confirmer que la source principale des IO est la communication named pipe de Docker CLI. Via les outils intégrés de VS Code et Windows Performance Analyzer, on peut quantifier davantage la fréquence d’appel et la latence. L’analyse de la cause racine révèle quatre facteurs superposés : le polling cross-border fréquent de Docker CLI est le goulot d’étranglement de performances principal, la fuite de connexions des processus de redirection de ports et la boucle de reconnexion de l’Extension Host sont des bugs logiciels confirmés, et l’amplification des IO cross-filesystem WSL2 est une limitation inhérente au niveau de la plate-forme.
Parmi les solutions, dev.containers.executeInWSL: true est la mesure la plus importante : elle élimine directement la surcharge de communication named pipe de Docker CLI traversant la frontière Windows/WSL, déplaçant l’exécution des appels CLI à haute fréquence de Windows vers l’intérieur de WSL, complétant l’IPC local via Unix socket. Les autres solutions (精简 des ports redirigés, optimisation Docker Desktop, optimisation de la configuration VS Code) servent de mesures auxiliaires pour atténuer les impacts des autres facteurs à des degrés divers. Pour les utilisateurs gênés par ce problème, il est recommandé de suivre le processus d’investigation de cet article pour confirmer la cause racine, puis d’activer en priorité executeInWSL: true, et de choisir des stratégies d’optimisation auxiliaires appropriées en fonction du scénario réel.