Skip to content

Commit fdd1a77

Browse files
committed
Properly drain event loop when navigating between pages
1 parent a5d87ab commit fdd1a77

File tree

4 files changed

+61
-35
lines changed

4 files changed

+61
-35
lines changed

src/browser/session.zig

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ pub const Session = struct {
7272

7373
pub fn deinit(self: *Session) void {
7474
if (self.page != null) {
75-
self.removePage();
75+
self.removePage() catch {};
7676
}
7777
self.cookie_jar.deinit();
7878
self.storage_shed.deinit();
@@ -104,14 +104,35 @@ pub const Session = struct {
104104
return page;
105105
}
106106

107-
pub fn removePage(self: *Session) void {
107+
pub fn removePage(self: *Session) !void {
108108
// Inform CDP the page is going to be removed, allowing other worlds to remove themselves before the main one
109109
self.browser.notification.dispatch(.page_remove, .{});
110110

111111
std.debug.assert(self.page != null);
112-
// Reset all existing callbacks.
113-
self.browser.app.loop.reset();
112+
113+
// Cleanup is a bit sensitive. We could still have inflight I/O. For
114+
// example, we could have an XHR request which is still in the connect
115+
// phase. It's important that we clean these up, as they're holding onto
116+
// limited resources (like our fixed-sized http state pool).
117+
//
118+
// First thing we do, is endScope() which will execute the destructor
119+
// of any type that registered a destructor (e.g. XMLHttpRequest).
120+
// This will shutdown any pending sockets, which begins our cleaning
121+
// processed
114122
self.executor.endScope();
123+
124+
// Second thing we do is reset the loop. This increments the loop ctx_id
125+
// so that any "stale" timeouts we process will get ignored. We need to
126+
// do this BEFORE running the loop because, at this point, things like
127+
// window.setTimeout and running microtasks should be ignored
128+
self.browser.app.loop.reset();
129+
130+
// Finally, we run the loop. Because of the reset just above, this will
131+
// ignore any timeouts. And, because of the endScope about this, it
132+
// should ensure that the http requests detect the shutdown socket and
133+
// release their resources.
134+
try self.browser.app.loop.run();
135+
115136
self.page = null;
116137

117138
// clear netsurf memory arena.
@@ -143,7 +164,7 @@ pub const Session = struct {
143164
// the final URL, possibly following redirects)
144165
const url = try self.page.?.url.resolve(self.transfer_arena, url_string);
145166

146-
self.removePage();
167+
try self.removePage();
147168
var page = try self.createPage();
148169
return page.navigate(url, opts);
149170
}

src/cdp/domains/input.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ fn clickNavigate(cmd: anytype, uri: std.Uri) !void {
9090
.disposition = "currentTab",
9191
}, .{ .session_id = bc.session_id.? });
9292

93-
bc.session.removePage();
93+
try bc.session.removePage();
9494
_ = try bc.session.createPage(null);
9595

9696
try @import("page.zig").navigateToUrl(cmd, url, false);

src/cdp/domains/target.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ fn closeTarget(cmd: anytype) !void {
220220
bc.session_id = null;
221221
}
222222

223-
bc.session.removePage();
223+
try bc.session.removePage();
224224
if (bc.isolated_world) |*world| {
225225
world.deinit();
226226
bc.isolated_world = null;

src/runtime/loop.zig

Lines changed: 33 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,11 @@ pub const Loop = struct {
3434
alloc: std.mem.Allocator, // TODO: unmanaged version ?
3535
io: IO,
3636

37-
// Used to track how many callbacks are to be called and wait until all
38-
// event are finished.
39-
events_nb: usize,
37+
// number of pending network events we have
38+
pending_network_count: usize,
39+
40+
// number of pending timeout events we have
41+
pending_timeout_count: usize,
4042

4143
// Used to stop repeating timeouts when loop.run is called.
4244
stopping: bool,
@@ -66,8 +68,9 @@ pub const Loop = struct {
6668
.alloc = alloc,
6769
.cancelled = .{},
6870
.io = try IO.init(32, 0),
69-
.events_nb = 0,
7071
.stopping = false,
72+
.pending_network_count = 0,
73+
.pending_timeout_count = 0,
7174
.timeout_pool = MemoryPool(ContextTimeout).init(alloc),
7275
.event_callback_pool = MemoryPool(EventCallbackContext).init(alloc),
7376
};
@@ -78,7 +81,7 @@ pub const Loop = struct {
7881

7982
// run tail events. We do run the tail events to ensure all the
8083
// contexts are correcly free.
81-
while (self.eventsNb() > 0) {
84+
while (self.hasPendinEvents()) {
8285
self.io.run_for_ns(10 * std.time.ns_per_ms) catch |err| {
8386
log.err(.loop, "deinit", .{ .err = err });
8487
break;
@@ -93,6 +96,21 @@ pub const Loop = struct {
9396
self.cancelled.deinit(self.alloc);
9497
}
9598

99+
// We can shutdown once all the pending network IO is complete.
100+
// In debug mode we also wait until al the pending timeouts are complete
101+
// but we only do this so that the `timeoutCallback` can free all allocated
102+
// memory and we won't report a leak.
103+
fn hasPendinEvents(self: *const Self) bool {
104+
if (self.pending_network_count > 0) {
105+
return true;
106+
}
107+
108+
if (builtin.mode != .Debug) {
109+
return false;
110+
}
111+
return self.pending_timeout_count > 0;
112+
}
113+
96114
// Retrieve all registred I/O events completed by OS kernel,
97115
// and execute sequentially their callbacks.
98116
// Stops when there is no more I/O events registered on the loop.
@@ -103,25 +121,12 @@ pub const Loop = struct {
103121
self.stopping = true;
104122
defer self.stopping = false;
105123

106-
while (self.eventsNb() > 0) {
124+
while (self.pending_network_count > 0) {
107125
try self.io.run_for_ns(10 * std.time.ns_per_ms);
108126
// at each iteration we might have new events registred by previous callbacks
109127
}
110128
}
111129

112-
// Register events atomically
113-
// - add 1 event and return previous value
114-
fn addEvent(self: *Self) void {
115-
_ = @atomicRmw(usize, &self.events_nb, .Add, 1, .acq_rel);
116-
}
117-
// - remove 1 event and return previous value
118-
fn removeEvent(self: *Self) void {
119-
_ = @atomicRmw(usize, &self.events_nb, .Sub, 1, .acq_rel);
120-
}
121-
// - get the number of current events
122-
fn eventsNb(self: *Self) usize {
123-
return @atomicLoad(usize, &self.events_nb, .seq_cst);
124-
}
125130

126131
// JS callbacks APIs
127132
// -----------------
@@ -152,7 +157,7 @@ pub const Loop = struct {
152157
const loop = ctx.loop;
153158

154159
if (ctx.initial) {
155-
loop.removeEvent();
160+
loop.pending_timeout_count -= 1;
156161
}
157162

158163
defer {
@@ -207,7 +212,7 @@ pub const Loop = struct {
207212
.callback_node = callback_node,
208213
};
209214

210-
self.addEvent();
215+
self.pending_timeout_count += 1;
211216
self.scheduleTimeout(nanoseconds, ctx, completion);
212217
return @intFromPtr(completion);
213218
}
@@ -244,17 +249,18 @@ pub const Loop = struct {
244249
) !void {
245250
const onConnect = struct {
246251
fn onConnect(callback: *EventCallbackContext, completion_: *Completion, res: ConnectError!void) void {
252+
callback.loop.pending_network_count -= 1;
247253
defer callback.loop.event_callback_pool.destroy(callback);
248-
callback.loop.removeEvent();
249254
cbk(@alignCast(@ptrCast(callback.ctx)), completion_, res);
250255
}
251256
}.onConnect;
252257

258+
253259
const callback = try self.event_callback_pool.create();
254260
errdefer self.event_callback_pool.destroy(callback);
255261
callback.* = .{ .loop = self, .ctx = ctx };
256262

257-
self.addEvent();
263+
self.pending_network_count += 1;
258264
self.io.connect(*EventCallbackContext, callback, onConnect, completion, socket, address);
259265
}
260266

@@ -271,8 +277,8 @@ pub const Loop = struct {
271277
) !void {
272278
const onSend = struct {
273279
fn onSend(callback: *EventCallbackContext, completion_: *Completion, res: SendError!usize) void {
280+
callback.loop.pending_network_count -= 1;
274281
defer callback.loop.event_callback_pool.destroy(callback);
275-
callback.loop.removeEvent();
276282
cbk(@alignCast(@ptrCast(callback.ctx)), completion_, res);
277283
}
278284
}.onSend;
@@ -281,7 +287,7 @@ pub const Loop = struct {
281287
errdefer self.event_callback_pool.destroy(callback);
282288
callback.* = .{ .loop = self, .ctx = ctx };
283289

284-
self.addEvent();
290+
self.pending_network_count += 1;
285291
self.io.send(*EventCallbackContext, callback, onSend, completion, socket, buf);
286292
}
287293

@@ -298,17 +304,16 @@ pub const Loop = struct {
298304
) !void {
299305
const onRecv = struct {
300306
fn onRecv(callback: *EventCallbackContext, completion_: *Completion, res: RecvError!usize) void {
307+
callback.loop.pending_network_count -= 1;
301308
defer callback.loop.event_callback_pool.destroy(callback);
302-
callback.loop.removeEvent();
303309
cbk(@alignCast(@ptrCast(callback.ctx)), completion_, res);
304310
}
305311
}.onRecv;
306312

307313
const callback = try self.event_callback_pool.create();
308314
errdefer self.event_callback_pool.destroy(callback);
309315
callback.* = .{ .loop = self, .ctx = ctx };
310-
311-
self.addEvent();
316+
self.pending_network_count += 1;
312317
self.io.recv(*EventCallbackContext, callback, onRecv, completion, socket, buf);
313318
}
314319
};

0 commit comments

Comments
 (0)