Skip to content

Commit f04b326

Browse files
committed
Make intervals easier and faster, add window.setInterval and clearInterval
When the browser microtask was added, zig-specific timeout functions were added to the loop. This was necessary for two reasons: 1 - The existing functions were JS specific 2 - We wanted a different reset counter for JS and Zig Like we did in #577, the loop is now JS-agnostic. It gets a Zig callback, and the Zig callback can execute JS (or do whatever). An intrusive node, like with events, is used to minimize allocations. Also, because the microtask was recently moved to the page, there is no longer a need for separate event counters. All timeouts are scoped to the page. The new timeout callback can now be used to efficiently reschedule a task. This reuses the IO.completion and Context, avoiding 2 allocations. More importantly it makes the internal timer_id static for the lifetime of an "interval". This is important for window.setInterval, where the callback can itself clear the interval, which we would need to detect in the callback handler to avoid re-scheduling. With the stable timer_id, the existing cancel mechanism works as expected. The loop no longer has a cbk_error. Callback code is expected to try/catch callbacks (or use callback.tryCall) and handle errors accordingly.
1 parent 7f2506d commit f04b326

File tree

5 files changed

+181
-199
lines changed

5 files changed

+181
-199
lines changed

src/browser/browser.zig

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const Walker = @import("dom/walker.zig").WalkerDepthFirst;
3232

3333
const Env = @import("env.zig").Env;
3434
const App = @import("../app.zig").App;
35+
const Loop = @import("../runtime/loop.zig").Loop;
3536

3637
const URL = @import("../url.zig").URL;
3738

