24 คะแนน โดย xguru 2024-07-19 | 6 ความคิดเห็น | แชร์ทาง WhatsApp
  • เมื่อ 3 ปีก่อน Notion ได้ปรับปรุงความเร็วของแอป Notion สำหรับ Mac และ Windows ได้สำเร็จ โดยใช้ฐานข้อมูล SQLite เพื่อแคชข้อมูลไว้ที่ฝั่งไคลเอนต์
  • ครั้งนี้ก็สามารถนำการปรับปรุงแบบเดียวกันมาให้กับผู้ใช้ที่เข้าใช้งาน Notion ผ่านเบราว์เซอร์ได้แล้ว
  • บทความนี้เป็นการวิเคราะห์เชิงลึกว่า Notion ปรับปรุงประสิทธิภาพบนเบราว์เซอร์อย่างไร โดยใช้ sqlite3 เวอร์ชันที่ทำงานบน WebAssembly (WASM)
  • การใช้ SQLite ช่วยให้เวลาในการนำทางระหว่างหน้าในเบราว์เซอร์สมัยใหม่ทั้งหมดดีขึ้น 20%
    • โดยเฉพาะสำหรับผู้ใช้ที่มีเวลาในการตอบสนองของ API ช้าเป็นพิเศษจากปัจจัยภายนอก เช่น การเชื่อมต่ออินเทอร์เน็ต ความแตกต่างยิ่งเห็นได้ชัด
    • ตัวอย่างเช่น ผู้ใช้ในออสเตรเลียเร็วขึ้น 28%, ผู้ใช้ในจีนเร็วขึ้น 31% และผู้ใช้ในอินเดียเร็วขึ้น 33%

เทคโนโลยีหลัก: OPFS และ Web Workers

  • ไลบรารี WASM SQLite ใช้ API เบราว์เซอร์สมัยใหม่ชื่อ Origin Private File System (OPFS) เพื่อเก็บข้อมูลให้คงอยู่ข้ามแต่ละเซสชัน
  • OPFS ใช้งานได้เฉพาะใน Web Workers เท่านั้น
  • Web Worker สามารถมองได้ว่าเป็นโค้ดที่รันอยู่บนเธรดแยกจาก main thread ซึ่งเป็นที่ที่ JavaScript ส่วนใหญ่ในเบราว์เซอร์ทำงาน
  • Notion ถูก bundle ร่วมกับ Webpack ซึ่งมีไวยากรณ์ที่ใช้งานง่ายสำหรับการโหลด Web Worker
  • ทางทีมได้ตั้งค่า Web Worker ให้สร้างไฟล์ฐานข้อมูล SQLite หรือโหลดไฟล์เดิมโดยใช้ OPFS และรันโค้ดแคชเดิมภายใน Web Worker นี้
  • ใช้ไลบรารี Comlink เพื่อจัดการการส่งข้อความระหว่าง main thread กับ Worker ได้ง่ายขึ้น

แนวทางที่อิงกับ SharedWorker

  • สถาปัตยกรรมสุดท้ายอ้างอิงจาก โซลูชันใหม่ที่ Roy Hashimoto เสนอไว้ในการพูดคุยบน GitHub
    • เป็นแนวทางที่ให้มีเพียงแท็บเดียวเท่านั้นที่เข้าถึง SQLite ได้ในแต่ละครั้ง แต่ยังเปิดให้แท็บอื่นรัน SQLite query ได้ด้วย
  • สถาปัตยกรรมใหม่นี้ทำงานอย่างไร?
    • พูดแบบสั้น ๆ คือ แต่ละแท็บจะมี Web Worker เฉพาะของตัวเองที่สามารถเขียนลง SQLite ได้
    • แต่ในทางปฏิบัติ จะมีเพียงแท็บเดียวเท่านั้นที่สามารถใช้ Web Worker ได้จริง
    • SharedWorker ทำหน้าที่จัดการว่าแท็บใดคือ "แท็บที่ใช้งานอยู่"
    • เมื่อแท็บที่ใช้งานอยู่ถูกปิด SharedWorker ก็จะรู้ว่าต้องเลือกแท็บที่ใช้งานอยู่แท็บใหม่
  • เมื่อต้องรัน SQLite query แต่ละแท็บจะให้ main thread ส่ง query นั้นไปยัง SharedWorker แล้ว SharedWorker จะเปลี่ยนเส้นทางไปยัง Worker เฉพาะของแท็บที่กำลังใช้งานอยู่
  • แต่ละแท็บสามารถรัน SQLite query พร้อมกันได้มากเท่าที่ต้องการ โดยทุกอย่างจะถูกส่งผ่านไปยังแท็บที่ใช้งานอยู่เพียงแท็บเดียวเสมอ
  • แต่ละ Web Worker เข้าถึงฐานข้อมูล SQLite ผ่าน OPFS SyncAccessHandle Pool VFS implementation ซึ่งทำงานได้บนเบราว์เซอร์หลักทั้งหมด

