VS Code Dev Container IO مرتفع: شرح إعداد executeInWSL وتحليل السبب الجذري

توثيق عملية التحقيق الكاملة لمشكلة ارتفاع IO التي يسببها امتداد VS Code Dev Container على Windows، من تحديد الظاهرة إلى تحليل السبب الجذري، مع حل مركزي يعتمد على dev.containers.executeInWSL لمعالجة عنق الزجاجة في التواصل بين Docker CLI والحدود.

عند استخدام امتداد Dev Container في VS Code لتطوير حاويات على Windows، قد يواجه بعض المستخدمين بطء واضح في النظام. يمكن ملاحظة ارتفاع مستمر في استهلاك CPU وقراءة IO للقرص من عملية Extension Host في مدير المهام، حتى دون أي تفاعل نشط. يوضح هذا المقال عملية التحقيق الكاملة بدءًا من الظاهرة، مرورًا بتحديد السبب الجذري، وصولاً إلى الحل الأساسي.

الظاهرة

يظهر بطء النظام بعد اتصال امتداد Dev Container بالحاوية. من خلال مدير المهام يمكن ملاحظة أن عملية Extension Host تستهلك قراءات IO للقرص وCPU بشكل مستمر، حتى عندما لا يقوم المستخدم بأي تحرير أو تشغيل طرفية. في الحالات القصوى، يتأثر استجابة سطح مكتب Windows بالكامل، وتظهر تأخيرات متقطعة في مؤشر الفأرة.

عملية التحقيق

تحديد مصدر IO باستخدام Process Monitor

Sysinternals Process Monitor هو الأداة الأولى للتحقق من هذه المشكلة. بعد تشغيل procmon، اضبط الفلتر على Process Name is Code.exe أو Process Name is Extension Host لتتبع جميع عمليات ReadFile/WriteFile والمسارات والتردد. في النتائج، تظهر عمليات الـ named pipe التي تبدأ بـ \\pipe\ بتردد غير طبيعي، تصل إلى عدة عشرات في الثانية. هذه العمليات تمثل التواصل بين Docker CLI وDocker daemon، مما يشير إلى أن Extension Host يستدعي Docker CLI بشكل متكرر.

التحقق باستخدام أدوات VS Code المدمجة

من خلال Help > Toggle Developer Tools افتح Chromium DevTools، وسجل ملف الأداء في لوحة Performance. ستظهر أن معظم وقت Extension Host يُقضى في إنشاء عمليات فرعية وقراءة أنابيب stdout. ضبط مستوى سجل “Dev Containers” إلى “trace” في لوحة Output يُظهر سلسلة الأوامر الكاملة: docker inspect --type container، docker version --format، docker exec، docker ps وغيرها تُنفّذ بشكل متكرر. من بيانات سجل issue #9194 يمكن قياس تكلفة كل استدعاء: docker inspect --type container يستغرق حوالي 1800 ms، وdocker version حوالي 620 ms.

تحليل IO على مستوى النظام باستخدام Windows Performance Analyzer

لتحليل أعمق، استخدم الأمر wpr.exe -start GeneralProfile -filemode لبدء تسجيل ETW، ثم أعد إنتاج المشكلة وأوقف التسجيل بـ wpr.exe -stop capture.etl. حمّل النتائج في Windows Performance Analyzer. يوضح عرض Disk I/O أن Extension Host هو المساهم الرئيسي في قراءة IO للقرص، بينما يُظهر عرض Process Life Cycle عددًا كبيرًا من العمليات الفرعية قصيرة العمر التي تُنشأ وتُدمّر باستمرار، وهذه العمليات هي استدعاءات Docker CLI.

تحليل السبب الجذري

بنية التواصل المتعدد العمليات في Dev Container

نتائج التحقيق تشير إلى بنية التواصل المتعدد العمليات في Dev Container. عندما يفتح المستخدم مساحة عمل حاوية عبر امتداد Dev Container على Windows، يتم إنشاء سلسلة من عمليات التواصل عبر حدود Windows وLinux.

