از پزشکی قانونی و کارآگاهی برای حل معماهای عملکرد جاوا اسکریپت استفاده کنید

مقدمه

در سال های اخیر، برنامه های کاربردی وب به طور قابل توجهی افزایش یافته اند. اکنون بسیاری از برنامه ها به اندازه کافی سریع اجرا می شوند که شنیده ام برخی از توسعه دهندگان با صدای بلند تعجب می کنند "آیا وب به اندازه کافی سریع است؟". برای برخی از برنامه ها ممکن است اینطور باشد، اما برای توسعه دهندگانی که روی برنامه های کاربردی با کارایی بالا کار می کنند، می دانیم که به اندازه کافی سریع نیست. علیرغم پیشرفت‌های شگفت‌انگیز در فناوری ماشین مجازی جاوا اسکریپت، یک مطالعه اخیر نشان داد که برنامه‌های گوگل بین ۵۰ تا ۷۰ درصد از زمان خود را در V8 صرف می‌کنند. برنامه شما مدت زمان محدودی دارد، چرخه تراشیدن یک سیستم به این معنی است که سیستم دیگر می تواند کارهای بیشتری انجام دهد. به یاد داشته باشید، برنامه هایی که با سرعت 60 فریم در ثانیه اجرا می شوند، تنها 16 میلی ثانیه در هر فریم دارند - jank . برای آشنایی با بهینه‌سازی جاوا اسکریپت و برنامه‌های کاربردی جاوا اسکریپت نمایه، ادامه مطلب را بخوانید.

جلسه Google I/O 2013

من این مطالب را در Google I/O 2013 ارائه کردم. ویدیوی زیر را ببینید:

چرا عملکرد مهم است؟

چرخه های CPU یک بازی با مجموع صفر هستند. استفاده کمتر از یک قسمت از سیستم به شما این امکان را می دهد که در قسمت دیگر بیشتر استفاده کنید یا به طور کلی نرم تر اجرا کنید. سریعتر دویدن و انجام کارهای بیشتر اغلب اهداف رقابتی هستند. کاربران ویژگی‌های جدیدی را می‌خواهند در حالی که انتظار دارند برنامه شما روان‌تر اجرا شود. ماشین‌های مجازی جاوا اسکریپت سریع‌تر می‌شوند، اما این دلیلی برای نادیده گرفتن مشکلات عملکردی نیست که امروزه می‌توانید آن‌ها را برطرف کنید، همانطور که بسیاری از توسعه‌دهندگان که از قبل با مشکلات عملکرد در برنامه‌های وب خود سروکار دارند، می‌دانند. در برنامه‌های بلادرنگ و با نرخ فریم بالا، فشار برای عاری بودن از jank بسیار مهم است. Insomniac Games مطالعه‌ای را انجام داد که نشان داد نرخ فریم ثابت و پایدار برای موفقیت یک بازی مهم است: "یک نرخ فریم ثابت هنوز هم نشانه‌ای از محصول حرفه‌ای و خوش‌ساخت است." توسعه دهندگان وب توجه داشته باشند.

حل مشکلات عملکرد

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

V8 CSI: Oz

ساختمان جادوگران شگفت انگیز Find Your Way to Oz به تیم V8 با یک مشکل عملکردی نزدیک شد که به تنهایی قادر به حل آن نبودند. گاهی اوقات اوز یخ می‌زد و باعث ژانک می‌شد. توسعه‌دهندگان Oz برخی تحقیقات اولیه را با استفاده از پنل Timeline در Chrome DevTools انجام داده بودند. با نگاهی به استفاده از حافظه، آنها با نمودار ترسناک دندان اره مواجه شدند. یک بار در ثانیه، زباله‌گیر 10 مگابایت زباله جمع‌آوری می‌کرد و مکث‌های جمع‌آوری زباله با جانک مطابقت داشت. مشابه تصویر زیر از Timeline در Chrome Devtools:

