چالش های قوانین نوشتن

این صفحه یک نمای کلی در سطح بالا از مسائل و چالش های خاص نوشتن قوانین کارآمد Bazel ارائه می دهد.

خلاصه الزامات

  • فرض: هدف برای درستی، توان عملیاتی، سهولت استفاده و تأخیر
  • فرض: مخازن در مقیاس بزرگ
  • فرض: BUILD-مانند زبان توضیحات
  • تاریخی: جداسازی سخت بین بارگذاری، تجزیه و تحلیل و اجرا قدیمی است، اما همچنان بر API تأثیر می گذارد.
  • ذاتی: اجرای از راه دور و ذخیره سازی سخت هستند
  • ذاتی: استفاده از اطلاعات تغییر برای ساخت‌های افزایشی صحیح و سریع به الگوهای کدگذاری غیرمعمول نیاز دارد.
  • ذاتی: اجتناب از مصرف زمان و حافظه درجه دوم سخت است

مفروضات

در اینجا برخی از فرضیات ساخته شده در مورد سیستم ساخت، مانند نیاز به درستی، سهولت استفاده، توان عملیاتی و مخازن در مقیاس بزرگ وجود دارد. بخش‌های زیر به این مفروضات می‌پردازند و دستورالعمل‌هایی را ارائه می‌کنند تا اطمینان حاصل شود که قوانین به شیوه‌ای مؤثر نوشته شده‌اند.

صحت، توان عملیاتی، سهولت استفاده و تأخیر را هدف قرار دهید

ما فرض می‌کنیم که سیستم ساخت باید اول و مهم‌تر از همه با توجه به ساخت‌های افزایشی صحیح باشد. برای یک درخت منبع معین، خروجی همان ساخت باید همیشه یکسان باشد، صرف نظر از اینکه درخت خروجی چگونه به نظر می رسد. در تقریب اول، این بدان معناست که Bazel باید تک تک ورودی‌هایی را که وارد یک مرحله ساخت می‌شوند، بداند، به طوری که در صورت تغییر هر یک از ورودی‌ها، بتواند آن مرحله را دوباره اجرا کند. برای اینکه Bazel چقدر می تواند درست کند محدودیت هایی وجود دارد، زیرا برخی از اطلاعات مانند تاریخ / زمان ساخت را به بیرون درز می کند و انواع خاصی از تغییرات مانند تغییرات در ویژگی های فایل را نادیده می گیرد. Sandboxing با جلوگیری از خواندن فایل‌های ورودی اعلام‌نشده، به اطمینان از صحت کمک می‌کند. علاوه بر محدودیت های ذاتی سیستم، چند مشکل صحت شناخته شده وجود دارد که بیشتر آنها مربوط به Fileset یا قوانین C++ است که هر دو مشکل سختی هستند. ما تلاش های طولانی مدتی برای رفع این موارد داریم.

هدف دوم سیستم ساخت، داشتن توان عملیاتی بالا است. ما به طور دائم مرزهای کاری را که می توان در تخصیص ماشین فعلی برای یک سرویس اجرای از راه دور انجام داد، جابجا می کنیم. اگر سرویس اجرای از راه دور بیش از حد بارگیری شود، هیچ کس نمی تواند کار را انجام دهد.

سهولت استفاده در مرحله بعدی قرار دارد. از بین چندین رویکرد صحیح با ردپای یکسان (یا مشابه) سرویس اجرای از راه دور، ما روشی را انتخاب می کنیم که استفاده از آن آسان تر باشد.

تأخیر نشان‌دهنده مدت زمانی است که از شروع ساخت تا دریافت نتیجه مورد نظر طول می‌کشد، خواه این یک گزارش آزمایشی از یک آزمون موفق یا ناموفق باشد، یا پیام خطایی مبنی بر اینکه یک فایل BUILD دارای اشتباه تایپی است.

توجه داشته باشید که این اهداف اغلب با هم همپوشانی دارند. تأخیر به همان اندازه تابعی از توان عملیاتی سرویس اجرای از راه دور است که صحت مربوط به سهولت استفاده است.

مخازن در مقیاس بزرگ

