Skip to content

Commit 669216e

Browse files
authored
Cleanups and refinements for new module registry (#4441)
1 parent ec0ef7c commit 669216e

File tree

4 files changed

+73
-50
lines changed

4 files changed

+73
-50
lines changed

src/workerd/api/tests/new-module-registry-test.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ import {
55
rejects,
66
strictEqual,
77
deepStrictEqual,
8-
} from 'assert';
8+
} from 'assert'; // Intentionally omit the 'node:' prefix
99
import { foo, default as def } from 'foo';
1010
import { default as fs } from 'node:fs';
11-
import { Buffer } from 'buffer';
11+
import { Buffer } from 'buffer'; // Intentionally omit the 'node:' prefix
1212
import { foo as foo2, default as def2 } from 'bar';
13-
import { createRequire } from 'module';
13+
import { createRequire } from 'module'; // Intentionally omit the 'node:' prefix
1414

1515
// Verify that import.meta.url is correct here.
1616
strictEqual(import.meta.url, 'file:///bundle/worker');
@@ -75,6 +75,7 @@ deepStrictEqual(json.default, { foo: 1 });
7575
// Synchronously resolved promises can be awaited.
7676
await Promise.resolve();
7777

78+
// CommonJS modules can be imported and should work as expected.
7879
import { default as cjs2 } from 'cjs2';
7980
strictEqual(cjs2.foo, 1);
8081
strictEqual(cjs2.bar, 2);
@@ -87,11 +88,17 @@ import { foo as cjs1foo, bar as cjs1bar } from 'cjs1';
8788
strictEqual(cjs1foo, 1);
8889
strictEqual(cjs1bar, 2);
8990

91+
// The createRequire API works as expected.
9092
const myRequire = createRequire(import.meta.url);
9193
const customRequireCjs = myRequire('cjs1');
9294
strictEqual(customRequireCjs.foo, cjs1foo);
9395
strictEqual(customRequireCjs.bar, cjs1bar);
9496

97+
const customRequireCjs2 = myRequire(import.meta.resolve('cjs2'));
98+
strictEqual(customRequireCjs2.foo, cjs2.foo);
99+
100+
// When the module being imported throws an error during evaluation,
101+
// the error is propagated correctly.
95102
await rejects(import('file:///bundle/cjs3'), {
96103
message: 'boom',
97104
});
@@ -130,6 +137,7 @@ export const alsPropagationDynamicImport = {
130137
globalThis.als = new AsyncLocalStorage();
131138
const res = await globalThis.als.run(123, () => import('als'));
132139
strictEqual(res.default, 123);
140+
delete globalThis.als;
133141
},
134142
};
135143

src/workerd/jsg/modules-new.c++

Lines changed: 55 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -26,31 +26,14 @@ kj::Maybe<const Module&> checkModule(const ResolveContext& context, const Module
2626
};
2727

2828
// Ensure that the given module has been instantiated or errored.
29+
// If false is returned, then an exception should have been scheduled
30+
// on the isolate.
2931
bool ensureInstantiated(Lock& js,
3032
v8::Local<v8::Module> module,
3133
const CompilationObserver& observer,
32-
const Module* self) {
33-
if (module->GetStatus() == v8::Module::kUninstantiated) {
34-
bool result;
35-
if (!self->instantiate(js, module, observer).To(&result)) {
36-
// If we got here, instantiation threw an error. Let's return
37-
// false and allow that error to propagate.
38-
return false;
39-
}
40-
// If we got here the instantiation may or may not have succeeded.
41-
// Let's check...
42-
if (!result) {
43-
// Nope, it failed. Let's throw an error.
44-
// Note here we are scheduling the exception on the isolate directly
45-
// rather than via the lock. This is because throwing via the lock
46-
// also throws JsExceptionThrown, which is not what we want. Here
47-
// we only want to schedule the exception in the isolate
48-
js.v8Isolate->ThrowError(
49-
js.str(kj::str("Failed to instantiate module: ", self->specifier())));
50-
return false;
51-
}
52-
}
53-
return true;
34+
const Module& self) {
35+
return module->GetStatus() != v8::Module::kUninstantiated ||
36+
self.instantiate(js, module, observer);
5437
}
5538

