الإطار الأفقي

التقييم المتوازي ونموذج التزايد في Bazel

نموذج البيانات

يتألف نموذج البيانات من العناصر التالية:

  • SkyValue. وتُعرف أيضًا باسم العُقد. SkyValues هي كائنات غير قابلة للتغيير تتضمّن جميع البيانات التي تم إنشاؤها على مدار الإصدار والبيانات التي يتم إدخالها فيها. والأمثلة هي: ملفات الإدخال، وملفات الإخراج، والاستهدافات والأهداف التي تم ضبطها.
  • SkyKey. اسم غير قابل للتغيير قصير للإشارة إلى SkyValue، على سبيل المثال، FILECONTENTS:/tmp/foo أو PACKAGE://foo
  • SkyFunction: يتم إنشاء العُقد استنادًا إلى مفاتيحها والعُقد التابعة.
  • رسم بياني للعقدة. بنية بيانات تحتوي على علاقة الاعتماد بين العُقد.
  • Skyframe. يعتمد اسم الرمز الخاص بإطار عمل التقييم المتزايد Bazel على.

تقييم

يتكون الإصدار من تقييم العقدة التي تمثّل طلب الإصدار (هذه هي الحالة التي نسعى لتحقيقها، ولكن هناك الكثير من الرمز القديم في الطريق). أولاً، يتم العثور على SkyFunction واستدعاءها باستخدام مفتاح SkyKey ذي المستوى الأعلى. تطلب الدالة بعد ذلك تقييم العُقد التي تحتاج إليها لتقييم عُقدة المستوى الأعلى، ما يؤدي بدوره إلى استدعاءات دوال أخرى، وما إلى ذلك، إلى أن يتم الوصول إلى عُقد رموز ورقة الشجر (التي عادةً ما تكون العُقد التي تمثل ملفات الإدخال في نظام الملفات). وأخيرًا، سينتهي بنا إلى قيمة SkyValue ذات المستوى الأعلى، وبعض التأثيرات الجانبية (مثل ملفات الناتج في نظام الملفات) ورسم بياني موجَّه دائري للتبعيات بين العُقد التي كانت متضمنة في الإصدار.

يمكن لمتصفّح SkyFunction طلب إدخال SkyKeys في بطاقات متعدّدة إذا تعذّر عليه تحديد كل العُقد التي تحتاج إلى إجرائها مسبقًا. ومن الأمثلة البسيطة على ذلك تقييم عقدة ملف الإدخال التي يتبيّن أنها رابط رمزي: تحاول الدالة قراءة الملف، وتدرك أنها عبارة عن رابط رمزي، وبالتالي تجلب عقدة نظام الملفات التي تمثّل الهدف المطابق للرمز المميز. ويمكن أن يكون ذلك رابطًا رمزيًا، وفي هذه الحالة ستحتاج الدالة الأصلية إلى جلب استهدافها أيضًا.

ويتم تمثيل الدوال في الرمز من خلال الواجهة SkyFunction والخدمات المقدَّمة له من خلال واجهة باسم SkyFunction.Environment. وفي ما يلي بعض الوظائف التي يمكنك تنفيذها:

  • يمكنك طلب تقييم عقدة أخرى من خلال طلب env.getValue. إذا كانت العقدة متاحة، يتم عرض قيمتها، وإلا سيتم عرض null ومن المتوقع أن تعرض الدالة نفسها null. في الحالة الثانية، يتم تقييم العُقدة التابعة ثم يتم استدعاء أداة إنشاء العُقدة الأصلية مرة أخرى، ولكن هذه المرة ستعرض الدالة env.getValue نفسها قيمة بخلاف null.
  • اطلب تقييم عُقد أخرى متعددة عن طريق طلب env.getValues(). ينطبق هذا الإجراء على الأساس، إلا أنه يتم تقييم العُقد المرتبطة بالتوازي.
  • إجراء الحوسبة أثناء الاستدعاء
  • مثل التأثيرات الجانبية، مثل كتابة الملفات إلى نظام الملفات ويجب الانتباه جيدًا إلى أنّ دالتَين مختلفتَين لا تتقدّمان في أصابع بعضهما البعض. بشكلٍ عام، لا مشكلة في التأثيرات الجانبية (عند تدفق البيانات إلى الخارج من Bazel)، حيث يمكن قراءة التأثيرات الجانبية (التي تتدفق فيها البيانات إلى Bazel بدون اعتمادية مسجّلة) لأنّها اعتمادية غير مسجّلة، وبالتالي يمكن أن تتسبّب في إنشاء إصدارات تدريجية غير صحيحة.