سیستم ساخت باید در مقیاس مخازن بزرگ کار کند که در مقیاس بزرگ به این معنی است که روی یک هارد دیسک قرار نمی‌گیرد، بنابراین امکان پرداخت کامل در تقریباً همه ماشین‌های توسعه‌دهنده غیرممکن است. یک بیلد با اندازه متوسط ​​نیاز به خواندن و تجزیه ده ها هزار فایل BUILD و ارزیابی صدها هزار گلوب دارد. در حالی که از نظر تئوری خواندن همه فایل‌های BUILD در یک دستگاه امکان‌پذیر است، اما هنوز نتوانسته‌ایم این کار را در مدت زمان و حافظه معقول انجام دهیم. به این ترتیب، بسیار مهم است که فایل های BUILD را بتوان به طور مستقل بارگیری و تجزیه کرد.

زبان توصیف BUILD مانند

در این زمینه، ما یک زبان پیکربندی را در نظر می گیریم که تقریباً مشابه فایل های BUILD در اعلان قوانین کتابخانه و باینری و وابستگی های متقابل آنها است. فایل‌های BUILD را می‌توان به‌طور مستقل خواند و تجزیه کرد، و ما حتی از نگاه کردن به فایل‌های منبع تا جایی که بتوانیم اجتناب می‌کنیم (به جز وجود).

تاریخی

تفاوت‌هایی بین نسخه‌های Bazel وجود دارد که باعث ایجاد چالش‌ها می‌شود و برخی از آنها در بخش‌های زیر توضیح داده شده‌اند.

جداسازی سخت بین بارگذاری، تجزیه و تحلیل و اجرا قدیمی است اما همچنان API را تحت تأثیر قرار می دهد.

از نظر فنی، کافی است که یک قانون، فایل های ورودی و خروجی یک اقدام را درست قبل از ارسال عمل به اجرای از راه دور بداند. با این حال، پایه کد اصلی Bazel دارای جدایی دقیقی از بارگیری بسته‌ها بود، سپس قوانین را با استفاده از یک پیکربندی (در اصل پرچم‌های خط فرمان) تجزیه و تحلیل می‌کرد و تنها پس از آن هر اقدامی را اجرا می‌کرد. این تمایز هنوز هم بخشی از API قوانین است، حتی اگر هسته Bazel دیگر به آن نیاز ندارد (جزئیات بیشتر در زیر).

این بدان معناست که API قوانین نیاز به توصیفی از رابط قوانین (چه ویژگی هایی دارد، انواع ویژگی ها) دارد. برخی استثناها وجود دارد که در آن API به کد سفارشی اجازه می دهد تا در مرحله بارگذاری اجرا شود تا نام ضمنی فایل های خروجی و مقادیر ضمنی ویژگی ها محاسبه شود. به عنوان مثال، یک قانون java_library با نام 'foo' به طور ضمنی خروجی ای به نام 'libfoo.jar' تولید می کند که می تواند از قوانین دیگر در نمودار ساخت ارجاع داده شود.

علاوه بر این، تجزیه و تحلیل یک قانون نمی تواند هیچ فایل منبعی را بخواند یا خروجی یک عمل را بازرسی کند. در عوض، باید یک گراف دوبخشی جهت دار جزئی از مراحل ساخت و نام فایل های خروجی ایجاد کند که فقط از طریق خود قانون و وابستگی های آن تعیین می شود.

ذاتی

برخی از ویژگی های ذاتی وجود دارد که نوشتن قوانین را چالش برانگیز می کند و برخی از رایج ترین آنها در بخش های زیر توضیح داده شده است.

اجرای از راه دور و ذخیره کش سخت است

اجرای از راه دور و حافظه پنهان زمان ساخت را در مخازن بزرگ تقریباً دو مرتبه در مقایسه با اجرای ساخت بر روی یک ماشین بهبود می بخشد. با این حال، مقیاسی که باید در آن انجام شود خیره کننده است: سرویس اجرای از راه دور Google برای رسیدگی به تعداد زیادی درخواست در ثانیه طراحی شده است، و پروتکل با دقت از رفت و آمدهای غیر ضروری و همچنین کارهای غیر ضروری در سمت سرویس جلوگیری می کند.

در این زمان، پروتکل مستلزم آن است که سیستم ساخت تمام ورودی‌های یک اقدام معین را زودتر از موعد بداند. سپس سیستم ساخت یک اثر انگشت اقدام منحصر به فرد را محاسبه می‌کند و از زمان‌بندی‌کننده می‌خواهد ضربه‌ای به حافظه پنهان بدهد. اگر یک ضربه حافظه پنهان یافت شود، زمان‌بندی‌کننده با خلاصه فایل‌های خروجی پاسخ می‌دهد. خود فایل‌ها بعداً توسط خلاصه بررسی می‌شوند. با این حال، این محدودیت‌هایی را بر قوانین Bazel اعمال می‌کند، که باید همه فایل‌های ورودی را زودتر از موعد اعلام کنند.

