Merge pull request #600 from lightpanda-io/timeouts_and_intervals

Make intervals easier and faster, add window.setInterval and clearInt…
This commit is contained in:
Pierre Tachoire
2025-05-06 15:18:15 +02:00
committed by GitHub
6 changed files with 183 additions and 201 deletions

View File

@@ -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});
}
};
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 {

View File

@@ -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);
}
};

View File

@@ -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;
try runner.loop.run();
if (try_catch.hasCaught()) {
err_out.* = (try try_catch.err(arena)) orelse "unknwon error";
}
return err;
};
}
// Check the final test status.

View File

@@ -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.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 {

View File

@@ -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,

View File

@@ -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", .{});
}
}
}