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
26 changes: 23 additions & 3 deletions src/js/builtins/CommonJS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,29 @@ export function main() {

// This function is bound when constructing instances of CommonJSModule
$visibility = "Private";
export function require(this: JSCommonJSModule, _: string) {
// Do not use $tailCallForwardArguments here, it causes https://github.com/oven-sh/bun/issues/9225
return $overridableRequire.$apply(this, arguments);
export function require(this: JSCommonJSModule, id: string) {
const ch = require("internal/module_tracing").requireChannel;
if (!ch.hasSubscribers) {
// Do not use $tailCallForwardArguments here, it causes https://github.com/oven-sh/bun/issues/9225
return $overridableRequire.$apply(this, arguments);
}
const context = { __proto__: null, id, parentFilename: this && this.filename };
const self = this;
const args = arguments;
const { start, end, error } = ch;
return start.runStores(context, () => {
try {
const result = $overridableRequire.$apply(self, args);
context.result = result;
return result;
} catch (err) {
context.error = err;
error.publish(context);
throw err;
} finally {
end.publish(context);
}
});
}

// overridableRequire can be overridden by setting `Module.prototype.require`
Expand Down
15 changes: 15 additions & 0 deletions src/js/internal/module_tracing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// node:diagnostics_channel TracingChannels for module loading.
// https://nodejs.org/api/diagnostics_channel.html#built-in-channels
const { tracingChannel } = require("node:diagnostics_channel");

const requireChannel = tracingChannel("module.require");
const importChannel = tracingChannel("module.import");

// Called from C++ GlobalObject::moduleLoaderImportModule to wrap a dynamic
// import() once a "tracing:module.*" subscriber exists.
function traceDynamicImport(promise, url, parentURL) {
if (!importChannel.hasSubscribers) return promise;
return importChannel.tracePromise(() => promise, { __proto__: null, parentURL, url });
}

export default { requireChannel, importChannel, traceDynamicImport };
17 changes: 17 additions & 0 deletions src/js/node/diagnostics_channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const SafeFinalizationRegistry = FinalizationRegistry;
const ArrayPrototypeAt = Array.prototype.at;
const ArrayPrototypeIndexOf = Array.prototype.indexOf;
const ArrayPrototypeSplice = Array.prototype.splice;
const StringPrototypeStartsWith = String.prototype.startsWith;
const ObjectGetPrototypeOf = Object.getPrototypeOf;
const ObjectSetPrototypeOf = Object.setPrototypeOf;
const SymbolHasInstance = Symbol.hasInstance;
Expand Down Expand Up @@ -58,10 +59,16 @@ class WeakRefMap extends SafeMap {
}
}

let enableModuleTracing: (() => void) | undefined;

function markActive(channel) {
ObjectSetPrototypeOf.$call(null, channel, ActiveChannel.prototype);
channel._subscribers = [];
channel._stores = new SafeMap();
// Notify the native dynamic-import hook that it now needs to publish.
if (typeof channel.name === "string" && StringPrototypeStartsWith.$call(channel.name, "tracing:module.")) {
(enableModuleTracing ??= $newCppFunction("NodeDiagnosticsChannel.cpp", "jsEnableModuleTracingSubscribers", 0))();
}
}

function maybeMarkInactive(channel) {
Expand Down Expand Up @@ -281,6 +288,16 @@ class TracingChannel {
}
}

get hasSubscribers() {
return (
this.start.hasSubscribers ||
this.end.hasSubscribers ||
this.asyncStart.hasSubscribers ||
this.asyncEnd.hasSubscribers ||
this.error.hasSubscribers
);
}

subscribe(handlers) {
for (const name of traceEvents) {
if (!handlers[name]) continue;
Expand Down
19 changes: 19 additions & 0 deletions src/jsc/bindings/NodeDiagnosticsChannel.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#include "config.h"

#include "ZigGlobalObject.h"

namespace Bun {

using namespace JSC;

// Called from node:diagnostics_channel when a subscriber is first added to any
// "tracing:module.*" channel. Flips a one-way de-opt flag so the native
// dynamic-import hook starts routing through the JS tracer.
JSC_DEFINE_HOST_FUNCTION(jsEnableModuleTracingSubscribers, (JSC::JSGlobalObject * globalObject, JSC::CallFrame*))
{
auto* global = uncheckedDowncast<Zig::GlobalObject>(globalObject);
global->hasModuleTracingSubscribers = true;
return JSC::JSValue::encode(JSC::jsUndefined());
}

}
9 changes: 9 additions & 0 deletions src/jsc/bindings/NodeDiagnosticsChannel.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#include "config.h"
#include "ZigGlobalObject.h"
#include <wtf/PlatformCallingConventions.h>

namespace Bun {

JSC_DECLARE_HOST_FUNCTION(jsEnableModuleTracingSubscribers);

}
64 changes: 61 additions & 3 deletions src/jsc/bindings/ZigGlobalObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2275,6 +2275,18 @@ void GlobalObject::finishCreation(VM& vm)
init.set(JSC::JSFunction::create(init.vm, init.owner, wasmStreamingConsumeStreamCodeGenerator(init.vm), init.owner));
});

m_traceDynamicImportFunction.initLater(
[](const Initializer<JSFunction>& init) {
auto scope = DECLARE_THROW_SCOPE(init.vm);
JSValue mod = uncheckedDowncast<Zig::GlobalObject>(init.owner)->internalModuleRegistry()->requireId(init.owner, init.vm, Bun::InternalModuleRegistry::Field::InternalModuleTracing);
RETURN_IF_EXCEPTION(scope, );
RELEASE_ASSERT(mod.isObject());
auto prop = mod.getObject()->getIfPropertyExists(init.owner, Identifier::fromString(init.vm, "traceDynamicImport"_s));
RETURN_IF_EXCEPTION(scope, );
ASSERT(prop);
init.set(uncheckedDowncast<JSFunction>(prop));
});

m_nativeMicrotaskTrampoline.initLater(
[](const Initializer<JSFunction>& init) {
init.set(JSFunction::create(init.vm, init.owner, 2, ""_s, functionNativeMicrotaskTrampoline, ImplementationVisibility::Private));
Expand Down Expand Up @@ -3511,6 +3523,39 @@ JSC::Identifier GlobalObject::moduleLoaderResolve(JSGlobalObject* jsGlobalObject
}
}

static JSC::JSPromise* traceDynamicImport(Zig::GlobalObject* globalObject, JSC::JSPromise* promise, JSC::JSString* moduleNameValue, const SourceOrigin& sourceOrigin)
{
if (!promise) [[unlikely]]
return promise;

VM& vm = JSC::getVM(globalObject);
auto scope = DECLARE_THROW_SCOPE(vm);

auto* tracer = globalObject->traceDynamicImportFunction();
if (scope.exception()) [[unlikely]] {
return JSC::JSPromise::rejectedPromiseWithCaughtException(globalObject, scope);
}
if (!tracer) [[unlikely]]
return promise;

JSC::MarkedArgumentBuffer args;
args.append(promise);
args.append(moduleNameValue);
auto& sourceURL = sourceOrigin.url();
args.append(sourceURL.isEmpty() ? JSC::jsUndefined() : JSC::jsString(vm, sourceURL.string()));
ASSERT(!args.hasOverflowed());

auto callData = JSC::getCallData(tracer);
auto result = JSC::profiledCall(globalObject, JSC::ProfilingReason::API, tracer, callData, JSC::jsUndefined(), args);
if (scope.exception()) [[unlikely]] {
return JSC::JSPromise::rejectedPromiseWithCaughtException(globalObject, scope);
}

if (result.inherits<JSC::JSPromise>())
return static_cast<JSC::JSPromise*>(result.asCell());
return promise;
}

JSC::JSPromise* GlobalObject::moduleLoaderImportModule(JSGlobalObject* jsGlobalObject,
JSModuleLoader*,
JSString* moduleNameValue,
Expand All @@ -3524,6 +3569,8 @@ JSC::JSPromise* GlobalObject::moduleLoaderImportModule(JSGlobalObject* jsGlobalO
VM& vm = JSC::getVM(globalObject);
auto scope = DECLARE_THROW_SCOPE(vm);

const bool needsTracing = globalObject->hasModuleTracingSubscribers;

{
JSC::JSPromise* result = NodeVM::importModule(globalObject, moduleNameValue, parameters, sourceOrigin);
RETURN_IF_EXCEPTION(scope, nullptr);
Expand Down Expand Up @@ -3562,7 +3609,10 @@ JSC::JSPromise* GlobalObject::moduleLoaderImportModule(JSGlobalObject* jsGlobalO

auto result = JSC::importModule(globalObject, resolvedIdentifier, JSC::Identifier(), parameters, nullptr, /* deferred */ false, referrerAsyncOrder);
if (scope.exception()) [[unlikely]] {
return JSC::JSPromise::rejectedPromiseWithCaughtException(globalObject, scope);
result = JSC::JSPromise::rejectedPromiseWithCaughtException(globalObject, scope);
}
if (needsTracing) [[unlikely]] {
RELEASE_AND_RETURN(scope, traceDynamicImport(globalObject, result, moduleNameValue, sourceOrigin));
}
return result;
}
Expand Down Expand Up @@ -3601,7 +3651,11 @@ JSC::JSPromise* GlobalObject::moduleLoaderImportModule(JSGlobalObject* jsGlobalO
moduleNameZ.deref();
sourceOriginZ.deref();

return JSC::JSPromise::rejectedPromiseWithCaughtException(globalObject, scope);
auto* rejected = JSC::JSPromise::rejectedPromiseWithCaughtException(globalObject, scope);
if (needsTracing) [[unlikely]] {
RELEASE_AND_RETURN(scope, traceDynamicImport(globalObject, rejected, moduleNameValue, sourceOrigin));
}
return rejected;
}

if (queryString.isEmpty()) {
Expand All @@ -3622,7 +3676,11 @@ JSC::JSPromise* GlobalObject::moduleLoaderImportModule(JSGlobalObject* jsGlobalO
auto result = JSC::importModule(globalObject, resolvedIdentifier,
JSC::Identifier(), WTF::move(parameters), nullptr, /* deferred */ false, referrerAsyncOrder);
if (scope.exception()) [[unlikely]] {
return JSC::JSPromise::rejectedPromiseWithCaughtException(globalObject, scope);
result = JSC::JSPromise::rejectedPromiseWithCaughtException(globalObject, scope);
}

if (needsTracing) [[unlikely]] {
RELEASE_AND_RETURN(scope, traceDynamicImport(globalObject, result, moduleNameValue, sourceOrigin));
}

ASSERT(result);
Expand Down
4 changes: 4 additions & 0 deletions src/jsc/bindings/ZigGlobalObject.h
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,7 @@ class GlobalObject : public Bun::GlobalScope {
JSC::JSFunction* utilInspectStylizeNoColorFunction() const { return m_utilInspectStylizeNoColorFunction.getInitializedOnMainThread(this); }

JSC::JSFunction* wasmStreamingConsumeStreamFunction() const { return m_wasmStreamingConsumeStreamFunction.getInitializedOnMainThread(this); }
JSC::JSFunction* traceDynamicImportFunction() const { return m_traceDynamicImportFunction.getInitializedOnMainThread(this); }

JSObject* requireFunctionUnbound() const { return m_requireFunctionUnbound.getInitializedOnMainThread(this); }
JSObject* requireResolveFunctionUnbound() const { return m_requireResolveFunctionUnbound.getInitializedOnMainThread(this); }
Expand Down Expand Up @@ -597,6 +598,7 @@ class GlobalObject : public Bun::GlobalScope {
V(private, LazyPropertyOfGlobalObject<JSFunction>, m_utilInspectStylizeColorFunction) \
V(private, LazyPropertyOfGlobalObject<JSFunction>, m_utilInspectStylizeNoColorFunction) \
V(private, LazyPropertyOfGlobalObject<JSFunction>, m_wasmStreamingConsumeStreamFunction) \
V(private, LazyPropertyOfGlobalObject<JSFunction>, m_traceDynamicImportFunction) \
V(private, LazyPropertyOfGlobalObject<WebCore::JSStreamsRuntime>, m_streamsRuntime) \
V(private, LazyPropertyOfGlobalObject<JSMap>, m_requireMap) \
V(private, LazyPropertyOfGlobalObject<JSObject>, m_JSArrayBufferControllerPrototype) \
Expand Down Expand Up @@ -768,6 +770,8 @@ class GlobalObject : public Bun::GlobalScope {
bool hasOverriddenModuleWrapper = false;
// De-optimization once `require("module").runMain` is written to
bool hasOverriddenModuleRunMain = false;
// De-optimization once a diagnostics_channel "tracing:module.*" subscriber is added
bool hasModuleTracingSubscribers = false;

// WeakGCMap<uint64_t, JSObject> — JS-level dedup of SecureContext by
// config digest. WeakGCMap self-registers with the heap, so no
Expand Down
Loading
Loading