جدول زمانی Devtools

کارآگاهان V8، جاکوب و یانگ پرونده را در دست گرفتند. اتفاقی که افتاد یک رفت و برگشت طولانی بین جاکوب و یانگ از تیم V8 و تیم اوز بود. من این مکالمه را به رویدادهای مهمی که به ردیابی این مشکل کمک کردند، خلاصه کردم.

شواهد

اولین قدم جمع آوری و مطالعه شواهد اولیه است.

ما به دنبال چه نوع برنامه ای هستیم؟

نسخه ی نمایشی Oz یک برنامه سه بعدی تعاملی است. به همین دلیل به مکث های ناشی از جمع آوری زباله بسیار حساس است. به یاد داشته باشید، یک برنامه تعاملی که با سرعت 60 فریم در ثانیه اجرا می‌شود، 16 میلی‌ثانیه برای انجام تمام کارهای جاوا اسکریپت دارد و باید مقداری از این زمان را برای پردازش تماس‌های گرافیکی و کشیدن صفحه به کروم بگذارد .

Oz محاسبات محاسباتی زیادی را روی مقادیر دوگانه انجام می دهد و تماس های مکرری با WebAudio و WebGL برقرار می کند.

چه نوع مشکل عملکردی را می بینیم؟

ما شاهد مکث هایی هستیم که با نام مستعار فریم در حال سقوط است. این مکث ها با دوره های جمع آوری زباله مرتبط است.

آیا توسعه دهندگان بهترین شیوه ها را دنبال می کنند؟

بله، توسعه دهندگان Oz به خوبی در عملکرد جاوا اسکریپت VM و تکنیک های بهینه سازی آشنا هستند. شایان ذکر است که توسعه دهندگان Oz از CoffeeScript به عنوان زبان مبدأ خود استفاده می کردند و کد جاوا اسکریپت را از طریق کامپایلر CoffeeScript تولید می کردند. این امر برخی از تحقیقات را به دلیل قطع ارتباط بین کد نوشته شده توسط توسعه دهندگان Oz و کد مصرف شده توسط V8 پیچیده تر کرد. Chrome DevTools اکنون از نقشه‌های منبع پشتیبانی می‌کند که این کار را آسان‌تر می‌کرد.

چرا زباله جمع کن کار می کند؟

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

نسل جوان با فرکانس بسیار بالاتری نسبت به نسل قدیمی جمع آوری می شود. این با طراحی است، زیرا مجموعه نسل جوان بسیار ارزان تر است. اغلب می توان تصور کرد که مکث های مکرر GC به دلیل جمع آوری نسل جوان ایجاد می شود.

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

حافظه جوان V8

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

به طور شهودی باید درک کنید که هر بار که یک شی به طور ضمنی یا صریح (از طریق فراخوانی به new، [] یا {}) تخصیص داده می شود، برنامه شما به یک مجموعه زباله و مکث وحشتناک برنامه نزدیک و نزدیکتر می شود.

آیا 10 مگابایت بر ثانیه زباله برای این برنامه پیش بینی می شود؟

خلاصه نه توسعه دهنده کاری انجام نمی دهد که انتظار 10 مگابایت در ثانیه زباله را داشته باشد.

مظنونین

مرحله بعدی تحقیقات، شناسایی مظنونین احتمالی و سپس کم کردن آنها است.

مظنون شماره 1

تماس جدید در طول فریم. به یاد داشته باشید که هر شیئی که تخصیص داده می شود شما را به یک مکث GC نزدیکتر می کند. برنامه هایی که با نرخ فریم بالا اجرا می شوند به طور خاص باید برای تخصیص صفر در هر فریم تلاش کنند. معمولاً این امر مستلزم یک سیستم بازیافت اشیاء است که به دقت فکر شده است. کارآگاهان V8 با تیم Oz چک کردند و آنها تماس جدیدی نداشتند. در واقع تیم اوز از قبل به خوبی از این نیاز آگاه بود و گفت: "این شرم آور خواهد بود". این یکی را از لیست حذف کنید.

