Skip to content

Commit 3f2f56d

Browse files
Merge pull request #1197 from lightpanda-io/module_loading
Module loading
2 parents 43b210d + d0d2850 commit 3f2f56d

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)