מדוע בנינו את ChainSolve ב-Rust ו-WebAssembly
צלילה טכנית עמוקה לשאלה מדוע בחרנו ב-Rust המורכב ל-WebAssembly עבור מנוע החישוב של ChainSolve, וההגברה בביצועים שהשגנו.
כשהתחלנו לבנות את 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 יש שלוש שכבות:
-
Step [counter(steps)] ייצוג גרף
גרף החישוב מאוחסן כרשימת סמיכות עם סדר טופולוגי. כאשר מבנה הגרף משתנה (בלוקים נוספו, הוסרו או חוברו מחדש), אנו מחשבים מחדש את הסדר הטופולוגי באמצעות אלגוריתם של Kahn. זה קובע את רצף ההערכה.
-
Step [counter(steps)] מנוע הערכה
המנוע עובר בסדר הטופולוגי ומעריך כל בלוק. בלוקים מיושמים כאובייקטי Rust עם ממשק
evaluate(&self, inputs: &[Value]) -> Result<Value, EvalError>משותף. זה מאפשר לנו להוסיף סוגי בלוקים חדשים ללא שינוי המנוע. -
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.1 | 0.18 | 11.7x |
| 1,000 בלוקים, גרף סועף | 34.2 | 2.4 | 14.3x |
| 5,000 בלוקים, DAG מורכב | 412.0 | 22.1 | 18.6x |
ההתאוצה גדלה עם מורכבות הגרף כי מנוע ה-Wasm נהנה מ-locality של קוש טוב יותר והעדר בדיקות סוג דינמי של JavaScript.
Web Worker Integration
מנוע החישוב פועל בתוך Web Worker כדי למנוע חסימה של הThrea הראשי. דפוס התקשורת הוא ישיר:
- Threa הראשי שולח מוטציות גרף (בלוק נוסף, חיבור שונה, קלט מעודכן) כהודעות מובנות ל-Worker.
- ה-Worker מחיל מוטציות למנוע ה-Wasm וקובע הערכה מחדש.
- תוצאות נשלחות חזרה ל-Threa הראשי דרך
postMessage. - לקבוצות תוצאות גדולות, אנו משתמשים ב-
SharedArrayBufferעם Atomics כדי להימנע מעלות ההסדרה שלpostMessage.
אדריכלות זו פירושה ש-Threa הממשק לעולם לא חסום בחישוב. גם אם גרף לוקח 500ms להערכה, המשתמש יכול להמשיך לגרור בלוקים, לגלול בקנבס, ולהשפיע על הממשק ללא jank.
שיעורים שנלמדו
בנייה של יישום Wasm בייצור לימדה אותנו כמה שיעורים:
-
גודל הבינארי של Wasm חשוב. הבנייה הראשונית שלנו הייתה 2.4MB. לאחר הפעלת LTO,
wasm-opt, והסרת סמלים ניפוי, הקטנו אותה ל-340KB gzipped. כל קילוביית ספורה עבור זמן טעינה ראשוני. -
הימנע מהקצאות קטנות תכופות. מקצה של Wasm פשוט יותר מאשר מקצה מקומי. אנו משתמשים בהקצאת arena עבור נתונים זמניים לכל הערכה, מה שהפחית עלויות הקצאה ב-60%.
-
פרופיל בדפדפן, לא בשורת הפקודה. מאפייני ביצועים שונים בין Rust מקומי ל-Wasm. Panel הביצועים של Chrome DevTools עם תמיכה בפרופיל Wasm היה כלי האופטימיזציה העיקרי שלנו.
-
בדוק את שני צדי הגשר. באגים בסדרה של
wasm-bindgenעדינים. אנו שומרים על חבילת בדיקות כוללת שפועלת גם ב-Rust מקומי (דרךcargo test) וגם בדפדפן (דרךwasm-pack test).
סיכום
Rust ו-WebAssembly היו הבחירה הנכונה עבור מנוע החישוב של ChainSolve. השילוב של בטיחות זיכרון, הפשטות ללא עלות, וביצועים קרובים לביצועים מקוריים בדפדפן מאפשר לנו להעניק חוויה חישוב חזותי בזמן אמת שלא הייתה אפשרית עם JavaScript בלבד.
אם אתה שוקל Wasm עבור היישום הבא שלך המבוסס על דפדפן, אנו מקווים שפוסט זה מספק הקשר שימושי להחלטה שלך. ואם אתה רוצה לראות את התוצאות בפעולה, נסה לבנות גרף חישוב ב-app.chainsolve.co.uk.