مظنون شماره 2

تغییر "شکل" یک شی خارج از سازنده. هر زمان که یک ویژگی جدید به یک شی خارج از سازنده اضافه شود این اتفاق می افتد. این یک کلاس پنهان جدید برای شی ایجاد می کند. وقتی کد بهینه‌سازی شده این کلاس پنهان جدید را مشاهده کرد، یک deopt فعال می‌شود، کد بهینه‌نشده اجرا می‌شود تا زمانی که کد به عنوان داغ طبقه‌بندی و دوباره بهینه‌سازی شود. این چرخش بهینه‌سازی، بهینه‌سازی مجدد منجر به jank می‌شود، اما به شدت با ایجاد زباله بیش از حد مرتبط نیست. پس از بررسی دقیق کد، تایید شد که اشکال اشیا ثابت هستند، بنابراین مشکوک شماره 2 رد شد.

مظنون شماره 3

محاسبات در کدهای بهینه نشده در کدهای بهینه نشده، تمام محاسبات منجر به تخصیص اشیاء واقعی می شود. به عنوان مثال، این قطعه:

var a = p * d;
var b = c + 3;
var c = 3.3 * dt;
point.x = a * b * c;

نتیجه در حال ایجاد 5 شی HeapNumber است. سه مورد اول برای متغیرهای a، b و c هستند. 4 برای مقدار ناشناس (a * b) و 5 از #4 * c است. 5 در نهایت به point.x اختصاص داده می شود.

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

مظنون شماره 4

ذخیره یک عدد با دقت دو برابر در یک ملک. یک شی HeapNumber باید ایجاد شود تا عدد و ویژگی تغییر داده شده برای اشاره به این شی جدید را ذخیره کند. تغییر ویژگی برای اشاره به HeapNumber باعث ایجاد زباله نمی شود. با این حال، ممکن است تعداد زیادی اعداد با دقت مضاعف به عنوان ویژگی های شی ذخیره شوند. کد مملو از عباراتی مانند زیر است:

sprite.position.x += 0.5 * (dt);

در کد بهینه شده، هر بار که x یک مقدار تازه محاسبه شده، یک عبارت به ظاهر بی ضرر، به آن اختصاص داده می شود، یک شی HeapNumber جدید به طور ضمنی تخصیص داده می شود که ما را به مکث جمع آوری زباله نزدیک می کند.

توجه داشته باشید که با استفاده از یک آرایه تایپ شده (یا یک آرایه معمولی که فقط دارای دو برابر است) می توانید از این مشکل خاص کاملاً جلوگیری کنید زیرا فضای ذخیره سازی برای عدد دقیق دوگانه فقط یک بار اختصاص داده می شود و تغییر مکرر مقدار نیازی به تخصیص فضای ذخیره جدید ندارد. .

مشکوک شماره 4 یک احتمال است.

پزشکی قانونی

در این مرحله، کارآگاهان دو مظنون احتمالی دارند: ذخیره اعداد پشته به عنوان ویژگی های شی و محاسبات حسابی که در توابع بهینه نشده اتفاق می افتد. وقت آن بود که به آزمایشگاه برویم و مشخص کنیم که کدام مظنون مقصر است. توجه: در این بخش از بازتولید مشکل موجود در کد منبع واقعی Oz استفاده خواهم کرد. این بازتولید مرتبه‌ای کوچکتر از کد اصلی است، بنابراین استدلال در مورد آن آسان‌تر است.

آزمایش شماره 1

بررسی مشکوک شماره 3 (محاسبات حسابی در توابع بهینه نشده). موتور V8 جاوا اسکریپت دارای یک سیستم ثبت گزارش داخلی است که می تواند بینش خوبی از آنچه در زیر کاپوت اتفاق می افتد ارائه دهد.