ทำไมแนวทางที่ง่ายกว่าจึงใช้ไม่ได้ผล

  • ก่อนจะสร้างสถาปัตยกรรมที่อธิบายไว้ข้างต้น ทางทีมเคยพยายามใช้ WASM SQLite ด้วยวิธีที่ง่ายกว่า คือให้แต่ละแท็บมี Web Worker เฉพาะของตัวเอง และให้แต่ละ Web Worker เขียนลงฐานข้อมูล SQLite
  • แต่ไม่มีแนวทางไหนที่ตอบโจทย์ของ Notion ได้มากพอหากนำมาใช้ตรง ๆ

อุปสรรค #1: cross-origin isolation

  • หากต้องการใช้ OPFS ผ่าน sqlite3_vfs เว็บไซต์จะต้องอยู่ในสถานะ "cross-origin isolation"
  • การเพิ่ม cross-origin isolation ให้กับหน้าเว็บต้องตั้งค่า security headers บางอย่างที่จำกัดว่าสคริปต์ใดบ้างที่โหลดได้
  • การตั้งค่า headers เหล่านี้อาจเป็นงานที่ใหญ่พอสมควร
  • Notion พึ่งพาสคริปต์จากบุคคลที่สามจำนวนมากเพื่อขับเคลื่อนฟังก์ชันต่าง ๆ ของโครงสร้างพื้นฐานเว็บ ดังนั้นการทำให้เกิด cross-origin isolation อย่างสมบูรณ์จึงต้องขอให้ผู้ให้บริการแต่ละรายตั้งค่า headers ใหม่และเปลี่ยนวิธีการทำงานของ iframe ซึ่งเป็นข้อกำหนดที่ทำได้ยากในทางปฏิบัติ
  • ในการทดสอบ ทีมสามารถเก็บข้อมูลประสิทธิภาพที่สำคัญได้โดยปล่อยเวอร์ชันนี้ให้กับผู้ใช้บางส่วน ผ่าน Origin Trials สำหรับ SharedArrayBuffer ที่ใช้งานได้ใน Chrome และ Edge
  • Origin Trials เหล่านี้ช่วยให้ข้ามข้อกำหนดเรื่อง cross-origin isolation ได้ชั่วคราว

