IlmHamroh
JavaScript Full-stack/24-qism. Node.js6/55-dars17 daqiqa
Mundarija (24)

Event loop'ni bloklash: aniqlash, o'lchash va oldini olish

Qisqacha: Node'da JS kodi bitta thread'da ishlaydi. Bitta og'ir sinxron hisob (katta sikl, 100 MB JSON.parse, yomon regex) tugamaguncha boshqa hech bir so'rov, taymer yoki callback ishlamaydi — server "qotadi". Buni event loop kechikishi (lag) bilan o'lchaysiz: perf_hooks.monitorEventLoopDelay va performance.eventLoopUtilization. Davolash: og'ir ishni bo'laklarga bo'lib orasida loop'ga nafas berish (setImmediate) yoki uni boshqa thread'ga (worker) chiqarish. Kanon o'lchovi: umumiy vaqt deyarli o'zgarmaydi (1 434 → 1 453 ms), lekin eng katta kechikish 1 390 ms dan 47 ms ga tushadi.

Bu darsda

  • Event loop'ni nima bloklashini aniq ayta olasiz: CPU-bound sikl, katta JSON.parse, ReDoS, sinxron I/O.
  • Kechikishni o'lchaysiz: monitorEventLoopDelay (p50, p99, max) va ELU.
  • --trace-sync-io bilan yashiringan sinxron chaqiruvlarni topasiz.
  • Og'ir ishni setImmediate bilan bo'laklaysiz va natijani o'lchov bilan isbotlaysiz.
  • Qachon bo'laklash yetarli, qachon worker kerakligini tanlay olasiz.

Oldin bilishingiz kerak: Microtask, nextTick va setImmediate, libuv thread pool, Asosiy oqimni bo'shatish, Regex amaliyotda va ReDoS.

1. Nega bu kerak?

Tasavvur qiling: valyuta kurslarini beradigan kichik server. Kunda 10 000 so'rov, har biri 2 ms da javob oladi. Bir kuni kimdir "oxirgi 5 yil kurslarini CSV qilib ber" degan tugmani bosdi. Server 1,5 soniya hisobladi. Shu 1,5 soniyada qolgan hamma foydalanuvchilar kutdi — oddiy "bugungi kurs" so'rovi ham.

libuv thread pool darsida pool to'lganda faqat fayl va DNS kutishini ko'rdik. Bu dars boshqa, xavfliroq holat haqida: asosiy thread'ning o'zi band. Bunda hech narsa ishlamaydi — taymerlar, tarmoq javoblari, hatto Ctrl+C ga javob ham kechikadi.

O'xshatish: bitta kassali do'kon. Kassir bitta mijozning 300 ta mahsulotini skanerlayapti. Navbatdagi "faqat non" olgan odam ham 10 daqiqa turadi. Kassir sekin emas — u shunchaki bitta.

Brauzerda buni Asosiy oqimni bo'shatish darsida ko'rgansiz: uzun task tugmalarni "muzlatadi". Node'da printsip xuddi shu, faqat muzlagan narsa — bitta foydalanuvchining ekrani emas, butun server.

2. Nima bloklaydi

2.1 To'rt asosiy sabab

