Skip to content

Commit ed23615

Browse files
authored
perf(webapp): add event loop utilization metric (#2471)
* perf(webapp): add event loop utilization metric * add event loop utilization logging as well
1 parent 436d951 commit ed23615

File tree

3 files changed

+55
-0
lines changed

3 files changed

+55
-0
lines changed

apps/webapp/app/env.server.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1167,6 +1167,8 @@ const EnvironmentSchema = z
11671167
AI_RUN_FILTER_MODEL: z.string().optional(),
11681168

11691169
EVENT_LOOP_MONITOR_THRESHOLD_MS: z.coerce.number().int().default(100),
1170+
EVENT_LOOP_MONITOR_UTILIZATION_INTERVAL_MS: z.coerce.number().int().default(1000),
1171+
EVENT_LOOP_MONITOR_UTILIZATION_SAMPLE_RATE: z.coerce.number().default(0.05),
11701172

11711173
VERY_SLOW_QUERY_THRESHOLD_MS: z.coerce.number().int().optional(),
11721174
})

apps/webapp/app/eventLoopMonitor.server.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { singleton } from "./utils/singleton";
33
import { tracer } from "./v3/tracer.server";
44
import { env } from "./env.server";
55
import { context, Context } from "@opentelemetry/api";
6+
import { performance } from "node:perf_hooks";
7+
import { logger } from "./services/logger.server";
68

79
const THRESHOLD_NS = env.EVENT_LOOP_MONITOR_THRESHOLD_MS * 1e6;
810

@@ -69,16 +71,46 @@ function after(asyncId: number) {
6971
export const eventLoopMonitor = singleton("eventLoopMonitor", () => {
7072
const hook = createHook({ init, before, after, destroy });
7173

74+
let stopEventLoopUtilizationMonitoring: () => void;
75+
7276
return {
7377
enable: () => {
7478
console.log("🥸 Initializing event loop monitor");
7579

7680
hook.enable();
81+
82+
stopEventLoopUtilizationMonitoring = startEventLoopUtilizationMonitoring();
7783
},
7884
disable: () => {
7985
console.log("🥸 Disabling event loop monitor");
8086

8187
hook.disable();
88+
89+
stopEventLoopUtilizationMonitoring?.();
8290
},
8391
};
8492
});
93+
94+
function startEventLoopUtilizationMonitoring() {
95+
let lastEventLoopUtilization = performance.eventLoopUtilization();
96+
97+
const interval = setInterval(() => {
98+
const currentEventLoopUtilization = performance.eventLoopUtilization();
99+
100+
const diff = performance.eventLoopUtilization(
101+
currentEventLoopUtilization,
102+
lastEventLoopUtilization
103+
);
104+
const utilization = Number.isFinite(diff.utilization) ? diff.utilization : 0;
105+
106+
if (Math.random() < env.EVENT_LOOP_MONITOR_UTILIZATION_SAMPLE_RATE) {
107+
logger.info("nodejs.event_loop.utilization", { utilization });
108+
}
109+
110+
lastEventLoopUtilization = currentEventLoopUtilization;
111+
}, env.EVENT_LOOP_MONITOR_UTILIZATION_INTERVAL_MS);
112+
113+
return () => {
114+
clearInterval(interval);
115+
};
116+
}

apps/webapp/app/v3/tracer.server.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import { flattenAttributes } from "@trigger.dev/core/v3";
5757
import { prisma } from "~/db.server";
5858
import { metricsRegister } from "~/metrics.server";
5959
import type { Prisma } from "@trigger.dev/database";
60+
import { performance } from "node:perf_hooks";
6061

6162
export const SEMINTATTRS_FORCE_RECORDING = "forceRecording";
6263

@@ -602,10 +603,17 @@ function configureNodejsMetrics({ meter }: { meter: Meter }) {
602603
description: "Event loop 99th percentile delay",
603604
unit: "s",
604605
});
606+
// ELU observable gauge (unit is a ratio, 0..1)
607+
const eluGauge = meter.createObservableGauge("nodejs.event_loop.utilization", {
608+
description: "Event loop utilization over the last collection interval",
609+
unit: "1", // OpenTelemetry convention for ratios
610+
});
605611

606612
// Get UV threadpool size (defaults to 4 if not set)
607613
const uvThreadpoolSize = parseInt(process.env.UV_THREADPOOL_SIZE || "4", 10);
608614

615+
let lastEventLoopUtilization = performance.eventLoopUtilization();
616+
609617
// Single helper to read metrics from prom-client
610618
async function readNodeMetrics() {
611619
const metrics = await metricsRegister.getMetricsAsJSON();
@@ -648,6 +656,16 @@ function configureNodejsMetrics({ meter }: { meter: Meter }) {
648656
}
649657
}
650658

659+
const currentEventLoopUtilization = performance.eventLoopUtilization();
660+
// Diff over [lastSnapshot, current]
661+
const diff = performance.eventLoopUtilization(
662+
currentEventLoopUtilization,
663+
lastEventLoopUtilization
664+
);
665+
666+
// diff.utilization is between 0 and 1 (fraction of time "active")
667+
const utilization = Number.isFinite(diff.utilization) ? diff.utilization : 0;
668+
651669
return {
652670
threadpoolSize: uvThreadpoolSize,
653671
handlesByType,
@@ -661,6 +679,7 @@ function configureNodejsMetrics({ meter }: { meter: Meter }) {
661679
p50: eventLoopLagP50?.values?.[0]?.value ?? 0,
662680
p90: eventLoopLagP90?.values?.[0]?.value ?? 0,
663681
p99: eventLoopLagP99?.values?.[0]?.value ?? 0,
682+
utilization,
664683
},
665684
};
666685
}
@@ -698,6 +717,7 @@ function configureNodejsMetrics({ meter }: { meter: Meter }) {
698717
res.observe(eventLoopLagP50Gauge, eventLoop.p50);
699718
res.observe(eventLoopLagP90Gauge, eventLoop.p90);
700719
res.observe(eventLoopLagP99Gauge, eventLoop.p99);
720+
res.observe(eluGauge, eventLoop.utilization);
701721
},
702722
[
703723
uvThreadpoolSizeGauge,
@@ -711,6 +731,7 @@ function configureNodejsMetrics({ meter }: { meter: Meter }) {
711731
eventLoopLagP50Gauge,
712732
eventLoopLagP90Gauge,
713733
eventLoopLagP99Gauge,
734+
eluGauge,
714735
]
715736
);
716737
}

0 commit comments

Comments
 (0)