Skip to content

Commit d0d2850

Browse files
committed
Improve module loading
This does two changes to module loading. First, for normal imports, it only instantiates and evaluates the top-level module. This ensures that circular dependencies can be resolved. This bug was introduced when I tried to deduplicate code between dynamic and normal modules - but it turns out that non-top-level normal modules do have a simpler flow (they just need to be compiled, and we let v8 deal with the rest). The other change is to handle more edge cases. Code like this should now be ok: ``` <script type=module> var a = await import('a.js'); </script> <script type=module> import a from a.js </script> ``` Previously, the dynamic import of a.js (first block) could interact badly with the normal import of a.js in the 2nd block. This change is built on top of #1191 which also helps reduce the number of cases by ensure that a script isn't evaluated while we're trying to evaluate a script.
1 parent f9087d3 commit d0d2850

File tree

2 files changed

+112
-103
lines changed

2 files changed

+112
-103
lines changed

src/browser/ScriptManager.zig

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,15 @@ pub fn getAsyncImport(self: *ScriptManager, url: [:0]const u8, cb: ImportAsync.C
392392
.stack = self.page.js.stackTrace() catch "???",
393393
});
394394

395+
// It's possible, but unlikely, for client.request to immediately finish
396+
// a request, thus calling our callback. We generally don't want a call
397+
// from v8 (which is why we're here), to result in a new script evaluation.
398+
// So we block even the slightest change that `client.request` immediately
399+
// executes a callback.
400+
const was_evaluating = self.is_evaluating;
401+
self.is_evaluating = true;
402+
defer self.is_evaluating = was_evaluating;
403+
395404
try self.client.request(.{
396405
.url = url,
397406
.method = .GET,

src/browser/js/Context.zig

Lines changed: 103 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -222,63 +222,54 @@ pub fn exec(self: *Context, src: []const u8, name: ?[]const u8) !js.Value {
222222
}
223223

224224
pub fn module(self: *Context, comptime want_result: bool, src: []const u8, url: []const u8, cacheable: bool) !(if (want_result) ModuleEntry else void) {
225-
if (cacheable) {
226-
if (self.module_cache.get(url)) |entry| {
227-
// The dynamic import will create an entry without the
228-
// module to prevent multiple calls from asynchronously
229-
// loading the same module. If we're here, without the
230-
// module, then it's time to load it.
231-
if (entry.module != null) {
232-
return if (comptime want_result) entry else {};
225+
const mod, const owned_url = blk: {
226+
const arena = self.arena;
227+
228+
// gop will _always_ initiated if cacheable == true
229+
var gop: std.StringHashMapUnmanaged(ModuleEntry).GetOrPutResult = undefined;
230+
if (cacheable) {
231+
gop = try self.module_cache.getOrPut(arena, url);
232+
if (gop.found_existing) {
233+
if (gop.value_ptr.module != null) {
234+
return if (comptime want_result) gop.value_ptr.* else {};
235+
}
236+
} else {
237+
// first time seing this
238+
gop.value_ptr.* = .{};
233239
}
234240
}
235-
}
236-
237-
const m = try compileModule(self.isolate, src, url);
238241

239-
const arena = self.arena;
240-
const owned_url = try arena.dupe(u8, url);
242+
const m = try compileModule(self.isolate, src, url);
243+
const owned_url = try arena.dupeZ(u8, url);
241244

242-
try self.module_identifier.putNoClobber(arena, m.getIdentityHash(), owned_url);
243-
errdefer _ = self.module_identifier.remove(m.getIdentityHash());
245+
if (cacheable) {
246+
// compileModule is synchronous - nothing can modify the cache during compilation
247+
std.debug.assert(gop.value_ptr.module == null);
244248

245-
const v8_context = self.v8_context;
246-
{
247-
// Non-async modules are blocking. We can download them in
248-
// parallel, but they need to be processed serially. So we
249-
// want to get the list of dependent modules this module has
250-
// and start downloading them asap.
251-
const requests = m.getModuleRequests();
252-
for (0..requests.length()) |i| {
253-
const req = requests.get(v8_context, @intCast(i)).castTo(v8.ModuleRequest);
254-
const specifier = try self.jsStringToZig(req.getSpecifier(), .{});
255-
const normalized_specifier = try self.script_manager.?.resolveSpecifier(
256-
self.call_arena,
257-
specifier,
258-
owned_url,
259-
);
260-
const gop = try self.module_cache.getOrPut(self.arena, normalized_specifier);
249+
gop.value_ptr.module = PersistentModule.init(self.isolate, m);
261250
if (!gop.found_existing) {
262-
const owned_specifier = try self.arena.dupeZ(u8, normalized_specifier);
263-
gop.key_ptr.* = owned_specifier;
264-
gop.value_ptr.* = .{};
265-
try self.script_manager.?.preloadImport(owned_specifier, url);
251+
gop.key_ptr.* = owned_url;
266252
}
267253
}
268-
}
269254

270-
if (try m.instantiate(v8_context, resolveModuleCallback) == false) {
255+
break :blk .{ m, owned_url };
256+
};
257+
258+
try self.postCompileModule(mod, owned_url);
259+
260+
const v8_context = self.v8_context;
261+
if (try mod.instantiate(v8_context, resolveModuleCallback) == false) {
271262
return error.ModuleInstantiationError;
272263
}
273264

274-
const evaluated = m.evaluate(v8_context) catch {
275-
std.debug.assert(m.getStatus() == .kErrored);
265+
const evaluated = mod.evaluate(v8_context) catch {
266+
std.debug.assert(mod.getStatus() == .kErrored);
276267

277268
// Some module-loading errors aren't handled by TryCatch. We need to
278269
// get the error from the module itself.
279270
log.warn(.js, "evaluate module", .{
280271
.specifier = owned_url,
281-
.message = self.valueToString(m.getException(), .{}) catch "???",
272+
.message = self.valueToString(mod.getException(), .{}) catch "???",
282273
});
283274
return error.EvaluationError;
284275
};
@@ -301,28 +292,46 @@ pub fn module(self: *Context, comptime want_result: bool, src: []const u8, url:
301292
// be cached
302293
std.debug.assert(cacheable);
303294

304-
const persisted_module = PersistentModule.init(self.isolate, m);
305-
const persisted_promise = PersistentPromise.init(self.isolate, .{ .handle = evaluated.handle });
306-
307-
var gop = try self.module_cache.getOrPut(arena, owned_url);
308-
if (gop.found_existing) {
309-
// If we're here, it's because we had a cache entry, but no
310-
// module. This happens because both our synch and async
311-
// module loaders create the entry to prevent concurrent
312-
// loads of the same resource (like Go's Singleflight).
313-
std.debug.assert(gop.value_ptr.module == null);
314-
std.debug.assert(gop.value_ptr.module_promise == null);
315-
316-
gop.value_ptr.module = persisted_module;
317-
gop.value_ptr.module_promise = persisted_promise;
318-
} else {
319-
gop.value_ptr.* = ModuleEntry{
320-
.module = persisted_module,
321-
.module_promise = persisted_promise,
322-
.resolver_promise = null,
323-
};
295+
// entry has to have been created atop this function
296+
const entry = self.module_cache.getPtr(owned_url).?;
297+
298+
// and the module must have been set after we compiled it
299+
std.debug.assert(entry.module != null);
300+
std.debug.assert(entry.module_promise == null);
301+
302+
entry.module_promise = PersistentPromise.init(self.isolate, .{ .handle = evaluated.handle });
303+
return if (comptime want_result) entry.* else {};
304+
}
305+
306+
// After we compile a module, whether it's a top-level one, or a nested one,
307+
// we always want to track its identity (so that, if this module imports other
308+
// modules, we can resolve the full URL), and preload any dependent modules.
309+
fn postCompileModule(self: *Context, mod: v8.Module, url: [:0]const u8) !void {
310+
try self.module_identifier.putNoClobber(self.arena, mod.getIdentityHash(), url);
311+
312+
const v8_context = self.v8_context;
313+
314+
// Non-async modules are blocking. We can download them in parallel, but
315+
// they need to be processed serially. So we want to get the list of
316+
// dependent modules this module has and start downloading them asap.
317+
const requests = mod.getModuleRequests();
318+
const script_manager = self.script_manager.?;
319+
for (0..requests.length()) |i| {
320+
const req = requests.get(v8_context, @intCast(i)).castTo(v8.ModuleRequest);
321+
const specifier = try self.jsStringToZig(req.getSpecifier(), .{});
322+
const normalized_specifier = try script_manager.resolveSpecifier(
323+
self.call_arena,
324+
specifier,
325+
url,
326+
);
327+
const nested_gop = try self.module_cache.getOrPut(self.arena, normalized_specifier);
328+
if (!nested_gop.found_existing) {
329+
const owned_specifier = try self.arena.dupeZ(u8, normalized_specifier);
330+
nested_gop.key_ptr.* = owned_specifier;
331+
nested_gop.value_ptr.* = .{};
332+
try script_manager.preloadImport(owned_specifier, url);
333+
}
324334
}
325-
return if (comptime want_result) gop.value_ptr.* else {};
326335
}
327336

328337
// == Creators ==
@@ -1189,31 +1198,14 @@ fn _resolveModuleCallback(self: *Context, referrer: v8.Module, specifier: []cons
11891198
};
11901199

11911200
const normalized_specifier = try self.script_manager.?.resolveSpecifier(
1192-
self.arena, // might need to survive until the module is loaded
1201+
self.arena,
11931202
specifier,
11941203
referrer_path,
11951204
);
11961205

1197-
const gop = try self.module_cache.getOrPut(self.arena, normalized_specifier);
1198-
if (gop.found_existing) {
1199-
if (gop.value_ptr.module) |m| {
1200-
return m.handle;
1201-
}
1202-
// We don't have a module, but we do have a cache entry for it
1203-
// That means we're already trying to load it. We just have
1204-
// to wait for it to be done.
1205-
} else {
1206-
// I don't think it's possible for us to be here. This is
1207-
// only ever called by v8 when we evaluate a module. But
1208-
// before evaluating, we should have already started
1209-
// downloading all of the module's nested modules. So it
1210-
// should be impossible that this is the first time we've
1211-
// heard about this module.
1212-
// But, I'm not confident enough in that, and ther's little
1213-
// harm in handling this case.
1214-
@branchHint(.unlikely);
1215-
gop.value_ptr.* = .{};
1216-
try self.script_manager.?.preloadImport(normalized_specifier, referrer_path);
1206+
const entry = self.module_cache.getPtr(normalized_specifier).?;
1207+
if (entry.module) |m| {
1208+
return m.castToModule().handle;
12171209
}
12181210

12191211
var source = try self.script_manager.?.waitForImport(normalized_specifier);
@@ -1223,26 +1215,10 @@ fn _resolveModuleCallback(self: *Context, referrer: v8.Module, specifier: []cons
12231215
try_catch.init(self);
12241216
defer try_catch.deinit();
12251217

1226-
const entry = self.module(true, source.src(), normalized_specifier, true) catch |err| {
1227-
switch (err) {
1228-
error.EvaluationError => {
1229-
// This is a sentinel value telling us that the error was already
1230-
// logged. Some module-loading errors aren't captured by Try/Catch.
1231-
// We need to handle those errors differently, where the module
1232-
// exists.
1233-
},
1234-
else => log.warn(.js, "compile resolved module", .{
1235-
.specifier = normalized_specifier,
1236-
.stack = try_catch.stack(self.call_arena) catch null,
1237-
.src = try_catch.sourceLine(self.call_arena) catch "err",
1238-
.line = try_catch.sourceLineNumber() orelse 0,
1239-
.exception = (try_catch.exception(self.call_arena) catch @errorName(err)) orelse @errorName(err),
1240-
}),
1241-
}
1242-
return null;
1243-
};
1244-
// entry.module is always set when returning from self.module()
1245-
return entry.module.?.handle;
1218+
const mod = try compileModule(self.isolate, source.src(), normalized_specifier);
1219+
try self.postCompileModule(mod, normalized_specifier);
1220+
entry.module = PersistentModule.init(self.isolate, mod);
1221+
return entry.module.?.castToModule().handle;
12461222
}
12471223

12481224
// Will get passed to ScriptManager and then passed back to us when
@@ -1317,7 +1293,31 @@ fn _dynamicModuleCallback(self: *Context, specifier: [:0]const u8, referrer: []c
13171293
// `dynamicModuleSourceCallback`, but we can skip some steps
13181294
// since the module is alrady loaded,
13191295
std.debug.assert(gop.value_ptr.module != null);
1320-
std.debug.assert(gop.value_ptr.module_promise != null);
1296+
1297+
// If the module hasn't been evaluated yet (it was only instantiated
1298+
// as a static import dependency), we need to evaluate it now.
1299+
if (gop.value_ptr.module_promise == null) {
1300+
const mod = gop.value_ptr.module.?.castToModule();
1301+
if (mod.getStatus() == .kEvaluated) {
1302+
// Module was already evaluated (shouldn't normally happen, but handle it).
1303+
// Create a pre-resolved promise with the module namespace.
1304+
const persisted_module_resolver = v8.Persistent(v8.PromiseResolver).init(isolate, v8.PromiseResolver.init(self.v8_context));
1305+
try self.persisted_promise_resolvers.append(self.arena, persisted_module_resolver);
1306+
var module_resolver = persisted_module_resolver.castToPromiseResolver();
1307+
_ = module_resolver.resolve(self.v8_context, mod.getModuleNamespace());
1308+
gop.value_ptr.module_promise = PersistentPromise.init(self.isolate, module_resolver.getPromise());
1309+
} else {
1310+
// the module was loaded, but not evaluated, we _have_ to evaluate it now
1311+
const evaluated = mod.evaluate(self.v8_context) catch {
1312+
std.debug.assert(mod.getStatus() == .kErrored);
1313+
const error_msg = v8.String.initUtf8(isolate, "Module evaluation failed");
1314+
_ = resolver.reject(self.v8_context, error_msg.toValue());
1315+
return promise;
1316+
};
1317+
std.debug.assert(evaluated.isPromise());
1318+
gop.value_ptr.module_promise = PersistentPromise.init(self.isolate, .{ .handle = evaluated.handle });
1319+
}
1320+
}
13211321

13221322
// like before, we want to set this up so that if anything else
13231323
// tries to load this module, it can just return our promise

0 commit comments

Comments
 (0)