CPU-bound (protsessorga bog'liq) ish — vaqtining asosiy qismi kutish emas, hisoblash bo'lgan ish. Uni await bilan "asinxron" qilib bo'lmaydi: hisobni kimdir bajarishi kerak, va u — asosiy thread.

Sabab Misol Nega bloklaydi
Og'ir sikl 400 mln Math.sqrt sof hisob
Katta JSON.parse / stringify 100 MB javob bitta sinxron chaqiruv, bo'linmaydi
ReDoS /^(a+)+$/ + yomon matn regex dvigateli eksponensial sinaydi
Sinxron I/O readFileSync serverda thread disk kutadi

Diagrammada asosiy thread'ga qarang: callback'lar navbatda turibdi, lekin ularni olib bajaradigan yagona thread band.

flowchart LR
  Q["navbat: taymer, HTTP javob,<br/>fayl tayyor"] -.->|kutadi| T
  T["asosiy thread<br/>JSON.parse 100 MB<br/>880 ms"]
  T -->|tugagach| N["keyingi callback"]

2.2 Katta JSON — o'lchov

JSON.parse — bitta funksiya chaqiruvi. U ichida to'xtab, "boshqalarga yo'l berib" bo'lmaydi. Biz CBU kurs yozuvini (6 maydonli obyekt) ko'paytirib, har xil hajmdagi JSON'ni o'qidik. Biz ishga tushirdik (Node 24.21.0, i5-12500H):

text
JSON.parse 10000 yozuv (1.0 MB): 9 ms
JSON.parse 100000 yozuv (10.3 MB): 72 ms
JSON.parse 1000000 yozuv (103.0 MB): 880 ms

Hajm 10 baravar oshsa, vaqt ham taxminan 10 baravar oshadi. 1 MB — sezilmaydi. 100 MB — deyarli bir soniya butun server qotadi. Katta ma'lumotni bo'laklab o'qish uchun oqimlar (stream) bor — ularni Stream nima darsidan boshlab o'rganasiz.

2.3 ReDoS — bitta regex serverni to'xtatadi

Regex amaliyotda va ReDoS darsida ReDoS (Regular expression Denial of Service — regex orqali xizmatni ishdan chiqarish) bilan tanishgansiz. Brauzerda u bitta tabni qotiradi. Serverda esa hamma foydalanuvchini. Ichma-ich + li naqsh mos kelmaydigan matnda hamma bo'linish variantini sinaydi. Biz ishga tushirdik (har n uchun 3 marta, mediana):

text
n=20: 9 ms
n=22: 53 ms
n=24: 171 ms
n=26: 640 ms
n=28: 2914 ms

Grafikda o'sish shaklini ko'ring — har 2 ta harf vaqtni taxminan 4 baravar oshiradi:

/^(a+)+$/ — 'a' × n + '!' ni tekshirish vaqti
vaqt, ms
2 91492028harflar soni (n)Node 24.21.0: 20 → 9 msNode 24.21.0: 22 → 53 msNode 24.21.0: 24 → 171 msNode 24.21.0: 26 → 640 msNode 24.21.0: 28 → 2 914 ms
/^(a+)+$/ — 'a' × n + '!' ni tekshirish vaqti
harflar soni (n)Node 24.21.0
209
2253
24171
26640
282 914

Manba: IlmHamroh yozuvchi o'lchovi: Node 24.21.0, Windows 11, i5-12500H, 3 marta mediana, 2026-10-23

30 harfli matn — bitta formadagi oddiy maydon. Hujumchi uni bir necha marta yuborsa, server daqiqalab band bo'ladi. Shuning uchun foydalanuvchi kiritgan matnga qo'llaniladigan regex'lar ehtiyotkorlik bilan yoziladi va matn uzunligi cheklanadi.

Tekshirib ko'ring: await JSON.parse(katta) deb yozilsa, loop bloklanmaydimi?

Javob

Bloklanadi. JSON.parse Promise qaytarmaydi — u sinxron. await oddiy qiymatni oladi va faqat keyingi qatorni microtask'ga suradi. Parse'ning o'zi baribir asosiy thread'da, bir bo'lakda bajariladi. await ishni boshqa joyga ko'chirmaydi.

3. Kechikishni o'lchash

3.1 G'oya: taymer qancha kechikdi

Event loop kechikishi (event loop lag / delay) — loop'ga qo'yilgan ish va uning haqiqiy bajarilishi orasidagi ortiqcha vaqt. 10 ms ga qo'yilgan taymer 300 ms da ishlasa, kechikish 290 ms.

Buni brauzerda ham, Node'da ham o'lchash mumkin. Quyidagi blok saytda ham ishlaydi. Har 10 ms da "yurak urishi" sanaladi, keyin loop ikki xil band qilinadi:

js
function busyWait(ms) {
  const end = Date.now() + ms;
  while (Date.now() < end) {} // CPU'ni band qiladi
}
const pause = () => new Promise((r) => setTimeout(r, 0));

let beats = 0;
const timer = setInterval(() => beats++, 10);

busyWait(300); // bitta bo'lak
await pause();
console.log(`bloklanganda yurak urishi: ${beats}`);

beats = 0;
for (let i = 0; i < 30; i++) {
  busyWait(10); // 30 ta kichik bo'lak
  await pause();
}
clearInterval(timer);
console.log(`bo'laklanganda ham urdimi: ${beats > 5}`);

Konsolda:

text
bloklanganda yurak urishi: 1
bo'laklanganda ham urdimi: true

300 ms davomida taymer 30 marta urishi kerak edi. U faqat bir marta urdi — loop bo'shagach, kechikkan bitta callback ishladi. Ikkinchi holatda ish hajmi bir xil (30 × 10 ms), lekin har bo'lakdan keyin loop taymerlarni bajarishga ulgurdi.

3.2 monitorEventLoopDelay — Node'ning o'lchagichi

Qo'lda sanash o'rniga Node'da tayyor vosita bor: perf_hooks modulidagi monitorEventLoopDelay. U fonda kechikishni yig'adi va gistogramma (qiymatlar taqsimoti) beradi: o'rtacha, p50, p99, max. p99 — o'lchovlarning 99 foizi shundan kichik bo'lgan qiymat; ya'ni "eng yomon 1 foiz" chegarasi.

Kanon tajribasi ikki usulni solishtirdi: bitta katta sikl (400 mln Math.sqrt) va o'sha ishning 40 bo'lagi (har biri 10 mln), orasida setImmediate:

js
import { monitorEventLoopDelay } from "node:perf_hooks";

function heavy(n) {
  let x = 0;
  for (let i = 0; i < n; i++) x += Math.sqrt(i);
  return x;
}
const ms = (ns) => (ns / 1e6).toFixed(1);

async function measure(name, work) {
  const h = monitorEventLoopDelay({ resolution: 1 });
  h.enable();
  const start = performance.now();
  await work();
  h.disable();
  const total = (performance.now() - start).toFixed(0);
  const p50 = ms(h.percentile(50));
  const p99 = ms(h.percentile(99));
  console.log(`${name}: jami ${total} ms, p50 ${p50}, ` +
    `p99 ${p99}, max ${ms(h.max)} ms`);
}

await measure("bitta sikl 4e8", async () => {
  await new Promise((r) => setTimeout(r, 10)); // isitish
  heavy(4e8);
  await new Promise((r) => setTimeout(r, 10));
});
await measure("40 bo'lak × 1e7", async () => {
  for (let i = 0; i < 40; i++) {
    heavy(1e7);
    await new Promise((r) => setImmediate(r));
  }
});

Kanonning haqiqiy chiqishi (Node 24.21.0, Windows 11, i5-12500H; qator uzunligi uchun format biroz qisqartirilgan, raqamlar xom fayldan):

text
bitta sikl 4e8: jami 1434 ms, p50 14.3, p99 1390.4, max 1390.4 ms
40 bo'lak × 1e7: jami 1453 ms, p50 35.4, p99 46.7, max 46.7 ms

Grafikda eng muhim ustunni solishtiring — eng katta kechikish:

Bir xil hisob: eng katta event loop kechikishi
  • bitta sikljami 1 434 ms1 390,4 ms
  • 40 bo'lak + setImmediatejami 1 453 ms46,7 ms

Manba: O'lchandi: IlmHamroh kanoni, monitorEventLoopDelay, Node 24.21.0, Windows 11, i5-12500H, 2026-10-23

Ikki xulosa:

  1. Jami vaqt deyarli bir xil (1 434 va 1 453 ms). Bo'laklash ishni tezlashtirmaydi — hatto biroz sekinlashtiradi (har bo'lakda loop aylanadi).
  2. Eng katta kechikish 30 baravar kamaydi (1 390 → 47 ms). Server endi 1,4 soniya emas, eng ko'pi 47 ms "qotadi". Shu vaqt ichida boshqa so'rovlar ham javob oladi.

