Skip to content

Commit 418dc6f

Browse files
committed
Start downloading all synchronous imports ASAP
This changes how non-async module loading works. In general, module loading is triggered by a v8 callback. We ask it to process a module (a <script type= module>) and then for every module that it depends on, we get a callback. This callback expects the nested v8.Module instance, so we need to load it then and there (as opposed to dynamic imports, where we only have to return a promise). Previously, we solved this by issuing a blocking HTTP get in each callback. The HTTP loop was able to continuing downloading already-queued resources, but if a module depended on 20 nested modules, we'd issue 20 blocking gets one after the other. Once a module is compiled, we can ask v8 for a list of its dependent module. We can them immediately start to download all of those modules. We then evaluate the original module, which will trigger our callback. At this point, we still need to block and wait for the response, but we've already started the download and it's much faster. Sure, for the first module, we might need to wait the same amount of time, but for the other 19, chances are by the time the callback executes, we already have it downloaded and ready.
1 parent 2aa4b03 commit 418dc6f

File tree

6 files changed

+159
-134
lines changed

6 files changed

+159
-134
lines changed

src/browser/ScriptManager.zig

Lines changed: 94 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,18 @@ client: *Http.Client,
6767
allocator: Allocator,
6868
buffer_pool: BufferPool,
6969
script_pool: std.heap.MemoryPool(PendingScript),
70+
sync_module_pool: std.heap.MemoryPool(SyncModule),
7071
async_module_pool: std.heap.MemoryPool(AsyncModule),
7172

73+
// We can download multiple sync modules in parallel, but we want to process
74+
// then in order. We can't use an OrderList, like the other script types,
75+
// because the order we load them might not be the order we want to process
76+
// them in (I'm not sure this is true, but as far as I can tell, v8 doesn't
77+
// make any guarantees about the list of sub-module dependencies it gives us
78+
// So this is more like a cache. When a SyncModule is complete, it's put here
79+
// and can be requested as needed.
80+
sync_modules: std.StringHashMapUnmanaged(*SyncModule),
81+
7282
const OrderList = std.DoublyLinkedList;
7383

7484
pub fn init(browser: *Browser, page: *Page) ScriptManager {
@@ -80,24 +90,42 @@ pub fn init(browser: *Browser, page: *Page) ScriptManager {
8090
.scripts = .{},
8191
.deferreds = .{},
8292
.asyncs_ready = .{},
93+
.sync_modules = .empty,
8394
.is_evaluating = false,
8495
.allocator = allocator,
8596
.client = browser.http_client,
8697
.static_scripts_done = false,
8798
.buffer_pool = BufferPool.init(allocator, 5),
8899
.script_pool = std.heap.MemoryPool(PendingScript).init(allocator),
100+
.sync_module_pool = std.heap.MemoryPool(SyncModule).init(allocator),
89101
.async_module_pool = std.heap.MemoryPool(AsyncModule).init(allocator),
90102
};
91103
}
92104

93105
pub fn deinit(self: *ScriptManager) void {
94106
self.reset();
107+
var it = self.sync_modules.valueIterator();
108+
while (it.next()) |value_ptr| {
109+
value_ptr.*.buffer.deinit(self.allocator);
110+
self.sync_module_pool.destroy(value_ptr.*);
111+
}
112+
95113
self.buffer_pool.deinit();
96114
self.script_pool.deinit();
115+
self.sync_module_pool.deinit();
97116
self.async_module_pool.deinit();
117+
118+
self.sync_modules.deinit(self.allocator);
98119
}
99120

