Skip to content

Commit fcf3784

Browse files
Merge pull request #6107 from cloudflare/pbacondarwin/improve-perf-hooks-compat
fix(perf_hooks): improve Node.js perf_hooks compatibility
2 parents 7ae2f9d + 15a98f3 commit fcf3784

6 files changed

Lines changed: 459 additions & 22 deletions

File tree

src/node/perf_hooks.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,64 @@ import { ERR_METHOD_NOT_IMPLEMENTED } from 'node-internal:internal_errors';
77
import { default as util } from 'node-internal:util';
88

99
export const constants = Object.freeze({
10+
// GC type constants
1011
NODE_PERFORMANCE_GC_MAJOR: 4,
1112
NODE_PERFORMANCE_GC_MINOR: 1,
1213
NODE_PERFORMANCE_GC_INCREMENTAL: 8,
1314
NODE_PERFORMANCE_GC_WEAKCB: 16,
15+
16+
// GC flags constants
1417
NODE_PERFORMANCE_GC_FLAGS_NO: 0,
1518
NODE_PERFORMANCE_GC_FLAGS_CONSTRUCT_RETAINED: 2,
1619
NODE_PERFORMANCE_GC_FLAGS_FORCED: 4,
1720
NODE_PERFORMANCE_GC_FLAGS_SYNCHRONOUS_PHANTOM_PROCESSING: 8,
1821
NODE_PERFORMANCE_GC_FLAGS_ALL_AVAILABLE_GARBAGE: 16,
1922
NODE_PERFORMANCE_GC_FLAGS_ALL_EXTERNAL_MEMORY: 32,
2023
NODE_PERFORMANCE_GC_FLAGS_SCHEDULE_IDLE: 64,
24+
25+
// Entry type constants
26+
NODE_PERFORMANCE_ENTRY_TYPE_GC: 0,
27+
NODE_PERFORMANCE_ENTRY_TYPE_HTTP: 1,
28+
NODE_PERFORMANCE_ENTRY_TYPE_HTTP2: 2,
29+
NODE_PERFORMANCE_ENTRY_TYPE_NET: 3,
30+
NODE_PERFORMANCE_ENTRY_TYPE_DNS: 4,
31+
32+
// Milestone constants
33+
NODE_PERFORMANCE_MILESTONE_TIME_ORIGIN_TIMESTAMP: 0,
34+
NODE_PERFORMANCE_MILESTONE_TIME_ORIGIN: 1,
35+
NODE_PERFORMANCE_MILESTONE_ENVIRONMENT: 2,
36+
NODE_PERFORMANCE_MILESTONE_NODE_START: 3,
37+
NODE_PERFORMANCE_MILESTONE_V8_START: 4,
38+
NODE_PERFORMANCE_MILESTONE_LOOP_START: 5,
39+
NODE_PERFORMANCE_MILESTONE_LOOP_EXIT: 6,
40+
NODE_PERFORMANCE_MILESTONE_BOOTSTRAP_COMPLETE: 7,
2141
});
2242

43+
// Type definitions for Node.js-specific extensions
44+
export interface EventLoopUtilization {
45+
idle: number;
46+
active: number;
47+
utilization: number;
48+
}
49+
50+
// Standalone function exports for Node.js compatibility
51+
export function eventLoopUtilization(
52+
_utilization1?: EventLoopUtilization,
53+
_utilization2?: EventLoopUtilization
54+
): EventLoopUtilization {
55+
// Return stub values - actual event loop utilization is not available in workerd
56+
return { idle: 0, active: 0, utilization: 0 };
57+
}
58+
59+
export function timerify<T extends (...params: unknown[]) => unknown>(
60+
fn: T
61+
): T {
62+
// Return the function as-is - timing wrapper is not implemented in workerd
63+
return fn;
64+
}
65+
66+
// Re-export globalThis.performance which includes nodeTiming when the
67+
// enable_nodejs_perf_hooks_module flag is enabled (handled in C++).
2368
export const performance = globalThis.performance;
2469