5639
constexpr ResolveContext::Type moduleTypeToResolveContextType(Module::Type type) {
@@ -108,54 +91,78 @@ class EsModule final: public Module {
10891
v8::ScriptOrigin origin(js.str(specifier().getHref()), resourceLineOffset, resourceColumnOffset,
10992
resourceIsSharedCrossOrigin, scriptId, {}, resourceIsOpaque, isWasm, true);
11093

111-
v8::ScriptCompiler::CachedData* data = nullptr;
11294
auto options = v8::ScriptCompiler::CompileOptions::kNoCompileOptions;
11395

11496
v8::Local<v8::Module> module;
11597
{
98+
v8::ScriptCompiler::CachedData* data = nullptr;
99+
116100
// Check to see if we have cached compilation data for this module.
101+
// Importantly, we want to allow multiple threads to be capable of
102+
// reading and using the cached data without blocking each other
103+
// (which is fine since using the cache does not modify it).
117104
auto lock = cachedData.lockShared();
118105
KJ_IF_SOME(c, *lock) {
119106
// We new new here because v8 will take ownership of the CachedData instance,
120107
// even tho we are maintaining ownership of the underlying buffer.
121108
data = new v8::ScriptCompiler::CachedData(
122109
c->data, c->length, v8::ScriptCompiler::CachedData::BufferPolicy::BufferNotOwned);
110+
auto check = data->CompatibilityCheck(js.v8Isolate);
111+
if (check != v8::ScriptCompiler::CachedData::kSuccess) {
112+
// The cached data is not compatible with the current isolate. Let's
113+
// not try using it.
114+
delete data;
115+
} else {
116+
observer.onCompileCacheFound(js.v8Isolate);
117+
}
123118
}
124119

125120
// Note that the Source takes ownership of the CachedData pointer that we pass in.
126-
// Do not use data after this point.
121+
// (but not the actual buffer it holds). Do not use data after this point.
127122
v8::ScriptCompiler::Source source(js.strExtern(this->source), origin, data);
128123

129124
auto maybeCached = source.GetCachedData();
130125
if (maybeCached != nullptr) {
131126
if (!maybeCached->rejected) {
132-
// We found valid cached data and need to set the option to consume it to avoid
127+
// We found valid cached data and set the option to consume it to avoid
133128
// compiling again below...
134129
options = v8::ScriptCompiler::CompileOptions::kConsumeCodeCache;
135130
} else {
136131
// In this case we'll just log a warning and continue on. This is potentially
137132
// a signal that something with the compile cache is not working correctly but
138133
// it is not a fatal error. If we spot this in the wild, it warrants some
139-
// investigation.
140-
LOG_WARNING_ONCE("NOSENTRY Cached data for ESM module was rejected");
134+
// investigation but is not critical.
135+
LOG_WARNING_ONCE("NOSENTRY Cached data for an ESM module was rejected");
136+
observer.onCompileCacheRejected(js.v8Isolate);
141137
}
142138
}
143139

140+
// Let's just double check that our options are valid. They should be
141+
// since we're either consuming cached data or not using any options at all.
142+
KJ_ASSERT(v8::ScriptCompiler::CompileOptionsIsValid(options));
144143
if (!v8::ScriptCompiler::CompileModule(js.v8Isolate, &source, options).ToLocal(&module)) {
145144
return v8::MaybeLocal<v8::Module>();
146145
}
147146
}
148147

149-
// If the options are still kNoCompileOptions, then we did not have or use cached
150-
// data. We should generate the cache now, if possible. We lock to ensure that
151-
// we do not generate the cache multiple times needlessly. When the lock is acquired
152-
// we check again to see if the cache is still empty, and skip generating if it is not.
148+
// If options is still kNoCompileOptions at this point, it means that we did not
149+
// find any cached data for this module, or the cached data was rejected. In the
150+
// case it was rejected, we just move on. If there is no cached data, we try
151+
// generating it and store it. Multiple threads can end up lining up here to
152+
// acquire the lock and generate the cache. We'll test to see if the cached
153+
// data is still empty once the lock is acquired, and if it is not, we'll skip
154+
// generation.
153155
if (options == v8::ScriptCompiler::CompileOptions::kNoCompileOptions) {
154156
auto lock = cachedData.lockExclusive();
155-
if (auto ptr = v8::ScriptCompiler::CreateCodeCache(module->GetUnboundModuleScript())) {
156-
kj::Own<v8::ScriptCompiler::CachedData> cached(
157-
ptr, kj::_::HeapDisposer<v8::ScriptCompiler::CachedData>::instance);
158-
*lock = kj::mv(cached);
157+
if (*lock == kj::none) {
158+
if (auto ptr = v8::ScriptCompiler::CreateCodeCache(module->GetUnboundModuleScript())) {
159+
kj::Own<v8::ScriptCompiler::CachedData> cached(
160+
ptr, kj::_::HeapDisposer<v8::ScriptCompiler::CachedData>::instance);
161+
*lock = kj::mv(cached);
162+
observer.onCompileCacheGenerated(js.v8Isolate);
163+
} else {
164+
observer.onCompileCacheGenerationFailed(js.v8Isolate);
165+
}
159166
}
160167
}
161168

@@ -172,7 +179,7 @@ class EsModule final: public Module {
172179
v8::Local<v8::Module> module,
173180
const CompilationObserver& observer,
174181
kj::Maybe<EvalCallback>& maybeEvalCallback) const override {
175-
if (!ensureInstantiated(js, module, observer, this)) {
182+
if (!ensureInstantiated(js, module, observer, *this)) {
176183
return v8::MaybeLocal<v8::Value>();
177184
};
178185

@@ -254,7 +261,7 @@ class SyntheticModule final: public Module {
254261
v8::Local<v8::Module> module,
255262
const CompilationObserver& observer,
256263
kj::Maybe<EvalCallback>& maybeEvalCallback) const override {
257-
if (!ensureInstantiated(js, module, observer, this)) {
264+
if (!ensureInstantiated(js, module, observer, *this)) {
258265
return v8::MaybeLocal<v8::Value>();
259266
}
260267

@@ -398,10 +405,10 @@ class IsolateModuleRegistry final {
398405
js.throwException(JsValue(module->GetException()));
399406
}
400407

401-
// TODO(new-module-registry): Circular dependencies should be fine
402-
// when we are talking strictly about CJS/Node.js style modules.
403-
// For ESM, it becomes more problematic because v8 will not allow
404-
// us to grab the default export while the module is still evaluating.
408+
// Circular dependencies should be fine when we are talking strictly
409+
// about CJS/Node.js style modules. For ESM, it becomes more problematic
410+
// because v8 will not allow us to grab the default export while the module
411+
// is still evaluating.
405412

406413
if (entry.module.isEsm() && status == v8::Module::kEvaluating) {
407414
JSG_FAIL_REQUIRE(Error, "Circular dependency when resolving module: ", specifier);
@@ -1294,12 +1301,16 @@ Module::Module(Url specifier, Type type, Flags flags)
12941301
type_(type),
12951302
flags_(flags) {}
12961303

1297-
v8::Maybe<bool> Module::instantiate(
1304+
bool Module::instantiate(
12981305
Lock& js, v8::Local<v8::Module> module, const CompilationObserver& observer) const {
12991306
if (module->GetStatus() != v8::Module::kUninstantiated) {
1300-
return v8::Just(true);
1307+
return true;
13011308
}
1302-
return module->InstantiateModule(js.v8Context(), resolveCallback);
1309+
// InstantiateModule is one of those methods that returns a Maybe<bool> but
1310+
// never returns Just(false). It either returns Just(true) or an empty Maybe
1311+
// to signal that the instantiation failed. Eventually I would expect V8 to
1312+
// replace the return value with a Maybe<void>.
1313+
return module->InstantiateModule(js.v8Context(), resolveCallback).IsJust();
13031314
}
13041315

13051316
bool Module::isEval() const {

src/workerd/jsg/modules-new.h

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -252,11 +252,10 @@ class Module {
252252
// Determines if this module can be resolved in the given context.
253253
virtual bool evaluateContext(const ResolveContext& context) const KJ_WARN_UNUSED_RESULT;
254254

255-
// Instantiates the given module. The return value follows the established v8
256-
// rules for Maybe. If the returned maybe is empty, then an exception should
255+
// Instantiates the given module. If false is returned, then an exception should
257256
// have been scheduled on the isolate via the lock. Do not throw C++ exceptions
258257
// from this method unless they are fatal.
259-
v8::Maybe<bool> instantiate(Lock& js,
258+
bool instantiate(Lock& js,
260259
v8::Local<v8::Module> module,
261260
const CompilationObserver& observer) const KJ_WARN_UNUSED_RESULT;
262261

src/workerd/jsg/observer.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,11 @@ struct CompilationObserver {
129129
virtual kj::Own<void> onJsonCompilationStart(v8::Isolate* isolate, size_t inputSize) const {
130130
return kj::Own<void>();
131131
}
132+
133+
virtual void onCompileCacheFound(v8::Isolate* isolate) const {}
134+
virtual void onCompileCacheRejected(v8::Isolate* isolate) const {}
135+
virtual void onCompileCacheGenerated(v8::Isolate* isolate) const {}
136+
virtual void onCompileCacheGenerationFailed(v8::Isolate* isolate) const {}
132137
};
133138

134139
struct InternalExceptionObserver {

0 commit comments

Comments
 (0)