อุปสรรค #2: ปัญหาความเสียหายของข้อมูล

  • เมื่อปล่อย OPFS ผ่าน sqlite3_vfs ให้ผู้ใช้จำนวนเล็กน้อยเริ่มใช้งาน ก็เริ่มเกิดบั๊กที่ร้ายแรงกับผู้ใช้บางราย
    • ผู้ใช้เหล่านี้เห็นข้อมูลผิดบนหน้าเว็บ
    • ตัวอย่างเช่น คอมเมนต์ที่ถูก assign ให้เพื่อนร่วมงานผิดคน หรือลิงก์ไปยังหน้าใหม่ที่พรีวิวกลับเป็นอีกหน้าหนึ่งโดยสิ้นเชิง
  • เมื่อตรวจดูไฟล์ฐานข้อมูลของผู้ใช้ที่ได้รับผลกระทบ พบรูปแบบที่ชี้ว่าฐานข้อมูล SQLite เสียหายในบางลักษณะ
    • การเลือกแถวจากบางตารางทำให้เกิดข้อผิดพลาด และเมื่อตรวจดูแถวจริงก็พบปัญหาความสอดคล้องของข้อมูล เช่น มีหลายแถวที่ใช้ ID เดียวกันแต่มีเนื้อหาต่างกัน
  • ทีมคาดเดาว่าสถานะดังกล่าวของฐานข้อมูล SQLite น่าจะเกิดจากปัญหา concurrency
    • เพราะมีหลายแท็บเปิดอยู่ และแต่ละแท็บมี Web Worker เฉพาะที่มีการเชื่อมต่อกับฐานข้อมูล SQLite แบบ active
    • แอปพลิเคชันของ Notion เขียนลงแคชบ่อยครั้งทุกครั้งที่ได้รับอัปเดตจากเซิร์ฟเวอร์ หมายความว่าแท็บหลายแท็บอาจเขียนลงไฟล์เดียวกันพร้อมกัน
  • แม้จะใช้แนวทาง transaction ที่ batch SQLite queries เข้าด้วยกันอยู่แล้ว แต่ทีมก็สงสัยอย่างมากว่าปัญหาความเสียหายนั้นเกิดจากการจัดการ concurrency ที่ไม่เพียงพอฝั่ง OPFS API
  • จึงเริ่มทำ logging สำหรับข้อผิดพลาดเรื่องความเสียหายของข้อมูล และลองวิธีแก้เฉพาะหน้า เช่น เพิ่ม Web Locks และให้เฉพาะแท็บที่โฟกัสอยู่เท่านั้นที่เขียนลง SQLite
    • การปรับเหล่านี้ช่วยลดอัตราความเสียหายลงได้ แต่ยังไม่เพียงพอที่จะเปิดฟีเจอร์นี้กลับมาใช้กับ production traffic
    • อย่างน้อยก็ช่วยยืนยันได้ว่าปัญหา concurrency มีส่วนสำคัญต่อความเสียหายของข้อมูล
  • ปัญหานี้ไม่เกิดขึ้นในแอปเดสก์ท็อปของ Notion
    • เพราะบนแพลตฟอร์มนั้นมีเพียง parent process เดียวที่เขียนลง SQLite
    • แม้ในแอปจะเปิดได้หลายแท็บตามต้องการ แต่จะมีเพียงเธรดเดียวเสมอที่เข้าถึงไฟล์ฐานข้อมูล

อุปสรรค #3: ทางเลือกใช้งานได้ครั้งละแท็บเดียว

  • ทีมยังได้ประเมิน OPFS SyncAccessHandle Pool VFS variant ด้วย
    • variant นี้ไม่ต้องใช้ SharedArrayBuffer จึงใช้งานได้ใน Safari, Firefox และเบราว์เซอร์อื่นที่ไม่มี Origin Trial สำหรับ SharedArrayBuffer
  • ข้อเสียของ variant นี้คือสามารถทำงานได้ทีละแท็บเดียวเท่านั้น
    • หากพยายามเปิดฐานข้อมูล SQLite จากแท็บถัดไป ก็จะเกิดข้อผิดพลาดทันที
  • ในแง่หนึ่ง นั่นหมายความว่า OPFS SyncAccessHandle Pool VFS ไม่มีปัญหา concurrency แบบเดียวกับ OPFS ผ่าน sqlite3_vfs
    • ซึ่งยืนยันได้จากการทดลองปล่อยให้ผู้ใช้กลุ่มเล็ก โดยไม่พบปัญหาฐานข้อมูลเสียหาย
  • แต่อีกด้านหนึ่ง ทีมไม่สามารถปล่อย variant นี้ตรง ๆ ได้ เพราะต้องการให้ทุกแท็บของผู้ใช้ได้รับประโยชน์จากการแคช

การแก้ปัญหา

  • ความจริงที่ว่าไม่มี variant ไหนใช้ได้ตรง ๆ คือจุดที่ผลักดันให้ทีมสร้างสถาปัตยกรรม SharedWorker ที่อธิบายไว้ข้างต้น
  • สถาปัตยกรรมนี้เข้ากันได้กับ SQLite variants เหล่านี้ตัวใดตัวหนึ่ง
  • เมื่อใช้ OPFS ผ่าน sqlite3_vfs จะมีเพียงแท็บเดียวที่เขียนได้ในแต่ละครั้ง จึงหลีกเลี่ยงปัญหาฐานข้อมูลเสียหายได้
  • เมื่อใช้ OPFS SyncAccessHandle Pool VFS ทุกแท็บก็สามารถใช้การแคชได้ด้วย SharedWorker
  • หลังจากยืนยันแล้วว่าสถาปัตยกรรมนี้ทำงานได้กับทั้งสอง variants, มีการปรับปรุงประสิทธิภาพที่เห็นได้ชัดในตัวชี้วัด และไม่มีปัญหาฐานข้อมูลเสียหาย ก็ถึงเวลาต้องตัดสินใจขั้นสุดท้ายว่าจะปล่อย variant ใด
  • ทีมเลือก OPFS SyncAccessHandle Pool VFS เพราะไม่มีข้อกำหนดเรื่อง cross-origin isolation จึงไม่เป็นอุปสรรคต่อการปล่อยไปยังเบราว์เซอร์อื่นนอกจาก Chrome และ Edge

