




















@@ -1,6 +1,9 @@
11import type { monitorEventLoopDelay, performance } from "node:perf_hooks";
22import { describe, expect, it, vi } from "vitest";
3-import { createGatewayEventLoopHealthMonitor } from "./event-loop-health.js";
3+import {
4+classifyGatewayEventLoopHealthReasons,
5+createGatewayEventLoopHealthMonitor,
6+} from "./event-loop-health.js";
4758type CpuUsage = ReturnType<typeof process.cpuUsage>;
69type DelayMonitor = ReturnType<typeof monitorEventLoopDelay>;
@@ -19,7 +22,10 @@ function createMonitorHarness(params?: { cpuMsPerWallMs?: number; utilization?:
1922const delayMonitor = {
2023enable: vi.fn(),
2124disable: vi.fn(),
22-reset: vi.fn(),
25+reset: vi.fn(() => {
26+delayP99Ms = 0;
27+delayMaxMs = 0;
28+}),
2329percentile: vi.fn(() => delayP99Ms * 1_000_000),
2430get max() {
2531return delayMaxMs * 1_000_000;
@@ -59,6 +65,7 @@ function createMonitorHarness(params?: { cpuMsPerWallMs?: number; utilization?:
59656066return {
6167 monitor,
68+ delayMonitor,
6269 cpuUsage,
6370 eventLoopUtilization,
6471setNow: (value: number) => {
@@ -71,8 +78,92 @@ function createMonitorHarness(params?: { cpuMsPerWallMs?: number; utilization?:
7178};
7279}
738081+describe("classifyGatewayEventLoopHealthReasons", () => {
82+it("does not degrade on utilization or CPU from a sub-second sample", () => {
83+expect(
84+classifyGatewayEventLoopHealthReasons({
85+intervalMs: 250,
86+delayP99Ms: 20,
87+delayMaxMs: 25,
88+utilization: 1,
89+cpuCoreRatio: 1,
90+}),
91+).toEqual([]);
92+});
93+94+it("does not degrade on utilization or CPU without delay co-evidence", () => {
95+expect(
96+classifyGatewayEventLoopHealthReasons({
97+intervalMs: 1_000,
98+delayP99Ms: 0,
99+delayMaxMs: 0,
100+utilization: 1,
101+cpuCoreRatio: 1,
102+}),
103+).toEqual([]);
104+});
105+106+it("degrades on utilization and CPU after a sustained sample window with delay co-evidence", () => {
107+expect(
108+classifyGatewayEventLoopHealthReasons({
109+intervalMs: 1_000,
110+delayP99Ms: 20,
111+delayMaxMs: 25,
112+utilization: 0.99,
113+cpuCoreRatio: 0.95,
114+}),
115+).toEqual(["event_loop_utilization", "cpu"]);
116+});
117+118+it.each([
119+{
120+cpuCoreRatio: 0.1,
121+expected: ["event_loop_utilization"],
122+name: "utilization only",
123+utilization: 0.99,
124+},
125+{
126+cpuCoreRatio: 0.95,
127+expected: ["cpu"],
128+name: "CPU only",
129+utilization: 0.1,
130+},
131+{
132+cpuCoreRatio: 0.1,
133+expected: [],
134+name: "neither load counter",
135+utilization: 0.1,
136+},
137+] as const)(
138+"classifies delay-backed sustained load when $name is saturated",
139+({ cpuCoreRatio, expected, utilization }) => {
140+expect(
141+classifyGatewayEventLoopHealthReasons({
142+intervalMs: 1_000,
143+delayP99Ms: 30,
144+delayMaxMs: 0,
145+ utilization,
146+ cpuCoreRatio,
147+}),
148+).toEqual(expected);
149+},
150+);
151+152+it("still degrades on event-loop delay from a short sample", () => {
153+expect(
154+classifyGatewayEventLoopHealthReasons({
155+intervalMs: 250,
156+delayP99Ms: 20,
157+delayMaxMs: 1_500,
158+utilization: 0.1,
159+cpuCoreRatio: 0.1,
160+}),
161+).toEqual(["event_loop_delay"]);
162+});
163+});
164+74165describe("createGatewayEventLoopHealthMonitor", () => {
75-it("waits for a sustained sample window before reporting CPU-only saturation", () => {
166+it("waits for delay co-evidence before reporting load-only saturation", () => {
76167const harness = createMonitorHarness();
7716878169harness.setNow(42);
@@ -81,11 +172,27 @@ describe("createGatewayEventLoopHealthMonitor", () => {
81172expect(harness.eventLoopUtilization).toHaveBeenCalledTimes(1);
8217383174harness.setNow(1_000);
175+expect(harness.monitor.snapshot()).toMatchObject({
176+degraded: false,
177+reasons: [],
178+intervalMs: 1_000,
179+delayP99Ms: 0,
180+delayMaxMs: 0,
181+utilization: 1,
182+cpuCoreRatio: 1,
183+});
184+});
185+186+it("reports CPU and utilization saturation when delay co-evidence is present", () => {
187+const harness = createMonitorHarness();
188+harness.setDelay({ p99Ms: 30 });
189+harness.setNow(1_000);
190+84191expect(harness.monitor.snapshot()).toMatchObject({
85192degraded: true,
86193reasons: ["event_loop_utilization", "cpu"],
87194intervalMs: 1_000,
88-delayP99Ms: 0,
195+delayP99Ms: 30,
89196delayMaxMs: 0,
90197utilization: 1,
91198cpuCoreRatio: 1,
@@ -118,4 +225,61 @@ describe("createGatewayEventLoopHealthMonitor", () => {
118225cpuCoreRatio: 0.1,
119226});
120227});
228+229+it("keeps rate baselines and the last snapshot until a full sample window is available", () => {
230+const harness = createMonitorHarness({ cpuMsPerWallMs: 0.1, utilization: 0.2 });
231+harness.setNow(1_000);
232+const first = harness.monitor.snapshot();
233+234+expect(first).toEqual(expect.objectContaining({ intervalMs: 1_000 }));
235+expect(harness.cpuUsage).toHaveBeenCalledTimes(3);
236+expect(harness.eventLoopUtilization).toHaveBeenCalledTimes(3);
237+238+harness.setNow(1_250);
239+expect(harness.monitor.snapshot()).toBe(first);
240+expect(harness.cpuUsage).toHaveBeenCalledTimes(3);
241+expect(harness.eventLoopUtilization).toHaveBeenCalledTimes(3);
242+243+harness.setNow(2_000);
244+const second = harness.monitor.snapshot();
245+246+expect(second).toEqual(expect.objectContaining({ intervalMs: 1_000 }));
247+expect(second).not.toBe(first);
248+});
249+250+it("preserves moderate delay co-evidence across rapid probes until the load window completes", () => {
251+const harness = createMonitorHarness();
252+harness.setNow(1_000);
253+const first = harness.monitor.snapshot();
254+255+harness.setDelay({ p99Ms: 30 });
256+harness.setNow(1_250);
257+expect(harness.monitor.snapshot()).toBe(first);
258+259+harness.setNow(2_000);
260+expect(harness.monitor.snapshot()).toMatchObject({
261+degraded: true,
262+reasons: ["event_loop_utilization", "cpu"],
263+intervalMs: 1_000,
264+delayP99Ms: 30,
265+delayMaxMs: 0,
266+utilization: 1,
267+cpuCoreRatio: 1,
268+});
269+});
270+271+it("clears the cached snapshot when stopped", () => {
272+const harness = createMonitorHarness({ cpuMsPerWallMs: 0.1, utilization: 0.2 });
273+harness.setNow(1_000);
274+275+expect(harness.monitor.snapshot()).toEqual(
276+expect.objectContaining({ degraded: false, intervalMs: 1_000 }),
277+);
278+279+harness.setNow(1_250);
280+harness.monitor.stop();
281+282+expect(harness.delayMonitor.disable).toHaveBeenCalledTimes(1);
283+expect(harness.monitor.snapshot()).toBeUndefined();
284+});
121285});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。