มาทำความรู้จักเฟสต่าง ๆ ของ libuv ความแตกต่างที่แท้จริงระหว่าง setImmediate กับ setTimeout และวิธีตรวจจับอีเวนต์ลูปที่ถูกบล็อก
เปิดบทเรียนนี้ใน KodokonNode.js มอบหมายอีเวนต์ลูปของมันให้ libuv ซึ่งจัดระเบียบมันออกเป็นเฟส (phase) ที่แยกจากกันชัดเจน ได้แก่ timers (setTimeout, setInterval), pending callbacks, idle/prepare (ภายใน), poll (I/O เช่น ซ็อกเก็ต ไฟล์), check (setImmediate) และ close callbacks ในแต่ละรอบ (tick) ลูปจะเดินผ่านเฟสเหล่านี้ตามลำดับนั้น มีรายละเอียดที่นักพัฒนาน้อยคนจะรู้ นั่นคือ ระหว่างแต่ละคอลแบ็ก Node จะระบายคิว process.nextTick ก่อน แล้วจึงตามด้วยไมโครทาสก์ของ promise และเป็นเช่นนี้มาตั้งแต่ Node 11 คือหลังจากทุกคอลแบ็ก ไม่ใช่แค่ระหว่างเฟสเท่านั้น
import { readFile } from "node:fs";
setTimeout(() => console.log("timeout"), 0);
setImmediate(() => console.log("immediate"));
readFile(new URL(import.meta.url), () => {
setTimeout(() => console.log("io timeout"), 0);
setImmediate(() => console.log("io immediate"));
});เมื่อสคริปต์เริ่มทำงาน ลำดับระหว่าง setTimeout(fn, 0) กับ setImmediate(fn) นั้น ไม่แน่นอน (non-deterministic) เพราะจริง ๆ แล้ว setTimeout(fn, 0) ถูกบังคับให้เป็นอย่างน้อย 1 มิลลิวินาที และถ้าลูปมาถึงเฟส timers ในเวลาน้อยกว่าหนึ่งมิลลิวินาที ตัวจับเวลานั้นก็ยังไม่พร้อมทำงาน แต่ภายในคอลแบ็ก I/O คุณกำลังอยู่ในเฟส poll ซึ่งเฟส check (setImmediate) จะมาก่อนการกลับไปยังเฟส timers เสมอ ดังนั้น io immediate จึงถูกพิมพ์ก่อน io timeout อย่างสม่ำเสมอ
setTimeout(() => {
process.nextTick(() => console.log("nextTick"));
Promise.resolve().then(() => console.log("promise"));
queueMicrotask(() => console.log("microtask"));
console.log("sync");
}, 0);
// sync, nextTick, promise, microtaskกับดักที่ละเอียดอ่อน คือที่ ระดับบนสุดของ ES module ลำดับนี้จะสลับกัน คือไมโครทาสก์ของ promise จะทำงานก่อน process.nextTick เหตุผลก็คือ การประเมิน ES module นั้นตัวมันเองถูกรันเป็น promise job อยู่แล้ว ดังนั้นคิวไมโครทาสก์จึงกำลังถูกระบายอยู่ตอนที่โค้ดแบบซิงโครนัสของคุณทำงานเสร็จ ส่วนใน CommonJS นั้น nextTick ยังคงลำดับความสำคัญของมันแม้ที่ระดับบนสุด จงตรวจสอบลำดับแบบนี้เสมอในรูปแบบโมดูลที่คุณนำไปใช้งานจริง
อีเวนต์ลูปทำงานบนเธรดเดียว การคำนวณแบบซิงโครนัสที่ใช้เวลานานใด ๆ ไม่ว่าจะเป็น JSON.parse ของ payload ขนาดใหญ่ นิพจน์ปกติ (regular expression) ที่หายนะ หรือการเข้ารหัสแบบซิงโครนัส ล้วนทำให้คำขอที่กำลังดำเนินอยู่ ทั้งหมด ค้าง ในระบบจริง จงวัดความหน่วงของลูปด้วย monitorEventLoopDelay ค่า p99 ที่สูงเกินกว่าไม่กี่สิบมิลลิวินาทีเป็นสัญญาณของการค้าง โค้ดต่อไปนี้จงใจกระตุ้นให้เกิดการค้างแล้วจึงวัดมัน
import { monitorEventLoopDelay }
from "node:perf_hooks";
const h = monitorEventLoopDelay({ resolution: 10 });
h.enable();
const end = Date.now() + 200;
while (Date.now() < end) {}
setTimeout(() => {
h.disable();
const ms = h.max / 1e6;
console.log(`max delay: ${ms.toFixed(1)} ms`);
}, 300);