يجب ألا تصل عمليات تنفيذ SkyFunction إلى البيانات بأي طريقة أخرى غير طلب المهام التابعة (مثل قراءة نظام الملفات مباشرةً)، لأن ذلك يؤدي إلى عدم تسجيل Bazel اعتمادية البيانات على الملف الذي تمت قراءته، ما يؤدي إلى إنشاء إصدارات متزايدة غير صحيحة.

بعد أن تتوفّر لدى الدالة بيانات كافية لتأدية مهمتها، من المفترض أن تعرض قيمة غير null تشير إلى اكتمالها.

تتميّز استراتيجية التقييم هذه بعدد من المزايا:

  • الزخارف. إذا لم تطلب الدوال بيانات الإدخال إلا عن طريق الاعتماد على العُقد الأخرى، يمكن أن يضمن Bazel أنه في حال كانت حالة الإدخال هي نفسها، سيتم عرض البيانات نفسها. وإذا كانت جميع وظائف السماء قابلة للتحديد، يعني ذلك أن الإصدار بأكمله سيكون أيضًا محددًا.
  • التزايد الصحيح والصحيح في حال تسجيل جميع بيانات الإدخال لجميع الدوال، لا يُمكن لتطبيق Bazel التحقّق من المجموعة الدقيقة فقط من العُقد التي تحتاج إلى إبطالها عند تغيير بيانات الإدخال.
  • التوازي بما أن الدوال لا يمكن أن تتفاعل مع بعضها البعض إلا عن طريق طلب المهام التابعة، يمكن تشغيل الدوال التي لا تعتمد على بعضها البعض في الوقت نفسه، ويمكن أن تضمن Bazel أن النتيجة تكون كما لو تم عرضها بشكل تسلسلي.

التزايد

بما أن الدوال يمكنها الوصول إلى بيانات الإدخال فقط بناءً على العُقد الأخرى، يمكن أن ينشئ Bazel رسمًا بيانيًا كاملاً لتدفق البيانات من ملفات الإدخال إلى ملفات الإخراج، ويستخدم هذه المعلومات لإعادة إنشاء هذه العُقد التي تحتاج إلى إعادة إنشائها: الإغلاق المؤقت العكسي لمجموعة ملفات الإدخال التي تم تغييرها.

وعلى وجه الخصوص، تتوفّر استراتيجيتَان متزايدتَان: الاستراتيجية من الأعلى إلى الأسفل. وتعتمد النسخة الأفضل على الشكل الذي يظهر به الرسم البياني للاعتمادية.

  • أثناء إلغاء صلاحية العرض من الأسفل إلى الأسفل، بعد إنشاء رسم بياني وتعريف مجموعة المدخلات التي تم تغييرها، يتم إبطال صلاحية جميع العُقد التي تعتمد على الملفات التي تم تغييرها بشكل مؤقت. ويعد هذا الأمر مثاليًا إذا كنا نعرف أنه سيتم إنشاء عقدة المستوى الأعلى نفسها مرة أخرى. يُرجى ملاحظة أنّ عملية إلغاء القيمة من القاعدة إلى أعلى تتطلب تشغيل stat() على جميع ملفات الإدخال ذات الإصدار السابق لتحديد ما إذا تم تغييرها. ويمكن تحسين هذا الإجراء باستخدام inotify أو آلية مشابهة للاطّلاع على الملفات التي تم تغييرها.

  • أثناء الإلغاء من أعلى لأسفل، يتم التحقق من الإغلاق العام للعقدة ذات المستوى الأعلى، ويتم الاحتفاظ فقط بالعُقد التي يتم إغلاق الإغلاق المتنقّل لها. وسيكون من الأفضل استخدام هذا الرسم البياني إذا كنا نعلم أن الرسم البياني الحالي للعقدة كبير جدًا، إلا أننا نحتاج فقط إلى مجموعة فرعية صغيرة منه في الإصدار التالي: سيؤدي إلغاء صلاحية القاعدة إلى أعلى إلى إبطال صلاحية الرسم البياني الأكبر للإصدار الأول، على عكس الإبطال من أعلى لأسفل، والذي ينقل فقط الرسم البياني الصغير للإصدار الثاني.

في الوقت الحالي، لا نُطبّق إلا طبقات إلغاء الاشتراك من الأسفل إلى الأعلى.

لمزيد من التزايد، نستخدم تقليم التغييرات: إذا تم إبطال صلاحية العُقدة، ولكن عند إعادة إنشائها، تبيّن أن قيمتها الجديدة هي القيمة نفسها، تتم إعادة عرض العُقد التي تم إبطالها بسبب تغيير في هذه العقدة.

على سبيل المثال، إذا غيّر مستخدم تعليقًا في ملف C++ مثلاً، سيكون الملف .o الذي تم إنشاؤه منه متطابقًا، وبالتالي لا نحتاج إلى طلب الربط مرة أخرى.