flowchart LR
    subgraph Windows["Windows宿主机"]
        A["VS Code Client<br/>(Electron)"]
        B["Extension Host<br/>(Node.js)"]
        C["Docker CLI<br/>(عملية فرعية)"]
    end

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

    subgraph Container["داخل الحاوية"]
        E["VS Code Server"]
        F["Remote Extension Host"]
        G["عملية تحويل المنفذ<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;

عميل VS Code المحلي (Electron) يتواصل عبر IPC مع Extension Host المحلي (Node.js). يحتاج Extension Host إلى الاتصال بـ VS Code Server داخل الحاوية، وهذا الاتصال يعتمد على Docker CLI كوسيط. بعد تشغيل VS Code Server داخل الحاوية، يتولى Remote Extension Host تنفيذ الامتدادات، وتوفر عملية تحويل المنفذ نفق TCP للوصول المحلي إلى خدمات الحاوية. كل حلقة في هذه السلسلة يمكن أن تكون مصدرًا لتضخم IO.

آلية استدعاء Docker CLI المتكررة

يمر امتداد Dev Containers باستدعاء أوامر Docker CLI بشكل متكرر للحصول على حالة الحاوية والحفاظ عليها. أثناء مرحلة تشغيل الحاوية، ينفّذ الامتداد بالتتابع docker inspect --type container لجلب بيانات التعريف، ثم docker version --format للتحقق من توفر Docker daemon، ثم docker exec لتشغيل أوامر استكشاف داخل الحاوية، وأخيرًا docker ps لسرد الحاويات العاملة. هذه الأوامر لا تُنفّذ مرة واحدة فقط عند الإقلاع، بل تُستدعى بشكل دوري طوال عمر الحاوية.

كل استدعاء لـ Docker CLI يخلق عملية فرعية جديدة، مما يضيف تكلفة إنشاء العملية، قراءة أنابيب stdout، وتحليل نتائج JSON—كلها عمليات IO. في الوضع الافتراضي، يجب أن تعبر هذه البيانات حدود Windows/WSL، وبروتوكول مشاركة الملفات 9P في WSL2 يعاني من أداء ضعيف عند التعامل مع عدد كبير من عمليات IO الصغيرة والاتصالات المتكررة. وفقًا لتوجيهات Microsoft الرسمية، يجب تجنّب عمليات النظام عبر حدود الملفات قدر الإمكان، لكن تصميم Dev Container يجعل ذلك صعبًا.

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
    DD-->>CLI: استجابة JSON
    CLI-->>EH: إخراج stdout
    EH->>CLI: spawn docker version
    CLI->>DD: طلب named pipe
    DD-->>CLI: معلومات الإصدار
    CLI-->>EH: إخراج stdout
    EH->>CLI: spawn docker exec
    CLI->>DD: طلب named pipe
    DD-->>CLI: نتيجة التنفيذ
    CLI-->>EH: إخراج stdout

تسرب عمليات تحويل المنفذ

آلية تحويل المنفذ في VS Code تنشئ عملية Node.js فرعية لكل منفذ محول. تتصل هذه العمليات عبر net.createConnection بالمنفذ المستهدف داخل الحاوية، وتعيد توجيه البيانات بين المنفذ المحلي والبعيد. المشكلة تظهر عندما يغلق المتصفح أو عميل آخر الاتصال دون تنظيف العملية؛ تبقى عملية التحويل نشطة بدلاً من الإغلاق.

وفقًا لتحليل microsoft/vscode-remote-release#5767، يستهلك كل عملية تحويل منفذ مسربة حوالي 26 MiB من الذاكرة. في بيئات تطوير تحتوي على عدة منافذ محولة وتُستخدم بشكل متكرر، يمكن أن يرتفع عدد العمليات من 2 إلى عشرات خلال دقائق. المقتطف التالي يوضح نمط عملية التحويل، حيث يُعد التعامل مع حدث client.on('close') المفتاح لإنهاء العملية بشكل صحيح.

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

عندما تنتهي عملية docker exec بشكل غير متوقع وتظل عملية Node.js داخل الحاوية تعمل، لا يمكن جمع هذه العمليات اليتيمة، مما يؤدي إلى زيادة الذاكرة. تم إصلاح جزء من هذه المشكلة في VS Code 1.62، لكن تحت ظروف شبكة معينة قد تظهر مرة أخرى. تجدر الإشارة إلى أن تسرب تحويل المنفذ غير مرتبط مباشرةً بـ executeInWSL؛ فهو عيب في آلية تحويل المنفذ نفسها.

حلقة إعادة الاتصال في Extension Host

وفقًا لتوثيق microsoft/vscode-remote-release#6178، عندما تُفقد الاتصال بالحاوية بسبب انقطاع الشبكة أو غيره، يحتوي منطق إعادة الاتصال في Extension Host على خطأ: دالة async تستدعي نفسها داخل كتلة catch، مما يسبب دورانًا لا نهائيًا للـ CPU. تُظهر مكدس الاستدعاءات أن الدالة تتكرر داخل processTicksAndRejections دون شرط خروج.

flowchart TD
    A["فقدان الاتصال"] e1@--> B["دالة إعادة الاتصال async"]
    B e2@--> C{"هل تم الاتصال بنجاح؟"}
    C e3@-->|نعم| D["استعادة الوضع الطبيعي"]
    C e4@-->|لا| E["كتلة 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;

في الوقت نفسه، يزداد استهلاك الذاكرة في Extension Host بمعدل ~1 MiB/دقيقة بسبب تراكم السياقات غير المُحررة في كل استدعاء متكرر. تم إصلاح هذا الخطأ في نسخة Remote‑Containers 0.221.0‑pre‑release، لكن المستخدمين الذين لم يُحدّثوا الامتداد قد يواجهون المشكلة عند محاكاة انقطاع الشبكة (مثل إيقاف Wi‑Fi)، حيث لا يظهر مربع حوار إشعار الانقطاع. هذا عيب مستقل لا علاقة له بإعداد executeInWSL، لكنه يزيد من الإحساس بالبطء.

تضخم IO عبر نظام ملفات WSL2

عند استخدام Docker Desktop مع خلفية WSL2 على Windows، توجد مشكلة أساسية في أداء IO عبر أنظمة الملفات. يتواصل امتداد VS Code مع Docker daemon داخل WSL2 عبر pipe؛ إذا كان مسار الملف داخل الحاوية يشير إلى /mnt/c (أي إلى قرص Windows)، فإن العملية تمر عبر بروتوكول مشاركة الملفات 9P. في سيناريوهات الوصول إلى عدد كبير من الملفات الصغيرة، يكون أداء 9P أبطأ 3‑5 مرات مقارنةً بالوصول المحلي.

بالإضافة إلى ذلك، وفقًا لتقرير microsoft/vscode-remote-release#9372، عند تشغيل حاويات x86 على أجهزة Mac ARM عبر Rosetta، تُعيّن عملية VS Code Server قناع affinity للمعالج إلى نواة واحدة فقط، مما يجعل nproc يُعيد 1 بدلاً من عدد الأنوية الفعلية. رغم أن هذه المشكلة تقتصر على macOS، إلا أنها تُظهر عدم اتساق التحكم في معلمات جدولة العمليات عبر المنصات، وقد تظهر بأشكال مختلفة على Windows/WSL2. ضبط executeInWSL: true ينقل تنفيذ Docker CLI إلى داخل WSL، مما يقلل من تكرار عمليات IO عبر نظام الملفات، لكنه لا يلغي تمامًا تكلفة الوصول إلى /mnt/c.

الحلول

تبسيط إعدادات تحويل المنفذ

قلل من قائمة forwardPorts في devcontainer.json لتشمل فقط المنافذ الضرورية. سيؤدي ذلك إلى تقليل عدد عمليات تحويل المنفذ وبالتالي خفض خطر تسرب العمليات المذكور في issue #5767. إغلاق نوافذ Dev Container غير المستخدمة يحرّر موارد Extension Host وعمليات التحويل فورًا.

تحسين Docker Desktop

في لوحة Settings > Resources داخل Docker Desktop، اضبط تخصيصات CPU والذاكرة لتجنب عمليات جمع القمامة المتكررة أو الـ swap داخل Docker daemon. احرص على استخدام أحدث نسخة من Docker Desktop، حيث تُحسّن كل نسخة أداء الخلفية WSL2. للسيناريوهات ذات المتطلبات العالية، يمكن تثبيت Docker Engine مباشرة داخل WSL2 وتجاوز طبقة الافتراضية في Docker Desktop، مما يقلل من النفقات الإضافية على حدود Windows/WSL.

تحسين إعدادات VS Code

عطّل الامتدادات غير الضرورية لتقليل حمل Extension Host، خاصةً تلك التي تعمل داخل الحاوية وتراقب الملفات (مثل TypeScript language service، ESLint). أضف قواعد إلى files.watcherExclude في settings.json لاستبعاد node_modules، .git، dist وغيرها من الأدلة الكبيرة، مما يقلل من IO الناتج عن مراقبة النظام للملفات. ضبط extensions.autoUpdate: false يمنع تحديثات الخلفية للامتدادات داخل الحاوية التي قد تُحدث عمليات IO إضافية.

بدائل

إذا لم تُرضِ الحلول السابقة متطلبات الأداء، يمكن استخدام امتداد VS Code Remote‑SSH للاتصال مباشرةً بـ WSL2 وإدارة الحاويات عبر Docker CLI داخل WSL. بهذه الطريقة يتحول استدعاء Docker CLI من اتصال Windows/WSL إلى اتصال محلي داخل WSL2. خيار آخر هو الاعتماد على Docker Compose لإدارة دورة حياة الحاوية، ثم استخدام Remote‑SSH للاتصال بالحاوية للتطوير، متجاوزًا آلية الاستطلاع المتكررة في امتداد Dev Container.

تفعيل executeInWSL (الحل الأساسي)

الإجراءات السابقة تخفّف من مشكلة ارتفاع IO بدرجات متفاوتة، لكن السبب الجذري هو تكلفة التواصل عبر named pipe بين Docker CLI والحدود Windows/WSL. الإعداد dev.containers.executeInWSL يهدف مباشرةً إلى حل هذه المشكلة.

كما أوضح عضو فريق VS Code chrmarti في microsoft/vscode-remote-release#9194، يحدد هذا الإعداد ما إذا كان أمر docker يُنفّذ على جانب Windows أو داخل WSL. عند ضبطه على true، تُنفّذ جميع استدعاءات Docker CLI (مثل docker inspect، docker version، docker exec، docker ps) داخل WSL، وتستخدم Unix socket للتواصل مباشرةً مع Docker daemon، متجنبةً named pipe و9P.

أضف الإعداد التالي إلى settings.json لتفعيل الميزة:

{
  "dev.containers.executeInWSL": true
}

لفهم لماذا يُحسّن هذا الإعداد من أداء IO، قارن مسار التواصل في الوضعين:

  • الوضع الافتراضي (executeInWSL: false أو غير محدد)

    • Extension Host (Windows) → docker.exe (عملية فرعية على Windows) → named pipe (\\pipe\\) → Docker Daemon (WSL2)
    • تشمل المراحل إنشاء العملية، نقل البيانات عبر named pipe، وقراءة stdout، كل منها يضيف تأخيرًا وعبء IO.
  • الوضع المحسّن (executeInWSL: true)

    • Extension Host (Windows) → wsl -e docker (تنفيذ داخل WSL) → Unix socket (IPC محلي) → Docker Daemon (WSL2)
    • يظل التواصل داخل نواة Linux، متجنبًا أي عبور للحدود.
flowchart TB
    subgraph Default["الوضع الافتراضي (executeInWSL: false)"]
        direction LR
        A1["Extension Host<br/>(Windows)"]
        A2["docker.exe<br/>(عملية فرعية على Windows)"]
        A3["named pipe<br/>(\\\\pipe\\\\)"]
        A4["Docker Daemon<br/>(WSL2)"]
        A1 e1@--> A2
        A2 e2@--> A3
        A3 e3@--> A4
    end

    subgraph Optimized["الوضع المحسّن (executeInWSL: true)"]
        direction LR
        B1["Extension Host<br/>(Windows)"]
        B2["wsl -e docker<br/>(داخل WSL)"]
        B3["Unix socket<br/>(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;

البيانات التي تم جمعها خلال مرحلة التحقيق تُظهر أن استدعاء docker inspect --type container يستغرق ~1800 ms، وdocker version ~620 ms في الوضع الافتراضي، نتيجة لتأخير الـ named pipe وإنشاء العملية. بعد تفعيل executeInWSL: true، يُقلّص زمن الاستدعاء إلى مستوى الملي ثانية، مما يُحدث تحسينًا ملحوظًا عند تكرار الاستدعاءات.

المشكلات المعروفة والاعتبارات

  • مشكلة تشغيل Docker Desktop تلقائيًا: وفقًا لـ issue #9695، لا يتم تشغيل Docker Desktop تلقائيًا عند تفعيل executeInWSL: true. تم إصلاح ذلك في نسخة Dev Containers 0.353.0‑pre‑release؛ لذا يجب استخدام نسخة أحدث.
  • إعادة توجيه خدمات WSL: حتى مع executeInWSL: false، قد يحاول الامتداد الاتصال بـ WSL (لـ display/ssh‑agent/gpg‑agent). يمكن التحكم في ذلك عبر الإعداد الجديد dev.containers.wslServiceForwarding منذ نسخة 0.337.0‑pre‑release.
  • توافق Rancher Desktop: عند استبدال Docker Desktop بـ Rancher Desktop، قد يتسبب executeInWSL: true في ظهور خطأ WSL1. لا يزال هذا المفتوح، وقد يحتاج المستخدمون إلى تعطيل الإعداد مؤقتًا.
  • التنشيط غير المقصود: وفقًا لـ issue #11005، قد يُفعّل executeInWSL: true عملية تهيئة Dev Container في مستودعات Windows المحلية. الحل هو تطبيق الإعداد على مساحة العمل المحددة فقط بدلاً من الإعداد العالمي.

الخلاصة

يتطلب التحقيق في مشكلة ارتفاع IO لامتداد VS Code Dev Container على Windows نهجًا منهجيًا يبدأ من ملاحظة الظاهرة، مرورًا بتحديد المصدر عبر Process Monitor وVS Code DevTools، ثم تحليل النظام باستخدام Windows Performance Analyzer. يكشف التحليل عن أربعة عوامل متراكبة:

  1. استدعاءات Docker CLI المتكررة عبر حدود Windows/WSL (العامل الأساسي).
  2. تسرب عمليات تحويل المنفذ.
  3. حلقة إعادة الاتصال في Extension Host.
  4. تضخم IO عبر نظام ملفات WSL2.

الحل الأساسي هو تفعيل dev.containers.executeInWSL: true، الذي يزيل عبء الـ named pipe ويجعل جميع استدعاءات Docker CLI تُنفّذ داخل WSL عبر Unix socket. تُكمل الحلول الأخرى (تبسيط تحويل المنافذ، تحسين Docker Desktop، ضبط إعدادات VS Code) تحسينات مساعدة لتقليل العوامل الثانوية. للمستخدمين المتأثرين، يُنصح باتباع خطوات التحقيق المذكورة، ثم تفعيل executeInWSL: true أولاً، وتطبيق التحسينات المساندة حسب الحاجة.