@@ -171,8 +172,7 @@ pub const Session = struct {
171172

172173
std.debug.assert(self.page != null);
173174
// Reset all existing callbacks.
174-
self.browser.app.loop.resetJS();
175-
self.browser.app.loop.resetZig();
175+
self.browser.app.loop.reset();
176176
self.executor.endScope();
177177
self.page = null;
178178

@@ -230,8 +230,11 @@ pub const Page = struct {
230230

231231
renderer: FlatRenderer,
232232

233+
microtask_node: Loop.CallbackNode,
234+
233235
window_clicked_event_node: parser.EventNode,
234236

237+
235238
scope: *Env.Scope,
236239

237240
// current_script is the script currently evaluated by the page.
@@ -248,6 +251,7 @@ pub const Page = struct {
248251
.url = URL.empty,
249252
.session = session,
250253
.renderer = FlatRenderer.init(arena),
254+
.microtask_node = .{ .func = microtaskCallback },
251255
.window_clicked_event_node = .{ .func = windowClicked },
252256
.state = .{
253257
.arena = arena,
@@ -264,13 +268,13 @@ pub const Page = struct {
264268
// load polyfills
265269
try polyfill.load(self.arena, self.scope);
266270

267-
self.microtaskLoop();
271+
// _ = try session.browser.app.loop.timeout(1 * std.time.ns_per_ms, &self.microtask_node);
268272
}
269273

270-
fn microtaskLoop(self: *Page) void {
271-
const browser = self.session.browser;
272-
browser.runMicrotasks();
273-
browser.app.loop.zigTimeout(1 * std.time.ns_per_ms, *Page, self, microtaskLoop);
274+
fn microtaskCallback(node: *Loop.CallbackNode, repeat_delay: *?u63) void {
275+
const self: *Page = @fieldParentPtr("microtask_node", node);
276+
self.session.browser.runMicrotasks();
277+
repeat_delay.* = 1 * std.time.ns_per_ms;
274278
}
275279

276280
// dump writes the page content into the given file.
@@ -297,20 +301,19 @@ pub const Page = struct {
297301
}
298302

299303
pub fn wait(self: *Page) !void {
300-
// try catch
301304
var try_catch: Env.TryCatch = undefined;
302305
try_catch.init(self.scope);
303306
defer try_catch.deinit();
304307

305-
self.session.browser.app.loop.run() catch |err| {
306-
if (try try_catch.err(self.arena)) |msg| {
307-
log.info("wait error: {s}", .{msg});
308-
return;
309-
} else {
310-
log.info("wait error: {any}", .{err});
311-
}
312-
};
313-
log.debug("wait: OK", .{});
308+
try self.session.browser.app.loop.run();
309+
310+
if (try_catch.hasCaught() == false) {
311+
log.debug("wait: OK", .{});
312+
return;
313+
}
314+
315+
const msg = (try try_catch.err(self.arena)) orelse "unknown";
316+
log.info("wait error: {s}", .{msg});
314317
}
315318

316319
pub fn origin(self: *const Page, arena: Allocator) ![]const u8 {

src/browser/html/window.zig

Lines changed: 88 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const std = @import("std");
2121
const parser = @import("../netsurf.zig");
2222
const Callback = @import("../env.zig").Callback;
2323
const SessionState = @import("../env.zig").SessionState;
24+
const Loop = @import("../../runtime/loop.zig").Loop;
2425

2526
const Navigator = @import("navigator.zig").Navigator;
2627
const History = @import("history.zig").History;
@@ -31,6 +32,8 @@ const EventTarget = @import("../dom/event_target.zig").EventTarget;
3132

3233
const storage = @import("../storage/storage.zig");
3334

35+
const log = std.log.scoped(.window);
36+
3437
// https://dom.spec.whatwg.org/#interface-window-extensions
3538
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#window
3639
pub const Window = struct {
@@ -45,10 +48,9 @@ pub const Window = struct {
4548
location: Location = .{},
4649
storage_shelf: ?*storage.Shelf = null,
4750

48-
// store a map between internal timeouts ids and pointers to uint.
49-
// the maximum number of possible timeouts is fixed.
50-
timeoutid: u32 = 0,
51-
timeoutids: [512]u64 = undefined,
51+
// counter for having unique timer ids
52+
timer_id: u31 = 0,
53+
timers: std.AutoHashMapUnmanaged(u32, *TimerCallback) = .{},
5254

5355
crypto: Crypto = .{},
5456
console: Console = .{},
@@ -129,23 +131,93 @@ pub const Window = struct {
129131

130132
// TODO handle callback arguments.
131133
pub fn _setTimeout(self: *Window, cbk: Callback, delay: ?u32, state: *SessionState) !u32 {
132-
if (self.timeoutid >= self.timeoutids.len) return error.TooMuchTimeout;
134+
return self.createTimeout(cbk, delay, state, false);
135+
}
133136

134-
const ddelay: u63 = delay orelse 0;
135-
const id = try state.loop.timeout(ddelay * std.time.ns_per_ms, cbk);
137+
// TODO handle callback arguments.
138+
pub fn _setInterval(self: *Window, cbk: Callback, delay: ?u32, state: *SessionState) !u32 {
139+
return self.createTimeout(cbk, delay, state, true);
140+
}
136141

137-
self.timeoutids[self.timeoutid] = id;
138-
defer self.timeoutid += 1;
142+
pub fn _clearTimeout(self: *Window, id: u32, state: *SessionState) !void {
143+
const kv = self.timers.fetchRemove(id) orelse return;
144+
try state.loop.cancel(kv.value.loop_id);
145+
}
139146

140-
return self.timeoutid;
147+
pub fn _clearInterval(self: *Window, id: u32, state: *SessionState) !void {
148+
const kv = self.timers.fetchRemove(id) orelse return;
149+
try state.loop.cancel(kv.value.loop_id);
141150
}
142151

143-
pub fn _clearTimeout(self: *Window, id: u32, state: *SessionState) !void {
144-
// I do would prefer return an error in this case, but it seems some JS
145-
// uses invalid id, in particular id 0.
146-
// So we silently ignore invalid id for now.
147-
if (id >= self.timeoutid) return;
152+
pub fn createTimeout(self: *Window, cbk: Callback, delay_: ?u32, state: *SessionState, comptime repeat: bool) !u32 {
153+
if (self.timers.count() > 512) {
154+
return error.TooManyTimeout;
155+
}
156+
const timer_id = self.timer_id +% 1;
157+
self.timer_id = timer_id;
158+
159+
const arena = state.arena;
160+
161+
const gop = try self.timers.getOrPut(arena, timer_id);
162+
if (gop.found_existing) {
163+
// this can only happen if we've created 2^31 timeouts.
164+
return error.TooManyTimeout;
165+
}
166+
errdefer _ = self.timers.remove(timer_id);
167+
168+
const delay: u63 = (delay_ orelse 0) * std.time.ns_per_ms;
169+
const callback = try arena.create(TimerCallback);
170+
171+
callback.* = .{
172+
.cbk = cbk,
173+
.loop_id = 0, // we're going to set this to a real value shortly
174+
.window = self,
175+
.timer_id = timer_id,
176+
.node = .{.func = TimerCallback.run},
177+
.repeat = if (repeat) delay else null,
178+
};
179+
callback.loop_id = try state.loop.timeout(delay, &callback.node);
180+
181+
gop.value_ptr.* = callback;
182+
return timer_id;
183+
}
184+
};
185+
186+
const TimerCallback = struct {
187+
// the internal loop id, need it when cancelling
188+
loop_id: usize,
189+
190+
// the id of our timer (windows.timers key)
191+
timer_id: u31,
192+
193+
// The JavaScript callback to execute
194+
cbk: Callback,
195+
196+
// This is the internal data that the event loop tracks. We'll get this
197+
// back in run and, from it, can get our TimerCallback instance
198+
node: Loop.CallbackNode = undefined,
199+
200+
// if the event should be repeated
201+
repeat: ?u63 = null,
202+
203+
window: *Window,
204+
205+
fn run(node: *Loop.CallbackNode, repeat_delay: *?u63) void {
206+
const self: *TimerCallback = @fieldParentPtr("node", node);
207+
208+
var result: Callback.Result = undefined;
209+
self.cbk.tryCall(.{}, &result) catch {
210+
log.err("timeout callback error: {s}", .{result.exception});
211+
log.debug("stack:\n{s}", .{result.stack orelse "???"});
212+
};
213+
214+
if (self.repeat) |r| {
215+
// setInterval
216+
repeat_delay.* = r;
217+
return;
218+
}
148219

149-
try state.loop.cancel(self.timeoutids[id], null);
220+
// setTimeout
221+
_ = self.window.timers.remove(self.timer_id);
150222
}
151223
};

src/main_wpt.zig

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -156,12 +156,11 @@ fn run(arena: Allocator, test_file: []const u8, loader: *FileLoader, err_out: *?
156156
var try_catch: Env.TryCatch = undefined;
157157
try_catch.init(runner.scope);
158158
defer try_catch.deinit();
159-
runner.loop.run() catch |err| {
160-
if (try try_catch.err(arena)) |msg| {
161-
err_out.* = msg;
162-
}
163-
return err;
164-
};
159+
try runner.loop.run();
160+
161+
if (try_catch.hasCaught()) {
162+
err_out.* = (try try_catch.err(arena)) orelse "unknwon error";
163+
}
165164
}
166165

167166
// Check the final test status.

0 commit comments

Comments
 (0)