การบรรเทาผลกระทบด้านประสิทธิภาพ

  • เมื่อเริ่มปล่อยการปรับปรุงนี้ให้ผู้ใช้ ก็พบการถดถอยด้านประสิทธิภาพบางอย่างที่ต้องแก้ เช่น เวลาโหลดที่ช้าลง

การโหลดหน้าช้าลง

  • สิ่งแรกที่พบคือ แม้การย้ายไปมาระหว่างหน้าใน Notion จะเร็วขึ้น แต่การโหลดหน้าแรกกลับช้าลง
    • จากการทำ profiling ทีมตระหนักว่าปกติแล้วการโหลดหน้าไม่ได้ติดคอขวดที่การดึงข้อมูล
    • โค้ดบูตแอปของ Notion จะทำงานอื่นไปพร้อมกันระหว่างรอ API call เสร็จ เช่น parsing JS, ตั้งค่าแอป ฯลฯ จึงไม่ได้รับประโยชน์จากการแคชด้วย SQLite มากเท่ากับการนำทาง
  • สาเหตุที่ช้าลงคือผู้ใช้ต้องดาวน์โหลดและประมวลผลไลบรารี WASM SQLite
    • สิ่งนี้บล็อกกระบวนการโหลดหน้า ทำให้งานโหลดหน้าอื่น ๆ ไม่สามารถเกิดขึ้นพร้อมกันได้
    • เนื่องจากไลบรารีนี้มีขนาดหลายร้อยกิโลไบต์ เวลาที่เพิ่มขึ้นจึงมองเห็นได้ชัดในตัวชี้วัด
  • เพื่อแก้ปัญหานี้ ทีมได้ปรับวิธีการโหลดไลบรารีเล็กน้อย
    • โดยโหลด WASM SQLite แบบ asynchronous เต็มรูปแบบ และไม่ให้บล็อกการโหลดหน้า
    • นั่นหมายความว่าข้อมูลหน้าแรกแทบจะไม่ถูกโหลดจาก SQLite
    • ซึ่งยอมรับได้ เพราะมีการประเมินเชิงวัตถุวิสัยแล้วว่าความเร็วที่ได้จากการโหลดหน้าแรกด้วย SQLite นั้นไม่ได้มากกว่าความช้าจากการดาวน์โหลดไลบรารี
  • หลังจากใช้การเปลี่ยนแปลงนี้ ตัวชี้วัดการโหลดหน้าแรกก็เท่ากันระหว่างกลุ่มทดสอบและกลุ่มควบคุมของการทดลอง

อุปกรณ์ช้าไม่ได้ประโยชน์จากการแคช

  • อีกสิ่งหนึ่งที่พบจากตัวชี้วัดคือ แม้ค่า median ของเวลาการย้ายจากหน้า Notion หนึ่งไปยังอีกหน้าจะเร็วขึ้น แต่เวลาที่ percentile ที่ 95 กลับช้าลง
    • อุปกรณ์บางประเภท เช่น โทรศัพท์มือถือที่เปิดเบราว์เซอร์เข้า Notion ไม่ได้รับประโยชน์จากการแคช และกลับแย่ลงด้วยซ้ำ
  • คำตอบของปริศนานี้มาจากการศึกษาก่อนหน้าที่ทีมมือถือเคยทำไว้
    • ตอนนำการแคชนี้ไปใช้ในแอปมือถือแบบเนทีฟ พบว่าอุปกรณ์บางรุ่น เช่น โทรศัพท์ Android รุ่นเก่า อ่านข้อมูลจากดิสก์ได้ช้ามาก
    • ดังนั้นจึงไม่สามารถสมมติได้ว่าการโหลดข้อมูลจาก disk cache จะเร็วกว่าโหลดข้อมูลเดียวกันจาก API เสมอไป
  • ผลจากการศึกษาฝั่งมือถือทำให้รู้ว่าก่อนหน้านี้มี logic บางอย่างอยู่แล้วที่ให้คำขอสองตัวแบบ asynchronous (SQLite และ API) "แข่งกัน" ระหว่างการโหลดหน้า
    • ทีมจึงนำ logic นี้กลับมาใช้ใหม่แบบตรงไปตรงมาใน code path สำหรับการคลิกนำทาง
    • สิ่งนี้ทำให้ค่า percentile ที่ 95 ของเวลาการนำทางเท่ากันระหว่างสองกลุ่มในการทดลอง