شروع با Chrome که اصلاً اجرا نمی شود، Chrome را با پرچم ها راه اندازی کنید:

--no-sandbox --js-flags="--prof --noprof-lazy --log-timer-events"

و سپس خروج کامل کروم منجر به یک فایل v8.log در فهرست فعلی می شود.

به منظور تفسیر محتویات v8.log، باید همان نسخه v8 را دانلود کنید که Chrome شما از آن استفاده می‌کند (درباره: نسخه را بررسی کنید)، و آن را بسازید .

پس از ساخت موفقیت آمیز v8، می توانید گزارش را با استفاده از پردازنده تیک پردازش کنید:

$ tools/linux-tick-processor /path/to/v8.log

(بسته به پلتفرم خود، مک یا ویندوز را جایگزین لینوکس کنید.) (این ابزار باید از فهرست منبع سطح بالای نسخه 8 اجرا شود.)

پردازنده تیک یک جدول مبتنی بر متن از توابع جاوا اسکریپت را نشان می دهد که بیشترین تیک ها را دارد:

[JavaScript]:
ticks  total  nonlib   name
167   61.2%   61.2%  LazyCompile: *opt demo.js:12
 40   14.7%   14.7%  LazyCompile: unopt demo.js:20
 15    5.5%    5.5%  Stub: KeyedLoadElementStub
 13    4.8%    4.8%  Stub: BinaryOpStub_MUL_Alloc_Number+Smi
  6    2.2%    2.2%  Stub: BinaryOpStub_ADD_OverwriteRight_Number+Number
  4    1.5%    1.5%  Stub: KeyedStoreElementStub
  4    1.5%    1.5%  KeyedLoadIC:  {12}
  2    0.7%    0.7%  KeyedStoreIC:  {13}
  1    0.4%    0.4%  LazyCompile: ~main demo.js:30

می توانید ببینید demo.js سه عملکرد داشت: opt، unopt و main. توابع بهینه شده دارای یک ستاره (*) در کنار نام خود هستند. توجه داشته باشید که تابع opt بهینه شده و unopt بهینه نشده است.

یکی دیگر از ابزارهای مهم در کیف ابزار کارآگاه V8، plot-timer-event است. می توان آن را به این صورت اجرا کرد:

$ tools/plot-timer-event /path/to/v8.log

پس از اجرا، یک فایل png به نام timer-events.png در دایرکتوری فعلی قرار دارد. با باز کردن آن باید چیزی شبیه به این را ببینید:

رویدادهای تایمر

جدا از نمودار در امتداد پایین، داده ها در ردیف ها نمایش داده می شوند. محور X زمان (ms) است. سمت چپ شامل برچسب‌هایی برای هر ردیف است:

رویدادهای تایمر محور Y

ردیف V8.Execute در هر تیک نمایه که در آن V8 کد جاوا اسکریپت را اجرا می کرد، خط عمودی سیاهی روی آن کشیده شده است. V8.GCScavenger یک خط عمودی آبی روی آن در هر تیک نمایه ای که V8 مجموعه نسل جدید را اجرا می کرد، کشیده شده است. به طور مشابه برای بقیه ایالات V8.

یکی از مهم ترین ردیف ها «نوع کد در حال اجرا» است. هر زمان که کد بهینه‌سازی شده اجرا می‌شود، سبز و زمانی که کد بهینه‌سازی نشده اجرا می‌شود، ترکیبی از قرمز و آبی خواهد بود. اسکرین شات زیر انتقال از بهینه به بهینه نشده و سپس بازگشت به کد بهینه شده را نشان می دهد:

نوع کد در حال اجرا است

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

