דלג לתוכן
rust webassembly performance engineering architecture

מדוע בנינו את ChainSolve ב-Rust ו-WebAssembly

צלילה טכנית עמוקה לשאלה מדוע בחרנו ב-Rust המורכב ל-WebAssembly עבור מנוע החישוב של ChainSolve, וההגברה בביצועים שהשגנו.

By godfrey-engineering
ויזואליזציה מופשטת של קוד Rust שמתורגם ל-WebAssembly עם גרפי ביצועים

כשהתחלנו לבנות את ChainSolve, היה לנו דרישה שלא ניתנת למשא ומתן: כל חישוב חייב להתבצע בדפדפן עם ביצועים קרובים לביצועים מקוריים. ללא העברת בקשות לשרת. ללא תלויות בענן. ללא נתונים שעוזבים את מכונת המשתמש. דרישה זו הובילה אותנו ל-Rust ו-WebAssembly.

דרישת הביצועים

ChainSolve מאפשר למשתמשים לבנות גרפי חישוב עם אלפים פוטנציאליים של בלוקים, כל אחד מבצע פעולה מתמטית על התשומות שלו ומעביר תוצאות לבלוקים במורד הזרם. כאשר משתמש משנה ערך קלט, כל תת-הגרף במורד הזרם חייב להיות מחודש ומעודכן בזמן אמת — אידיאלית בתוך מסגרת אנימציה יחידה (16ms ב-60fps).

יישום JavaScript טהור של מנוע החישוב פגע בקיר בסביבות 200 בלוקים לפני שירידות במסגרות הפכו לבולטות. היינו זקוקים לשיפור של לפחות 10x כדי לתמוך בהיעד שלנו של 5,000+ בלוקים לכל גרף.

מדוע Rust?

הערכנו מספר שפות שמתורגמות ל-WebAssembly: C, C++, Go, ו-Rust. בחרנו Rust משלוש סיבות.

בטיחות זיכרון ללא ריקול אשפה

מערכת ההקנייה של Rust מבטיחה בטיחות זיכרון בזמן קומפילציה ללא אשפה קולט. זה קריטי עבור WebAssembly כי מודל הזיכרון הליניארי של Wasm פירושו ששגיאה use-after-free או overflow בפuffer לא רק מתרסק בלשונית שלך — היא מונעת שקט בכל מרחב הזיכרון. Rust חוסל את הבאגים הללו בבנייה.

הפשטות ללא עלות

ההפשטות של Rust (generics, iterators, pattern matching) מתורגמות לאותו קוד מכונה שתיכתוב בעצמך ב-C. פירוש הדבר שאנו יכולים לכתוב קוד נקי ובעל ביטוי ללא משלמת ביצועים.

ערכת כלים WebAssembly מעולה

כלי ה-wasm-pack, ה-crate של wasm-bindgen, והאקוסיסטם הרחב יותר של Rust-to-Wasm בוגרים ותועדו היטב. אנו יכולים לחשוף פונקציות Rust ל-JavaScript עם boilerplate מינימלי וללא קוד FFI ידני.

סקירת ארכיטקטורה

למנוע החישוב של ChainSolve יש שלוש שכבות:

  1. Step [counter(steps)] ייצוג גרף

    גרף החישוב מאוחסן כרשימת סמיכות עם סדר טופולוגי. כאשר מבנה הגרף משתנה (בלוקים נוספו, הוסרו או חוברו מחדש), אנו מחשבים מחדש את הסדר הטופולוגי באמצעות אלגוריתם של Kahn. זה קובע את רצף ההערכה.

  2. Step [counter(steps)] מנוע הערכה

    המנוע עובר בסדר הטופולוגי ומעריך כל בלוק. בלוקים מיושמים כאובייקטי Rust עם ממשק evaluate(&self, inputs: &[Value]) -> Result<Value, EvalError> משותף. זה מאפשר לנו להוסיף סוגי בלוקים חדשים ללא שינוי המנוע.

  3. Step [counter(steps)] גשר JavaScript

    שכבת הגשר משתמשת ב-wasm-bindgen כדי לחשוף API מינימלי ל-JavaScript: create_graph(), add_block(), connect(), set_input(), evaluate(), ו-get_results(). נתונים מסודרים כ-JSON עבור סוגים מורכבים ומועברים כמערכי f64 גולמי עבור נתונים מספריים כדי למזער את עלות ההסדרה.

