Wanzul Blog

Nota, pendapat, dan pengajaran yang aku bayar sendiri.

Two overlapping panels on paper: the same inbox payload traversed a second time

Chatwoot Rasa Lambat. Server Tak Bersalah.

Setiap beberapa minggu ada orang beritahu aku satu halaman rasa lambat. Dan setiap kali itu juga, suspek pertama ialah server.

Kali ni server tak bersalah. Aku ambil dua pembetulan yang salah sebelum dapat buktikan benda tu.

Ini cerita inbox Chatwoot yang ambil beberapa saat untuk buka satu perbualan — nombor-nombor yang membebaskan server, dan satu baris kod frontend yang buat kerja mahal yang sama dua kali.

Mula dengan ukuran yang membosankan

Sebelum sentuh apa-apa, aku tarik nombornya dulu. Enam jam telemetri baca-sahaja merentas seluruh stack:

  • CPU instance web: purata 5.4%, puncak 13.2%. Kredit CPU penuh.
  • Worker: purata 4.7%, kredit tak terusik.
  • Database: CPU 6%, latency baca dan tulis sekitar 1 ms, 15–20 connection, data 246 MB.
  • Lapisan cache: CPU 3%, sifar eviction.
  • Respons aplikasi di load balancer: 172–413 ms. Tiada satu pun permintaan lebih setengah saat.

Jadi server elok. Rangkaian elok. Tetapi halaman masih rasa macam dia berfikir lima saat sebelum buat apa-apa.

Jurang itu — backend sihat, manusia tak puas hati — ialah tanda kerja sedang berlaku di tempat yang salah.

Lepas tu ukur dari browser sebelah server

Ukur dari laptop sendiri tak buktikan apa-apa, sebab talian aku sendiri jadi sebahagian eksperimen. Jadi aku sambung sesi debug pada Chrome yang berjalan dalam data centre yang sama, buka perbualan yang sama, dan rekod timeline dua kali: sekali cold, sekali warm.

  • DOMContentLoaded: 2.4–3.2 saat
  • JavaScript dan CSS dinyahkod pada setiap muat: sekitar 23.7 MB
  • Long task selepas klik perbualan: ~8.4 saat, satu task paling teruk 5 saat
  • Permintaan keluar dalam tujuh saat pertama: tiada

Baris terakhir tu yang ubah segalanya. Halaman tu bukan tunggu server. Ia beku sebelum sempat tanya server apa-apa.

Kerja scaling yang membantu — dan bahagian yang buktikan aku salah

Aku tetap buat kerja infrastruktur yang munasabah dulu, sebab aplikasi lambat atas kotak yang terlalu kecil memang masalah sebenar, walaupun bukan masalah kali ini:

  • Pindah instance web dari jenis burstable kecil ke sederhana (dua vCPU sama, memori berganda).
  • Set WEB_CONCURRENCY=2 supaya Puma betul-betul guna teras kedua: 2 worker × 5 thread.
  • Tukar deploy dari sekali gus ke rolling, supaya release akan datang tak jatuhkan seluruh environment.

Lapan permintaan senarai perbualan serentak turun dari 2.82–4.52 s ke 1.17–2.24 s. Nyata, boleh diukur, patut dikekalkan.

Lepas tu aku uji langkah seterusnya yang nampak jelas, dan dua-dua gagal gerakkan nombor:

  • Tiga worker Puma ganti dua: dalam lingkungan varians, tiada keuntungan.
  • Thread setiap worker 5, kemudian 10, kemudian 15: laluan lambat kekal lambat, dan larian terakhir jadi lebih teruk.

Lebih banyak thread tak betulkan halaman yang tersangkut di main thread. Eksperimen tu habiskan satu petang, tetapi selamatkan aku sebulan andaian yang salah.

Pembetulan pertama aku di tempat yang salah

Hipotesis pertama tu munasabah: satu inbox getter sedang menyelongkar payload template mesej WhatsApp yang besar padahal ia cuma perlukan inbox.

Aku tulis pembetulan, ukur dalam production, dan dapat ini:

  • Deep walk: 5,548 ms → 4,669 ms
  • Long task selepas klik: 9.5 s → 8.4 s
  • Permintaan masih tak keluar dalam tujuh saat