2570
export function createHistogram(): void {
@@ -51,6 +96,8 @@ export default {
5196
PerformanceResourceTiming,
5297
monitorEventLoopDelay,
5398
createHistogram,
99+
eventLoopUtilization,
100+
timerify,
54101
performance,
55102
constants,
56103
};

src/workerd/api/node/tests/perf-hooks-nodejs-test.js

Lines changed: 224 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import {
99
PerformanceResourceTiming,
1010
createHistogram,
1111
monitorEventLoopDelay,
12+
eventLoopUtilization,
13+
timerify,
1214
constants,
1315
} from 'node:perf_hooks';
1416
import {
@@ -32,6 +34,8 @@ export const testPerformanceExports = {
3234
ok(PerformanceResourceTiming);
3335
ok(createHistogram);
3436
ok(monitorEventLoopDelay);
37+
ok(eventLoopUtilization);
38+
ok(timerify);
3539
ok(constants);
3640

3741
// Test that performance is an instance of Performance
@@ -42,17 +46,37 @@ export const testPerformanceExports = {
4246
export const testPerformanceConstants = {
4347
test() {
4448
deepStrictEqual(constants, {
45-
NODE_PERFORMANCE_GC_FLAGS_ALL_AVAILABLE_GARBAGE: 16,
46-
NODE_PERFORMANCE_GC_FLAGS_ALL_EXTERNAL_MEMORY: 32,
47-
NODE_PERFORMANCE_GC_FLAGS_CONSTRUCT_RETAINED: 2,
48-
NODE_PERFORMANCE_GC_FLAGS_FORCED: 4,
49-
NODE_PERFORMANCE_GC_FLAGS_NO: 0,
50-
NODE_PERFORMANCE_GC_FLAGS_SCHEDULE_IDLE: 64,
51-
NODE_PERFORMANCE_GC_FLAGS_SYNCHRONOUS_PHANTOM_PROCESSING: 8,
52-
NODE_PERFORMANCE_GC_INCREMENTAL: 8,
49+
// GC type constants
5350
NODE_PERFORMANCE_GC_MAJOR: 4,
5451
NODE_PERFORMANCE_GC_MINOR: 1,
52+
NODE_PERFORMANCE_GC_INCREMENTAL: 8,
5553
NODE_PERFORMANCE_GC_WEAKCB: 16,
54+
55+
// GC flags constants
56+
NODE_PERFORMANCE_GC_FLAGS_NO: 0,
57+
NODE_PERFORMANCE_GC_FLAGS_CONSTRUCT_RETAINED: 2,
58+
NODE_PERFORMANCE_GC_FLAGS_FORCED: 4,
59+
NODE_PERFORMANCE_GC_FLAGS_SYNCHRONOUS_PHANTOM_PROCESSING: 8,
60+
NODE_PERFORMANCE_GC_FLAGS_ALL_AVAILABLE_GARBAGE: 16,
61+
NODE_PERFORMANCE_GC_FLAGS_ALL_EXTERNAL_MEMORY: 32,
62+
NODE_PERFORMANCE_GC_FLAGS_SCHEDULE_IDLE: 64,
63+
64+
// Entry type constants
65+
NODE_PERFORMANCE_ENTRY_TYPE_GC: 0,
66+
NODE_PERFORMANCE_ENTRY_TYPE_HTTP: 1,
67+
NODE_PERFORMANCE_ENTRY_TYPE_HTTP2: 2,
68+
NODE_PERFORMANCE_ENTRY_TYPE_NET: 3,
69+
NODE_PERFORMANCE_ENTRY_TYPE_DNS: 4,
70+
71+
// Milestone constants
72+
NODE_PERFORMANCE_MILESTONE_TIME_ORIGIN_TIMESTAMP: 0,
73+
NODE_PERFORMANCE_MILESTONE_TIME_ORIGIN: 1,
74+
NODE_PERFORMANCE_MILESTONE_ENVIRONMENT: 2,
75+
NODE_PERFORMANCE_MILESTONE_NODE_START: 3,
76+
NODE_PERFORMANCE_MILESTONE_V8_START: 4,
77+
NODE_PERFORMANCE_MILESTONE_LOOP_START: 5,
78+
NODE_PERFORMANCE_MILESTONE_LOOP_EXIT: 6,
79+
NODE_PERFORMANCE_MILESTONE_BOOTSTRAP_COMPLETE: 7,
5680
});
5781
ok(Object.isFrozen(constants));
5882
},
@@ -80,13 +104,205 @@ export const testPerformanceBasicFunctionality = {
80104

81105
export const testUnimplementedMethods = {
82106
test() {
107+
// These methods throw because they cannot be meaningfully stubbed
83108
throws(() => createHistogram(), { message: /is not implemented/ });
84109
throws(() => monitorEventLoopDelay(), {
85110
message: /is not implemented/,
86111
});
87112
},
88113
};
89114

115+
export const testStandaloneEventLoopUtilization = {
116+
test() {
117+
// eventLoopUtilization returns stub values for compatibility
118+
const result = eventLoopUtilization();
119+
ok(typeof result === 'object', 'should return an object');
120+
strictEqual(result.idle, 0, 'idle should be 0');
121+
strictEqual(result.active, 0, 'active should be 0');
122+
strictEqual(result.utilization, 0, 'utilization should be 0');
123+
124+
// Should accept optional parameters without error
125+
const result2 = eventLoopUtilization(result);
126+
ok(
127+
typeof result2 === 'object',
128+
'should return an object with one argument'
129+
);
130+
131+
const result3 = eventLoopUtilization(result, result2);
132+
ok(
133+
typeof result3 === 'object',
134+
'should return an object with two arguments'
135+
);
136+
},
137+
};
138+
139+
export const testStandaloneTimerify = {
140+
test() {
141+
// timerify returns the function as-is (stub behavior)
142+
const testFn = () => 42;
143+
const timerified = timerify(testFn);
144+
145+
strictEqual(timerified, testFn, 'timerify should return the same function');
146+
strictEqual(timerified(), 42, 'timerified function should work correctly');
147+
},
148+
};
149+
150+
export const testPerformanceNodeTiming = {
151+
test() {
152+
// nodeTiming is a Node.js-specific property available on both
153+
// the perf_hooks performance export and globalThis.performance
154+
ok(
155+
perfHooksPerformance.nodeTiming,
156+
'nodeTiming should exist on perf_hooks'
157+
);
158+
ok(
159+
globalThis.performance.nodeTiming,
160+
'nodeTiming should exist on globalThis'
161+
);
162+
163+
const nodeTiming = perfHooksPerformance.nodeTiming;
164+
strictEqual(nodeTiming.name, 'node', 'name should be "node"');
165+
strictEqual(nodeTiming.entryType, 'node', 'entryType should be "node"');
166+
strictEqual(
167+
typeof nodeTiming.startTime,
168+
'number',
169+
'startTime should be a number'
170+
);
171+
strictEqual(
172+
typeof nodeTiming.duration,
173+
'number',
174+
'duration should be a number'
175+
);
176+
strictEqual(
177+
typeof nodeTiming.nodeStart,
178+
'number',
179+
'nodeStart should be a number'
180+
);
181+
strictEqual(
182+
typeof nodeTiming.v8Start,
183+
'number',
184+
'v8Start should be a number'
185+
);
186+
strictEqual(
187+
typeof nodeTiming.bootstrapComplete,
188+
'number',
189+
'bootstrapComplete should be a number'
190+
);
191+
strictEqual(
192+
typeof nodeTiming.environment,
193+
'number',
194+
'environment should be a number'
195+
);
196+
strictEqual(
197+
typeof nodeTiming.loopStart,
198+
'number',
199+
'loopStart should be a number'
200+
);
201+
strictEqual(
202+
typeof nodeTiming.loopExit,
203+
'number',
204+
'loopExit should be a number'
205+
);
206+
strictEqual(
207+
typeof nodeTiming.idleTime,
208+
'number',
209+
'idleTime should be a number'
210+
);
211+
212+
// uvMetricsInfo should return an object with libuv metrics (stub values)
213+
ok(
214+
typeof nodeTiming.uvMetricsInfo === 'object',
215+
'uvMetricsInfo should be an object'
216+
);
217+
strictEqual(
218+
nodeTiming.uvMetricsInfo.loopCount,
219+
0,
220+
'uvMetricsInfo.loopCount should be 0'
221+
);
222+
strictEqual(
223+
nodeTiming.uvMetricsInfo.events,
224+
0,
225+
'uvMetricsInfo.events should be 0'
226+
);
227+
strictEqual(
228+
nodeTiming.uvMetricsInfo.eventsWaiting,
229+
0,
230+
'uvMetricsInfo.eventsWaiting should be 0'
231+
);
232+
233+
// Verify that nodeTiming properties are instance (own) properties, not prototype properties
234+
// This matches Node.js behavior where Reflect.ownKeys(performance.nodeTiming) includes
235+
// all properties like nodeStart, v8Start, etc.
236+
const ownKeys = Reflect.ownKeys(nodeTiming);
237+
ok(ownKeys.includes('nodeStart'), 'nodeStart should be an own property');
238+
ok(ownKeys.includes('v8Start'), 'v8Start should be an own property');
239+
ok(
240+
ownKeys.includes('bootstrapComplete'),
241+
'bootstrapComplete should be an own property'
242+
);
243+
ok(
244+
ownKeys.includes('environment'),
245+
'environment should be an own property'
246+
);
247+
ok(ownKeys.includes('loopStart'), 'loopStart should be an own property');
248+
ok(ownKeys.includes('loopExit'), 'loopExit should be an own property');
249+
ok(ownKeys.includes('idleTime'), 'idleTime should be an own property');
250+
ok(
251+
ownKeys.includes('uvMetricsInfo'),
252+
'uvMetricsInfo should be an own property'
253+
);
254+
255+
// Verify prototype only has constructor and toJSON (matching Node.js behavior)
256+
const protoKeys = Reflect.ownKeys(Object.getPrototypeOf(nodeTiming));
257+
ok(protoKeys.includes('constructor'), 'prototype should have constructor');
258+
ok(protoKeys.includes('toJSON'), 'prototype should have toJSON');
259+
260+
// toJSON should work and include uvMetricsInfo
261+
ok(typeof nodeTiming.toJSON === 'function', 'toJSON should be a function');
262+
const json = nodeTiming.toJSON();
263+
ok(typeof json === 'object', 'toJSON should return an object');
264+
ok(
265+
typeof json.uvMetricsInfo === 'object',
266+
'toJSON should include uvMetricsInfo'
267+
);
268+
},
269+
};
270+
271+
export const testPerformanceEventLoopUtilization = {
272+
test() {
273+
// performance.eventLoopUtilization() should return stub values (not throw)
274+
const result = perfHooksPerformance.eventLoopUtilization();
275+
ok(typeof result === 'object', 'should return an object');
276+
strictEqual(result.idle, 0, 'idle should be 0');
277+
strictEqual(result.active, 0, 'active should be 0');
278+
strictEqual(result.utilization, 0, 'utilization should be 0');
279+
},
280+
};
281+
282+
export const testPerformanceTimerify = {
283+
test() {
284+
// performance.timerify() should return the function as-is (stub behavior)
285+
const testFn = () => 'test';
286+
const timerified = perfHooksPerformance.timerify(testFn);
287+
288+
strictEqual(timerified, testFn, 'timerify should return the same function');
289+
strictEqual(
290+
timerified(),
291+
'test',
292+
'timerified function should work correctly'
293+
);
294+
},
295+
};
296+
297+
export const testPerformanceMarkResourceTiming = {
298+
test() {
299+
// markResourceTiming should not throw (no-op stub)
300+
perfHooksPerformance.markResourceTiming();
301+
// If we get here without throwing, the test passes
302+
ok(true, 'markResourceTiming should not throw');
303+
},
304+
};
305+
90306
export const testPerformanceMark = {
91307
test() {
92308
perfHooksPerformance.clearMarks();

src/workerd/api/performance.c++

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,28 @@ jsg::JsObject PerformanceResourceTiming::toJSON(jsg::Lock& js) {
6262
JSG_FAIL_REQUIRE(Error, "PerformanceResourceTiming.toJSON is not implemented"_kj);
6363
}
6464

65+
jsg::JsObject PerformanceNodeTiming::toJSON(jsg::Lock& js) {
66+
auto obj = js.objNoProto();
67+
obj.set(js, "name"_kj, js.str(name));
68+
obj.set(js, "entryType"_kj, js.str(entryType));
69+
obj.set(js, "startTime"_kj, js.num(startTime));
70+
obj.set(js, "duration"_kj, js.num(duration));
71+
obj.set(js, "nodeStart"_kj, js.num(0));
72+
obj.set(js, "v8Start"_kj, js.num(0));
73+
obj.set(js, "bootstrapComplete"_kj, js.num(0));
74+
obj.set(js, "environment"_kj, js.num(0));
75+
obj.set(js, "loopStart"_kj, js.num(0));
76+
obj.set(js, "loopExit"_kj, js.num(0));
77+
obj.set(js, "idleTime"_kj, js.num(0));
78+
// Include uvMetricsInfo in the JSON representation
79+
auto uvObj = js.objNoProto();
80+
uvObj.set(js, "loopCount"_kj, js.num(0));
81+
uvObj.set(js, "events"_kj, js.num(0));
82+
uvObj.set(js, "eventsWaiting"_kj, js.num(0));
83+
obj.set(js, "uvMetricsInfo"_kj, uvObj);
84+
return kj::mv(obj);
85+
}
86+
6587
void Performance::clearMarks(jsg::Optional<kj::String> name) {
6688
kj::Vector<jsg::Ref<PerformanceEntry>> filtered;
6789

@@ -271,12 +293,23 @@ kj::ArrayPtr<const kj::StringPtr> PerformanceObserver::getSupportedEntryTypes()
271293
return supportedEntryTypes.asPtr();
272294
}
273295

274-
void Performance::eventLoopUtilization() {
275-
JSG_FAIL_REQUIRE(Error, "Performance.eventLoopUtilization is not implemented");
296+
Performance::EventLoopUtilization Performance::eventLoopUtilization() {
297+
// Return stub values - actual event loop utilization metrics are not available in workerd.
298+
// This provides compatibility with code that expects Node.js-style performance APIs.
299+
return EventLoopUtilization{
300+
.idle = 0,
301+
.active = 0,
302+
.utilization = 0,
303+
};
304+
}
305+
306+
jsg::Ref<PerformanceNodeTiming> Performance::getNodeTiming(jsg::Lock& js) {
307+
return js.alloc<PerformanceNodeTiming>();
276308
}
277309

278310
void Performance::markResourceTiming() {
279-
JSG_FAIL_REQUIRE(Error, "Performance.markResourceTiming is not implemented");
311+
// No-op stub - resource timing is not applicable in the Workers context.
312+
// This provides compatibility with code that expects this API to exist.
280313
}
281314

282315
jsg::Function<void()> Performance::timerify(jsg::Lock& js, jsg::Function<void()> fn) {

0 commit comments

Comments
 (0)