استفاده از اطلاعات تغییر برای ساخت های افزایشی صحیح و سریع نیاز به الگوهای کدگذاری غیرمعمول دارد

در بالا، ما استدلال کردیم که برای درست بودن، Bazel باید همه فایل‌های ورودی را که وارد مرحله ساخت می‌شوند بداند تا تشخیص دهد که آیا آن مرحله ساخت هنوز به‌روز است یا خیر. همین امر در مورد بارگذاری بسته و تجزیه و تحلیل قوانین نیز صادق است و ما Skyframe را برای رسیدگی به این موضوع به طور کلی طراحی کرده ایم. Skyframe یک کتابخانه گراف و چارچوب ارزیابی است که یک گره هدف (مانند 'build //foo with these options') را می گیرد و آن را به بخش های تشکیل دهنده آن تجزیه می کند، که سپس ارزیابی و ترکیب می شوند تا این نتیجه را به دست آورند. به عنوان بخشی از این فرآیند، Skyframe بسته ها را می خواند، قوانین را تجزیه و تحلیل می کند و اقدامات را اجرا می کند.

در هر گره، Skyframe دقیقاً از کدام گره‌ها برای محاسبه خروجی خود استفاده می‌کند، از گره هدف گرفته تا فایل‌های ورودی (که گره‌های Skyframe نیز هستند). داشتن این نمودار به طور صریح در حافظه نمایش داده شده به سیستم ساخت اجازه می دهد تا دقیقاً تشخیص دهد که کدام گره ها تحت تأثیر یک تغییر معین در یک فایل ورودی (شامل ایجاد یا حذف یک فایل ورودی) قرار می گیرند، و حداقل کار را برای بازگرداندن درخت خروجی به آن انجام می دهد. حالت مورد نظر

به عنوان بخشی از این، هر گره یک فرآیند کشف وابستگی را انجام می دهد. هر گره می تواند وابستگی ها را اعلام کند و سپس از محتویات آن وابستگی ها برای اعلام وابستگی های بیشتر استفاده کند. در اصل، این به خوبی با یک مدل نخ در هر گره نگاشت می شود. با این حال، ساخت‌های با اندازه متوسط ​​شامل صدها هزار گره Skyframe هستند که با فناوری جاوا فعلی به راحتی امکان‌پذیر نیست (و به دلایل تاریخی، ما در حال حاضر به استفاده از جاوا گره خورده‌ایم، بنابراین بدون رشته‌های سبک وزن و بدون ادامه).

در عوض، Bazel از یک استخر نخ با اندازه ثابت استفاده می کند. با این حال، این بدان معناست که اگر یک گره وابستگی را اعلام کند که هنوز در دسترس نیست، ممکن است مجبور شویم آن ارزیابی را لغو کنیم و آن را مجددا راه اندازی کنیم (احتمالاً در رشته دیگری)، زمانی که وابستگی در دسترس باشد. این به نوبه خود به این معنی است که گره ها نباید این کار را بیش از حد انجام دهند. گره ای که N وابستگی را به صورت سریال اعلام می کند، به طور بالقوه می تواند N بار مجددا راه اندازی شود که هزینه آن O(N^2) زمان است. درعوض، هدف ما اعلام انبوه وابستگی‌ها است که گاهی نیاز به سازماندهی مجدد کد یا حتی تقسیم یک گره به چندین گره برای محدود کردن تعداد راه‌اندازی مجدد دارد.

توجه داشته باشید که این فناوری در حال حاضر در API قوانین موجود نیست. در عوض، API قوانین هنوز با استفاده از مفاهیم قدیمی فازهای بارگذاری، تحلیل و اجرا تعریف می‌شود. با این حال، یک محدودیت اساسی این است که تمام دسترسی‌ها به گره‌های دیگر باید از طریق چارچوب انجام شود تا بتواند وابستگی‌های مربوطه را ردیابی کند. صرف نظر از زبانی که سیستم ساخت با آن پیاده‌سازی می‌شود یا قوانین در آن نوشته شده‌اند (لازم نیست که یکسان باشند)، نویسندگان قوانین نباید از کتابخانه‌های استاندارد یا الگوهایی استفاده کنند که Skyframe را دور می‌زنند. برای جاوا، این به معنای پرهیز از java.io.File و همچنین هر شکلی از بازتاب و هر کتابخانه ای است که این کار را انجام می دهد. کتابخانه هایی که از تزریق وابستگی این رابط های سطح پایین پشتیبانی می کنند هنوز باید برای Skyframe به درستی راه اندازی شوند.

