diff --git a/src/browser/ScriptManager.zig b/src/browser/ScriptManager.zig index 0c2e66d61..93ffc17f3 100644 --- a/src/browser/ScriptManager.zig +++ b/src/browser/ScriptManager.zig @@ -67,6 +67,17 @@ client: *Http.Client, allocator: Allocator, buffer_pool: BufferPool, script_pool: std.heap.MemoryPool(PendingScript), +sync_module_pool: std.heap.MemoryPool(SyncModule), +async_module_pool: std.heap.MemoryPool(AsyncModule), + +// We can download multiple sync modules in parallel, but we want to process +// then in order. We can't use an OrderList, like the other script types, +// because the order we load them might not be the order we want to process +// them in (I'm not sure this is true, but as far as I can tell, v8 doesn't +// make any guarantees about the list of sub-module dependencies it gives us +// So this is more like a cache. When a SyncModule is complete, it's put here +// and can be requested as needed. +sync_modules: std.StringHashMapUnmanaged(*SyncModule), const OrderList = std.DoublyLinkedList; @@ -79,22 +90,42 @@ pub fn init(browser: *Browser, page: *Page) ScriptManager { .scripts = .{}, .deferreds = .{}, .asyncs_ready = .{}, + .sync_modules = .empty, .is_evaluating = false, .allocator = allocator, .client = browser.http_client, .static_scripts_done = false, .buffer_pool = BufferPool.init(allocator, 5), .script_pool = std.heap.MemoryPool(PendingScript).init(allocator), + .sync_module_pool = std.heap.MemoryPool(SyncModule).init(allocator), + .async_module_pool = std.heap.MemoryPool(AsyncModule).init(allocator), }; } pub fn deinit(self: *ScriptManager) void { self.reset(); + var it = self.sync_modules.valueIterator(); + while (it.next()) |value_ptr| { + value_ptr.*.buffer.deinit(self.allocator); + self.sync_module_pool.destroy(value_ptr.*); + } + self.buffer_pool.deinit(); self.script_pool.deinit(); + self.sync_module_pool.deinit(); + self.async_module_pool.deinit(); + + self.sync_modules.deinit(self.allocator); } pub fn reset(self: *ScriptManager) void { + var it = self.sync_modules.valueIterator(); + while (it.next()) |value_ptr| { + value_ptr.*.buffer.deinit(self.allocator); + self.sync_module_pool.destroy(value_ptr.*); + } + self.sync_modules.clearRetainingCapacity(); + self.clearList(&self.asyncs); self.clearList(&self.scripts); self.clearList(&self.deferreds); @@ -256,52 +287,100 @@ pub fn addFromElement(self: *ScriptManager, element: *parser.Element) !void { // Unlike external modules which can only ever be executed after releasing an // http handle, these are executed without there necessarily being a free handle. // Thus, Http/Client.zig maintains a dedicated handle for these calls. -pub fn blockingGet(self: *ScriptManager, url: [:0]const u8) !BlockingResult { - std.debug.assert(self.is_blocking == false); - - self.is_blocking = true; - defer { - self.is_blocking = false; - - // we blocked evaluation while loading this script, there could be - // scripts ready to process. - self.evaluate(); +pub fn getModule(self: *ScriptManager, url: [:0]const u8) !void { + const gop = try self.sync_modules.getOrPut(self.allocator, url); + if (gop.found_existing) { + // already requested + return; } + errdefer _ = self.sync_modules.remove(url); - var blocking = Blocking{ - .allocator = self.allocator, - .buffer_pool = &self.buffer_pool, - }; + const sync = try self.sync_module_pool.create(); + errdefer self.sync_module_pool.destroy(sync); + + sync.* = .{ .manager = self }; + gop.value_ptr.* = sync; var headers = try self.client.newHeaders(); try self.page.requestCookie(.{}).headersForRequest(self.page.arena, url, &headers); - var client = self.client; - try client.blockingRequest(.{ + try self.client.request(.{ .url = url, + .ctx = sync, .method = .GET, .headers = headers, .cookie_jar = self.page.cookie_jar, - .ctx = &blocking, .resource_type = .script, - .start_callback = if (log.enabled(.http, .debug)) Blocking.startCallback else null, - .header_callback = Blocking.headerCallback, - .data_callback = Blocking.dataCallback, - .done_callback = Blocking.doneCallback, - .error_callback = Blocking.errorCallback, + .start_callback = if (log.enabled(.http, .debug)) SyncModule.startCallback else null, + .header_callback = SyncModule.headerCallback, + .data_callback = SyncModule.dataCallback, + .done_callback = SyncModule.doneCallback, + .error_callback = SyncModule.errorCallback, }); +} + +pub fn waitForModule(self: *ScriptManager, url: [:0]const u8) !GetResult { + std.debug.assert(self.is_blocking == false); + self.is_blocking = true; + defer self.is_blocking = false; + + // Normally it's dangerous to hold on to map pointers. But here, the map + // can't change. It's possible that by calling `tick`, other entries within + // the map will have their value change, but the map itself is immutable + // during this tick. + const entry = self.sync_modules.getEntry(url) orelse { + return error.UnknownModule; + }; + const sync = entry.value_ptr.*; - // rely on http's timeout settings to avoid an endless/long loop. + var client = self.client; while (true) { - _ = try client.tick(200); - switch (blocking.state) { - .running => {}, - .done => |result| return result, + switch (sync.state) { + .loading => {}, + .done => { + // Our caller has its own higher level cache (caching the + // actual compiled module). There's no reason for us to keep this + defer self.sync_module_pool.destroy(sync); + defer self.sync_modules.removeByPtr(entry.key_ptr); + return .{ + .buffer = sync.buffer, + .buffer_pool = &self.buffer_pool, + }; + }, .err => |err| return err, } + // rely on http's timeout settings to avoid an endless/long loop. + _ = try client.tick(200); } } +pub fn getAsyncModule(self: *ScriptManager, url: [:0]const u8, cb: AsyncModule.Callback, cb_data: *anyopaque) !void { + const async = try self.async_module_pool.create(); + errdefer self.async_module_pool.destroy(async); + + async.* = .{ + .cb = cb, + .manager = self, + .cb_data = cb_data, + }; + + var headers = try self.client.newHeaders(); + try self.page.requestCookie(.{}).headersForRequest(self.page.arena, url, &headers); + + try self.client.request(.{ + .url = url, + .method = .GET, + .headers = headers, + .cookie_jar = self.page.cookie_jar, + .ctx = async, + .resource_type = .script, + .start_callback = if (log.enabled(.http, .debug)) AsyncModule.startCallback else null, + .header_callback = AsyncModule.headerCallback, + .data_callback = AsyncModule.dataCallback, + .done_callback = AsyncModule.doneCallback, + .error_callback = AsyncModule.errorCallback, + }); +} pub fn staticScriptsDone(self: *ScriptManager) void { std.debug.assert(self.static_scripts_done == false); self.static_scripts_done = true; @@ -594,7 +673,7 @@ const Script = struct { .javascript => _ = js_context.eval(content, url) catch break :blk false, .module => { // We don't care about waiting for the evaluation here. - _ = js_context.module(content, url, cacheable) catch break :blk false; + js_context.module(false, content, url, cacheable) catch break :blk false; }, } break :blk true; @@ -751,16 +830,15 @@ const BufferPool = struct { } }; -const Blocking = struct { - allocator: Allocator, - buffer_pool: *BufferPool, - state: State = .{ .running = {} }, +const SyncModule = struct { + manager: *ScriptManager, buffer: std.ArrayListUnmanaged(u8) = .{}, + state: State = .loading, const State = union(enum) { - running: void, + done, + loading, err: anyerror, - done: BlockingResult, }; fn startCallback(transfer: *Http.Transfer) !void { @@ -776,12 +854,13 @@ const Blocking = struct { .content_type = header.contentType(), }); + var self: *SyncModule = @ptrCast(@alignCast(transfer.ctx)); if (header.status != 200) { + self.finished(.{ .err = error.InvalidStatusCode }); return error.InvalidStatusCode; } - var self: *Blocking = @ptrCast(@alignCast(transfer.ctx)); - self.buffer = self.buffer_pool.get(); + self.buffer = self.manager.buffer_pool.get(); } fn dataCallback(transfer: *Http.Transfer, data: []const u8) !void { @@ -791,8 +870,8 @@ const Blocking = struct { // .blocking = true, // }); - var self: *Blocking = @ptrCast(@alignCast(transfer.ctx)); - self.buffer.appendSlice(self.allocator, data) catch |err| { + var self: *SyncModule = @ptrCast(@alignCast(transfer.ctx)); + self.buffer.appendSlice(self.manager.allocator, data) catch |err| { log.err(.http, "SM.dataCallback", .{ .err = err, .len = data.len, @@ -804,29 +883,101 @@ const Blocking = struct { } fn doneCallback(ctx: *anyopaque) !void { - var self: *Blocking = @ptrCast(@alignCast(ctx)); - self.state = .{ .done = .{ + var self: *SyncModule = @ptrCast(@alignCast(ctx)); + self.finished(.done); + } + + fn errorCallback(ctx: *anyopaque, err: anyerror) void { + var self: *SyncModule = @ptrCast(@alignCast(ctx)); + self.finished(.{ .err = err }); + } + + fn finished(self: *SyncModule, state: State) void { + self.state = state; + } +}; + +pub const AsyncModule = struct { + cb: Callback, + cb_data: *anyopaque, + manager: *ScriptManager, + buffer: std.ArrayListUnmanaged(u8) = .{}, + + pub const Callback = *const fn (ptr: *anyopaque, result: anyerror!GetResult) void; + + fn startCallback(transfer: *Http.Transfer) !void { + log.debug(.http, "script fetch start", .{ .req = transfer, .async = true }); + } + + fn headerCallback(transfer: *Http.Transfer) !void { + const header = &transfer.response_header.?; + log.debug(.http, "script header", .{ + .req = transfer, + .async = true, + .status = header.status, + .content_type = header.contentType(), + }); + + if (header.status != 200) { + return error.InvalidStatusCode; + } + + var self: *AsyncModule = @ptrCast(@alignCast(transfer.ctx)); + self.buffer = self.manager.buffer_pool.get(); + } + + fn dataCallback(transfer: *Http.Transfer, data: []const u8) !void { + // too verbose + // log.debug(.http, "script data chunk", .{ + // .req = transfer, + // .blocking = true, + // }); + + var self: *AsyncModule = @ptrCast(@alignCast(transfer.ctx)); + self.buffer.appendSlice(self.manager.allocator, data) catch |err| { + log.err(.http, "SM.dataCallback", .{ + .err = err, + .len = data.len, + .ascyn = true, + .transfer = transfer, + }); + return err; + }; + } + + fn doneCallback(ctx: *anyopaque) !void { + var self: *AsyncModule = @ptrCast(@alignCast(ctx)); + defer self.manager.async_module_pool.destroy(self); + self.cb(self.cb_data, .{ .buffer = self.buffer, - .buffer_pool = self.buffer_pool, - } }; + .buffer_pool = &self.manager.buffer_pool, + }); } fn errorCallback(ctx: *anyopaque, err: anyerror) void { - var self: *Blocking = @ptrCast(@alignCast(ctx)); - self.state = .{ .err = err }; - self.buffer_pool.release(self.buffer); + var self: *AsyncModule = @ptrCast(@alignCast(ctx)); + + if (err != error.Abort) { + self.cb(self.cb_data, err); + } + + if (self.buffer.items.len > 0) { + self.manager.buffer_pool.release(self.buffer); + } + + self.manager.async_module_pool.destroy(self); } }; -pub const BlockingResult = struct { +pub const GetResult = struct { buffer: std.ArrayListUnmanaged(u8), buffer_pool: *BufferPool, - pub fn deinit(self: *BlockingResult) void { + pub fn deinit(self: *GetResult) void { self.buffer_pool.release(self.buffer); } - pub fn src(self: *const BlockingResult) []const u8 { + pub fn src(self: *const GetResult) []const u8 { return self.buffer.items; } }; diff --git a/src/browser/mimalloc.zig b/src/browser/mimalloc.zig index 4e2dea287..d396a642e 100644 --- a/src/browser/mimalloc.zig +++ b/src/browser/mimalloc.zig @@ -62,8 +62,8 @@ pub fn getRSS() i64 { const data = writer.written(); const index = std.mem.indexOf(u8, data, "rss: ") orelse return -1; const sep = std.mem.indexOfScalarPos(u8, data, index + 5, ' ') orelse return -2; - const value = std.fmt.parseFloat(f64, data[index+5..sep]) catch return -3; - const unit = data[sep+1..]; + const value = std.fmt.parseFloat(f64, data[index + 5 .. sep]) catch return -3; + const unit = data[sep + 1 ..]; if (std.mem.startsWith(u8, unit, "KiB,")) { return @as(i64, @intFromFloat(value)) * 1024; } diff --git a/src/browser/page.zig b/src/browser/page.zig index 87b425341..451adc7c6 100644 --- a/src/browser/page.zig +++ b/src/browser/page.zig @@ -143,7 +143,7 @@ pub const Page = struct { .main_context = undefined, }; - self.main_context = try session.executor.createJsContext(&self.window, self, self, true, Env.GlobalMissingCallback.init(&self.polyfill_loader)); + self.main_context = try session.executor.createJsContext(&self.window, self, &self.script_manager, true, Env.GlobalMissingCallback.init(&self.polyfill_loader)); try polyfill.preload(self.arena, self.main_context); try self.scheduler.add(self, runMicrotasks, 5, .{ .name = "page.microtasks" }); @@ -255,11 +255,6 @@ pub const Page = struct { try Node.prepend(head, &[_]Node.NodeOrText{.{ .node = parser.elementToNode(base) }}); } - pub fn fetchModuleSource(ctx: *anyopaque, src: [:0]const u8) !ScriptManager.BlockingResult { - const self: *Page = @ptrCast(@alignCast(ctx)); - return self.script_manager.blockingGet(src); - } - pub fn wait(self: *Page, wait_ms: i32) Session.WaitResult { return self._wait(wait_ms) catch |err| { switch (err) { diff --git a/src/cdp/cdp.zig b/src/cdp/cdp.zig index f0a9cbfa5..dcc5a7e15 100644 --- a/src/cdp/cdp.zig +++ b/src/cdp/cdp.zig @@ -693,7 +693,7 @@ const IsolatedWorld = struct { _ = try self.executor.createJsContext( &page.window, page, - {}, + null, false, Env.GlobalMissingCallback.init(&self.polyfill_loader), ); diff --git a/src/http/Client.zig b/src/http/Client.zig index c9464047e..830466065 100644 --- a/src/http/Client.zig +++ b/src/http/Client.zig @@ -86,9 +86,6 @@ allocator: Allocator, // request. These wil come and go with each request. transfer_pool: std.heap.MemoryPool(Transfer), -// see ScriptManager.blockingGet -blocking: Handle, - // To notify registered subscribers of events, the browser sets/nulls this for us. notification: ?*Notification = null, @@ -121,16 +118,12 @@ pub fn init(allocator: Allocator, ca_blob: ?c.curl_blob, opts: Http.Opts) !*Clie var handles = try Handles.init(allocator, client, ca_blob, &opts); errdefer handles.deinit(allocator); - var blocking = try Handle.init(client, ca_blob, &opts); - errdefer blocking.deinit(); - client.* = .{ .queue = .{}, .active = 0, .intercepted = 0, .multi = multi, .handles = handles, - .blocking = blocking, .allocator = allocator, .http_proxy = opts.http_proxy, .user_agent = opts.user_agent, @@ -142,7 +135,6 @@ pub fn init(allocator: Allocator, ca_blob: ?c.curl_blob, opts: Http.Opts) !*Clie pub fn deinit(self: *Client) void { self.abort(); - self.blocking.deinit(); self.handles.deinit(self.allocator); _ = c.curl_multi_cleanup(self.multi); @@ -263,12 +255,6 @@ pub fn fulfillTransfer(self: *Client, transfer: *Transfer, status: u16, headers: return transfer.fulfill(status, headers, body); } -// See ScriptManager.blockingGet -pub fn blockingRequest(self: *Client, req: Request) !void { - const transfer = try self.makeTransfer(req); - return self.makeRequest(&self.blocking, transfer); -} - fn makeTransfer(self: *Client, req: Request) !*Transfer { errdefer req.headers.deinit(); @@ -329,7 +315,6 @@ pub fn changeProxy(self: *Client, proxy: [:0]const u8) !void { for (self.handles.handles) |*h| { try errorCheck(c.curl_easy_setopt(h.conn.easy, c.CURLOPT_PROXY, proxy.ptr)); } - try errorCheck(c.curl_easy_setopt(self.blocking.conn.easy, c.CURLOPT_PROXY, proxy.ptr)); } // Same restriction as changeProxy. Should be ok since this is only called on @@ -341,7 +326,6 @@ pub fn restoreOriginalProxy(self: *Client) !void { for (self.handles.handles) |*h| { try errorCheck(c.curl_easy_setopt(h.conn.easy, c.CURLOPT_PROXY, proxy)); } - try errorCheck(c.curl_easy_setopt(self.blocking.conn.easy, c.CURLOPT_PROXY, proxy)); } fn makeRequest(self: *Client, handle: *Handle, transfer: *Transfer) !void { @@ -504,7 +488,7 @@ fn endTransfer(self: *Client, transfer: *Transfer) void { log.fatal(.http, "Failed to remove handle", .{ .err = err }); }; - self.handles.release(self, handle); + self.handles.release(handle); transfer._handle = null; self.active -= 1; } @@ -563,13 +547,7 @@ const Handles = struct { return null; } - fn release(self: *Handles, client: *Client, handle: *Handle) void { - if (handle == &client.blocking) { - // the handle we've reserved for blocking request doesn't participate - // int he in_use/available pools - return; - } - + fn release(self: *Handles, handle: *Handle) void { var node = &handle.node; self.in_use.remove(node); node.prev = null; @@ -747,7 +725,7 @@ pub const Transfer = struct { fn deinit(self: *Transfer) void { self.req.headers.deinit(); if (self._handle) |handle| { - self.client.handles.release(self.client, handle); + self.client.handles.release(handle); } self.arena.deinit(); self.client.transfer_pool.destroy(self); diff --git a/src/runtime/js.zig b/src/runtime/js.zig index 977c96bed..d6f861ba9 100644 --- a/src/runtime/js.zig +++ b/src/runtime/js.zig @@ -22,6 +22,7 @@ const v8 = @import("v8"); const log = @import("../log.zig"); const SubType = @import("subtype.zig").SubType; +const ScriptManager = @import("../browser/ScriptManager.zig"); const Allocator = std.mem.Allocator; const ArenaAllocator = std.heap.ArenaAllocator; @@ -176,6 +177,8 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { meta_lookup: [Types.len]TypeMeta, + context_id: usize, + const Self = @This(); const TYPE_LOOKUP = TypeLookup{}; @@ -221,6 +224,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { errdefer allocator.destroy(env); env.* = .{ + .context_id = 0, .platform = platform, .isolate = isolate, .templates = undefined, @@ -406,19 +410,9 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { // when the handle_scope is freed. // We also maintain our own "context_arena" which allows us to have // all page related memory easily managed. - pub fn createJsContext(self: *ExecutionWorld, global: anytype, state: State, module_loader: anytype, enter: bool, global_callback: ?GlobalMissingCallback) !*JsContext { + pub fn createJsContext(self: *ExecutionWorld, global: anytype, state: State, script_manager: ?*ScriptManager, enter: bool, global_callback: ?GlobalMissingCallback) !*JsContext { std.debug.assert(self.js_context == null); - const ModuleLoader = switch (@typeInfo(@TypeOf(module_loader))) { - .@"struct" => @TypeOf(module_loader), - .pointer => |ptr| ptr.child, - .void => ErrorModuleLoader, - else => @compileError("invalid module_loader"), - }; - - // If necessary, turn a void context into something we can safely ptrCast - const safe_module_loader: *anyopaque = if (ModuleLoader == ErrorModuleLoader) @ptrCast(@constCast(&{})) else module_loader; - const env = self.env; const isolate = env.isolate; const Global = @TypeOf(global.*); @@ -527,20 +521,20 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { return error.ConsoleDeleteError; } } + const context_id = env.context_id; + env.context_id = context_id + 1; self.js_context = JsContext{ .state = state, + .id = context_id, .isolate = isolate, .v8_context = v8_context, .templates = &env.templates, .meta_lookup = &env.meta_lookup, .handle_scope = handle_scope, + .script_manager = script_manager, .call_arena = self.call_arena.allocator(), .context_arena = self.context_arena.allocator(), - .module_loader = .{ - .ptr = safe_module_loader, - .func = ModuleLoader.fetchModuleSource, - }, .global_callback = global_callback, }; @@ -632,6 +626,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { // Loosely maps to a Browser Page. pub const JsContext = struct { + id: usize, state: State, isolate: v8.Isolate, // This context is a persistent object. The persistent needs to be recovered and reset. @@ -683,12 +678,6 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { // the function that resolves/rejects them. persisted_promise_resolvers: std.ArrayListUnmanaged(v8.Persistent(v8.PromiseResolver)) = .empty, - // When we need to load a resource (i.e. an external script), we call - // this function to get the source. This is always a reference to the - // Page's fetchModuleSource, but we use a function pointer - // since this js module is decoupled from the browser implementation. - module_loader: ModuleLoader, - // Some Zig types have code to execute to cleanup destructor_callbacks: std.ArrayListUnmanaged(DestructorCallback) = .empty, @@ -702,21 +691,28 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { // necessary to lookup/store the dependent module in the module_cache. module_identifier: std.AutoHashMapUnmanaged(u32, []const u8) = .empty, + // the page's script manager + script_manager: ?*ScriptManager, + // Global callback is called on missing property. global_callback: ?GlobalMissingCallback = null, - const ModuleLoader = struct { - ptr: *anyopaque, - func: *const fn (ptr: *anyopaque, url: [:0]const u8) anyerror!BlockingResult, - - // Don't like having to reach into ../browser/ here. But can't think - // of a good way to fix this. - const BlockingResult = @import("../browser/ScriptManager.zig").BlockingResult; - }; - const ModuleEntry = struct { - module: PersistentModule, - promise: PersistentPromise, + // Can be null if we're asynchrously loading the module, in + // which case resolver_promise cannot be null. + module: ?PersistentModule = null, + + // The promise of the evaluating module. The resolved value is + // meaningless to us, but the resolver promise needs to chain + // to this, since we need to know when it's complete. + module_promise: ?PersistentPromise = null, + + // The promise for the resolver which is loading the module. + // (AKA, the first time we try to load it). This resolver will + // chain to the module_promise and, when it's done evaluating + // will resolve its namespace. Any other attempt to load the + // module willchain to this. + resolver_promise: ?PersistentPromise = null, }; // no init, started with executor.createJsContext() @@ -751,8 +747,15 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { { var it = self.module_cache.valueIterator(); while (it.next()) |entry| { - entry.module.deinit(); - entry.promise.deinit(); + if (entry.module) |*mod| { + mod.deinit(); + } + if (entry.module_promise) |*p| { + p.deinit(); + } + if (entry.resolver_promise) |*p| { + p.deinit(); + } } } @@ -813,13 +816,16 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { return self.createValue(value); } - // compile and eval a JS module - // It returns null if the module is already compiled and in the cache. - // It returns a v8.Promise if the module must be evaluated. - pub fn module(self: *JsContext, src: []const u8, url: []const u8, cacheable: bool) !ModuleEntry { + pub fn module(self: *JsContext, comptime want_result: bool, src: []const u8, url: []const u8, cacheable: bool) !(if (want_result) ModuleEntry else void) { if (cacheable) { if (self.module_cache.get(url)) |entry| { - return entry; + // The dynamic import will create an entry without the + // module to prevent multiple calls from asynchronously + // loading the same module. If we're here, without the + // module, then it's time to load it. + if (entry.module != null) { + return if (comptime want_result) entry else {}; + } } } errdefer _ = self.module_cache.remove(url); @@ -828,11 +834,37 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { const arena = self.context_arena; const owned_url = try arena.dupe(u8, url); + try self.module_identifier.putNoClobber(arena, m.getIdentityHash(), owned_url); errdefer _ = self.module_identifier.remove(m.getIdentityHash()); - // resolveModuleCallback loads module's dependencies. const v8_context = self.v8_context; + { + // Non-async modules are blocking. We can download them in + // parallel, but they need to be processed serially. So we + // want to get the list of dependent modules this module has + // and start downloading them asap. + const requests = m.getModuleRequests(); + const isolate = self.isolate; + for (0..requests.length()) |i| { + const req = requests.get(v8_context, @intCast(i)).castTo(v8.ModuleRequest); + const specifier = try jsStringToZig(self.call_arena, req.getSpecifier(), isolate); + const normalized_specifier = try @import("../url.zig").stitch( + self.call_arena, + specifier, + owned_url, + .{ .alloc = .if_needed, .null_terminated = true }, + ); + const gop = try self.module_cache.getOrPut(self.context_arena, normalized_specifier); + if (!gop.found_existing) { + const owned_specifier = try self.context_arena.dupeZ(u8, normalized_specifier); + gop.key_ptr.* = owned_specifier; + gop.value_ptr.* = .{}; + try self.script_manager.?.getModule(owned_specifier); + } + } + } + if (try m.instantiate(v8_context, resolveModuleCallback) == false) { return error.ModuleInstantiationError; } @@ -842,16 +874,42 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { // Must be a promise that gets returned here. std.debug.assert(evaluated.isPromise()); - const entry = ModuleEntry{ - .module = PersistentModule.init(self.isolate, m), - .promise = PersistentPromise.init(self.isolate, .{.handle = evaluated.handle}), - }; - if (cacheable) { - try self.module_cache.putNoClobber(arena, owned_url, entry); - try self.module_identifier.put(arena, m.getIdentityHash(), owned_url); + if (comptime !want_result) { + // avoid creating a bunch of persisted objects if it isn't + // cacheable and the caller doesn't care about results. + // This is pretty common, i.e. every