Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 7 additions & 10 deletions src/js/node/perf_hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,11 @@ const cppCreateHistogram = $newCppFunction("JSNodePerformanceHooksHistogram.cpp"
figures: number,
) => import("node:perf_hooks").RecordableHistogram;

var {
Performance,
PerformanceEntry,
PerformanceMark,
PerformanceMeasure,
PerformanceObserver: NodePerformanceObserver,
PerformanceObserverEntryList,
} = globalThis;
var { Performance, PerformanceEntry, PerformanceMark, PerformanceMeasure, PerformanceObserverEntryList } = globalThis;

// globalThis.PerformanceObserver now resolves to this module's subclass, so the
// native class is fetched directly to avoid re-entering this module on load.
var NodePerformanceObserver = $cpp("JSPerformanceObserver.cpp", "getPerformanceObserverConstructor");

Comment thread
coderabbitai[bot] marked this conversation as resolved.
var constants = {
NODE_PERFORMANCE_ENTRY_TYPE_DNS: 4,
Expand Down Expand Up @@ -120,8 +117,8 @@ const kObserverCallback = Symbol("kObserverCallback");
* The native (WebCore) observer only understands mark/measure/resource.
* Node-only entry types ('net', 'dns', ...) are routed to the JS-side
* registry in internal/shared; everything else is delegated to the native
* observer unchanged. (`NodePerformanceObserver` is the existing alias for
* the native class destructured from globalThis above.)
* observer unchanged. `NodePerformanceObserver` is the native class, fetched
* via `getPerformanceObserverConstructor` above.
*/
class PerformanceObserverForNodeTypes extends NodePerformanceObserver {
constructor(callback) {
Expand Down
20 changes: 19 additions & 1 deletion src/jsc/bindings/ZigGlobalObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1190,7 +1190,23 @@ WEBCORE_GENERATED_CONSTRUCTOR_GETTER(Performance);
WEBCORE_GENERATED_CONSTRUCTOR_GETTER(PerformanceEntry);
WEBCORE_GENERATED_CONSTRUCTOR_GETTER(PerformanceMark);
WEBCORE_GENERATED_CONSTRUCTOR_GETTER(PerformanceMeasure);
WEBCORE_GENERATED_CONSTRUCTOR_GETTER(PerformanceObserver);
// globalThis.PerformanceObserver === require("node:perf_hooks").PerformanceObserver.
// Not a static-table PropertyCallback: abstractResolve() reifies those under a
// VMInquiry slot that forbids VM re-entry, and loading perf_hooks runs JS.
JSC_DEFINE_CUSTOM_GETTER(getPerformanceObserverGlobal, (JSGlobalObject * lexicalGlobalObject, EncodedJSValue thisValue, PropertyName property))
{
auto& vm = JSC::getVM(lexicalGlobalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
auto* globalObject = defaultGlobalObject(lexicalGlobalObject);
JSValue perfHooks = globalObject->internalModuleRegistry()->requireId(globalObject, vm, Bun::InternalModuleRegistry::Field::NodePerfHooks);
RETURN_IF_EXCEPTION(scope, {});
RELEASE_ASSERT(perfHooks.isObject());
JSValue result = perfHooks.getObject()->get(globalObject, Identifier::fromString(vm, "PerformanceObserver"_s));
RETURN_IF_EXCEPTION(scope, {});
if (auto* thisObject = JSValue::decode(thisValue).getObject())
thisObject->putDirect(vm, property, result, 0);
return JSValue::encode(result);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
WEBCORE_GENERATED_CONSTRUCTOR_GETTER(PerformanceObserverEntryList)
WEBCORE_GENERATED_CONSTRUCTOR_GETTER(PerformanceResourceTiming)
WEBCORE_GENERATED_CONSTRUCTOR_GETTER(PerformanceServerTiming)
Expand Down Expand Up @@ -2986,6 +3002,8 @@ void GlobalObject::addBuiltinGlobals(JSC::VM& vm)
JSFunction::create(vm, this, 0, "set"_s, functionSetSelf, ImplementationVisibility::Public)),
PropertyAttribute::Accessor | 0);

putDirectCustomAccessor(vm, JSC::Identifier::fromString(vm, "PerformanceObserver"_s), JSC::CustomGetterSetter::create(vm, getPerformanceObserverGlobal, nullptr), PropertyAttribute::CustomValue | 0);

Comment thread
robobun marked this conversation as resolved.
// TODO: this should be usable on the lookup table. it crashed las time i tried it
putDirectCustomAccessor(vm, JSC::Identifier::fromString(vm, "onmessage"_s), JSC::CustomGetterSetter::create(vm, globalOnMessage, setGlobalOnMessage), 0);
putDirectCustomAccessor(vm, JSC::Identifier::fromString(vm, "onerror"_s), JSC::CustomGetterSetter::create(vm, globalOnError, setGlobalOnError), 0);
Expand Down
1 change: 0 additions & 1 deletion src/jsc/bindings/ZigGlobalObject.lut.txt
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@
PerformanceEntry PerformanceEntryConstructorCallback PropertyCallback
PerformanceMark PerformanceMarkConstructorCallback PropertyCallback
PerformanceMeasure PerformanceMeasureConstructorCallback PropertyCallback
PerformanceObserver PerformanceObserverConstructorCallback PropertyCallback
PerformanceObserverEntryList PerformanceObserverEntryListConstructorCallback PropertyCallback
PerformanceResourceTiming PerformanceResourceTimingConstructorCallback PropertyCallback
PerformanceServerTiming PerformanceServerTimingConstructorCallback PropertyCallback
Expand Down
5 changes: 5 additions & 0 deletions src/jsc/bindings/webcore/JSPerformanceObserver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -423,4 +423,9 @@ PerformanceObserver* JSPerformanceObserver::toWrapped(JSC::VM&, JSC::JSValue val
return nullptr;
}

JSC::JSValue getPerformanceObserverConstructor(Zig::GlobalObject* globalObject)
{
return WebCore::JSPerformanceObserver::getConstructor(globalObject->vm(), globalObject);
}

}
2 changes: 2 additions & 0 deletions src/jsc/bindings/webcore/JSPerformanceObserver.h
Original file line number Diff line number Diff line change
Expand Up @@ -97,4 +97,6 @@ template<> struct JSDOMWrapperConverterTraits<PerformanceObserver> {
};
template<> PerformanceObserver::Init convertDictionary<PerformanceObserver::Init>(JSC::JSGlobalObject&, JSC::JSValue);

JSC::JSValue getPerformanceObserverConstructor(Zig::GlobalObject* globalObject);

} // namespace WebCore
83 changes: 83 additions & 0 deletions test/js/node/perf_hooks/perf_hooks.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe } from "harness";
import perf from "perf_hooks";

