diff --git a/src/browser/browser.zig b/src/browser/browser.zig index b9549e5b..b68c4d67 100644 --- a/src/browser/browser.zig +++ b/src/browser/browser.zig @@ -32,6 +32,7 @@ const Walker = @import("dom/walker.zig").WalkerDepthFirst; const Env = @import("env.zig").Env; const App = @import("../app.zig").App; +const Loop = @import("../runtime/loop.zig").Loop; const URL = @import("../url.zig").URL; @@ -171,8 +172,7 @@ pub const Session = struct { std.debug.assert(self.page != null); // Reset all existing callbacks. - self.browser.app.loop.resetJS(); - self.browser.app.loop.resetZig(); + self.browser.app.loop.reset(); self.executor.endScope(); self.page = null; @@ -230,6 +230,8 @@ pub const Page = struct { renderer: FlatRenderer, + microtask_node: Loop.CallbackNode, + window_clicked_event_node: parser.EventNode, scope: *Env.Scope, @@ -248,6 +250,7 @@ pub const Page = struct { .url = URL.empty, .session = session, .renderer = FlatRenderer.init(arena), + .microtask_node = .{ .func = microtaskCallback }, .window_clicked_event_node = .{ .func = windowClicked }, .state = .{ .arena = arena, @@ -264,13 +267,13 @@ pub const Page = struct { // load polyfills try polyfill.load(self.arena, self.scope); - self.microtaskLoop(); + // _ = try session.browser.app.loop.timeout(1 * std.time.ns_per_ms, &self.microtask_node); } - fn microtaskLoop(self: *Page) void { - const browser = self.session.browser; - browser.runMicrotasks(); - browser.app.loop.zigTimeout(1 * std.time.ns_per_ms, *Page, self, microtaskLoop); + fn microtaskCallback(node: *Loop.CallbackNode, repeat_delay: *?u63) void { + const self: *Page = @fieldParentPtr("microtask_node", node); + self.session.browser.runMicrotasks(); + repeat_delay.* = 1 * std.time.ns_per_ms; } // dump writes the page content into the given file. @@ -297,20 +300,19 @@ pub const Page = struct { } pub fn wait(self: *Page) !void { - // try catch var try_catch: Env.TryCatch = undefined; try_catch.init(self.scope); defer try_catch.deinit(); - self.session.browser.app.loop.run() catch |err| { - if (try try_catch.err(self.arena)) |msg| { - log.info("wait error: {s}", .{msg}); - return; - } else { - log.info("wait error: {any}", .{err}); - } - }; - log.debug("wait: OK", .{}); + try self.session.browser.app.loop.run(); + + if (try_catch.hasCaught() == false) { + log.debug("wait: OK", .{}); + return; + } + + const msg = (try try_catch.err(self.arena)) orelse "unknown"; + log.info("wait error: {s}", .{msg}); } pub fn origin(self: *const Page, arena: Allocator) ![]const u8 { diff --git a/src/browser/html/window.zig b/src/browser/html/window.zig index 4d95b5a0..8ca52231 100644 --- a/src/browser/html/window.zig +++ b/src/browser/html/window.zig @@ -21,6 +21,7 @@ const std = @import("std"); const parser = @import("../netsurf.zig"); const Callback = @import("../env.zig").Callback; const SessionState = @import("../env.zig").SessionState; +const Loop = @import("../../runtime/loop.zig").Loop; const Navigator = @import("navigator.zig").Navigator; const History = @import("history.zig").History; @@ -31,6 +32,8 @@ const EventTarget = @import("../dom/event_target.zig").EventTarget; const storage = @import("../storage/storage.zig"); +const log = std.log.scoped(.window); + // https://dom.spec.whatwg.org/#interface-window-extensions // https://html.spec.whatwg.org/multipage/nav-history-apis.html#window pub const Window = struct { @@ -45,10 +48,9 @@ pub const Window = struct { location: Location = .{}, storage_shelf: ?*storage.Shelf = null, - // store a map between internal timeouts ids and pointers to uint. - // the maximum number of possible timeouts is fixed. - timeoutid: u32 = 0, - timeoutids: [512]u64 = undefined, + // counter for having unique timer ids + timer_id: u31 = 0, + timers: std.AutoHashMapUnmanaged(u32, *TimerCallback) = .{}, crypto: Crypto = .{}, console: Console = .{}, @@ -129,23 +131,93 @@ pub const Window = struct { // TODO handle callback arguments. pub fn _setTimeout(self: *Window, cbk: Callback, delay: ?u32, state: *SessionState) !u32 { - if (self.timeoutid >= self.timeoutids.len) return error.TooMuchTimeout; + return self.createTimeout(cbk, delay, state, false); + } - const ddelay: u63 = delay orelse 0; - const id = try state.loop.timeout(ddelay * std.time.ns_per_ms, cbk); - - self.timeoutids[self.timeoutid] = id; - defer self.timeoutid += 1; - - return self.timeoutid; + // TODO handle callback arguments. + pub fn _setInterval(self: *Window, cbk: Callback, delay: ?u32, state: *SessionState) !u32 { + return self.createTimeout(cbk, delay, state, true); } pub fn _clearTimeout(self: *Window, id: u32, state: *SessionState) !void { - // I do would prefer return an error in this case, but it seems some JS - // uses invalid id, in particular id 0. - // So we silently ignore invalid id for now. - if (id >= self.timeoutid) return; + const kv = self.timers.fetchRemove(id) orelse return; + try state.loop.cancel(kv.value.loop_id); + } - try state.loop.cancel(self.timeoutids[id], null); + pub fn _clearInterval(self: *Window, id: u32, state: *SessionState) !void { + const kv = self.timers.fetchRemove(id) orelse return; + try state.loop.cancel(kv.value.loop_id); + } + + pub fn createTimeout(self: *Window, cbk: Callback, delay_: ?u32, state: *SessionState, comptime repeat: bool) !u32 { + if (self.timers.count() > 512) { + return error.TooManyTimeout; + } + const timer_id = self.timer_id +% 1; + self.timer_id = timer_id; + + const arena = state.arena; + + const gop = try self.timers.getOrPut(arena, timer_id); + if (gop.found_existing) { + // this can only happen if we've created 2^31 timeouts. + return error.TooManyTimeout; + } + errdefer _ = self.timers.remove(timer_id); + + const delay: u63 = (delay_ orelse 0) * std.time.ns_per_ms; + const callback = try arena.create(TimerCallback); + + callback.* = .{ + .cbk = cbk, + .loop_id = 0, // we're going to set this to a real value shortly + .window = self, + .timer_id = timer_id, + .node = .{ .func = TimerCallback.run }, + .repeat = if (repeat) delay else null, + }; + callback.loop_id = try state.loop.timeout(delay, &callback.node); + + gop.value_ptr.* = callback; + return timer_id; + } +}; + +const TimerCallback = struct { + // the internal loop id, need it when cancelling + loop_id: usize, + + // the id of our timer (windows.timers key) + timer_id: u31, + + // The JavaScript callback to execute + cbk: Callback, + + // This is the internal data that the event loop tracks. We'll get this + // back in run and, from it, can get our TimerCallback instance + node: Loop.CallbackNode = undefined, + + // if the event should be repeated + repeat: ?u63 = null, + + window: *Window, + + fn run(node: *Loop.CallbackNode, repeat_delay: *?u63) void { + const self: *TimerCallback = @fieldParentPtr("node", node); + + var result: Callback.Result = undefined; + self.cbk.tryCall(.{}, &result) catch { + log.err("timeout callback error: {s}", .{result.exception}); + log.debug("stack:\n{s}", .{result.stack orelse "???"}); + }; + + if (self.repeat) |r| { + // setInterval + repeat_delay.* = r; + return; + } + + // setTimeout + _ = self.window.timers.remove(self.timer_id); } }; diff --git a/src/main_wpt.zig b/src/main_wpt.zig index c3dbd2f3..744c188b 100644 --- a/src/main_wpt.zig +++ b/src/main_wpt.zig @@ -156,12 +156,11 @@ fn run(arena: Allocator, test_file: []const u8, loader: *FileLoader, err_out: *? var try_catch: Env.TryCatch = undefined; try_catch.init(runner.scope); defer try_catch.deinit(); - runner.loop.run() catch |err| { - if (try try_catch.err(arena)) |msg| { - err_out.* = msg; - } - return err; - }; + try runner.loop.run(); + + if (try_catch.hasCaught()) { + err_out.* = (try try_catch.err(arena)) orelse "unknwon error"; + } } // Check the final test status. diff --git a/src/runtime/loop.zig b/src/runtime/loop.zig index 2a181bc3..35799604 100644 --- a/src/runtime/loop.zig +++ b/src/runtime/loop.zig @@ -37,24 +37,15 @@ pub const Loop = struct { alloc: std.mem.Allocator, // TODO: unmanaged version ? io: IO, - // both events_nb are used to track how many callbacks are to be called. - // We use these counters to wait until all the events are finished. - js_events_nb: usize, - zig_events_nb: usize, + // Used to track how many callbacks are to be called and wait until all + // event are finished. + events_nb: usize, - cbk_error: bool = false, - - // js_ctx_id is incremented each time the loop is reset for JS. - // All JS callbacks store an initial js_ctx_id and compare before execution. + // ctx_id is incremented each time the loop is reset. + // All callbacks store an initial ctx_id and compare before execution. // If a ctx is outdated, the callback is ignored. - // This is a weak way to cancel all future JS callbacks. - js_ctx_id: u32 = 0, - - // zig_ctx_id is incremented each time the loop is reset for Zig. - // All Zig callbacks store an initial zig_ctx_id and compare before execution. - // If a ctx is outdated, the callback is ignored. - // This is a weak way to cancel all future Zig callbacks. - zig_ctx_id: u32 = 0, + // This is a weak way to cancel all future callbacks. + ctx_id: u32 = 0, // We use this to track cancellation ids and, on the timeout callback, // we can can check here to see if it's been cancelled. @@ -66,32 +57,27 @@ pub const Loop = struct { const Self = @This(); pub const Completion = IO.Completion; - pub const ConnectError = IO.ConnectError; pub const RecvError = IO.RecvError; pub const SendError = IO.SendError; + pub const ConnectError = IO.ConnectError; pub fn init(alloc: std.mem.Allocator) !Self { return Self{ .alloc = alloc, .cancelled = .{}, .io = try IO.init(32, 0), - .js_events_nb = 0, - .zig_events_nb = 0, + .events_nb = 0, .timeout_pool = MemoryPool(ContextTimeout).init(alloc), .event_callback_pool = MemoryPool(EventCallbackContext).init(alloc), }; } pub fn deinit(self: *Self) void { - // first disable callbacks for existing events. - // We don't want a callback re-create a setTimeout, it could create an - // infinite loop on wait for events. - self.resetJS(); - self.resetZig(); + self.reset(); // run tail events. We do run the tail events to ensure all the // contexts are correcly free. - while (self.eventsNb(.js) > 0 or self.eventsNb(.zig) > 0) { + while (self.eventsNb() > 0) { self.io.run_for_ns(10 * std.time.ns_per_ms) catch |err| { log.err("deinit run tail events: {any}", .{err}); break; @@ -112,40 +98,24 @@ pub const Loop = struct { // Note that I/O events callbacks might register more I/O events // on the go when they are executed (ie. nested I/O events). pub fn run(self: *Self) !void { - while (self.eventsNb(.js) > 0) { + while (self.eventsNb() > 0) { try self.io.run_for_ns(10 * std.time.ns_per_ms); // at each iteration we might have new events registred by previous callbacks } - // TODO: return instead immediatly on the first JS callback error - // and let the caller decide what to do next - // (typically retrieve the exception through the TryCatch and - // continue the execution of callbacks with a new call to loop.run) - if (self.cbk_error) { - return error.JSExecCallback; - } - } - - const Event = enum { js, zig }; - - fn eventsPtr(self: *Self, comptime event: Event) *usize { - return switch (event) { - .zig => &self.zig_events_nb, - .js => &self.js_events_nb, - }; } // Register events atomically // - add 1 event and return previous value - fn addEvent(self: *Self, comptime event: Event) void { - _ = @atomicRmw(usize, self.eventsPtr(event), .Add, 1, .acq_rel); + fn addEvent(self: *Self) void { + _ = @atomicRmw(usize, &self.events_nb, .Add, 1, .acq_rel); } // - remove 1 event and return previous value - fn removeEvent(self: *Self, comptime event: Event) void { - _ = @atomicRmw(usize, self.eventsPtr(event), .Sub, 1, .acq_rel); + fn removeEvent(self: *Self) void { + _ = @atomicRmw(usize, &self.events_nb, .Sub, 1, .acq_rel); } // - get the number of current events - fn eventsNb(self: *Self, comptime event: Event) usize { - return @atomicLoad(usize, self.eventsPtr(event), .seq_cst); + fn eventsNb(self: *Self) usize { + return @atomicLoad(usize, &self.events_nb, .seq_cst); } // JS callbacks APIs @@ -153,10 +123,18 @@ pub const Loop = struct { // Timeout + // The state that we add to a timeout. This is what we get back from a + // timeoutCallback. It contains the function to execute. The user is expected + // to be able to turn a reference to this into whatever state it needs, + // probably by inserting this node into its own stae and using @fieldParentPtr + pub const CallbackNode = struct { + func: *const fn (node: *CallbackNode, repeat: *?u63) void, + }; + const ContextTimeout = struct { loop: *Self, - js_cbk: ?JSCallback, - js_ctx_id: u32, + ctx_id: u32, + callback_node: ?*CallbackNode, }; fn timeoutCallback( @@ -164,21 +142,25 @@ pub const Loop = struct { completion: *IO.Completion, result: IO.TimeoutError!void, ) void { + var repeating = false; const loop = ctx.loop; + defer { - loop.removeEvent(.js); - loop.timeout_pool.destroy(ctx); - loop.alloc.destroy(completion); + loop.removeEvent(); + if (repeating == false) { + loop.timeout_pool.destroy(ctx); + loop.alloc.destroy(completion); + } } if (loop.cancelled.remove(@intFromPtr(completion))) { return; } - // If the loop's context id has changed, don't call the js callback - // function. The callback's memory has already be cleaned and the - // events nb reset. - if (ctx.js_ctx_id != loop.js_ctx_id) return; + // Abort if this completion was created for a different version of the loop. + if (ctx.ctx_id != loop.ctx_id) { + return; + } // TODO: return the error to the callback result catch |err| { @@ -189,56 +171,51 @@ pub const Loop = struct { return; }; - // js callback - if (ctx.js_cbk) |*js_cbk| { - js_cbk.call(null) catch { - loop.cbk_error = true; - }; + if (ctx.callback_node) |cn| { + var repeat_in: ?u63 = null; + cn.func(cn, &repeat_in); + if (repeat_in) |r| { + // prevents our context and completion from being cleaned up + repeating = true; + loop.scheduleTimeout(r, ctx, completion); + } } } - pub fn timeout(self: *Self, nanoseconds: u63, js_cbk: ?JSCallback) !usize { + pub fn timeout(self: *Self, nanoseconds: u63, callback_node: ?*CallbackNode) !usize { const completion = try self.alloc.create(Completion); errdefer self.alloc.destroy(completion); completion.* = undefined; const ctx = try self.timeout_pool.create(); errdefer self.timeout_pool.destroy(ctx); - ctx.* = ContextTimeout{ + ctx.* = .{ .loop = self, - .js_cbk = js_cbk, - .js_ctx_id = self.js_ctx_id, + .ctx_id = self.ctx_id, + .callback_node = callback_node, }; - self.addEvent(.js); - self.io.timeout(*ContextTimeout, ctx, timeoutCallback, completion, nanoseconds); + self.scheduleTimeout(nanoseconds, ctx, completion); return @intFromPtr(completion); } - pub fn cancel(self: *Self, id: usize, js_cbk: ?JSCallback) !void { + fn scheduleTimeout(self: *Self, nanoseconds: u63, ctx: *ContextTimeout, completion: *Completion) void { + self.addEvent(); + self.io.timeout(*ContextTimeout, ctx, timeoutCallback, completion, nanoseconds); + } + + pub fn cancel(self: *Self, id: usize) !void { try self.cancelled.put(self.alloc, id, {}); - if (js_cbk) |cbk| { - cbk.call(null) catch { - self.cbk_error = true; - }; - } } - // Reset all existing JS callbacks. + // Reset all existing callbacks. // The existing events will happen and their memory will be cleanup but the // corresponding callbacks will not be called. - pub fn resetJS(self: *Self) void { - self.js_ctx_id += 1; + pub fn reset(self: *Self) void { + self.ctx_id += 1; self.cancelled.clearRetainingCapacity(); } - // Reset all existing Zig callbacks. - // The existing events will happen and their memory will be cleanup but the - // corresponding callbacks will not be called. - pub fn resetZig(self: *Self) void { - self.zig_ctx_id += 1; - } - // IO callbacks APIs // ----------------- @@ -256,7 +233,7 @@ pub const Loop = struct { const onConnect = struct { fn onConnect(callback: *EventCallbackContext, completion_: *Completion, res: ConnectError!void) void { defer callback.loop.event_callback_pool.destroy(callback); - callback.loop.removeEvent(.js); + callback.loop.removeEvent(); cbk(@alignCast(@ptrCast(callback.ctx)), completion_, res); } }.onConnect; @@ -265,7 +242,7 @@ pub const Loop = struct { errdefer self.event_callback_pool.destroy(callback); callback.* = .{ .loop = self, .ctx = ctx }; - self.addEvent(.js); + self.addEvent(); self.io.connect(*EventCallbackContext, callback, onConnect, completion, socket, address); } @@ -283,7 +260,7 @@ pub const Loop = struct { const onSend = struct { fn onSend(callback: *EventCallbackContext, completion_: *Completion, res: SendError!usize) void { defer callback.loop.event_callback_pool.destroy(callback); - callback.loop.removeEvent(.js); + callback.loop.removeEvent(); cbk(@alignCast(@ptrCast(callback.ctx)), completion_, res); } }.onSend; @@ -292,7 +269,7 @@ pub const Loop = struct { errdefer self.event_callback_pool.destroy(callback); callback.* = .{ .loop = self, .ctx = ctx }; - self.addEvent(.js); + self.addEvent(); self.io.send(*EventCallbackContext, callback, onSend, completion, socket, buf); } @@ -310,7 +287,7 @@ pub const Loop = struct { const onRecv = struct { fn onRecv(callback: *EventCallbackContext, completion_: *Completion, res: RecvError!usize) void { defer callback.loop.event_callback_pool.destroy(callback); - callback.loop.removeEvent(.js); + callback.loop.removeEvent(); cbk(@alignCast(@ptrCast(callback.ctx)), completion_, res); } }.onRecv; @@ -319,76 +296,9 @@ pub const Loop = struct { errdefer self.event_callback_pool.destroy(callback); callback.* = .{ .loop = self, .ctx = ctx }; - self.addEvent(.js); + self.addEvent(); self.io.recv(*EventCallbackContext, callback, onRecv, completion, socket, buf); } - - // Zig timeout - - const ContextZigTimeout = struct { - loop: *Self, - zig_ctx_id: u32, - - context: *anyopaque, - callback: *const fn ( - context: ?*anyopaque, - ) void, - }; - - fn zigTimeoutCallback( - ctx: *ContextZigTimeout, - completion: *IO.Completion, - result: IO.TimeoutError!void, - ) void { - const loop = ctx.loop; - defer { - loop.removeEvent(.zig); - loop.alloc.destroy(ctx); - loop.alloc.destroy(completion); - } - - // If the loop's context id has changed, don't call the js callback - // function. The callback's memory has already be cleaned and the - // events nb reset. - if (ctx.zig_ctx_id != loop.zig_ctx_id) return; - - result catch |err| { - switch (err) { - error.Canceled => {}, - else => log.err("zig timeout callback: {any}", .{err}), - } - return; - }; - - // callback - ctx.callback(ctx.context); - } - - // zigTimeout performs a timeout but the callback is a zig function. - pub fn zigTimeout( - self: *Self, - nanoseconds: u63, - comptime Context: type, - context: Context, - comptime callback: fn (context: Context) void, - ) void { - const completion = self.alloc.create(IO.Completion) catch unreachable; - completion.* = undefined; - const ctxtimeout = self.alloc.create(ContextZigTimeout) catch unreachable; - ctxtimeout.* = ContextZigTimeout{ - .loop = self, - .zig_ctx_id = self.zig_ctx_id, - .context = context, - .callback = struct { - fn wrapper(ctx: ?*anyopaque) void { - callback(@ptrCast(@alignCast(ctx))); - } - }.wrapper, - }; - - self.addEvent(.zig); - self.io.timeout(*ContextZigTimeout, ctxtimeout, zigTimeoutCallback, completion, nanoseconds); - } }; const EventCallbackContext = struct { diff --git a/src/runtime/testing.zig b/src/runtime/testing.zig index 57550879..a7524509 100644 --- a/src/runtime/testing.zig +++ b/src/runtime/testing.zig @@ -26,7 +26,9 @@ pub const allocator = std.testing.allocator; // browser.Env or the browser.SessionState pub fn Runner(comptime State: type, comptime Global: type, comptime types: anytype) type { const AdjustedTypes = if (Global == void) generate.Tuple(.{ types, DefaultGlobal }) else types; - const Env = js.Env(State, struct {pub const Interfaces = AdjustedTypes;}); + const Env = js.Env(State, struct { + pub const Interfaces = AdjustedTypes; + }); return struct { env: *Env, diff --git a/src/server.zig b/src/server.zig index 0a65c5b0..12372048 100644 --- a/src/server.zig +++ b/src/server.zig @@ -1042,9 +1042,6 @@ pub fn run( // - JS callbacks events from scripts while (true) { try loop.io.run_for_ns(10 * std.time.ns_per_ms); - if (loop.cbk_error) { - log.err("JS error", .{}); - } } }