100121
pub fn reset(self: *ScriptManager) void {
122+
var it = self.sync_modules.valueIterator();
123+
while (it.next()) |value_ptr| {
124+
value_ptr.*.buffer.deinit(self.allocator);
125+
self.sync_module_pool.destroy(value_ptr.*);
126+
}
127+
self.sync_modules.clearRetainingCapacity();
128+
101129
self.clearList(&self.asyncs);
102130
self.clearList(&self.scripts);
103131
self.clearList(&self.deferreds);
@@ -259,49 +287,70 @@ pub fn addFromElement(self: *ScriptManager, element: *parser.Element) !void {
259287
// Unlike external modules which can only ever be executed after releasing an
260288
// http handle, these are executed without there necessarily being a free handle.
261289
// Thus, Http/Client.zig maintains a dedicated handle for these calls.
262-
pub fn blockingGet(self: *ScriptManager, url: [:0]const u8) !GetResult {
263-
std.debug.assert(self.is_blocking == false);
264-
265-
self.is_blocking = true;
266-
defer {
267-
self.is_blocking = false;
268-
269-
// we blocked evaluation while loading this script, there could be
270-
// scripts ready to process.
271-
self.evaluate();
290+
pub fn getModule(self: *ScriptManager, url: [:0]const u8) !void {
291+
const gop = try self.sync_modules.getOrPut(self.allocator, url);
292+
if (gop.found_existing) {
293+
// already requested
294+
return;
272295
}
296+
errdefer _ = self.sync_modules.remove(url);
273297

274-
var blocking = Blocking{
275-
.allocator = self.allocator,
276-
.buffer_pool = &self.buffer_pool,
277-
};
298+
const sync = try self.sync_module_pool.create();
299+
errdefer self.sync_module_pool.destroy(sync);
300+
301+
sync.* = .{ .manager = self };
302+
gop.value_ptr.* = sync;
278303

279304
var headers = try self.client.newHeaders();
280305
try self.page.requestCookie(.{}).headersForRequest(self.page.arena, url, &headers);
281306

282-
var client = self.client;
283-
try client.blockingRequest(.{
307+
try self.client.request(.{
284308
.url = url,
309+
.ctx = sync,
285310
.method = .GET,
286311
.headers = headers,
287312
.cookie_jar = self.page.cookie_jar,
288-
.ctx = &blocking,
289313
.resource_type = .script,
290-
.start_callback = if (log.enabled(.http, .debug)) Blocking.startCallback else null,
291-
.header_callback = Blocking.headerCallback,
292-
.data_callback = Blocking.dataCallback,
293-
.done_callback = Blocking.doneCallback,
294-
.error_callback = Blocking.errorCallback,
314+
.start_callback = if (log.enabled(.http, .debug)) SyncModule.startCallback else null,
315+
.header_callback = SyncModule.headerCallback,
316+
.data_callback = SyncModule.dataCallback,
317+
.done_callback = SyncModule.doneCallback,
318+
.error_callback = SyncModule.errorCallback,
295319
});
320+
}
321+
322+
pub fn waitForModule(self: *ScriptManager, url: [:0]const u8) !GetResult {
323+
std.debug.assert(self.is_blocking == false);
324+
self.is_blocking = true;
325+
defer self.is_blocking = false;
326+
327+
// Normally it's dangerous to hold on to map pointers. But here, the map
328+
// can't change. It's possible that by calling `tick`, other entries within
329+
// the map will have their value change, but the map itself is immutable
330+
// during this tick.
331+
const entry = self.sync_modules.getEntry(url) orelse {
332+
return error.UnknownModule;
333+
};
334+
const sync = entry.value_ptr.*;
296335

297-
// rely on http's timeout settings to avoid an endless/long loop.
336+
var client = self.client;
298337
while (true) {
299-
_ = try client.tick(200);
300-
switch (blocking.state) {
301-
.running => {},
302-
.done => |result| return result,
338+
switch (sync.state) {
339+
.loading => {},
340+
.done => {
341+
// Our caller has its own higher level cache (caching the
342+
// actual compiled module). There's no reason for us to keep this
343+
defer self.sync_module_pool.destroy(sync);
344+
defer self.sync_modules.removeByPtr(entry.key_ptr);
345+
return .{
346+
.buffer = sync.buffer,
347+
.buffer_pool = &self.buffer_pool,
348+
};
349+
},
303350
.err => |err| return err,
304351
}
352+
// rely on http's timeout settings to avoid an endless/long loop.
353+
_ = try client.tick(200);
305354
}
306355
}
307356

@@ -332,7 +381,6 @@ pub fn getAsyncModule(self: *ScriptManager, url: [:0]const u8, cb: AsyncModule.C
332381
.error_callback = AsyncModule.errorCallback,
333382
});
334383
}
335-
336384
pub fn staticScriptsDone(self: *ScriptManager) void {
337385
std.debug.assert(self.static_scripts_done == false);
338386
self.static_scripts_done = true;
@@ -782,16 +830,15 @@ const BufferPool = struct {
782830
}
783831
};
784832

785-
const Blocking = struct {
786-
allocator: Allocator,
787-
buffer_pool: *BufferPool,
788-
state: State = .{ .running = {} },
833+
const SyncModule = struct {
834+
manager: *ScriptManager,
789835
buffer: std.ArrayListUnmanaged(u8) = .{},
836+
state: State = .loading,
790837

791838
const State = union(enum) {
792-
running: void,
839+
done,
840+
loading,
793841
err: anyerror,
794-
done: GetResult,
795842
};
796843

797844
fn startCallback(transfer: *Http.Transfer) !void {
@@ -807,12 +854,13 @@ const Blocking = struct {
807854
.content_type = header.contentType(),
808855
});
809856

857+
var self: *SyncModule = @ptrCast(@alignCast(transfer.ctx));
810858
if (header.status != 200) {
859+
self.finished(.{ .err = error.InvalidStatusCode });
811860
return error.InvalidStatusCode;
812861
}
813862

814-
var self: *Blocking = @ptrCast(@alignCast(transfer.ctx));
815-
self.buffer = self.buffer_pool.get();
863+
self.buffer = self.manager.buffer_pool.get();
816864
}
817865

818866
fn dataCallback(transfer: *Http.Transfer, data: []const u8) !void {
@@ -822,8 +870,8 @@ const Blocking = struct {
822870
// .blocking = true,
823871
// });
824872

825-
var self: *Blocking = @ptrCast(@alignCast(transfer.ctx));
826-
self.buffer.appendSlice(self.allocator, data) catch |err| {
873+
var self: *SyncModule = @ptrCast(@alignCast(transfer.ctx));
874+
self.buffer.appendSlice(self.manager.allocator, data) catch |err| {
827875
log.err(.http, "SM.dataCallback", .{
828876
.err = err,
829877
.len = data.len,
@@ -835,19 +883,17 @@ const Blocking = struct {
835883
}
836884

837885
fn doneCallback(ctx: *anyopaque) !void {
838-
var self: *Blocking = @ptrCast(@alignCast(ctx));
839-
self.state = .{ .done = .{
840-
.buffer = self.buffer,
841-
.buffer_pool = self.buffer_pool,
842-
} };
886+
var self: *SyncModule = @ptrCast(@alignCast(ctx));
887+
self.finished(.done);
843888
}
844889

845890
fn errorCallback(ctx: *anyopaque, err: anyerror) void {
846-
var self: *Blocking = @ptrCast(@alignCast(ctx));
847-
self.state = .{ .err = err };
848-
if (self.buffer.items.len > 0) {
849-
self.buffer_pool.release(self.buffer);
850-
}
891+
var self: *SyncModule = @ptrCast(@alignCast(ctx));
892+
self.finished(.{ .err = err });
893+
}
894+
895+
fn finished(self: *SyncModule, state: State) void {
896+
self.state = state;
851897
}
852898
};
853899

src/browser/page.zig

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ pub const Page = struct {
143143
.main_context = undefined,
144144
};
145145

146-
self.main_context = try session.executor.createJsContext(&self.window, self, self, true, Env.GlobalMissingCallback.init(&self.polyfill_loader));
146+
self.main_context = try session.executor.createJsContext(&self.window, self, &self.script_manager, true, Env.GlobalMissingCallback.init(&self.polyfill_loader));
147147
try polyfill.preload(self.arena, self.main_context);
148148

149149
try self.scheduler.add(self, runMicrotasks, 5, .{ .name = "page.microtasks" });
@@ -255,16 +255,6 @@ pub const Page = struct {
255255
try Node.prepend(head, &[_]Node.NodeOrText{.{ .node = parser.elementToNode(base) }});
256256
}
257257

258-
pub fn fetchModuleSource(ctx: *anyopaque, url: [:0]const u8) !ScriptManager.GetResult {
259-
const self: *Page = @ptrCast(@alignCast(ctx));
260-
return self.script_manager.blockingGet(url);
261-
}
262-
263-
pub fn fetchAsyncModuleSource(ctx: *anyopaque, url: [:0]const u8, cb: ScriptManager.AsyncModule.Callback, cb_data: *anyopaque) !void {
264-
const self: *Page = @ptrCast(@alignCast(ctx));
265-
return self.script_manager.getAsyncModule(url, cb, cb_data);
266-
}
267-
268258
pub fn wait(self: *Page, wait_ms: i32) Session.WaitResult {
269259
return self._wait(wait_ms) catch |err| {
270260
switch (err) {

src/cdp/cdp.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -693,7 +693,7 @@ const IsolatedWorld = struct {
693693
_ = try self.executor.createJsContext(
694694
&page.window,
695695
page,
696-
{},
696+
null,
697697
false,
698698
Env.GlobalMissingCallback.init(&self.polyfill_loader),
699699
);

src/http/Client.zig

Lines changed: 3 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,6 @@ allocator: Allocator,
8686
// request. These wil come and go with each request.
8787
transfer_pool: std.heap.MemoryPool(Transfer),
8888

89-
// see ScriptManager.blockingGet
90-
blocking: Handle,
91-
9289
// To notify registered subscribers of events, the browser sets/nulls this for us.
9390
notification: ?*Notification = null,
9491

@@ -121,16 +118,12 @@ pub fn init(allocator: Allocator, ca_blob: ?c.curl_blob, opts: Http.Opts) !*Clie
121118
var handles = try Handles.init(allocator, client, ca_blob, &opts);
122119
errdefer handles.deinit(allocator);
123120

124-
var blocking = try Handle.init(client, ca_blob, &opts);
125-
errdefer blocking.deinit();
126-
127121
client.* = .{
128122
.queue = .{},
129123
.active = 0,
130124
.intercepted = 0,
131125
.multi = multi,
132126
.handles = handles,
133-
.blocking = blocking,
134127
.allocator = allocator,
135128
.http_proxy = opts.http_proxy,
136129
.user_agent = opts.user_agent,
@@ -142,7 +135,6 @@ pub fn init(allocator: Allocator, ca_blob: ?c.curl_blob, opts: Http.Opts) !*Clie
142135

143136
pub fn deinit(self: *Client) void {
144137
self.abort();
145-
self.blocking.deinit();
146138
self.handles.deinit(self.allocator);
147139

148140
_ = c.curl_multi_cleanup(self.multi);
@@ -263,12 +255,6 @@ pub fn fulfillTransfer(self: *Client, transfer: *Transfer, status: u16, headers:
263255
return transfer.fulfill(status, headers, body);
264256
}
265257

266-
// See ScriptManager.blockingGet
267-
pub fn blockingRequest(self: *Client, req: Request) !void {
268-
const transfer = try self.makeTransfer(req);
269-
return self.makeRequest(&self.blocking, transfer);
270-
}
271-
272258
fn makeTransfer(self: *Client, req: Request) !*Transfer {
273259
errdefer req.headers.deinit();
274260

@@ -329,7 +315,6 @@ pub fn changeProxy(self: *Client, proxy: [:0]const u8) !void {
329315
for (self.handles.handles) |*h| {
330316
try errorCheck(c.curl_easy_setopt(h.conn.easy, c.CURLOPT_PROXY, proxy.ptr));
331317
}
332-
try errorCheck(c.curl_easy_setopt(self.blocking.conn.easy, c.CURLOPT_PROXY, proxy.ptr));
333318
}
334319

335320
// Same restriction as changeProxy. Should be ok since this is only called on
@@ -341,7 +326,6 @@ pub fn restoreOriginalProxy(self: *Client) !void {
341326
for (self.handles.handles) |*h| {
342327
try errorCheck(c.curl_easy_setopt(h.conn.easy, c.CURLOPT_PROXY, proxy));
343328
}
344-
try errorCheck(c.curl_easy_setopt(self.blocking.conn.easy, c.CURLOPT_PROXY, proxy));
345329
}
346330

347331
fn makeRequest(self: *Client, handle: *Handle, transfer: *Transfer) !void {
@@ -504,7 +488,7 @@ fn endTransfer(self: *Client, transfer: *Transfer) void {
504488
log.fatal(.http, "Failed to remove handle", .{ .err = err });
505489
};
506490

507-
self.handles.release(self, handle);
491+
self.handles.release(handle);
508492
transfer._handle = null;
509493
self.active -= 1;
510494
}
@@ -563,13 +547,7 @@ const Handles = struct {
563547
return null;
564548
}
565549

566-
fn release(self: *Handles, client: *Client, handle: *Handle) void {
567-
if (handle == &client.blocking) {
568-
// the handle we've reserved for blocking request doesn't participate
569-
// int he in_use/available pools
570-
return;
571-
}
572-
550+
fn release(self: *Handles, handle: *Handle) void {
573551
var node = &handle.node;
574552
self.in_use.remove(node);
575553
node.prev = null;
@@ -747,7 +725,7 @@ pub const Transfer = struct {
747725
fn deinit(self: *Transfer) void {
748726
self.req.headers.deinit();
749727
if (self._handle) |handle| {
750-
self.client.handles.release(self.client, handle);
728+
self.client.handles.release(handle);
751729
}
752730
self.arena.deinit();
753731
self.client.transfer_pool.destroy(self);

0 commit comments

Comments
 (0)