Bo'lakli variantda p50 kattaroq (35 ms) — har bo'lak ~35 ms davom etgani uchun. Bo'lakni kichikroq qilsangiz, kechikish ham kamayadi, lekin ortiqcha xarajat oshadi. Bu — murosa.

3.3 ELU — loop qancha band

Ikkinchi o'lchagich — ELU (Event Loop Utilization — loop'ning bandlik ulushi). 0 — loop doim bo'sh kutyapti, 1 — bir lahza ham bo'sh emas. Biz ishga tushirdik:

js
import { performance } from "node:perf_hooks";

function heavy(n) {
  let x = 0;
  for (let i = 0; i < n; i++) x += Math.sqrt(i);
  return x;
}
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

const a = performance.eventLoopUtilization();
await sleep(200); // hech narsa qilmaymiz
const idle = performance.eventLoopUtilization(a);
console.log(`bo'sh: ELU ${idle.utilization.toFixed(2)}`);

const b = performance.eventLoopUtilization();
heavy(3e8);
await sleep(0);
const busy = performance.eventLoopUtilization(b);
console.log(`band: ELU ${busy.utilization.toFixed(2)}`);
text
bo'sh: ELU 0.04
band: ELU 0.99

Kechikish va ELU bir-birini to'ldiradi. Kechikish — "foydalanuvchi qancha kutdi", ELU — "server qanchalik to'lgan". ELU doim 0.9 dan yuqori bo'lsa — server yuklamaga dosh bermayapti, kechikish hali kichik bo'lsa ham.