اگر تا این حد پیش رفته‌اید، شایان ذکر است که می‌توانید با تنظیم مجدد برنامه‌تان خیلی سریع‌تر کار کنید تا بتواند در پوسته اشکال‌زدایی v8 اجرا شود: d8. استفاده از d8 زمان‌های تکرار سریع‌تری را با ابزارهای تیک-پردازنده و نمودار-تایمر-رویداد به شما می‌دهد. یکی دیگر از عوارض جانبی استفاده از d8 این است که جداسازی مشکل واقعی آسان تر می شود و میزان نویز موجود در داده ها کاهش می یابد.

با نگاهی به نمودار رویدادهای تایمر از کد منبع Oz، انتقال از کد بهینه به کد بهینه‌نشده را نشان داد و در حین اجرای کد بهینه‌نشده، بسیاری از مجموعه‌های نسل جدید فعال شدند، مشابه تصویر زیر (زمان توجه در وسط حذف شده است):

طرح رویدادهای تایمر

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

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

function updateSprites(dt) {
    for (var sprite in sprites) {
        sprite.position.x += 0.5 * dt;
        // 20 more lines of arithmetic computation.
    }
}

با دانستن V8 به خوبی آنها، بلافاصله متوجه شدند که ساختار حلقه for-i-in گاهی اوقات توسط V8 بهینه نمی شود. به عبارت دیگر، اگر تابعی دارای ساختار حلقه for-i-in باشد، ممکن است بهینه نشده باشد. این یک مورد خاص امروز است و احتمالاً در آینده تغییر خواهد کرد، یعنی V8 ممکن است روزی این ساختار حلقه را بهینه کند. از آنجایی که ما کارآگاه V8 نیستیم و V8 را مانند پشت دست خود نمی شناسیم، چگونه می توانیم تشخیص دهیم که چرا updateSprites بهینه نشده است؟

آزمایش شماره 2

اجرای Chrome با این پرچم:

--js-flags="--trace-deopt --trace-opt-verbose"

گزارش مفصلی از داده های بهینه سازی و عدم بهینه سازی را نمایش می دهد. با جستجو در داده‌ها برای updateSprites، متوجه می‌شویم:

[بهینه سازی غیرفعال برای updateSprites، دلیل: ForInStatement مورد سریع نیست]

درست همانطور که کارآگاهان فرض کردند، ساختار حلقه for-i-in دلیل آن بود.

پرونده بسته شد

پس از کشف دلیل بهینه نشدن updateSprites، رفع آن ساده بود، به سادگی محاسبات را به تابع خود منتقل کنید، یعنی:

function updateSprite(sprite, dt) {
    sprite.position.x += 0.5 * dt;
    // 20 more lines of arithmetic computation.
}

function updateSprites(dt) {
    for (var sprite in sprites) {
        updateSprite(sprite, dt);
    }
}

updateSprite بهینه می شود و در نتیجه اشیاء HeapNumber بسیار کمتر می شود و در نتیجه مکث های GC کمتر می شود. تأیید این موضوع با انجام آزمایش های مشابه با کد جدید برای شما آسان است. خواننده دقیق متوجه خواهد شد که اعداد مضاعف همچنان به عنوان ویژگی ذخیره می شوند. اگر نمایه‌سازی نشان می‌دهد که ارزش آن را دارد، تغییر موقعیت به آرایه‌ای از دوتایی یا آرایه داده‌های تایپ‌شده، تعداد اشیاء ایجاد شده را بیشتر کاهش می‌دهد.

پایان

توسعه دهندگان Oz به همین جا بسنده نکردند. با استفاده از ابزارها و تکنیک‌هایی که کارآگاهان V8 با آن‌ها به اشتراک گذاشته بودند، توانستند چند تابع دیگر را پیدا کنند که در جهنم بهینه‌سازی گیر کرده بودند و کد محاسباتی را در توابع برگ که بهینه‌سازی شده بودند، درآوردند و در نتیجه عملکرد بهتری داشتند.

بیرون بروید و شروع به حل برخی از جرایم عملکرد کنید!