บทสรุป

  • การนำการปรับปรุงประสิทธิภาพของ SQLite มาใช้กับ Notion บนเบราว์เซอร์มีความท้าทายในแบบของมันเอง
  • โดยเฉพาะเมื่อเกี่ยวข้องกับเทคโนโลยีใหม่ ๆ ทีมต้องเผชิญกับสิ่งที่ไม่รู้หลายอย่าง และได้บทเรียนบางข้อระหว่างทาง:
    • โดยพื้นฐานแล้ว OPFS ไม่ได้จัดการ concurrency ได้อย่างสวยงาม นักพัฒนาจึงต้องตระหนักเรื่องนี้และออกแบบให้เหมาะสม
    • Web Workers และ SharedWorkers (รวมถึงญาติของมันอย่าง Service Workers ที่ไม่ได้กล่าวถึงในบทความนี้) มีความสามารถต่างกัน และการนำมาผสานกันอาจมีประโยชน์เมื่อจำเป็น
    • ณ ฤดูใบไม้ผลิปี 2024 การทำ cross-origin isolation ให้สมบูรณ์ในเว็บแอประดับซับซ้อนยังไม่ใช่เรื่องง่าย โดยเฉพาะหากมีการใช้สคริปต์จากภายนอก
  • ผลจากการแคชข้อมูลด้วย SQLite บนเบราว์เซอร์ให้ผู้ใช้ ทำให้เห็นการปรับปรุงเวลาในการนำทาง 20% ตามที่กล่าวไว้ก่อนหน้า และไม่พบว่าตัวชี้วัดอื่นแย่ลง
    • ที่สำคัญคือไม่พบปัญหาที่เกิดจาก SQLite เสียหาย
    • ทีมมองว่าความสำเร็จและความเสถียรของแนวทางสุดท้ายนี้ต้องยกเครดิตให้กับทีมที่ดูแล WASM implementation อย่างเป็นทางการของ SQLite และ Roy Hashimoto ที่แบ่งปันแนวทางเชิงทดลองให้สาธารณะได้ใช้งาน

6 ความคิดเห็น

 
[ความคิดเห็นนี้ถูกซ่อน]
 
cometkim 2024-07-19

นี่แหละว่าทำไมบริการที่ต้องทำงานร่วมกับบุคคลที่สามควรเปิดใช้ cross-origin isolation ตั้งแต่การเปิดตัวครั้งแรก...

 
freedomzero 2024-07-20

โอ๊ะ คุณ cometkim ยินดีที่ได้เจอนะครับ

 
sixmen 2024-07-19

พอฉันเปิดหน้า Notion ใน firefox มันค้างจนใช้งานไม่ได้เลย นี่อาจเป็นเพราะเรื่องนี้หรือเปล่านะ.. (แต่แอป Notion ใช้งานได้ดี เลยตอนนี้ใช้ตัวนั้นอยู่)

 
hellworld 2024-07-20

น่าจะเป็นแบบนั้นครับ Enda เองก็รองรับการเขียนไฟล์แบบโลคัลเฉพาะบน Chrome และ Edge เท่านั้น

 
freedomzero 2024-07-20

ฉันเคยเจอแบบนี้บนแล็ปท็อปลินุกซ์เครื่องเก่าเหมือนกัน แต่พอเปิดด้วยโหมดไม่ระบุตัวตนก็ใช้งานได้