השוואות

השוונו את מנוע ה-Wasm לעומת יישום JavaScript טהור שווה ערך על שלוש עומסי עבודה:

עומס עבודהJS (ms)Wasm (ms)תאוצה
100 בלוקים, שרשרת ליניארית2.10.1811.7x
1,000 בלוקים, גרף סועף34.22.414.3x
5,000 בלוקים, DAG מורכב412.022.118.6x

ההתאוצה גדלה עם מורכבות הגרף כי מנוע ה-Wasm נהנה מ-locality של קוש טוב יותר והעדר בדיקות סוג דינמי של JavaScript.

Web Worker Integration

מנוע החישוב פועל בתוך Web Worker כדי למנוע חסימה של הThrea הראשי. דפוס התקשורת הוא ישיר:

  1. Threa הראשי שולח מוטציות גרף (בלוק נוסף, חיבור שונה, קלט מעודכן) כהודעות מובנות ל-Worker.
  2. ה-Worker מחיל מוטציות למנוע ה-Wasm וקובע הערכה מחדש.
  3. תוצאות נשלחות חזרה ל-Threa הראשי דרך postMessage.
  4. לקבוצות תוצאות גדולות, אנו משתמשים ב-SharedArrayBuffer עם Atomics כדי להימנע מעלות ההסדרה של postMessage.

אדריכלות זו פירושה ש-Threa הממשק לעולם לא חסום בחישוב. גם אם גרף לוקח 500ms להערכה, המשתמש יכול להמשיך לגרור בלוקים, לגלול בקנבס, ולהשפיע על הממשק ללא jank.

שיעורים שנלמדו

בנייה של יישום Wasm בייצור לימדה אותנו כמה שיעורים:

  1. גודל הבינארי של Wasm חשוב. הבנייה הראשונית שלנו הייתה 2.4MB. לאחר הפעלת LTO, wasm-opt, והסרת סמלים ניפוי, הקטנו אותה ל-340KB gzipped. כל קילוביית ספורה עבור זמן טעינה ראשוני.

  2. הימנע מהקצאות קטנות תכופות. מקצה של Wasm פשוט יותר מאשר מקצה מקומי. אנו משתמשים בהקצאת arena עבור נתונים זמניים לכל הערכה, מה שהפחית עלויות הקצאה ב-60%.

  3. פרופיל בדפדפן, לא בשורת הפקודה. מאפייני ביצועים שונים בין Rust מקומי ל-Wasm. Panel הביצועים של Chrome DevTools עם תמיכה בפרופיל Wasm היה כלי האופטימיזציה העיקרי שלנו.

  4. בדוק את שני צדי הגשר. באגים בסדרה של wasm-bindgen עדינים. אנו שומרים על חבילת בדיקות כוללת שפועלת גם ב-Rust מקומי (דרך cargo test) וגם בדפדפן (דרך wasm-pack test).

סיכום

Rust ו-WebAssembly היו הבחירה הנכונה עבור מנוע החישוב של ChainSolve. השילוב של בטיחות זיכרון, הפשטות ללא עלות, וביצועים קרובים לביצועים מקוריים בדפדפן מאפשר לנו להעניק חוויה חישוב חזותי בזמן אמת שלא הייתה אפשרית עם JavaScript בלבד.

אם אתה שוקל Wasm עבור היישום הבא שלך המבוסס על דפדפן, אנו מקווים שפוסט זה מספק הקשר שימושי להחלטה שלך. ואם אתה רוצה לראות את התוצאות בפעולה, נסה לבנות גרף חישוב ב-app.chainsolve.co.uk.