Skip to content

Commit d8ec503

Browse files
committed
Fix module loading
When V8 calls the ResolveModuleCallback that we give it, it passes the specifier which is essentially the string given to `from`: ``` import {x} from './blah.js'; ``` We were taking that specifier and giving it to the page. The page knew the currently executing script, an thus could resolve the full URL. Given the full URL, it could either return the JS content from its module cache or fetch the source. At best though, this isn't efficient. If two files import the same module, yes we cache the src, but we still ask v8 to re-compile it. At worse, it crashes due to resource exhaustion in the case of cyclical dependencies. ResolveModuleCallback should instead detect that it has already loaded the module and return the previously loaded module. Essentially, we shouldn't be caching the JavaScript source, we should be caching the v8 module. However, in order to do this, we need more than the specifier, which might only be a relative path (and thus isn't unique). So, in addition to a module cache, we now also maintain an module identifier lookup. Given a module, we can get its full path. Thankfully ResolveModuleCallback gives us the referring module, so we can look up that modules URL, stitch it to the specifier, and get the full url (the unique identifier) within the JS runtime. Need more real world testing, and a fully working example before I celebrate, but for sites with many import, this appears to improve performance by many orders of magnitude.
1 parent 5dcc3db commit d8ec503

File tree

2 files changed

+115
-114
lines changed

2 files changed

+115
-114
lines changed

src/browser/page.zig

Lines changed: 29 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -87,13 +87,6 @@ pub const Page = struct {
8787
// execute any JavaScript
8888
main_context: *Env.JsContext,
8989

90-
// List of modules currently fetched/loaded.
91-
module_map: std.StringHashMapUnmanaged([]const u8),
92-
93-
// current_script is the script currently evaluated by the page.
94-
// current_script could by fetch module to resolve module's url to fetch.
95-
current_script: ?*const Script = null,
96-
9790
// indicates intention to navigate to another page on the next loop execution.
9891
delayed_navigation: bool = false,
9992

@@ -119,7 +112,6 @@ pub const Page = struct {
119112
.notification = browser.notification,
120113
}),
121114
.main_context = undefined,
122-
.module_map = .empty,
123115
};
124116
self.main_context = try session.executor.createJsContext(&self.window, self, self, true);
125117

@@ -147,34 +139,9 @@ pub const Page = struct {
147139
try Dump.writeHTML(doc, out);
148140
}
149141