Tekshirib ko'ring: Server p50 kechikishi 1 ms, p99 — 900 ms. Bu nimani bildiradi?

Javob

Odatda hammasi tez, lekin vaqti-vaqti bilan (1 foiz hollarda) loop deyarli bir soniya bloklanadi. Bu — kamdan-kam ishlaydigan og'ir yo'l belgisi: masalan katta hisobot, katta JSON yoki ReDoS'ga uchragan regex. O'rtacha qiymat buni yashirardi — shuning uchun p99 va max kuzatiladi.

4. Sinxron I/O'ni topish: --trace-sync-io

Sinxron fayl o'qish ham loop'ni bloklaydi. Dastur boshida (sozlamani o'qish) bu joiz — Blocking va non-blocking I/O darsida aytganimizdek, CLI va skriptlarda ham joiz. Lekin serverda, so'rov ishlovchisi ichida — yo'q. Begona kutubxona ichida yashiringan readFileSync ni qanday topasiz?

Node'da maxsus bayroq bor: --trace-sync-io. U dastur birinchi aylanishdan (tick) keyin ishlatilgan har sinxron chaqiruv uchun ogohlantirish va stek izini chiqaradi:

js
import { readFileSync } from "node:fs";

setTimeout(() => {
  readFileSync(import.meta.filename);
  console.log("o'qildi");
}, 10);

node --trace-sync-io sinxron.mjs — biz ishga tushirdik (yo'l qisqartirilgan):

text
(node:13348) WARNING: Detected use of sync API
    at openSync (node:fs:621:18)
    at readFileSync (node:fs:487:35)
    at file:///C:/Users/…/sinxron.mjs:4:3
    at listOnTimeout (node:internal/timers:685:17)

Tarjimasi: "Sinxron API ishlatilgani aniqlandi". Stek izida aybdor qator ko'rinadi. readFileSync ichida bir nechta sinxron chaqiruv bor (openSync, tryStatSync …), shuning uchun ogohlantirish bir necha marta chiqadi. Bu bayroq faqat tashxis uchun — production'da yoqilmaydi.

5. Oldini olish

5.1 Bo'laklash: setImmediate

Og'ir ishni kichik bo'laklarga bo'lamiz va har bo'lakdan keyin loop'ga bir aylanish beramiz. Node'da buning uchun eng mos vosita — setImmediate. U callback'ni check fazasiga qo'yadi — ya'ni loop avval kutayotgan I/O callback'larini bajaradi, keyin bizga qaytadi (Microtask, nextTick va setImmediate).

Quyidagi funksiya katta massivni 10 000 talik bo'laklarda qayta ishlaydi:

js
const yieldToLoop = () => new Promise((r) => setImmediate(r));

async function sumInChunks(items, chunkSize = 10_000) {
  let total = 0;
  for (let i = 0; i < items.length; i += chunkSize) {
    const end = Math.min(i + chunkSize, items.length);
    for (let j = i; j < end; j++) total += items[j];
    await yieldToLoop(); // boshqalarga navbat
  }
  return total;
}

const rates = Array.from({ length: 1_000_000 }, (_, i) => i % 100);
let ticks = 0;
const timer = setInterval(() => ticks++, 0);
console.log(await sumInChunks(rates));
clearInterval(timer);
console.log(`hisob paytida taymer ishladi: ${ticks > 0}`);

Konsolda:

text
49500000
hisob paytida taymer ishladi: true

Nega process.nextTick yoki Promise.resolve() bilan emas? Ular microtask — loop fazasiga qaytmasdan bajariladi. Har bo'lakdan keyin await Promise.resolve() qilsangiz, loop baribir bloklanadi: taymer va I/O microtask navbati bo'shamaguncha kutadi (Microtask, nextTick va setImmediate darsidagi "starvation"). Loop'ga haqiqatan yo'l berish uchun makrotask kerak: setImmediate yoki setTimeout.

Maslahat: setTimeout(r, 0) ham ishlaydi, lekin Node'da u kamida 1 ms kutadi va timers fazasiga qaraydi. Ko'p bo'lakli ishda bu sezilarli qo'shimcha vaqt beradi. setImmediate — Node'da bo'laklash uchun odatiy tanlov. Brauzerda setImmediate yo'q — u yerda scheduler.yield() (Asosiy oqimni bo'shatish).

5.2 Worker'ga chiqarish

Bo'laklash har doim ham mumkin emas. JSON.parse yoki tayyor kutubxona funksiyasini bo'lib bo'lmaydi. Bunday ishni boshqa thread'ga chiqarish kerak. Node'da buning uchun worker_threads moduli bor. Uni worker_threads darsida to'liq o'rganasiz; hozir g'oyani ko'rish uchun eng kichik misol:

js
import { Worker, isMainThread, parentPort, workerData }
  from "node:worker_threads";

function heavy(n) {
  let x = 0;
  for (let i = 0; i < n; i++) x += Math.sqrt(i);
  return x;
}

if (isMainThread) {
  let beats = 0; // hisob paytida loop necha marta "nafas oldi"
  const timer = setInterval(() => beats++, 10);
  const worker = new Worker(new URL(import.meta.url), {
    workerData: 3e8,
  });
  const result = await new Promise((resolve, reject) => {
    worker.once("message", resolve);
    worker.once("error", reject);
  });
  clearInterval(timer);
  console.log(`natija tayyor: ${result > 0}`);
  console.log(`hisob paytida loop ishladi: ${beats > 5}`);
} else {
  parentPort.postMessage(heavy(workerData));
}

Konsolda:

text
natija tayyor: true
hisob paytida loop ishladi: true

Bitta fayl ikki rolda ishlaydi: asosiy thread'da u worker'ni yaratadi va kutadi, worker ichida esa hisoblaydi. Hisob ~0,3 soniya davom etdi, lekin asosiy loop'ning taymeri shu vaqtda bemalol urib turdi. Brauzerdagi Web Workers bilan g'oya bir xil: alohida thread, ma'lumot xabar bilan almashinadi.

Worker bepul emas: uni ishga tushirish o'nlab millisekund va alohida xotira oladi. Shuning uchun bir martalik 5 ms li ish uchun worker yaratish ma'nosiz. Ko'p takrorlanadigan og'ir ish uchun esa oldindan yaratilgan worker'lar to'plami ishlatiladi (Worker pool: Piscina).

5.3 Qaysi birini tanlash

Vaziyat Yechim
Sikl, o'zingiz yozgan, bo'lish mumkin setImmediate bilan bo'laklash
Bo'linmaydigan chaqiruv (JSON.parse, kutubxona) worker yoki oqim (stream)
Katta fayl yoki javob oqim bilan bo'lak-bo'lak o'qish
Foydalanuvchi matni + regex uzunlikni cheklash, xavfsiz naqsh
Sinxron I/O serverda async variant (fs/promises)
Juda ko'p og'ir ish worker pool yoki alohida xizmat

6. CLI qadami: holat yo'q

valyuta CLI'ga bu darsdan kod qo'shilmaydi. Kanon buni o'lchab hal qildi:

  • CLI'ning eng og'ir ishi — CBU javobini (74 valyuta, 25.5 KB) JSON.parse qilish. 1 MB 9 ms o'qilsa, 25 KB — millisekunddan ham kam.
  • CLI'da bitta foydalanuvchi va bitta buyruq bor. Uni "bloklab" kutib qoladigan boshqa so'rov yo'q. Ctrl+C esa signal — u ham faqat uzoq sinxron ishda kechikadi, bizda bunday ish yo'q.

Keyinroq valyuta 1 million qatorli CSV'ni qayta ishlaydi (Amaliyot: katta faylni stream bilan) — u yerda xotira va kechikish muammosini oqimlar hal qiladi. Bo'laklash g'oyasi o'shanda yana uchraydi: oqim ham ma'lumotni bo'laklab, loop'ga navbat berib o'qiydi.

7. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
Server vaqti-vaqti bilan bir necha soniya javob bermaydi og'ir sinxron yo'l (hisobot, katta JSON) p99/max ni kuzating, ishni bo'laklang yoki worker'ga
async funksiya ichida sikl — baribir qotadi async kodni boshqa thread'ga o'tkazmaydi setImmediate bilan bo'laklang
Bo'laklar orasida await Promise.resolve() — yordam bermadi microtask loop fazasiga qaytmaydi setImmediate (makrotask)
Bitta regex CPU'ni 100% qildi ReDoS: ichma-ich takror + uzun matn naqshni tuzating, uzunlikni cheklang
O'rtacha javob vaqti yaxshi, foydalanuvchilar shikoyat qiladi o'rtacha qiymat dumni yashiradi p99 va max ni o'lchang

8. Mashqlar

Mashqlar kurs/mashqlar/24/06-bloklash/ da.

1-mashq (oson): bloklaydimi?

Har qator uchun javob bering: asosiy thread'ni uzoq bloklaydi — ha yoki yo'q.

  • await fetch(url) (javob 2 soniyada keladi) —
  • JSON.parse 200 MB matn —
  • await fsPromises.readFile("katta.csv") —
  • /^(\d+)+$/.test("1".repeat(40) + "x") —
  • for sikl, 1 milliard qadam, async funksiya ichida —
Yechim
  • yo'q — kutish OS'da, loop bo'sh.
  • ha — sinxron, bo'linmaydi (~1,7 soniya, «Katta JSON» o'lchoviga ko'ra).
  • yo'q — o'qish pool'da (libuv thread pool). Lekin natijani bitta katta satrga aylantirish va keyin uni qayta ishlash — yana asosiy thread.
  • ha — ReDoS: ichma-ich +, oxirida mos kelmaydigan belgi.
  • ha — async so'zi kodni asinxron qilmaydi; sikl o'rtasida await yo'q.

2-mashq (o'rta): bo'lakli map

mapInChunks(items, fn, chunkSize) funksiyasini yozing: massivning har elementiga fn ni qo'llaydi, har chunkSize elementdan keyin loop'ga navbat beradi (setImmediate). Natija — yangi massiv. 250 000 ta so'm summasini (i * 1000) dollarga o'giring (kurs 11 903.15, 2 xonagacha yaxlitlang) va natijaning uzunligini, oxirgi elementini va loop necha marta navbat olganini chiqaring.

Yechim
js
const yieldToLoop = () => new Promise((r) => setImmediate(r));

async function mapInChunks(items, fn, chunkSize = 10_000) {
  const out = new Array(items.length);
  let yields = 0;
  for (let i = 0; i < items.length; i += chunkSize) {
    const end = Math.min(i + chunkSize, items.length);
    for (let j = i; j < end; j++) out[j] = fn(items[j]);
    await yieldToLoop();
    yields++;
  }
  return { out, yields };
}

const soms = Array.from({ length: 250_000 }, (_, i) => i * 1000);
const toUsd = (som) => Math.round((som / 11903.15) * 100) / 100;
const { out, yields } = await mapInChunks(soms, toUsd);
console.log(out.length, out.at(-1), yields);

Konsolda:

text
250000 21002.76 25

250 000 ÷ 10 000 = 25 bo'lak — loop 25 marta navbat oldi. out oldindan to'liq uzunlikda yaratilgan, shuning uchun massiv har qadamda kengaymaydi.

3-mashq (qiyin): kechikish soqchisi

watchLag(limitMs) funksiyasini yozing: monitorEventLoopDelay ni yoqadi va qaytargan stop() chaqirilganda { maxMs, ok } beradi (ok — max chegaradan kichikmi). Uni ikki holatda sinang: (a) 300 ms busyWait, (b) 30 × 10 ms busyWait orasida setImmediate. Chegara — 100 ms. maxMs ni chiqarmang (har safar boshqa), faqat ok ni.

Yechim
js
import { monitorEventLoopDelay } from "node:perf_hooks";

const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

async function watchLag(limitMs) {
  const h = monitorEventLoopDelay({ resolution: 5 });
  h.enable();
  await sleep(20); // o'lchagich "isinib" olsin
  return async () => {
    await sleep(20); // oxirgi kechikish ham yozilsin
    h.disable();
    const maxMs = h.max / 1e6;
    return { maxMs, ok: maxMs < limitMs };
  };
}
function busyWait(ms) {
  const end = Date.now() + ms;
  while (Date.now() < end) {}
}

let stop = await watchLag(100);
busyWait(300);
console.log(`bitta bo'lak: ok=${(await stop()).ok}`);

stop = await watchLag(100);
for (let i = 0; i < 30; i++) {
  busyWait(10);
  await sleep(0);
}
console.log(`30 bo'lak: ok=${(await stop()).ok}`);

Konsolda:

text
bitta bo'lak: ok=false
30 bo'lak: ok=true

Gistogramma kechikishni o'zining ichki taymeri orqali o'lchaydi: taymer resolution (bu yerda 5 ms) oralig'ida uyg'onishi kerak. 300 ms blokda u uyg'ona olmaydi — max ~300 ms. Bo'laklarda esa eng ko'pi ~10 ms. Ikkita sleep(20) ga e'tibor bering. Biz birinchi variantni ularsiz yozdik va bitta bo'lak ham ok=true chiqdi (max 15 ms). Sabab: o'lchagich taymeri birinchi urishigacha hech narsa yozmaydi, blok esa aynan shu oraliqqa tushgan edi. Saboq: o'lchagichni ishdan oldin yoqing va unga bir-ikki urishga vaqt bering. Production'da xuddi shunday soqchi har daqiqada p99 ni logga yozadi va chegaradan oshsa ogohlantiradi.

9. Real ishda

  • Express va boshqa serverlar — Express'ga kirish darslarida ko'rasiz: bitta ishlovchidagi og'ir sinxron kod butun serverni sekinlashtiradi. Ko'p jamoalarda "ishlovchida 50 ms dan ortiq sinxron ish yo'q" degan qoida bor.
  • Monitoring — APM vositalari (Datadog, New Relic, OpenTelemetry) event loop kechikishini alohida grafikda ko'rsatadi. Uning ostida aynan shu monitorEventLoopDelay.
  • Health check — ba'zi serverlar kechikish 1 soniyadan oshsa o'zini "kasal" deb e'lon qiladi va yuk balanslovchi unga so'rov yubormay turadi.
  • Intervyu savoli: "Node'da CPU-og'ir ishni qanday qilasiz?" — javob: bo'laklash (setImmediate), worker_threads, alohida xizmat; va avval o'lchash (monitorEventLoopDelay, profiling — CPU profiling).

Xulosa

  • Node'da JS — bitta thread: og'ir sinxron ish (sikl, katta JSON.parse, ReDoS, sinxron I/O) butun serverni to'xtatadi.
  • O'lchash: monitorEventLoopDelay (p50, p99, max) va ELU; o'rtacha emas, dumga qarang.
  • --trace-sync-io yashiringan sinxron chaqiruvlarni stek izi bilan ko'rsatadi.
  • Bo'laklash (setImmediate) jami vaqtni o'zgartirmaydi, eng katta kechikishni keskin kamaytiradi (1 390 → 47 ms).
  • Bo'linmaydigan ish — worker'ga; microtask (Promise.resolve) loop'ga navbat bermaydi.

Keyingi dars: Global obyektlar: Node va brauzer farqi — Node'da window yo'q, lekin globalThis, process, Buffer va ko'plab veb API'lar bor.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Event loop'ni bloklash: aniqlash, o'lchash va oldini olish — IlmHamroh