این به شدت پیشنهاد می کند که در وهله اول از قرار دادن نویسندگان قوانین در معرض یک زمان اجرای کامل زبان خودداری کنید. خطر استفاده تصادفی از چنین APIهایی بسیار بزرگ است - چندین باگ Bazel در گذشته به دلیل قوانینی که از APIهای ناامن استفاده می کردند، ایجاد می شد، حتی اگر این قوانین توسط تیم Bazel یا سایر متخصصان دامنه نوشته شده بود.

اجتناب از مصرف زمان درجه دوم و حافظه سخت است

بدتر از همه، جدا از الزامات اعمال شده توسط Skyframe، محدودیت های تاریخی استفاده از جاوا، و قدیمی بودن API قوانین، معرفی تصادفی زمان درجه دوم یا مصرف حافظه یک مشکل اساسی در هر سیستم ساخت مبتنی بر قوانین کتابخانه و باینری است. دو الگوی بسیار رایج وجود دارد که مصرف حافظه درجه دوم (و در نتیجه مصرف زمان درجه دوم) را معرفی می کند.

  1. زنجیره ای از قوانین کتابخانه - موردی را در نظر بگیرید که یک زنجیره از قوانین کتابخانه A به B بستگی دارد، به C بستگی دارد و غیره. سپس، می‌خواهیم برخی از ویژگی‌ها را بر روی بسته شدن گذرا این قوانین محاسبه کنیم، مانند مسیر کلاس زمان اجرا جاوا، یا دستور پیوند C++ برای هر کتابخانه. ساده لوحانه، ممکن است یک لیست استاندارد را پیاده سازی کنیم. با این حال، این قبلاً مصرف حافظه درجه دوم را معرفی می کند: کتابخانه اول شامل یک ورودی در مسیر کلاس، دومی دو ورودی، سه ورودی سوم، و به همین ترتیب، برای مجموع 1+2+3+...+N = O(N است. ^2) ورودی ها.

  2. قوانین باینری بسته به قوانین کتابخانه یکسان - موردی را در نظر بگیرید که در آن مجموعه ای از باینری ها به قوانین کتابخانه یکسانی وابسته هستند - مثلاً اگر تعدادی قانون آزمایشی دارید که همان کد کتابخانه را آزمایش می کند. فرض کنید از قوانین N، نیمی از قوانین قوانین باینری هستند و نیمی دیگر قوانین کتابخانه ای. حال در نظر بگیرید که هر دودویی یک کپی از برخی از ویژگی‌های محاسبه‌شده در هنگام بسته شدن گذرا قوانین کتابخانه مانند مسیر کلاس زمان اجرا جاوا یا خط فرمان پیوند دهنده C++ می‌سازد. به عنوان مثال، می تواند نمایش رشته خط فرمان عمل پیوند C++ را گسترش دهد. N/2 کپی از عناصر N/2 حافظه O(N^2) است.

کلاس های مجموعه سفارشی برای جلوگیری از پیچیدگی درجه دوم

Bazel به شدت تحت تأثیر هر دوی این سناریوها قرار گرفته است، بنابراین ما مجموعه ای از کلاس های مجموعه سفارشی را معرفی کردیم که به طور موثر اطلاعات را در حافظه با اجتناب از کپی در هر مرحله فشرده می کند. تقریباً همه این ساختارهای داده معنایی مجموعه دارند، بنابراین ما آن را depset نامیدیم (در پیاده سازی داخلی به نام NestedSet نیز شناخته می شود). اکثر تغییرات برای کاهش مصرف حافظه Bazel در چند سال گذشته، تغییراتی در استفاده از دپست ها به جای هر آنچه قبلا استفاده می شد، بود.

متأسفانه، استفاده از depsets به طور خودکار همه مسائل را حل نمی کند. به طور خاص، حتی تکرار بیش از یک depset در هر قانون، مصرف زمان درجه دوم را مجدداً معرفی می کند. در داخل، NestedSets همچنین دارای برخی از روش‌های کمکی برای تسهیل قابلیت همکاری با کلاس‌های مجموعه عادی است. متأسفانه، ارسال تصادفی NestedSet به یکی از این روش ها منجر به رفتار کپی شده و مصرف حافظه درجه دوم را مجدداً معرفی می کند.