Thread pool và chặn event loop — đo, tìm, chữa
Node có hai chỗ nghẽn khác nhau mà người mới hay gộp làm một: main thread bị chặn (một callback
JS chạy lâu) và thread pool bị đầy (quá nhiều việc fs/dns/crypto cùng lúc). Triệu chứng đều là
"server chậm", nhưng metric khác nhau và cách chữa khác nhau. Bài này dựng lại cả hai bằng lab, đo bằng
perf_hooks, tìm thủ phạm bằng CPU profile, rồi chữa bằng worker_threads và cluster.
Thuật ngữ dùng trong bài
thread pool (nhóm thread cố định của libuv, mặc định 4) · event loop delay / lag (loop trễ bao nhiêu so với lẽ ra phải chạy) · ELU — event loop utilization (tỉ lệ thời gian loop bận, không ngủ trong poll) · CPU-bound (việc tốn CPU) vs I/O-bound (việc chủ yếu chờ) · ReDoS (regex denial of service — regex backtracking theo hàm mũ làm treo CPU) · head-of-line blocking (việc đứng đầu hàng chặn mọi việc phía sau) · worker_threads (thread chạy JS riêng, V8 isolate riêng) · cluster (nhiều process Node chia nhau cổng nghe).
Kiểm chứng: Context7: Node docs v22 (dns — implementation considerations, cli —UV_THREADPOOL_SIZE, crypto), libuv docs (threadpool). Source libuv v1.51.0src/threadpool.c. Output thật trên Node 22.23.1 (host) vànode:22-bookworm-slimcho lab DNS. Máy lab 4 vCPU dùng chung, load average 7–30 trong lúc chạy: một số lần chạy bị nhiễu rõ — bài ghi cả lần nhiễu, đừng so số tuyệt đối.
1. Bài toán gốc và bối cảnh ra đời
Event loop chỉ giải được I/O nào mà kernel có API "báo khi sẵn sàng" (socket qua epoll). Nhưng có ba loại việc không có API như vậy:
- File thường — epoll từ chối file thường;
read()có thể chặn chờ đĩa. - DNS qua
getaddrinfo()— hàm chặn của libc, nhưng lại là cách duy nhất đọc đúng/etc/hosts,nsswitch.conf. - Việc CPU của thư viện C —
pbkdf2,scrypt,zlib.
libuv giải bằng một thread pool nhỏ, cố định: main thread đẩy việc vào hàng đợi, worker làm xong
thì báo lại cho event loop. Còn code JS của bạn thì không được đẩy tự động đi đâu cả — nếu nó nặng, nó chặn main
thread. Node 10.5 (2018) thêm worker_threads để giải phần đó.
Tối ưu cho: một lượng vừa phải việc chặn ngắn, để main thread không phải chờ. Không tối ưu cho: khối lượng lớn việc CPU (pool nhỏ, chia chung với fs/dns) và việc chặn lâu (một DNS timeout 5s chiếm một thread suốt 5s).
2. Mô hình tư duy cốt lõi
- Main thread là một làn đường duy nhất cho JS. Mọi việc CPU trên đó xếp hàng tuần tự; một việc 200ms = mọi request khác chờ 200ms. Đo bằng event loop delay.
- Thread pool là một hàng đợi FIFO dùng chung với N làn. fs, dns.lookup, crypto, zlib tranh nhau N làn đó. Pool đầy thì việc nhẹ (đọc file 1KB) phải chờ sau việc nặng (pbkdf2). Main thread vẫn rảnh — event loop delay không tăng, nên metric đó không bắt được loại nghẽn này.
3. Internals và quyết định thiết kế
3.1 Cấu trúc thread pool trong libuv 1.51
// libuv v1.51.0 — src/threadpool.c (trích)
#define MAX_THREADPOOL_SIZE 1024
static uv_thread_t default_threads[4]; // mặc định 4 thread
static unsigned int slow_io_work_running;
static struct uv__queue slow_io_pending_wq; // hàng riêng cho việc "slow I/O"
static unsigned int slow_work_thread_threshold(void) {
return (nthreads + 1) / 2; // slow I/O dùng tối đa một nửa pool
}
- Một hàng đợi FIFO chung, không có ưu tiên. Đơn giản, công bằng theo thứ tự đến — đổi lại không phân biệt việc 1ms với việc 500ms.
- Thread tạo lười ở lần đầu có việc (lab bài 1:
libuv-worker×4chỉ xuất hiện saufs.readFileđầu tiên). - Kích thước cố định lúc khởi động, đọc từ biến môi trường
UV_THREADPOOL_SIZE, tối đa 1024. Docs Node: đặtprocess.env.UV_THREADPOOL_SIZEtừ trong code "không đảm bảo có tác dụng" — phải đặt trước khi process chạy. - Ngoại lệ "slow I/O":
getaddrinfođược đánh dấuUV__WORK_SLOW_IOvà chỉ được chiếm tối đa(nthreads + 1) / 2thread — với pool 4 là 2. Chi tiết này docs Node không nói, nhưng thay đổi hẳn kết quả lab 3.3.
3.2 Lab: pbkdf2 làm fs.readFile chờ 1 giây
// 30-pool.js — 8 job pbkdf2 cùng lúc, rồi đọc 1 file nhỏ, rồi 1 timer 10ms
for (let i = 1; i <= 8; i++)
crypto.pbkdf2('pw', 'salt', 300_000, 64, 'sha512', () => console.log(`${at()}ms pbkdf2 #${i} xong`));
fs.readFile(__filename, () => console.log(`${at()}ms fs.readFile xong <-- file nhỏ, lẽ ra ~1ms`));
setTimeout(() => console.log(`${at()}ms setTimeout 10ms (main thread vẫn rảnh)`), 10);
$ node 30-pool.js # UV_THREADPOOL_SIZE mặc định = 4
8ms setTimeout 10ms (main thread vẫn rảnh)
597ms pbkdf2 #4 xong
638ms pbkdf2 #3 xong
785ms pbkdf2 #5 xong
979ms pbkdf2 #2 xong
1006ms pbkdf2 #7 xong
1007ms fs.readFile xong <-- file nhỏ, lẽ ra ~1ms
1028ms pbkdf2 #6 xong
1097ms pbkdf2 #1 xong
2417ms pbkdf2 #8 xong
$ UV_THREADPOOL_SIZE=8 node 30-pool.js
74ms setTimeout 10ms (main thread vẫn rảnh)
462ms pbkdf2 #3 xong
465ms fs.readFile xong <-- file nhỏ, lẽ ra ~1ms
531ms pbkdf2 #6 xong
...
672ms pbkdf2 #5 xong
Đọc kết quả: timer 10ms chạy đúng giờ (main thread rảnh), nhưng đọc file 1KB mất ~1 giây với pool 4. Tăng pool lên 8 giúp readFile nhanh hơn một nửa (465ms) — nhưng readFile vẫn chờ, vì 8 job pbkdf2 chiếm hết 8 thread trước nó. Và máy chỉ có 4 core: 8 thread CPU không làm pbkdf2 nhanh hơn, chỉ chia nhỏ CPU ra. (Lần chạy pool 4 khác bị nhiễu nặng: timer 10ms chạy ở 576ms, pbkdf2 cuối ở 3394ms — máy lab dùng chung.)
3.3 Lab: DNS chậm — và giới hạn slow I/O cứu fs
Dựng container tạm với DNS server không tồn tại (--dns 10.255.255.1, timeout 3s, 1 lần thử):
// 34-dns-starve.js
for (let i = 1; i <= 4; i++)
dns.lookup(`api-${i}.partner.example`, (err) => console.log(`${at()}ms dns.lookup #${i}: ${err?.code}`));
fs.readFile('/etc/hostname', () => console.log(`${at()}ms fs.readFile /etc/hostname xong`));
dns.resolve4('api-5.partner.example', (err) => console.log(`${at()}ms dns.resolve4 (c-ares, không dùng pool): ${err?.code}`));
$ docker run --rm --name tmp-node-dns --dns 10.255.255.1 --dns-option timeout:3 --dns-option attempts:1 \
-v $PWD:/lab -w /lab node:22-bookworm-slim node 34-dns-starve.js
10ms fs.readFile /etc/hostname xong ← fs KHÔNG bị chặn
6014ms dns.lookup #1: EAI_AGAIN
6014ms dns.lookup #2: EAI_AGAIN ← chỉ 2 lookup chạy cùng lúc
12060ms dns.lookup #4: EAI_AGAIN
12060ms dns.lookup #3: EAI_AGAIN ← lookup #3, #4 phải chờ đợt đầu: 12 giây
43579ms dns.resolve4 (c-ares, không dùng pool): ETIMEOUT
Nhiều bài viết nói "4 DNS lookup chậm làm treo cả fs". Với libuv hiện tại không đúng: DNS chỉ được dùng 2/4
thread, fs vẫn còn 2 thread và xong trong 10ms. Nhưng failure mode thật vẫn còn, chỉ đổi chỗ: các DNS lookup xếp
hàng sau nhau. Service gọi 50 API bên ngoài khi DNS chậm → lookup thứ 50 chờ 25 lượt × 6s. Và mỗi
fetch()/http.request() tới hostname đều đi qua dns.lookup mặc định.
(Mỗi lookup mất 6s chứ không phải 3s vì glibc hỏi cả A và AAAA — suy luận, chưa verify.)
3.4 Main thread bị chặn: JSON.stringify làm server đứng 1 giây
// 31-block-server.js — /fast trả ngay; /report JSON.stringify 300k object trên main thread
if (req.url === '/report') {
const s = performance.now(); const len = JSON.stringify(big).length;
return res.end(`stringify ${len} bytes mất ${(performance.now() - s).toFixed(0)}ms`);
}
// server tự đo bằng perf_hooks:
const h = monitorEventLoopDelay({ resolution: 10 }); h.enable();
const u = performance.eventLoopUtilization(elu);
$ node 31-probe.mjs # gọi /fast lúc rảnh, rồi 5 /report cùng lúc + 1 /fast chen giữa
/fast lúc rảnh: 5ms | ELU=0.15 lag p99=0ms max=0ms
/fast khi có 5 /report: 874ms | report: 164ms, 149ms, 193ms, 134ms, 228ms
metrics sau đó: ELU=0.97 lag p99=707ms max=707ms
Mỗi JSON.stringify chỉ 130–230ms — không ai nghĩ là "nặng". Nhưng 5 cái chạy tuần tự trên main thread
làm request /fast (lẽ ra 5ms) phải chờ 874ms. Lần chạy khác: 1334ms. Cả ELU (0.97) lẫn event loop delay
(707ms) đều bắt được — trái ngược với nghẽn thread pool ở 3.2.
/fast đi qua ngay (12ms thay vì 874ms — số đo lab 4 bên dưới).3.5 worker_threads: cách chữa và cái giá của nó
// 31b-worker-server.js — /report chạy trong 1 worker, dữ liệu nằm sẵn trong worker
const worker = new Worker(__filename); const waiting = new Map(); let seq = 0;
worker.on('message', ({ id, text }) => { waiting.get(id)(text); waiting.delete(id); });
if (req.url === '/report') {
const id = ++seq;
return res.end(await new Promise((r) => { waiting.set(id, r); worker.postMessage(id); }));
}
$ node 31-probe.mjs # cùng probe như 3.4
/fast lúc rảnh: 6ms | ELU=0.38 lag p99=0ms max=0ms
/fast khi có 5 /report: 12ms | report: 192ms, 780ms, 147ms, 124ms, 157ms
metrics sau đó: ELU=0.05 lag p99=114ms max=127ms
/fast từ 874ms xuống 12ms, ELU của main từ 0.97 xuống 0.05. Nhưng đọc kỹ các cái giá:
- Worker có V8 isolate riêng → heap riêng, không chia sẻ object. Dữ liệu qua lại phải
structured clone (copy) qua
postMessage, trừArrayBuffertransfer hoặcSharedArrayBuffer. Lab này để dữ liệu nằm sẵn trong worker — nếu phải gửi 300k object sang mỗi lần, chi phí copy ăn lại trên main thread. - Một worker = một hàng đợi tuần tự khác. 5 report vào một worker vẫn chạy lần lượt (report thứ 2 mất 780ms).
Production cần pool worker (≈ số core − 1) — thư viện
piscinalàm việc này (chưa dùng trong lab). - Khởi động worker tốn thời gian và RAM (mỗi worker một isolate). Tạo trước, dùng lại — không tạo mới mỗi request.
- Lần chạy thứ 2 của lab này có một report 3459ms và lag max 1430ms trên main — máy lab đang tải cao; worker không làm Node miễn nhiễm với thiếu CPU.
3.6 cluster: nhân process để dùng nhiều core
// 35-cluster.js — cùng handler /cpu 100ms, N process chia cổng 3100
if (cluster.isPrimary) { for (let i = 0; i < N; i++) cluster.fork(); }
else http.createServer((req, res) => { busy(100); res.end(`pid ${process.pid}`); }).listen(3100, '127.0.0.1');
== 1 worker
/cpu x20: tổng 2328ms | p50 1106ms | p99 2075ms
/cpu x20: tổng 3066ms | p50 1966ms | p99 2877ms
== 3 worker
/cpu x20: tổng 2400ms | p50 524ms | p99 825ms ← lần đầu: tổng bị nhiễu, nhưng p50/p99 đã giảm
/cpu x20: tổng 793ms | p50 423ms | p99 722ms ← ~3 lần nhanh hơn
cluster không làm một request nhanh hơn — nó cho N request CPU chạy song song trên N core. Trên Kubernetes, cách tương đương và phổ biến hơn là nhiều pod, mỗi pod 1 process, CPU limit ~1 core.
4. Đảm bảo và giới hạn
| Điều | Giá trị / hành vi |
|---|---|
| Kích thước pool | Mặc định 4, tối đa 1024, cố định sau khi khởi động |
| Thứ tự trong pool | FIFO, không ưu tiên (trừ hàng slow I/O riêng) |
DNS getaddrinfo | Tối đa (n+1)/2 thread cùng lúc |
| Throughput JS của 1 process | ≈ 1 / (CPU JS mỗi request) — định luật Amdahl: phần tuần tự trên main thread là trần |
| Pool lớn hơn số core | Chỉ có lợi cho việc chờ (fs trên đĩa mạng chậm, DNS); việc CPU không nhanh hơn |
| worker_threads | Song song thật, nhưng không chia sẻ heap (trừ SharedArrayBuffer) |
5. Failure modes
5.1 ReDoS — một chuỗi 27 ký tự treo process 1 giây
// 32-redos.js
const re = /^(a+)+$/;
re.test('aaaa!'); // warm-up: cho V8 compile regex trước
for (const n of [20, 22, 24, 26]) {
const s = 'a'.repeat(n) + '!';
const t = performance.now(); re.test(s);
console.log(`input ${n + 1} ký tự: ${(performance.now() - t).toFixed(0)}ms`);
}
$ node 32-redos.js
input 21 ký tự: 12ms
input 23 ký tự: 41ms
input 25 ký tự: 200ms
input 27 ký tự: 1101ms ← thêm 2 ký tự ≈ ×4–5 thời gian
Regex chạy trên main thread, không cắt ngang được. Một request có input xấu (cố ý hay vô tình) treo cả process →
chậm toàn bộ, không mất dữ liệu. Lần chạy đầu không warm-up cho số lộn xộn (n=20 mất 2327ms, lâu hơn n=24) vì V8
chạy regex bằng bytecode trước rồi mới compile ra mã máy — lại một lý do phải đo nhiều lần.
Chữa: giới hạn độ dài input trước khi test regex; tránh nhóm lặp lồng nhau ((a+)+,
(\w+\s?)*); dùng thư viện re2 (không backtracking) cho regex nhận input người dùng (chưa verify).
5.2 Nghẽn thread pool không hiện trên event loop delay
Lab 3.2: main thread rảnh, lag ≈ 0, nhưng fs chậm 1 giây. Nếu dashboard chỉ có event loop delay, sự cố này "vô hình".
Dấu hiệu: latency của thao tác fs/dns/crypto tăng trong khi ELU thấp. Cần đo latency của chính các thao tác đó
(bọc timer quanh fs.promises.*, dns.lookup) — Node không có metric độ dài hàng đợi thread pool sẵn.
5.3 Hàm *Sync trong request
crypto.pbkdf2Sync, fs.readFileSync, zlib.gzipSync, child_process.execSync chạy trên
main thread — tất cả biến việc của thread pool thành việc chặn loop. Phổ biến nhất: hash password bằng
bcrypt/pbkdf2Sync trong endpoint login → login đông người là cả API chậm.
5.4 Tải tăng đột biến trên process đã bão hoà
ELU chạm 1.0: mọi request chậm đều nhau, healthcheck trễ, orchestrator restart pod giữa lúc tải cao → tải dồn sang pod còn lại → sụp dây chuyền. Chống: bắn 503 sớm khi event loop delay vượt ngưỡng (kỹ thuật "load shedding" — từ chối bớt tải), readiness probe phản ánh quá tải, liveness probe để timeout rộng.
6. Vận hành trong production
| Metric | Bắt được | Không bắt được |
|---|---|---|
| Event loop delay p99/max | Callback chặn main thread, GC dài | Nghẽn thread pool |
| ELU | Main thread gần bão hoà (> 0.8) | Nghẽn thread pool, chờ DB |
| Latency theo loại thao tác (fs, dns, crypto) | Nghẽn thread pool | — |
| CPU% của process | Tổng tải | Không phân biệt main thread với pool (CPU 150% nghĩa là pool đang làm việc) |
Tìm thủ phạm bằng CPU profile — lab: service có hàm validate tên bằng regex và hàm tính tổng giỏ hàng, đoán xem cái nào ăn CPU:
$ node --cpu-prof --cpu-prof-dir=prof2 36-hot.js
$ node 36-top.js prof2/*.cpuprofile # cộng self-time theo hàm, in top 3
376ms 40.7% RegExp: ^([a-zA-Z0-9]+\s?)*$ :0
200ms 21.6% validateName 36-hot.js:2
182ms 19.7% (garbage collector) :0
Profile chỉ thẳng vào regex, không phải hàm reduce trên 100k phần tử mà ai cũng nghi. Trong production: mở
inspector bằng kill -USR1 <pid> (chỉ bind 127.0.0.1) rồi profile từ Chrome DevTools, hoặc chạy
--cpu-prof trên một instance canary. (clinic doctor/0x là công cụ hay dùng — chưa verify trong lab.)
Cấu hình:
UV_THREADPOOL_SIZE: tăng (8–16) khi nhiều fs/dns trên I/O chậm; không tăng để "chữa" việc CPU. Đặt ở biến môi trường của container.- DNS: cache ở tầng OS (nscd, dnsmasq, NodeLocal DNSCache trên K8s) hoặc dùng agent HTTP keep-alive để giảm số lookup.
- Số worker thread cho việc CPU ≈ số core − 1; số process ≈ số core (hoặc số pod).
7. So sánh và ma trận quyết định
| Cách | Song song thật | Chia sẻ bộ nhớ | Chi phí | Cô lập lỗi | Dùng khi |
|---|---|---|---|---|---|
Chia nhỏ + setImmediate | Không | Có | Rất thấp | — | Việc dài vừa phải, cần giữ loop thở |
worker_threads | Có | Chỉ SharedArrayBuffer | TB (isolate/worker) | Worker lỗi không giết main | Việc CPU trong request: resize ảnh, parse lớn, hash |
cluster / nhiều pod | Có | Không | Cao (process đầy đủ) | Tốt | Scale toàn bộ service theo core |
child_process | Có | Không | Cao | Tốt nhất | Chạy tool ngoài (ffmpeg), code không tin cậy |
| Queue + service riêng | Có | Không | Cao (hạ tầng) | Tốt nhất | Việc nặng không cần trả kết quả ngay (báo cáo, export) |
Dùng worker_threads khi việc CPU phải nằm trong đường request và ngắn (dưới vài trăm ms). Tránh worker_threads khi việc có thể chạy nền — đưa vào queue sẽ đơn giản và bền hơn.
8. Góc nhìn tech lead: tác động tổ chức
- Phân loại endpoint khi thiết kế: I/O-bound ở lại Node; CPU-bound ngắn → worker pool; CPU-bound dài → queue + worker service. Ghi rõ vào ADR để mọi người biết chỗ nào được phép nặng.
- Dashboard phải có cả hai loại metric (event loop + latency thao tác thread pool). Thiếu một là có một loại sự cố vô hình.
- Review checklist: regex nhận input người dùng, hàm
*Sync,JSON.parse/stringifytrên dữ liệu không giới hạn kích thước. - Chi phí hạ tầng: Node thường chạy nhiều pod nhỏ (1 core) thay vì ít pod to. Ước lượng số pod theo CPU JS mỗi request, không theo số kết nối.
- Exit strategy: khi phần CPU lớn dần, tách thành service riêng qua queue/gRPC; code worker viết thành hàm thuần (input → output) để dễ chuyển ngôn ngữ.
9. Anti-patterns và bài học thực tế
- Tăng
UV_THREADPOOL_SIZElên 128 "cho chắc" — việc CPU không nhanh hơn, chỉ thêm thread tranh CPU với main thread. - Tạo
new Worker()mỗi request — tốn thời gian khởi động isolate; dùng pool. - Gửi object khổng lồ qua
postMessage— chi phí clone chạy trên main thread, mất lợi ích worker. - Chỉ theo dõi CPU% và latency tổng — không phân biệt được main thread bị chặn với pool nghẽn.
- Regex tự viết cho input người dùng không giới hạn độ dài (5.1).
- bcrypt/pbkdf2 đồng bộ trong login (5.3).
Postmortem công khai: Cloudflare 2019-07-02 — một regex backtracking đưa CPU lên 100% trên toàn bộ edge (blog.cloudflare.com, "Details of the Cloudflare outage on July 2, 2019"). Stack Overflow 2016-07-20 — một regex cắt khoảng trắng gặp chuỗi 20.000 khoảng trắng làm trang chủ sập 34 phút (stackstatus.net, postmortem 20/07/2016). Cả hai không phải Node, nhưng trên Node hậu quả còn nặng hơn vì chỉ có một thread chạy JS.
10. Câu hỏi phụ interviewer hay hỏi tiếp
- Những gì chạy trong thread pool?
- fs (trừ watch), dns.lookup, crypto async (pbkdf2, scrypt, randomBytes, randomFill, generateKeyPair), zlib async. Socket/HTTP thì không.
- Tăng UV_THREADPOOL_SIZE có luôn tốt không?
- Chỉ tốt khi việc trong pool là chờ. Việc CPU thì trần là số core; thêm thread chỉ tranh CPU với main thread.
- DNS chậm có làm treo fs không?
- Không hoàn toàn: libuv cho getaddrinfo tối đa (n+1)/2 thread. Nhưng các lookup xếp hàng sau nhau — lab: lookup #3 chờ 12s.
- worker_threads khác cluster thế nào?
- worker: thread trong cùng process, isolate riêng, giao tiếp bằng message, dùng cho một việc CPU. cluster: nhiều process, mỗi process một event loop đầy đủ, dùng để scale cả server.
- Làm sao biết event loop bị chặn và bởi cái gì?
- monitorEventLoopDelay + ELU để biết có; CPU profile (
--cpu-prof/ inspector) để biết cái gì. - setImmediate có làm việc CPU chạy song song không?
- Không. Chỉ chia nhỏ để request khác chen vào giữa; tổng thời gian CPU không đổi.
Phụ lục: lab đã chạy
Môi trường: Node v22.23.1 (libuv 1.51.0) trên Linux 6.8, 4 vCPU dùng chung (load 7–30). Lab DNS dùng
container tạm node:22-bookworm-slim (Node 22.23.3). Code chính ở các mục trên; phần còn lại:
// 31-probe.mjs
const get = async (p) => { const s = performance.now(); const t = await (await fetch(`http://127.0.0.1:3101${p}`)).text(); return [performance.now() - s, t]; };
await get('/fast'); await get('/metrics'); // warm-up + reset số đo
const [base] = await get('/fast');
console.log(`/fast lúc rảnh: ${base.toFixed(0)}ms | ${(await get('/metrics'))[1]}`);
const reports = Array.from({ length: 5 }, () => get('/report'));
await new Promise((r) => setTimeout(r, 5));
const [busy] = await get('/fast');
const rs = await Promise.all(reports);
console.log(`/fast khi có 5 /report: ${busy.toFixed(0)}ms | report: ${rs.map(([, t]) => t.split(' mất ')[1]).join(', ')}`);
console.log(`metrics sau đó: ${(await get('/metrics'))[1]}`);
// 36-top.js — đọc .cpuprofile, cộng self-time theo hàm
const p = JSON.parse(require('fs').readFileSync(process.argv[2]));
const dt = {}; p.samples.forEach((id, i) => { dt[id] = (dt[id] || 0) + (p.timeDeltas[i] || 0); });
const byFn = {};
for (const n of p.nodes) { const k = `${n.callFrame.functionName || '(anonymous)'} ${n.callFrame.url.split('/').pop()}:${n.callFrame.lineNumber + 1}`; byFn[k] = (byFn[k] || 0) + (dt[n.id] || 0); }
const total = Object.values(byFn).reduce((a, b) => a + b, 0);
Object.entries(byFn).sort((a, b) => b[1] - a[1]).slice(0, 3)
.forEach(([k, v]) => console.log(`${(v / 1000).toFixed(0).padStart(6)}ms ${(100 * v / total).toFixed(1).padStart(5)}% ${k}`));
// 36-hot.js
const validateName = (s) => /^([a-zA-Z0-9]+\s?)*$/.test(s);
const sumPrices = (items) => items.reduce((a, b) => a + b.price, 0);
const items = Array.from({ length: 100_000 }, (_, i) => ({ price: i }));
for (let i = 0; i < 20; i++) { sumPrices(items); validateName('a'.repeat(20) + '!'); }
node 30-pool.js; UV_THREADPOOL_SIZE=8 node 30-pool.js
node 31-block-server.js & node 31-probe.mjs # rồi làm lại với 31b-worker-server.js
WORKERS=1 node 35-cluster.js & node 22-client.mjs /cpu 20 # rồi WORKERS=3
curl -sL https://raw.githubusercontent.com/libuv/libuv/v1.51.0/src/threadpool.c | grep -n "slow_io\|nthreads + 1"
Chưa verify: vì sao mỗi lookup mất 6s thay vì 3s (nghi A + AAAA); piscina, re2,
clinic; hành vi thread pool khi bật UV_USE_IO_URING=1.