تجميع / رابط متزايد

يتمثل القيد الرئيسي في هذا النموذج في أن إبطال العُقدة يمثّل مشكلة لا علاقة لها بأي شيء: عند تغيير تبعية، تتم دائمًا إعادة إنشاء العُقدة التابعة من البداية، حتى إذا توفّرت خوارزمية أفضل من شأنها تغيير القيمة القديمة للعقدة استنادًا إلى التغييرات. في ما يلي بعض الأمثلة على الحالات التي يكون فيها ذلك مفيدًا:

  • الربط المتزايد
  • عند تغيير ملف واحد في .class في .jar، يمكننا تعديل ملف .jar نظريًا بدلاً من إعادة إنشائه من البداية.

ويعود سبب عدم توافق Bazel في الوقت الحالي إلى هذه الطريقة بشكل أساسي (لدينا مقياس من أجل توفير الدعم التدريجي للربط، إلا أنّه لا يتم تنفيذه في إطار Skyframe)، حيث حصلنا على مكاسب محدودة في الأداء وكان من الصعب ضمان أن تكون التحوّل مماثلاً لعملية إعادة التصميم النظيفة، وأنّ قيم Google تتضمّن إصدارات قابلة للتكرار.

حتى الآن، كان بإمكاننا دائمًا تحقيق أداء جيد جيد من خلال تقسيم خطوة إصدار مكلفة وإعادة تقييم جزئي بهذه الطريقة، بحيث يتم تقسيم جميع الصفوف في التطبيق إلى مجموعات متعددة والتقسيم عليها بشكل منفصل. وبهذه الطريقة، إذا لم تتغير الصفوف في مجموعة، لا حاجة إلى إعادة الاسترجاع.

ربط بمفاهيم Bazel

هذه نظرة عامة تقريبية على بعض عمليات التنفيذ التي تخصّ SkyFunction شركة Bazel لإجراء التعديلات:

  • FileStateValue: نتيجة lstat(). بالنسبة إلى الملفات الحالية، نحسب أيضًا معلومات إضافية لاكتشاف التغييرات التي تم إجراؤها على الملف. هذه هي عقدة المستوى الأدنى في الرسم البياني Skyframe ولا تتضمن أي تبعيات.
  • FileValue: يتم استخدامه من قِبل أي عنصر يهتم بالمحتوى الفعلي و/أو المسار الذي تم حله باستخدام الملف. يعتمد على FileStateValue وأي رموز مطابِقة يجب حلّها (مثل FileValue الخاص بـ a/b).
  • DirectoryListingValue: هذه النتيجة هي readdir() في الأساس. يعتمد على FileValue المرتبط المرتبط بالدليل.
  • PackageValue: وتمثّل النسخة التي تم تحليلها من ملف BUILD. يعتمد على FileValue من ملف BUILD المرتبط، وأيضًا على أي DirectoryListingValue يُستخدم لحلّ العلاَم في الحزمة (بنية البيانات التي تمثّل محتوى ملف BUILD داخليًا)
  • ConfiguredTargetValue. وتمثّل القيمة المستهدَفة التي تم ضبطها، وهي عبارة عن مجموعة من الإجراءات التي يتم إنشاؤها أثناء تحليل القيمة المستهدَفة والمعلومات المقدَّمة للاستهدافات التي تم ضبطها والتي تعتمد على هذا الهدف. يعتمد على PackageValue الهدف المستهدف، وConfiguredTargetValues من التبعيات المباشرة، وعقدة خاصة تمثل إعداد الإصدار.
  • ArtifactValue: وتمثّل ملفًا في الإصدار، سواء كان مصدرًا أو عناصر ناتج (تتماثل العناصر مع الملفات تقريبًا، ويتم استخدامها للإشارة إلى الملفات أثناء التنفيذ الفعلي لخطوات الإصدار). بالنسبة إلى الملفات المصدر، يعتمد ذلك على FileValue في العقدة المرتبطة، وبالنسبة إلى عناصر الإخراج، يعتمد ذلك على ActionExecutionValue لأي إجراء يؤدي إلى إنشاء العنصر.
  • ActionExecutionValue. يمثّل تنفيذ إجراء. يعتمد على ArtifactValues من ملفات الإدخال الخاصة به. الإجراء الذي ينفّذه حاليًا مضمّن في مفتاح السماء، وذلك يخالف مفهوم أن مفاتيح السماء يجب أن تكون صغيرة. ونحن نعمل على حلّ هذا التناقض (ملاحظة: لا يتم استخدام ActionExecutionValue وArtifactValue في حال عدم تنفيذ مرحلة التنفيذ في Skyframe).