Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions src/bun_core/env_var.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ pub mod feature_flag {
new_feature_flag!(pub BUN_FEATURE_FLAG_DISABLE_ADDRCONFIG, "BUN_FEATURE_FLAG_DISABLE_ADDRCONFIG", {});
new_feature_flag!(pub BUN_FEATURE_FLAG_DISABLE_ASYNC_TRANSPILER, "BUN_FEATURE_FLAG_DISABLE_ASYNC_TRANSPILER", {});
new_feature_flag!(pub BUN_FEATURE_FLAG_DISABLE_ISOLATION_SOURCE_CACHE, "BUN_FEATURE_FLAG_DISABLE_ISOLATION_SOURCE_CACHE", {});
new_feature_flag!(pub BUN_FEATURE_FLAG_DISABLE_ISOLATION_GLOBAL_REUSE, "BUN_FEATURE_FLAG_DISABLE_ISOLATION_GLOBAL_REUSE", {});
new_feature_flag!(pub BUN_FEATURE_FLAG_DISABLE_DNS_CACHE, "BUN_FEATURE_FLAG_DISABLE_DNS_CACHE", {});
new_feature_flag!(pub BUN_FEATURE_FLAG_DISABLE_DNS_CACHE_LIBINFO, "BUN_FEATURE_FLAG_DISABLE_DNS_CACHE_LIBINFO", {});
// Force the event loop to use epoll_pwait(2) instead of epoll_pwait2(2).
Expand Down
6 changes: 6 additions & 0 deletions src/js/internal-for-testing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,12 @@ export const dnsCacheSeed = $newRustFunction("runtime/dns_jsc/dns.rs", "internal
addresses: string[],
) => number[];

export const testIsolationResetStats = $newCppFunction(
"InternalForTesting.cpp",
"jsFunction_testIsolationResetStats",
0,
) as () => { reuse: number; swap: number };

export const fetchH2Internals = {
liveCounts: $newRustFunction("http/H2Client.rs", "TestingAPIs.liveCounts", 0) as () => {
sessions: number;
Expand Down
10 changes: 10 additions & 0 deletions src/jsc/JSGlobalObject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1386,6 +1386,14 @@ impl JSGlobalObject {
Zig__GlobalObject__createForTestIsolation(old_global, console)
}

pub(crate) fn capture_test_isolation_baseline(global: &JSGlobalObject) {
Zig__GlobalObject__captureTestIsolationBaseline(global)
}

pub(crate) fn try_reset_for_test_isolation(global: &JSGlobalObject) -> bool {
Zig__GlobalObject__tryResetForTestIsolation(global)
}

pub fn report_uncaught_exception_from_error(&self, proof: JsError) {
crate::mark_binding();
let exc = self
Expand Down Expand Up @@ -1649,6 +1657,8 @@ unsafe extern "C" {
old_global: &JSGlobalObject,
console: *mut c_void,
) -> *mut JSGlobalObject;
safe fn Zig__GlobalObject__captureTestIsolationBaseline(global: &JSGlobalObject);
safe fn Zig__GlobalObject__tryResetForTestIsolation(global: &JSGlobalObject) -> bool;
}

impl ScriptExecutionContextIdentifier {
Expand Down
27 changes: 26 additions & 1 deletion src/jsc/VirtualMachine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,12 @@
#[derive(Default)]
pub struct TestIsolationState {
pub saved_cwd: Option<Box<[u8]>>,
/// Set once the current global's post-preload own-property baseline has
/// been captured (see `Zig__GlobalObject__captureTestIsolationBaseline`);
/// cleared on every full swap so the next file re-captures.
Comment thread
robobun marked this conversation as resolved.
Outdated
pub baseline_captured: bool,
/// Opt-out of the reuse fast path.
pub force_full_swap: bool,
}

// ──────────────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -4454,6 +4460,11 @@
}
}

if self.test_isolation_enabled && !self.test_isolation_state.baseline_captured {
JSGlobalObject::capture_test_isolation_baseline(self.global());
self.test_isolation_state.baseline_captured = true;
}

Check warning on line 4466 in src/jsc/VirtualMachine.rs

View check run for this annotation

Claude / Claude Code Review

Baseline capture runs even when force_full_swap disables reuse

nit: This guard doesn't check `force_full_swap`. When `BUN_FEATURE_FLAG_DISABLE_ISOLATION_GLOBAL_REUSE=1` is set, `try_reset_for_test_isolation` is never called (short-circuited in `swap_global_for_test_isolation`), yet `captureTestIsolationBaseline` still runs on every fresh global — calling `reifyAllStaticProperties` and snapshotting all own properties into a HashMap that is never read. Consider adding `&& !self.test_isolation_state.force_full_swap` so the escape hatch stays as close to the pr
Comment thread
claude[bot] marked this conversation as resolved.
Outdated

// Note: reshaped for borrowck.
let global = self.global;
let main_str = bun_core::String::from_bytes(self.main());
Expand Down Expand Up @@ -4690,13 +4701,27 @@
self.unhandled_error_counter = 0;

let old_global = self.global;
let old_global_ref = JSGlobalObject::opaque_ref(old_global);

// The file left the global in its post-preload shape (no built-in
// overwritten, no prototype watchpoint fired, no top-level lexical
// bindings added): scrub leaked properties, clear the module
// registries, and reuse it. Linked CodeBlocks and JIT'd code survive,
// so subsequent files skip module-body re-tiering.
Comment thread
robobun marked this conversation as resolved.
Outdated
if !self.test_isolation_state.force_full_swap
&& JSGlobalObject::try_reset_for_test_isolation(old_global_ref)
{
return;
}
Comment thread
robobun marked this conversation as resolved.

// `old_global` valid for VM lifetime (safe ZST-handle deref);
// `console` is the live per-VM ConsoleObject.
let new_global: *mut JSGlobalObject = JSGlobalObject::create_for_test_isolation(
JSGlobalObject::opaque_ref(old_global),
old_global_ref,
self.console.cast(),
);
self.global = new_global;
self.test_isolation_state.baseline_captured = false;
VMHolder::set_cached_global_object(Some(new_global));
self.regular_event_loop.global = NonNull::new(new_global);
self.macro_event_loop.global = NonNull::new(new_global);
Expand Down
21 changes: 21 additions & 0 deletions src/jsc/bindings/BunClientData.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ class GlobalObject;

namespace Bun {
class StrongRootBlock;
struct TestIsolationBaseline;
struct InternalModuleExecutableCache;
}

namespace WebCore {
Expand Down Expand Up @@ -141,6 +143,25 @@ class JSVMClientData : public JSC::VM::ClientData {
// after every swap.
WTF::UncheckedKeyHashMap<WTF::String, RefPtr<JSC::SourceProvider>> isolationSourceProviderCache;

// bun test --isolate: snapshot of the global's own-property set + key
// watchpoints captured after a fresh global runs preload, used to decide
// whether the global can be reused for the next file (scrubbing leaked
// properties) instead of creating a new one. Lazy — null until first
// capture. See Zig__GlobalObject__captureTestIsolationBaseline.
Comment thread
robobun marked this conversation as resolved.
Outdated
struct TestIsolationBaselineDeleter {
void operator()(Bun::TestIsolationBaseline*) const;
};
std::unique_ptr<Bun::TestIsolationBaseline, TestIsolationBaselineDeleter> testIsolationBaseline;

// bun test --isolate: per-VM UnlinkedFunctionExecutable + SourceCode for
// internal JS modules (src/js/*), indexed by InternalModuleRegistry::Field.
// createBuiltinExecutable bypasses CodeCache, so without this each fresh
// global re-parses and re-bytecodegens every builtin module body. Lazy.
Comment thread
robobun marked this conversation as resolved.
Outdated
struct InternalModuleExecutableCacheDeleter {
void operator()(Bun::InternalModuleExecutableCache*) const;
};
std::unique_ptr<Bun::InternalModuleExecutableCache, InternalModuleExecutableCacheDeleter> internalModuleExecutableCache;

private:
bool isWebCoreJSClientData() const final { return true; }

Expand Down
1 change: 1 addition & 0 deletions src/jsc/bindings/BunProcess.h
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ class Process : public WebCore::JSEventEmitter {

JSString* cachedCwd() { return m_cachedCwd.get(); }
void setCachedCwd(JSC::VM& vm, JSString* cwd) { m_cachedCwd.set(vm, this, cwd); }
void clearCachedCwd() { m_cachedCwd.clear(); }

JSValue getArgv(JSGlobalObject* globalObject);
void setArgv(JSGlobalObject* globalObject, JSValue argv);
Expand Down
12 changes: 12 additions & 0 deletions src/jsc/bindings/InternalForTesting.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#endif

extern "C" void BunString__toThreadSafe(BunString* str);
extern "C" void Zig__GlobalObject__testIsolationResetStats(Zig::GlobalObject*, uint32_t*, uint32_t*);

namespace Bun {

Expand All @@ -31,6 +32,17 @@ JSC_DEFINE_HOST_FUNCTION(jsFunction_lowercaseHeaderNameSIMD, (JSC::JSGlobalObjec
return JSC::JSValue::encode(JSC::jsString(vm, WebCore::lowercaseHeaderName(string)));
}

JSC_DEFINE_HOST_FUNCTION(jsFunction_testIsolationResetStats, (JSC::JSGlobalObject * globalObject, JSC::CallFrame*))
{
auto& vm = globalObject->vm();
uint32_t reuse = 0, swap = 0;
Zig__GlobalObject__testIsolationResetStats(defaultGlobalObject(globalObject), &reuse, &swap);
auto* obj = JSC::constructEmptyObject(globalObject);
obj->putDirect(vm, Identifier::fromString(vm, "reuse"_s), jsNumber(reuse));
obj->putDirect(vm, Identifier::fromString(vm, "swap"_s), jsNumber(swap));
return JSValue::encode(obj);
}

JSC_DEFINE_HOST_FUNCTION(jsFunction_arrayBufferViewHasBuffer, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame))
{
auto value = callFrame->argument(0);
Expand Down
1 change: 1 addition & 0 deletions src/jsc/bindings/InternalForTesting.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@ JSC_DECLARE_HOST_FUNCTION(jsFunction_BunString_toThreadSafeRefCountDelta);
JSC_DECLARE_HOST_FUNCTION(jsFunction_lowercaseHeaderNameSIMD);
JSC_DECLARE_HOST_FUNCTION(jsFunction_emitMemoryPressure);
JSC_DECLARE_HOST_FUNCTION(jsFunction_isMemoryPressureWatcherInstalled);
JSC_DECLARE_HOST_FUNCTION(jsFunction_testIsolationResetStats);

}
87 changes: 67 additions & 20 deletions src/jsc/bindings/InternalModuleRegistry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,37 @@
#include <JavaScriptCore/LazyPropertyInlines.h>
#include <JavaScriptCore/VMTrapsInlines.h>
#include <JavaScriptCore/JSModuleLoader.h>
#include <JavaScriptCore/StrongInlines.h>
#include <JavaScriptCore/Debugger.h>
#include <utility>

#include "InternalModuleRegistryConstants.h"
#include "wtf/Forward.h"

#include "NativeModuleImpl.h"

extern "C" bool isBunTest;
extern "C" bool Bun__VM__useIsolationSourceProviderCache(void* bunVM);

namespace Bun {

// createBuiltinExecutable bypasses CodeCache and allocates a fresh
// UnlinkedFunctionExecutable every call, which under bun test --isolate forces
// every fresh global to re-parse + re-bytecodegen each internal module body.
// This per-VM cache keeps the executable (and its SourceCode) alive so a new
// global only re-link()s and re-evaluates. Strong handles because a Weak would
// clear the moment the outgoing global's JSFunction is collected.
Comment thread
robobun marked this conversation as resolved.
Outdated
struct InternalModuleExecutableCache {
WTF_DEPRECATED_MAKE_FAST_ALLOCATED(InternalModuleExecutableCache);

public:
struct Entry {
JSC::SourceCode source;
JSC::Strong<JSC::UnlinkedFunctionExecutable> executable;
};
std::array<Entry, BUN_INTERNAL_MODULE_COUNT> entries {};
};

extern "C" bool BunTest__shouldGenerateCodeCoverage(BunString sourceURL);
extern "C" void ByteRangeMapping__generate(BunString sourceURL, BunString code, int sourceID);

Expand All @@ -33,24 +55,44 @@
// JS builtin that acts as a module. In debug mode, we use a different implementation that reads
// from the developer's filesystem. This allows reloading code without recompiling bindings.

JSC::JSValue generateModule(JSC::JSGlobalObject* globalObject, JSC::VM& vm, const String& SOURCE, const String& moduleName, const String& urlString)
JSC::JSValue generateModule(JSC::JSGlobalObject* globalObject, JSC::VM& vm, InternalModuleRegistry::Field id, const String& SOURCE, const String& moduleName, const String& urlString)
{
auto throwScope = DECLARE_THROW_SCOPE(vm);
auto&& origin = SourceOrigin(WTF::URL(urlString));
SourceCode source = JSC::makeSource(SOURCE, origin, JSC::SourceTaintedOrigin::Untainted, moduleName);
maybeAddCodeCoverage(vm, source);
JSFunction* func
= JSFunction::create(
vm, globalObject,
createBuiltinExecutable(
vm, source,
Identifier::fromString(vm, moduleName),
ImplementationVisibility::Public,
ConstructorKind::None,
ConstructAbility::CannotConstruct,
InlineAttribute::None)
->link(vm, nullptr, source),
static_cast<JSC::JSGlobalObject*>(globalObject));
auto* clientData = WebCore::clientData(vm);

JSC::UnlinkedFunctionExecutable* unlinked = nullptr;
JSC::SourceCode source;
if (auto* cache = clientData->internalModuleExecutableCache.get()) {
auto& entry = cache->entries[static_cast<unsigned>(id)];
if (entry.executable) {
unlinked = entry.executable.get();
source = entry.source;
}
}
if (!unlinked) {
auto&& origin = SourceOrigin(WTF::URL(urlString));
source = JSC::makeSource(SOURCE, origin, JSC::SourceTaintedOrigin::Untainted, moduleName);
maybeAddCodeCoverage(vm, source);
unlinked = createBuiltinExecutable(
vm, source,
Identifier::fromString(vm, moduleName),
ImplementationVisibility::Public,
ConstructorKind::None,
ConstructAbility::CannotConstruct,
InlineAttribute::None);
if (isBunTest && Bun__VM__useIsolationSourceProviderCache(clientData->bunVM)) {
if (!clientData->internalModuleExecutableCache)
clientData->internalModuleExecutableCache.reset(new InternalModuleExecutableCache);
auto& entry = clientData->internalModuleExecutableCache->entries[static_cast<unsigned>(id)];
entry.source = source;
entry.executable.set(vm, unlinked);
}
}

JSFunction* func = JSFunction::create(

Check warning on line 92 in src/jsc/bindings/InternalModuleRegistry.cpp

View check run for this annotation

Claude / Claude Code Review

InternalModuleExecutableCache defeats BUN_DYNAMIC_JS_LOAD_PATH mid-run reload

nit (debug-only): under `BUN_DYNAMIC_JS_LOAD_PATH`, `initializeInternalModuleFromDisk` still reads fresh source from disk each call, but `generateModule` now returns the cached `UnlinkedFunctionExecutable` and ignores the freshly-read `SOURCE`. So on the full-swap path of `bun bd test --isolate`, mid-run edits to `src/js/*` stop taking effect after the first file (and the disk read is wasted). Consider wrapping the cache lookup/store in `#ifndef BUN_DYNAMIC_JS_LOAD_PATH` — the edit-then-rerun wo
Comment thread
claude[bot] marked this conversation as resolved.
Outdated
vm, globalObject,
unlinked->link(vm, nullptr, source),
static_cast<JSC::JSGlobalObject*>(globalObject));

RETURN_IF_EXCEPTION(throwScope, {});
if (globalObject->hasDebugger() && globalObject->debugger()->isInteractivelyDebugging()) [[unlikely]] {
Expand Down Expand Up @@ -98,12 +140,12 @@
}

#ifdef BUN_DYNAMIC_JS_LOAD_PATH
JSValue initializeInternalModuleFromDisk(JSGlobalObject* globalObject, VM& vm, const WTF::String& moduleName, WTF::String fileBase, const WTF::String& urlString)
JSValue initializeInternalModuleFromDisk(JSGlobalObject* globalObject, VM& vm, InternalModuleRegistry::Field id, const WTF::String& moduleName, WTF::String fileBase, const WTF::String& urlString)
{
WTF::String file = makeString(ASCIILiteral::fromLiteralUnsafe(BUN_DYNAMIC_JS_LOAD_PATH), "/"_s, WTF::move(fileBase));
if (auto contents = WTF::FileSystemImpl::readEntireFile(file)) {
auto string = WTF::String::fromUTF8(contents.value());
return generateModule(globalObject, vm, string, moduleName, urlString);
return generateModule(globalObject, vm, id, string, moduleName, urlString);
} else {
printf("\nFATAL: bun-debug failed to load bundled version of \"%s\" at \"%s\" (was it deleted?)\n"
"Please re-compile Bun to continue.\n\n",
Expand All @@ -112,15 +154,15 @@
}
}
#define INTERNAL_MODULE_REGISTRY_GENERATE(globalObject, vm, moduleId, filename, OFFSET, LENGTH, urlString) \
return initializeInternalModuleFromDisk(globalObject, vm, moduleId, filename, urlString)
return initializeInternalModuleFromDisk(globalObject, vm, id, moduleId, filename, urlString)
#else

// The module sources are linked as one read-only blob (bun_internal_modules_data,
// see the generated InternalModuleRegistryConstants.S); each module is a span at
// a known offset/length. createWithoutCopying is the same path the old
// ASCIILiteral → String conversion took.
#define INTERNAL_MODULE_REGISTRY_GENERATE(globalObject, vm, moduleId, filename, OFFSET, LENGTH, urlString) \
return generateModule(globalObject, vm, \
return generateModule(globalObject, vm, id, \
WTF::String(WTF::StringImpl::createWithoutCopying(std::span<const char>(bun_internal_modules_data + (OFFSET), (LENGTH)))), \
moduleId, urlString)
#endif
Expand Down Expand Up @@ -197,4 +239,9 @@

} // namespace Bun

void WebCore::JSVMClientData::InternalModuleExecutableCacheDeleter::operator()(Bun::InternalModuleExecutableCache* p) const
{
delete p;
}

#undef INTERNAL_MODULE_REGISTRY_GENERATE
Loading
Loading