150-
pub fn fetchModuleSource(ctx: *anyopaque, specifier: []const u8) !?[]const u8 {
142+
pub fn fetchModuleSource(ctx: *anyopaque, src: []const u8) !?[]const u8 {
151143
const self: *Page = @ptrCast(@alignCast(ctx));
152-
const base = if (self.current_script) |s| s.src else null;
153-
154-
const src = blk: {
155-
if (base) |_base| {
156-
break :blk try URL.stitch(self.arena, specifier, _base, .{});
157-
} else break :blk specifier;
158-
};
159-
160-
if (self.module_map.get(src)) |module| {
161-
log.debug(.http, "fetching module", .{
162-
.src = src,
163-
.cached = true,
164-
});
165-
return module;
166-
}
167-
168-
log.debug(.http, "fetching module", .{
169-
.src = src,
170-
.base = base,
171-
.cached = false,
172-
.specifier = specifier,
173-
});
174-
175-
const module = try self.fetchData(specifier, base);
176-
if (module) |_module| try self.module_map.putNoClobber(self.arena, src, _module);
177-
return module;
144+
return self.fetchData("module", src);
178145
}
179146

180147
pub fn wait(self: *Page) !void {
@@ -473,26 +440,20 @@ pub const Page = struct {
473440
log.err(.browser, "clear document script", .{ .err = err });
474441
};
475442

476-
var script_source: ?[]const u8 = null;
477-
defer self.current_script = null;
478-
if (script.src) |src| {
479-
self.current_script = script;
480-
481-
// https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-classic-script
482-
script_source = (try self.fetchData(src, null)) orelse {
483-
// TODO If el's result is null, then fire an event named error at
484-
// el, and return
485-
return;
486-
};
487-
} else {
443+
const src = script.src orelse {
488444
// source is inline
489445
// TODO handle charset attribute
490-
script_source = try parser.nodeTextContent(parser.elementToNode(script.element));
491-
}
446+
const script_source = try parser.nodeTextContent(parser.elementToNode(script.element)) orelse return;
447+
return script.eval(self, script_source);
448+
};
492449

493-
if (script_source) |ss| {
494-
try script.eval(self, ss);
495-
}
450+
// https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-classic-script
451+
const script_source = (try self.fetchData("script", src)) orelse {
452+
// TODO If el's result is null, then fire an event named error at
453+
// el, and return
454+
return;
455+
};
456+
return script.eval(self, script_source);
496457

497458
// TODO If el's from an external file is true, then fire an event
498459
// named load at el.
@@ -502,34 +463,32 @@ pub const Page = struct {
502463
// It resolves src using the page's uri.
503464
// If a base path is given, src is resolved according to the base first.
504465
// the caller owns the returned string
505-
fn fetchData(self: *const Page, src: []const u8, base: ?[]const u8) !?[]const u8 {
466+
fn fetchData(
467+
self: *const Page,
468+
comptime reason: []const u8,
469+
src: []const u8,
470+
) !?[]const u8 {
506471
const arena = self.arena;
507472

508473
// Handle data URIs.
509474
if (try DataURI.parse(arena, src)) |data_uri| {
510475
return data_uri.data;
511476
}
512477

513-
var res_src = src;
514-
515-
// if a base path is given, we resolve src using base.
516-
if (base) |_base| {
517-
res_src = try URL.stitch(arena, src, _base, .{ .alloc = .if_needed });
518-
}
519-
520478
var origin_url = &self.url;
521-
const url = try origin_url.resolve(arena, res_src);
479+
const url = try origin_url.resolve(arena, src);
522480

523481
var status_code: u16 = 0;
524482
log.debug(.http, "fetching script", .{
525483
.url = url,
526484
.src = src,
527-
.base = base,
485+
.reason = reason,
528486
});
529487

530488
errdefer |err| log.err(.http, "fetch error", .{
531489
.err = err,
532490
.url = url,
491+
.reason = reason,
533492
.status = status_code,
534493
});
535494

@@ -563,6 +522,7 @@ pub const Page = struct {
563522

564523
log.info(.http, "fetch complete", .{
565524
.url = url,
525+
.reason = reason,
566526
.status = status_code,
567527
.content_length = arr.items.len,
568528
});
@@ -1025,25 +985,16 @@ const Script = struct {
1025985
try_catch.init(page.main_context);
1026986
defer try_catch.deinit();
1027987

1028-
const src = self.src orelse "inline";
988+
const src = self.src orelse page.url.raw;
1029989

1030990
log.debug(.browser, "executing script", .{ .src = src, .kind = self.kind });
1031991

1032-
_ = switch (self.kind) {
1033-
.javascript => page.main_context.exec(body, src),
1034-
.module => blk: {
1035-
switch (try page.main_context.module(body, src)) {
1036-
.value => |v| break :blk v,
1037-
.exception => |e| {
1038-
log.warn(.user_script, "eval module", .{
1039-
.src = src,
1040-
.err = try e.exception(page.arena),
1041-
});
1042-
return error.JsErr;
1043-
},
1044-
}
1045-
},
1046-
} catch {
992+
const result = switch (self.kind) {
993+
.javascript => page.main_context.eval(body, src),
994+
.module => page.main_context.module(body, src),
995+
};
996+
997+
result catch {
1047998
if (page.delayed_navigation) {
1048999
return error.Terminated;
10491000
}

src/runtime/js.zig

Lines changed: 86 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
515515
};
516516

517517
const PersistentObject = v8.Persistent(v8.Object);
518+
const PersistentModule = v8.Persistent(v8.Module);
518519
const PersistentFunction = v8.Persistent(v8.Function);
519520

520521
// Loosely maps to a Browser Page.
@@ -572,6 +573,16 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
572573
// Some Zig types have code to execute when the call scope ends
573574
call_scope_end_callbacks: std.ArrayListUnmanaged(CallScopeEndCallback) = .empty,
574575

576+
// Our module cache: normalized module specifier => module.
577+
module_cache: std.StringHashMapUnmanaged(PersistentModule) = .empty,
578+
579+
// Module => Path. The key is the module hashcode (module.getIdentityHash)
580+
// and the value is the full path to the module. We need to capture this
581+
// so that when we're asked to resolve a dependent module, and all we're
582+
// given is the specifier, we can form the full path. The full path is
583+
// necessary to lookup/store the dependent module in the module_cache.
584+
module_identifier: std.AutoHashMapUnmanaged(u32, []const u8) = .empty,
585+
575586
const ModuleLoader = struct {
576587
ptr: *anyopaque,
577588
func: *const fn (ptr: *anyopaque, specifier: []const u8) anyerror!?[]const u8,
@@ -605,6 +616,13 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
605616
}
606617
}
607618

619+
{
620+
var it = self.module_cache.valueIterator();
621+
while (it.next()) |p| {
622+
p.deinit();
623+
}
624+
}
625+
608626
for (self.callbacks.items) |*cb| {
609627
cb.deinit();
610628
}
@@ -646,6 +664,10 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
646664
}
647665

648666
// Executes the src
667+
pub fn eval(self: *JsContext, src: []const u8, name: ?[]const u8) !void {
668+
_ = try self.exec(src, name);
669+
}
670+
649671
pub fn exec(self: *JsContext, src: []const u8, name: ?[]const u8) !Value {
650672
const isolate = self.isolate;
651673
const v8_context = self.v8_context;
@@ -669,25 +691,31 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
669691

670692
// compile and eval a JS module
671693
// It doesn't wait for callbacks execution
672-
pub fn module(self: *JsContext, src: []const u8, name: []const u8) !union(enum) { value: Value, exception: Exception } {
673-
const v8_context = self.v8_context;
674-
const m = try compileModule(self.isolate, src, name);
694+
pub fn module(self: *JsContext, src: []const u8, url: []const u8) !void {
695+
const arena = self.context_arena;
675696

676-
// instantiate
677-
// resolveModuleCallback loads module's dependencies.
678-
const ok = m.instantiate(v8_context, resolveModuleCallback) catch {
679-
return error.ExecutionError;
680-
};
697+
const gop = try self.module_cache.getOrPut(arena, url);
698+
if (gop.found_existing) {
699+
return;
700+
}
701+
errdefer _ = self.module_cache.remove(url);
702+
703+
const m = try compileModule(self.isolate, src, url);
704+
705+
const owned_url = try arena.dupe(u8, url);
706+
try self.module_identifier.putNoClobber(arena, m.getIdentityHash(), owned_url);
707+
errdefer _ = self.module_identifier.remove(m.getIdentityHash());
708+
709+
gop.key_ptr.* = owned_url;
710+
gop.value_ptr.* = PersistentModule.init(self.isolate, m);
681711

682-
if (!ok) {
712+
// resolveModuleCallback loads module's dependencies.
713+
const v8_context = self.v8_context;
714+
if (try m.instantiate(v8_context, resolveModuleCallback) == false) {
683715
return error.ModuleInstantiationError;
684716
}
685717

686-
// evaluate
687-
const value = m.evaluate(v8_context) catch {
688-
return .{ .exception = self.createException(m.getException()) };
689-
};
690-
return .{ .value = self.createValue(value) };
718+
_ = try m.evaluate(v8_context);
691719
}
692720

693721
// Wrap a v8.Exception
@@ -1234,52 +1262,74 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
12341262
c_context: ?*const v8.C_Context,
12351263
c_specifier: ?*const v8.C_String,
12361264
import_attributes: ?*const v8.C_FixedArray,
1237-
referrer: ?*const v8.C_Module,
1265+
c_referrer: ?*const v8.C_Module,
12381266
) callconv(.C) ?*const v8.C_Module {
12391267
_ = import_attributes;
1240-
_ = referrer;
12411268

1242-
std.debug.assert(c_context != null);
12431269
const v8_context = v8.Context{ .handle = c_context.? };
1244-
12451270
const self: *JsContext = @ptrFromInt(v8_context.getEmbedderData(1).castTo(v8.BigInt).getUint64());
12461271

1247-
// build the specifier value.
1248-
const specifier = valueToString(
1249-
self.call_arena,
1250-
.{ .handle = c_specifier.? },
1251-
self.isolate,
1252-
v8_context,
1253-
) catch |e| {
1254-
log.err(.js, "resolve module specifier", .{ .err = e });
1272+
const specifier = jsStringToZig(self.call_arena, .{ .handle = c_specifier.? }, self.isolate) catch |err| {
1273+
log.err(.js, "resolve module", .{ .err = err });
12551274
return null;
12561275
};
1276+
const referrer = v8.Module{ .handle = c_referrer.? };
12571277

1258-
// not currently needed
1259-
// const referrer_module = if (referrer) |ref| v8.Module{ .handle = ref } else null;
1260-
const module_loader = self.module_loader;
1261-
const source = module_loader.func(module_loader.ptr, specifier) catch |err| {
1262-
log.err(.js, "resolve module fetch", .{
1278+
return self._resolveModuleCallback(referrer, specifier) catch |err| {
1279+
log.err(.js, "resolve module", .{
12631280
.err = err,
12641281
.specifier = specifier,
12651282
});
12661283
return null;
1267-
} orelse return null;
1284+
};
1285+
}
1286+
1287+
fn _resolveModuleCallback(
1288+
self: *JsContext,
1289+
referrer: v8.Module,
1290+
specifier: []const u8,
1291+
) !?*const v8.C_Module {
1292+
const referrer_path = self.module_identifier.get(referrer.getIdentityHash()) orelse {
1293+
// Shouldn't be possible.
1294+
return error.UnknownModuleReferrer;
1295+
};
1296+
1297+
const normalized_specifier = try @import("../url.zig").stitch(
1298+
self.call_arena,
1299+
specifier,
1300+
referrer_path,
1301+
.{ .alloc = .if_needed },
1302+
);
1303+
1304+
if (self.module_cache.get(normalized_specifier)) |pm| {
1305+
return pm.handle;
1306+
}
1307+
1308+
const module_loader = self.module_loader;
1309+
const source = try module_loader.func(module_loader.ptr, normalized_specifier) orelse return null;
12681310

12691311
var try_catch: TryCatch = undefined;
12701312
try_catch.init(self);
12711313
defer try_catch.deinit();
12721314

12731315
const m = compileModule(self.isolate, source, specifier) catch |err| {
1274-
log.err(.js, "resolve module compile", .{
1316+
log.warn(.js, "compile resolved module", .{
12751317
.specifier = specifier,
1276-
.stack = try_catch.stack(self.context_arena) catch null,
1277-
.src = try_catch.sourceLine(self.context_arena) catch "err",
1318+
.stack = try_catch.stack(self.call_arena) catch null,
1319+
.src = try_catch.sourceLine(self.call_arena) catch "err",
12781320
.line = try_catch.sourceLineNumber() orelse 0,
1279-
.exception = (try_catch.exception(self.context_arena) catch @errorName(err)) orelse @errorName(err),
1321+
.exception = (try_catch.exception(self.call_arena) catch @errorName(err)) orelse @errorName(err),
12801322
});
12811323
return null;
12821324
};
1325+
1326+
// We were hoping to find the module in our cache, and thus used
1327+
// the short-lived call_arena to create the normalized_specifier.
1328+
// But now this'll live for the lifetime of the context.
1329+
const arena = self.context_arena;
1330+
const owned_specifier = try arena.dupe(u8, normalized_specifier);
1331+
try self.module_cache.put(arena, owned_specifier, PersistentModule.init(self.isolate, m));
1332+
try self.module_identifier.putNoClobber(arena, m.getIdentityHash(), owned_specifier);
12831333
return m.handle;
12841334
}
12851335
};

0 commit comments

Comments
 (0)