Elok di atas kertas, tak berguna dalam praktik. Deep walk tu masih berlaku — cuma di tempat lain pada laluan yang sama. Aku tutup pull request tu daripada berlagak satu pengoptimuman mikro dah selesaikan sesuatu.

Profile main thread, jangan teka

Jadi aku berhenti baca kod dan mula rekod dia. Tiga alat murah:

  1. Hook pada Array.prototype.map yang hanya log array lebih 500 entri.
  2. CPU profile diambil sekitar satu klik.
  3. PerformanceObserver pada longtask, supaya aku nampak beku itu sebagai tempoh, bukan sebagai rasa.

Stack tu balik dengan bentuk yang sama dua kali:

Array.map (2,000+ entries)  <-  message templates
  <-  Vue reactive proxy
  <-  map-obj deep walk
  <-  map-obj deep walk        (ya, dua kali)

Puncanya satu composable yang tukar rekod inbox ke camelCase. Ia baca dari getter yang sudah camelCase seluruh rekod, lepas tu jalankan useCamelCase(..., { deep: true }) atas hasil tu. Dua deep walk penuh atas objek yang sama, pada setiap perubahan perbualan, di main thread — sebab header, meta mesej dan bubble semuanya bergantung pada computed value tu.

Satu baris, dibuat dua kali

Pembetulannya buang walk kedua dan kekalkan tingkah laku. Tiada benda pintar, tiada dependency baharu, tiada cache yang perlu dibuang kemudian. Cuma: jangan buat kerja mahal yang sama dua kali.

Diukur dalam production selepas deploy aset yang dibina:

  • Long task selepas klik perbualan: 8,368 ms → 1,984 ms
  • Task tunggal paling teruk: 5,007 ms → 447 ms
  • Deep walk: hilang dari profile
  • Panggilan map lebih 500 entri: 25 → 0

Itu beza antara “aplikasi ni lambat” dan “aplikasi ni ok”.

Hantar sebelum upstream kata ya

Aku tak nak tunggu satu pusingan review sebelum pasukan aku dapat inbox yang boleh dipakai, jadi pembetulan tu sekarang berjalan sebagai overlay aset frontend di atas imej release. Backend dan database tak disentuh — yang berbeza cuma JavaScript yang dibina.

Itu datang dengan risiko operasi yang nyata, dan aku tulis dalam runbook dalaman: apa-apa deploy akan datang yang guna label versi berbeza akan ganti aset tu secara senyap dan buang pembetulan ni tanpa bunyi. Regresi senyap lebih teruk daripada yang bising, jadi runbook tu namakan label tepat dan syarat untuk buang overlay tu.

Upstream

Pembetulan yang betul ialah pull request pada Chatwoot sendiri: perf: stop deep-camelCasing inbox records twice on every conversation change, dengan issue yang terikat yang jelaskan simptom, punca dan cara menghasilkan semula.

Aku juga bandingkan dengan pull request yang sudah merged dalam repo tu dan tulis semula ikut konvensyen mereka — rujukan issue, template, langkah ujian, checklist. Versi pertama aku ikut struktur sendiri, dan itu cara yang bagus untuk diabaikan.

Sekarang pull request tu masih terbuka, semua check lulus, dan belum ada yang review. Aku juga tinggalkan komen pada issue lama dengan simptom yang sama kelas — beku bila buka perbualan — terangkan punca yang kami jumpa dan kaedah profiling yang sesiapa boleh ulang, sambil jelas bahawa pencetus di sana mungkin berbeza.

Jadi keadaan sekarang: production laju, pembetulan didokumenkan, pull request upstream terbuka, dan aku tunggu ia diluluskan. Kalau masuk, semua orang yang guna Chatwoot dapat faedahnya dan aku boleh buang overlay tu.

Kalau tak masuk, sekurang-kurangnya profiler tu sekarang ada tempat tetap dalam kotak alat aku — dan orang seterusnya yang kata “server rasa lambat” akan ditanya satu soalan dulu: kau dah ukur dari browser belum?

Wan Zulkarnain Avatar

Written by 


Comments

Leave a Reply