Skip to content

Commit 20cb6cd

Browse files
authored
Merge pull request #1091 from lightpanda-io/concurrent_blocking_imports
Concurrent blocking imports
2 parents 477a5e5 + 418dc6f commit 20cb6cd

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);
@@ -260,49 +288,70 @@ pub fn addFromElement(self: *ScriptManager, element: *parser.Element) !void {
260288
// Unlike external modules which can only ever be executed after releasing an
261289
// http handle, these are executed without there necessarily being a free handle.
262290
// Thus, Http/Client.zig maintains a dedicated handle for these calls.
263-
pub fn blockingGet(self: *ScriptManager, url: [:0]const u8) !GetResult {
264-
std.debug.assert(self.is_blocking == false);
265-
266-
self.is_blocking = true;
267-
defer {
268-
self.is_blocking = false;
269-
270-
// we blocked evaluation while loading this script, there could be
271-
// scripts ready to process.
272-
self.evaluate();
291+
pub fn getModule(self: *ScriptManager, url: [:0]const u8) !void {
292+
const gop = try self.sync_modules.getOrPut(self.allocator, url);
293+
if (gop.found_existing) {
294+
// already requested
295+
return;
273296
}
297+
errdefer _ = self.sync_modules.remove(url);
274298

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

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

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

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

@@ -333,7 +382,6 @@ pub fn getAsyncModule(self: *ScriptManager, url: [:0]const u8, cb: AsyncModule.C
333382
.error_callback = AsyncModule.errorCallback,
334383
});
335384
}
336-
337385
pub fn staticScriptsDone(self: *ScriptManager) void {
338386
std.debug.assert(self.static_scripts_done == false);
339387
self.static_scripts_done = true;
@@ -783,16 +831,15 @@ const BufferPool = struct {
783831
}
784832
};
785833

786-
const Blocking = struct {
787-
allocator: Allocator,
788-
buffer_pool: *BufferPool,
789-
state: State = .{ .running = {} },
834+
const SyncModule = struct {
835+
manager: *ScriptManager,
790836
buffer: std.ArrayListUnmanaged(u8) = .{},
837+
state: State = .loading,
791838

792839
const State = union(enum) {
793-
running: void,
840+
done,
841+
loading,
794842
err: anyerror,
795-
done: GetResult,
796843
};
797844

798845
fn startCallback(transfer: *Http.Transfer) !void {
@@ -808,12 +855,13 @@ const Blocking = struct {
808855
.content_type = header.contentType(),
809856
});
810857

858+
var self: *SyncModule = @ptrCast(@alignCast(transfer.ctx));
811859
if (header.status != 200) {
860+
self.finished(.{ .err = error.InvalidStatusCode });
812861
return error.InvalidStatusCode;
813862
}
814863

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

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

826-
var self: *Blocking = @ptrCast(@alignCast(transfer.ctx));
827-
self.buffer.appendSlice(self.allocator, data) catch |err| {
874+
var self: *SyncModule = @ptrCast(@alignCast(transfer.ctx));
875+
self.buffer.appendSlice(self.manager.allocator, data) catch |err| {
828876
log.err(.http, "SM.dataCallback", .{
829877
.err = err,
830878
.len = data.len,
@@ -836,19 +884,17 @@ const Blocking = struct {
836884
}
837885

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

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

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)