test("stubs", () => {
Expand All @@ -21,3 +22,85 @@ test("doesn't throw", () => {
expect(() => performance.timeOrigin).not.toThrow();
expect(() => performance.markResourceTiming()).not.toThrow();
});

test("globalThis.PerformanceObserver is the node:perf_hooks PerformanceObserver", () => {
expect(globalThis.PerformanceObserver).toBe(perf.PerformanceObserver);
const types = globalThis.PerformanceObserver.supportedEntryTypes;
for (const nodeType of ["http", "net", "dns"]) {
expect(types).toContain(nodeType);
}
});

// Instrumentation written against globalThis.PerformanceObserver must receive the
// same node-only entries (http/net/dns) that the node:perf_hooks observer gets.
test("globalThis.PerformanceObserver delivers node-only entry types", async () => {
// The global is resolved as a bare identifier (not via globalThis.) so that
// scope resolution exercises the lazy custom accessor. node:perf_hooks is not
// loaded until after the observer is constructed.
const src = `
const http = require("node:http");

const srv = http.createServer((q, r) => r.end("hello"));
srv.listen(0, "127.0.0.1", () => {
const viaGlobal = [];
const viaModule = [];
const og = new PerformanceObserver(list => {
for (const e of list.getEntries()) viaGlobal.push(e.entryType + ":" + e.name);
});
og.observe({ entryTypes: ["http", "net"] });
const ModulePO = require("node:perf_hooks").PerformanceObserver;
const om = new ModulePO(list => {
for (const e of list.getEntries()) viaModule.push(e.entryType + ":" + e.name);
});
om.observe({ entryTypes: ["http", "net"] });

const req = http.request(
{ host: "127.0.0.1", port: srv.address().port, path: "/x", agent: false },
res => {
res.resume();
res.on("end", () => {
// Observers dispatch on a fresh tick; let the buffered entries flush.
setImmediate(() => {
setImmediate(() => {
og.disconnect();
om.disconnect();
srv.close();
process.stdout.write(
JSON.stringify({
sameClass: PerformanceObserver === ModulePO,
viaGlobal: viaGlobal.sort(),
viaModule: viaModule.sort(),
}),
);
});
});
});
},
);
req.on("error", err => {
process.stderr.write(String(err));
process.exit(1);
});
req.end();
});
`;
await using proc = Bun.spawn({
cmd: [bunExe(), "-e", src],
env: bunEnv,
stdio: ["ignore", "pipe", "pipe"],
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
let result;
try {
result = JSON.parse(stdout);
} catch {
result = { parseFailed: true, stdout };
}
expect({ ...result, stderr, exitCode }).toEqual({
sameClass: true,
viaGlobal: ["http:HttpClient", "http:HttpRequest", "net:connect"],
viaModule: ["http:HttpClient", "http:HttpRequest", "net:connect"],
stderr: "",
exitCode: 0,
});
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Loading