من یان کیلپاتریک هستم، یک مدیر مهندسی در تیم طرحبندی Blink، همراه با کوجی ایشی. قبل از کار بر روی تیم Blink، من یک مهندس فرانت اند بودم (قبل از اینکه گوگل نقش "مهندس جلویی" را داشته باشد)، ویژگی هایی را در Google Docs، Drive و Gmail ایجاد می کردم. بعد از حدود پنج سال در آن نقش، من یک قمار بزرگ را انجام دادم و به تیم Blink تغییر دادم، به طور موثر C++ را در حین کار یاد گرفتم، و تلاش کردم بر روی پایگاه کد بسیار پیچیده Blink پیشرفت کنم. حتی امروز، من فقط بخش نسبتا کمی از آن را درک می کنم. از زمانی که در این مدت در اختیارم گذاشتند سپاسگزارم. من از این واقعیت که بسیاری از مهندسان فرانتاند در حال بهبودی بودند، قبل از من به یک «مهندس مرورگر» تبدیل شدند، آرام شدم.
تجربه قبلی من شخصاً من را در زمانی که در تیم Blink هستم راهنمایی کرده است. بهعنوان یک مهندس فرانتاند، دائماً با ناهماهنگیهای مرورگر، مشکلات عملکرد، اشکالات رندر و ویژگیهای از دست رفته مواجه میشدم. LayoutNG فرصتی برای من بود تا به حل سیستماتیک این مشکلات در سیستم چیدمان Blink کمک کنم و مجموع تلاشهای بسیاری از مهندسان را در طول سالها نشان میدهد.
در این پست، توضیح خواهم داد که چگونه یک تغییر بزرگ معماری مانند این می تواند انواع مختلف باگ ها و مشکلات عملکرد را کاهش دهد و کاهش دهد.
نمای 30000 فوتی از معماری موتورهای چیدمان
قبلاً، درخت چیدمان Blink چیزی بود که من از آن به عنوان "درخت قابل تغییر" یاد می کنم.
هر شی در درخت چیدمان حاوی اطلاعات ورودی بود، مانند اندازه موجود تحمیل شده توسط والد، موقعیت هر شناور و اطلاعات خروجی ، به عنوان مثال، عرض و ارتفاع نهایی شی یا موقعیت x و y آن.
این اشیاء در بین رندرها نگهداری می شدند. وقتی تغییری در سبک رخ داد، آن شی را به عنوان کثیف و به همین ترتیب تمام والدین آن را در درخت علامتگذاری کردیم. وقتی مرحله طرحبندی خط لوله رندر اجرا شد، درخت را تمیز میکردیم، اشیاء کثیف را راه میرفتیم، سپس طرحبندی را اجرا میکردیم تا آنها را به حالت تمیز برسانیم.
ما متوجه شدیم که این معماری به دستههای زیادی از مسائل منجر میشود که در زیر توضیح خواهیم داد. اما ابتدا، اجازه دهید به عقب برگردیم و ورودی ها و خروجی های طرح بندی را در نظر بگیریم.
اجرای طرحبندی روی یک گره در این درخت بهطور مفهومی «Style plus DOM» را میگیرد و هرگونه محدودیت والد از سیستم چیدمان والد (شبکه، بلوک یا انعطافپذیر)، الگوریتم محدودیت طرحبندی را اجرا میکند و نتیجه را تولید میکند.
معماری جدید ما این مدل مفهومی را رسمیت می بخشد. ما هنوز درخت layout را داریم، اما از آن در درجه اول برای نگه داشتن ورودی ها و خروجی های طرح استفاده می کنیم. برای خروجی، یک شی کاملا جدید و تغییرناپذیر به نام درخت قطعه تولید می کنیم.
من درخت قطعه تغییرناپذیر را قبلاً پوشش دادم و توضیح دادم که چگونه برای استفاده مجدد از بخشهای بزرگ درخت قبلی برای چیدمانهای افزایشی طراحی شده است.
علاوه بر این، ما شیء محدودیتهای والد را که آن قطعه را تولید کرده است، ذخیره میکنیم. ما از این به عنوان یک کلید کش استفاده می کنیم که در ادامه بیشتر به آن خواهیم پرداخت.
الگوریتم طرح بندی درون خطی (متن) نیز برای مطابقت با معماری تغییرناپذیر جدید بازنویسی شده است. این نه تنها نمایش لیست مسطح تغییرناپذیر را برای چیدمان درون خطی تولید می کند، بلکه دارای کش در سطح پاراگراف برای پخش سریع تر، شکل در هر پاراگراف برای اعمال ویژگی های فونت در عناصر و کلمات، الگوریتم دو طرفه یونیکد جدید با استفاده از ICU، صحت زیاد است. رفع، و بیشتر.
انواع باگ های چیدمان
اشکالات چیدمان به طور کلی به چهار دسته مختلف تقسیم می شوند که هر کدام دلایل ریشه ای متفاوتی دارند.
صحت
وقتی به اشکالات سیستم رندر فکر می کنیم، معمولاً به درستی فکر می کنیم، برای مثال: "مرورگر A دارای رفتار X است، در حالی که مرورگر B دارای رفتار Y است"، یا "مرورگرهای A و B هر دو خراب هستند". قبلاً این چیزی بود که ما زمان زیادی را صرف آن می کردیم و در این روند دائماً با سیستم درگیر بودیم. یک حالت شکست رایج این بود که یک اصلاح بسیار هدفمند را برای یک باگ اعمال کنیم، اما هفتهها بعد متوجه شدیم که در قسمت دیگر (به ظاهر نامرتبط) سیستم باعث رگرسیون شدهایم.
همانطور که در پست های قبلی توضیح داده شد، این نشانه یک سیستم بسیار شکننده است. مخصوصاً برای چیدمان، ما یک قرارداد تمیز بین هیچ کلاسی نداشتیم، که باعث شد مهندسان مرورگر به وضعیتی که نباید وابسته باشند یا مقداری از قسمت دیگری از سیستم را اشتباه تفسیر کنند.
به عنوان مثال، در یک نقطه ما زنجیره ای متشکل از 10 اشکال در طول بیش از یک سال داشتیم که مربوط به طرح بندی انعطاف پذیر بود. هر اصلاحی یا مشکلی در صحت یا عملکرد در بخشی از سیستم ایجاد می کرد که منجر به یک باگ دیگر می شد.
اکنون که LayoutNG قرارداد بین تمام اجزای سیستم چیدمان را به وضوح تعریف می کند، متوجه شدیم که می توانیم تغییرات را با اطمینان بسیار بیشتری اعمال کنیم. ما همچنین از پروژه عالی تستهای پلتفرم وب (WPT) سود زیادی میبریم، که به چندین طرف اجازه میدهد در یک مجموعه آزمایشی وب مشترک مشارکت کنند.
امروزه متوجه میشویم که اگر یک رگرسیون واقعی را در کانال پایدار خود منتشر کنیم، معمولاً هیچ آزمایش مرتبطی در مخزن WPT ندارد و ناشی از درک نادرست قراردادهای مؤلفه نیست. علاوه بر این، به عنوان بخشی از خطمشی رفع اشکال، ما همیشه یک آزمایش WPT جدید اضافه میکنیم که به اطمینان حاصل میشود که هیچ مرورگری نباید دوباره اشتباه را تکرار کند.
بی اعتباری
اگر تا به حال با یک باگ مرموز مواجه شده اید که در آن تغییر اندازه پنجره مرورگر یا تغییر یک ویژگی CSS به طور جادویی باعث از بین رفتن باگ می شود، با مشکل عدم اعتبارسنجی مواجه شده اید. عملاً بخشی از درخت قابل تغییر تمیز تلقی میشد، اما به دلیل برخی تغییرات در محدودیتهای والد، خروجی درستی را نشان نمیداد.
این حالت در حالتهای چیدمان دو پاسی (دوبار راه رفتن روی درخت چیدمان برای تعیین وضعیت طرحبندی نهایی) بسیار رایج است. قبلا کد ما به این صورت بود:
if (/* some very complicated statement */) {
child->ForceLayout();
}
یک راه حل برای این نوع باگ معمولاً به صورت زیر است:
if (/* some very complicated statement */ ||
/* another very complicated statement */) {
child->ForceLayout();
}
یک راه حل برای این نوع مشکل معمولاً باعث رگرسیون شدید عملکرد می شود (به بی اعتباری بیش از حد در زیر مراجعه کنید)، و برای درست کردن آن بسیار ظریف بود.
امروزه (همانطور که در بالا توضیح داده شد) یک شیء محدودیت والد تغییرناپذیر داریم که تمام ورودیهای طرحبندی والد را به فرزند توصیف میکند. ما این را با قطعه تغییرناپذیر حاصل ذخیره می کنیم. به همین دلیل، ما یک مکان متمرکز داریم که در آن این دو ورودی را با هم متفاوت می کنیم تا مشخص کنیم که آیا کودک نیاز به انجام مجوز طرح بندی دیگری دارد یا خیر. این منطق متمایز پیچیده است، اما به خوبی گنجانده شده است. اشکال زدایی این دسته از مسائل با عدم اعتبار معمولاً منجر به بازرسی دستی دو ورودی و تصمیم گیری در مورد آنچه در ورودی تغییر کرده است به گونه ای که مجوز طرح بندی دیگری مورد نیاز است، می شود.
رفع این کدهای متفاوت معمولاً ساده هستند و به دلیل سادگی ایجاد این اشیاء مستقل، به راحتی قابل آزمایش واحد هستند.
کد تفاوت برای مثال بالا به این صورت است:
if (width.IsPercent()) {
if (old_constraints.WidthPercentageSize()
!= new_constraints.WidthPercentageSize())
return kNeedsLayout;
}
if (height.IsPercent()) {
if (old_constraints.HeightPercentageSize()
!= new_constraints.HeightPercentageSize())
return kNeedsLayout;
}
هیسترزیس
این دسته از اشکالات شبیه به عدم اعتبار هستند. اساساً، در سیستم قبلی، اطمینان از عدم توانمندی طرحبندی فوقالعاده دشوار بود - یعنی اجرای مجدد طرحبندی با ورودیهای یکسان، به همان خروجی منجر شد.
در مثال زیر ما به سادگی یک ویژگی CSS را بین دو مقدار تغییر می دهیم. با این حال این منجر به یک مستطیل "بی نهایت در حال رشد" می شود.
با درخت قابل تغییر قبلی ما، معرفی اشکالاتی مانند این فوق العاده آسان بود. اگر کد در خواندن اندازه یا موقعیت یک شی در زمان یا مرحله نادرست اشتباه می کرد (به عنوان مثال، اندازه یا موقعیت قبلی را پاک نکردیم)، فوراً یک اشکال هیسترزیس ظریف اضافه می کنیم. این اشکالات معمولاً در آزمایش ظاهر نمی شوند زیرا اکثر تست ها بر روی یک طرح بندی و رندر متمرکز می شوند. حتی نگرانکنندهتر، میدانستیم که برخی از این پسماندها برای اینکه برخی از حالتهای چیدمان به درستی کار کنند، مورد نیاز است. ما اشکالاتی داشتیم که در آنها یک بهینهسازی برای حذف مجوز طرحبندی انجام میدادیم، اما یک «اشکال» را معرفی میکردیم زیرا حالت طرحبندی برای دریافت خروجی صحیح نیاز به دو پاس داشت.
با LayoutNG، از آنجایی که ما ساختارهای داده ورودی و خروجی صریح داریم و دسترسی به حالت قبلی مجاز نیست، به طور کلی این دسته از اشکالات را از سیستم طرح بندی کاهش داده ایم.
بی اعتباری و عملکرد بیش از حد
این دقیقاً مخالف کلاس باگهای کم اعتبار است. اغلب هنگام رفع یک باگ عدم اعتبار، یک پرتگاه عملکرد ایجاد میکنیم.
ما اغلب مجبور بودیم انتخابهای دشواری را انتخاب کنیم که صحت را به عملکرد ترجیح میدادیم. در بخش بعدی بیشتر به چگونگی کاهش این نوع مشکلات عملکرد خواهیم پرداخت.
ظهور طرحبندیهای دو پاس و صخرههای عملکرد
طرحبندی انعطافپذیر و شبکهای نشاندهنده تغییر در بیان طرحبندیها در وب بود. با این حال، این الگوریتمها اساساً با الگوریتم طرحبندی بلوکی که قبل از آنها آمده بود متفاوت بودند.
چیدمان بلوک (تقریباً در همه موارد) فقط به موتور نیاز دارد که دقیقاً یک بار طرح بندی را روی همه فرزندان خود انجام دهد. این برای عملکرد عالی است، اما در نهایت آنقدر که توسعهدهندگان وب میخواهند گویا نیست.
به عنوان مثال، اغلب شما می خواهید اندازه همه کودکان به اندازه بزرگتر شود. برای حمایت از این موضوع، طرحبندی والدین (فلکس یا شبکهای) یک پاس اندازهگیری برای تعیین اندازه هر یک از فرزندان انجام میدهد، سپس یک پاس طرح برای کشش همه کودکان به این اندازه انجام میدهد. این رفتار پیشفرض برای طرحبندی انعطافپذیر و شبکهای است.
این طرحبندیهای دو پاس در ابتدا از نظر عملکرد قابل قبول بودند، زیرا مردم معمولاً آنها را عمیقاً لانه نمیکردند. با این حال، با ظهور محتوای پیچیدهتر، شروع به مشاهده مشکلات عملکرد قابل توجهی کردیم. اگر نتیجه مرحله اندازهگیری را در حافظه پنهان ذخیره نکنید، درخت طرحبندی بین حالت اندازهگیری و وضعیت طرحبندی نهایی آن قرار میگیرد.
قبلاً سعی میکردیم حافظه پنهان بسیار خاصی را به طرحبندی انعطافپذیر و شبکهای اضافه کنیم تا بتوانیم با این نوع صخرههای عملکردی مقابله کنیم. این کار کرد (و ما با Flex خیلی پیش رفتیم)، اما دائماً با باگهای ابطالشده کم و بیش از آن مبارزه میکردیم.
LayoutNG به ما امکان می دهد ساختارهای داده صریح را هم برای ورودی و هم برای خروجی طرح ایجاد کنیم، و علاوه بر آن، حافظه پنهان اندازه گیری و پاس های طرح بندی را ساخته ایم. این پیچیدگی را به O(n) برمیگرداند که منجر به عملکرد خطی قابل پیشبینی برای توسعهدهندگان وب میشود. اگر موردی وجود داشته باشد که یک طرح، طرحبندی سه پاس را انجام دهد، ما به سادگی آن پاس را نیز در حافظه پنهان میکنیم. این ممکن است فرصتهایی را برای معرفی ایمن حالتهای طرحبندی پیشرفتهتر در آینده باز کند - نمونهای از اینکه چگونه RenderingNG اساساً توسعهپذیری را در سراسر صفحه باز میکند . در برخی موارد طرحبندی شبکهای میتواند به طرحبندیهای سه پاسی نیاز داشته باشد، اما در حال حاضر بسیار نادر است.
ما متوجه میشویم که وقتی توسعهدهندگان بهطور خاص در طرحبندی با مشکلات عملکردی مواجه میشوند، معمولاً به دلیل یک اشکال زمان طرحبندی نمایی است نه توان عملیاتی خام مرحله طرحبندی خط لوله. اگر یک تغییر تدریجی کوچک (یک عنصر تغییر یک ویژگی منفرد css) منجر به طرحبندی 50-100 میلیثانیه شود، احتمالاً این یک اشکال طرحبندی نمایی است.
به طور خلاصه
Layout یک منطقه بسیار پیچیده است، و ما انواع جزئیات جالب مانند بهینه سازی طرح بندی درون خطی را پوشش ندادیم (واقعاً چگونه کل زیر سیستم درون خطی و متنی کار می کند)، و حتی مفاهیمی که در اینجا در مورد آنها صحبت می شود واقعاً فقط سطح را خراش می دهند. و بسیاری از جزئیات را پنهان کرد. با این حال، امیدواریم نشان دادهایم که چگونه بهبود سیستماتیک معماری یک سیستم میتواند منجر به دستاوردهای بزرگ در دراز مدت شود.
با این حال، ما می دانیم که هنوز کار زیادی در پیش داریم. ما از کلاسهایی از مسائل (هم عملکرد و هم صحت) که در حال حل آنها هستیم آگاه هستیم و از ویژگیهای طرحبندی جدید که به CSS میآیند هیجانزده هستیم. ما معتقدیم معماری LayoutNG حل این مشکلات را ایمن و قابل حل می کند.
یک تصویر (می دانید کدام یک!) توسط Una Kravets .