mirror of
				https://github.com/lightpanda-io/browser.git
				synced 2025-10-30 15:41:48 +00:00 
			
		
		
		
	Rework page wait again
Further reducing bouncing between page and server for loop polling. If there is a page, the page polls. If there isn't a page, the server polls. Simpler.
This commit is contained in:
		| @@ -44,6 +44,7 @@ pub fn reset(self: *Scheduler) void { | |||||||
|  |  | ||||||
| const AddOpts = struct { | const AddOpts = struct { | ||||||
|     name: []const u8 = "", |     name: []const u8 = "", | ||||||
|  |     low_priority: bool = false, | ||||||
| }; | }; | ||||||
| pub fn add(self: *Scheduler, ctx: *anyopaque, func: Task.Func, ms: u32, opts: AddOpts) !void { | pub fn add(self: *Scheduler, ctx: *anyopaque, func: Task.Func, ms: u32, opts: AddOpts) !void { | ||||||
|     if (ms > 5_000) { |     if (ms > 5_000) { | ||||||
| @@ -51,7 +52,9 @@ pub fn add(self: *Scheduler, ctx: *anyopaque, func: Task.Func, ms: u32, opts: Ad | |||||||
|         // ignore any task that we're almost certainly never going to run |         // ignore any task that we're almost certainly never going to run | ||||||
|         return; |         return; | ||||||
|     } |     } | ||||||
|     return self.primary.add(.{ |  | ||||||
|  |     var q = if (opts.low_priority) &self.secondary else &self.primary; | ||||||
|  |     return q.add(.{ | ||||||
|         .ms = std.time.milliTimestamp() + ms, |         .ms = std.time.milliTimestamp() + ms, | ||||||
|         .ctx = ctx, |         .ctx = ctx, | ||||||
|         .func = func, |         .func = func, | ||||||
|   | |||||||
| @@ -428,7 +428,7 @@ fn errorCallback(ctx: *anyopaque, err: anyerror) void { | |||||||
| // It could be pending because: | // It could be pending because: | ||||||
| //   (a) we're still downloading its content or | //   (a) we're still downloading its content or | ||||||
| //   (b) this is a non-async script that has to be executed in order | //   (b) this is a non-async script that has to be executed in order | ||||||
| const PendingScript = struct { | pub const PendingScript = struct { | ||||||
|     script: Script, |     script: Script, | ||||||
|     complete: bool, |     complete: bool, | ||||||
|     node: OrderList.Node, |     node: OrderList.Node, | ||||||
|   | |||||||
| @@ -215,7 +215,11 @@ pub const Window = struct { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     pub fn _requestAnimationFrame(self: *Window, cbk: Function, page: *Page) !u32 { |     pub fn _requestAnimationFrame(self: *Window, cbk: Function, page: *Page) !u32 { | ||||||
|         return self.createTimeout(cbk, 5, page, .{ .animation_frame = true, .name = "animationFrame" }); |         return self.createTimeout(cbk, 5, page, .{ | ||||||
|  |             .animation_frame = true, | ||||||
|  |             .name = "animationFrame", | ||||||
|  |             .low_priority = true, | ||||||
|  |         }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     pub fn _cancelAnimationFrame(self: *Window, id: u32) !void { |     pub fn _cancelAnimationFrame(self: *Window, id: u32) !void { | ||||||
| @@ -269,6 +273,7 @@ pub const Window = struct { | |||||||
|         args: []Env.JsObject = &.{}, |         args: []Env.JsObject = &.{}, | ||||||
|         repeat: bool = false, |         repeat: bool = false, | ||||||
|         animation_frame: bool = false, |         animation_frame: bool = false, | ||||||
|  |         low_priority: bool = false, | ||||||
|     }; |     }; | ||||||
|     fn createTimeout(self: *Window, cbk: Function, delay_: ?u32, page: *Page, opts: CreateTimeoutOpts) !u32 { |     fn createTimeout(self: *Window, cbk: Function, delay_: ?u32, page: *Page, opts: CreateTimeoutOpts) !u32 { | ||||||
|         const delay = delay_ orelse 0; |         const delay = delay_ orelse 0; | ||||||
| @@ -319,7 +324,10 @@ pub const Window = struct { | |||||||
|             .repeat = if (opts.repeat) delay + 1 else null, |             .repeat = if (opts.repeat) delay + 1 else null, | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
|         try page.scheduler.add(callback, TimerCallback.run, delay, .{ .name = opts.name }); |         try page.scheduler.add(callback, TimerCallback.run, delay, .{ | ||||||
|  |             .name = opts.name, | ||||||
|  |             .low_priority = opts.low_priority, | ||||||
|  |         }); | ||||||
|  |  | ||||||
|         return timer_id; |         return timer_id; | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -90,16 +90,6 @@ pub const Page = struct { | |||||||
|  |  | ||||||
|     load_state: LoadState = .parsing, |     load_state: LoadState = .parsing, | ||||||
|  |  | ||||||
|     // Page.wait balances waiting for resources / tasks and producing an output. |  | ||||||
|     // Up until a timeout, Page.wait will always wait for inflight or pending |  | ||||||
|     // HTTP requests, via the Http.Client.active counter. However, intercepted |  | ||||||
|     // requests (via CDP, but it could be anything), aren't considered "active" |  | ||||||
|     // connection. So it's possible that we have intercepted requests (which are |  | ||||||
|     // pending on some driver to continue/abort) while Http.Client.active == 0. |  | ||||||
|     // This boolean exists to supplment Http.Client.active and inform Page.wait |  | ||||||
|     // of pending connections. |  | ||||||
|     request_intercepted: bool = false, |  | ||||||
|  |  | ||||||
|     const Mode = union(enum) { |     const Mode = union(enum) { | ||||||
|         pre: void, |         pre: void, | ||||||
|         err: anyerror, |         err: anyerror, | ||||||
| @@ -262,23 +252,26 @@ pub const Page = struct { | |||||||
|         return self.script_manager.blockingGet(src); |         return self.script_manager.blockingGet(src); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     pub fn wait(self: *Page, wait_sec: u16) void { |     pub fn wait(self: *Page, wait_ms: i32) Session.WaitResult { | ||||||
|         self._wait(wait_sec) catch |err| switch (err) { |         return self._wait(wait_ms) catch |err| { | ||||||
|             error.JsError => {}, // already logged (with hopefully more context) |             switch (err) { | ||||||
|             else => { |                 error.JsError => {}, // already logged (with hopefully more context) | ||||||
|                 // There may be errors from the http/client or ScriptManager |                 else => { | ||||||
|                 // that we should not treat as an error like this. Will need |                     // There may be errors from the http/client or ScriptManager | ||||||
|                 // to run this through more real-world sites and see if we need |                     // that we should not treat as an error like this. Will need | ||||||
|                 // to expand the switch (err) to have more customized logs for |                     // to run this through more real-world sites and see if we need | ||||||
|                 // specific messages. |                     // to expand the switch (err) to have more customized logs for | ||||||
|                 log.err(.browser, "page wait", .{ .err = err }); |                     // specific messages. | ||||||
|             }, |                     log.err(.browser, "page wait", .{ .err = err }); | ||||||
|  |                 }, | ||||||
|  |             } | ||||||
|  |             return .done; | ||||||
|         }; |         }; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     fn _wait(self: *Page, wait_sec: u16) !void { |     fn _wait(self: *Page, wait_ms: i32) !Session.WaitResult { | ||||||
|         var timer = try std.time.Timer.start(); |         var timer = try std.time.Timer.start(); | ||||||
|         var ms_remaining: i32 = @intCast(wait_sec * 1000); |         var ms_remaining = wait_ms; | ||||||
|  |  | ||||||
|         var try_catch: Env.TryCatch = undefined; |         var try_catch: Env.TryCatch = undefined; | ||||||
|         try_catch.init(self.main_context); |         try_catch.init(self.main_context); | ||||||
| @@ -287,40 +280,42 @@ pub const Page = struct { | |||||||
|         var scheduler = &self.scheduler; |         var scheduler = &self.scheduler; | ||||||
|         var http_client = self.http_client; |         var http_client = self.http_client; | ||||||
|  |  | ||||||
|  |         // I'd like the page to know NOTHING about extra_socket / CDP, but the | ||||||
|  |         // fact is that the behavior of wait changes depending on whether or | ||||||
|  |         // not we're using CDP. | ||||||
|  |         // If we aren't using CDP, as soon as we think there's nothing left | ||||||
|  |         // to do, we can exit - we'de done. | ||||||
|  |         // But if we are using CDP, we should wait for the whole `wait_ms` | ||||||
|  |         // because the http_click.tick() also monitors the CDP socket. And while | ||||||
|  |         // we could let CDP poll http (like it does for HTTP requests), the fact | ||||||
|  |         // is that we know more about the timing of stuff (e.g. how long to | ||||||
|  |         // poll/sleep) in the page. | ||||||
|  |         const exit_when_done = http_client.extra_socket == null; | ||||||
|  |  | ||||||
|         // for debugging |         // for debugging | ||||||
|         // defer self.printWaitAnalysis(); |         // defer self.printWaitAnalysis(); | ||||||
|  |  | ||||||
|         while (true) { |         while (true) { | ||||||
|             SW: switch (self.mode) { |             switch (self.mode) { | ||||||
|                 .pre, .raw, .text => { |                 .pre, .raw, .text => { | ||||||
|                     if (self.request_intercepted) { |  | ||||||
|                         // the page request was intercepted. |  | ||||||
|  |  | ||||||
|                         // there shouldn't be any active requests; |  | ||||||
|                         std.debug.assert(http_client.active == 0); |  | ||||||
|  |  | ||||||
|                         // nothing we can do for this, need to kick the can up |  | ||||||
|                         // the chain and wait for activity (e.g. a CDP message) |  | ||||||
|                         // to unblock this. |  | ||||||
|                         return; |  | ||||||
|                     } |  | ||||||
|  |  | ||||||
|                     // The main page hasn't started/finished navigating. |                     // The main page hasn't started/finished navigating. | ||||||
|                     // There's no JS to run, and no reason to run the scheduler. |                     // There's no JS to run, and no reason to run the scheduler. | ||||||
|                     if (http_client.active == 0) { |                     if (http_client.active == 0 and exit_when_done) { | ||||||
|                         // haven't started navigating, I guess. |                         // haven't started navigating, I guess. | ||||||
|                         return; |                         return .done; | ||||||
|                     } |                     } | ||||||
|  |  | ||||||
|                     // There should only be 1 active http transfer, the main page |                     // Either we have active http connections, or we're in CDP | ||||||
|  |                     // mode with an extra socket. Either way, we're waiting | ||||||
|  |                     // for http traffic | ||||||
|                     if (try http_client.tick(ms_remaining) == .extra_socket) { |                     if (try http_client.tick(ms_remaining) == .extra_socket) { | ||||||
|                         // data on a socket we aren't handling, return to caller |                         // data on a socket we aren't handling, return to caller | ||||||
|                         return; |                         return .extra_socket; | ||||||
|                     } |                     } | ||||||
|                 }, |                 }, | ||||||
|                 .html, .parsed => { |                 .html, .parsed => { | ||||||
|                     // The HTML page was parsed. We now either have JS scripts to |                     // The HTML page was parsed. We now either have JS scripts to | ||||||
|                     // download, or timeouts to execute, or both. |                     // download, or scheduled tasks to execute, or both. | ||||||
|  |  | ||||||
|                     // scheduler.run could trigger new http transfers, so do not |                     // scheduler.run could trigger new http transfers, so do not | ||||||
|                     // store http_client.active BEFORE this call and then use |                     // store http_client.active BEFORE this call and then use | ||||||
| @@ -333,72 +328,56 @@ pub const Page = struct { | |||||||
|                         return error.JsError; |                         return error.JsError; | ||||||
|                     } |                     } | ||||||
|  |  | ||||||
|                     if (http_client.active == 0) { |                     if (http_client.active == 0 and exit_when_done) { | ||||||
|                         if (ms_to_next_task) |ms| { |                         const ms = ms_to_next_task orelse { | ||||||
|                             // There are no HTTP transfers, so there's no point calling |                             // no http transfers, no cdp extra socket, no | ||||||
|                             // http_client.tick. |                             // scheduled tasks, we're done. | ||||||
|                             // TODO: should we just force-run the scheduler?? |                             return .done; | ||||||
|  |                         }; | ||||||
|  |  | ||||||
|                             if (ms > ms_remaining) { |                         if (ms > ms_remaining) { | ||||||
|                                 // we'd wait to long, might as well exit early. |                             // same as above, except we have a scheduled task, | ||||||
|                                 return; |                             // it just happens to be too far into the future.s | ||||||
|                             } |                             return .done; | ||||||
|                             _ = try scheduler.runLowPriority(); |  | ||||||
|  |  | ||||||
|                             // We must use a u64 here b/c ms is a u32 and the |  | ||||||
|                             // conversion to ns can generate an integer |  | ||||||
|                             // overflow. |  | ||||||
|                             const _ms: u64 = @intCast(ms); |  | ||||||
|  |  | ||||||
|                             std.Thread.sleep(std.time.ns_per_ms * _ms); |  | ||||||
|                             break :SW; |  | ||||||
|                         } |                         } | ||||||
|  |  | ||||||
|                         // We have no active http transfer and no pending |                         _ = try scheduler.runLowPriority(); | ||||||
|                         // schedule tasks. We're done |                         // we have a task to run in the not-so-distant future. | ||||||
|                         return; |                         // You might think we can just sleep until that task is | ||||||
|                     } |                         // ready, but we should continue to run lowPriority tasks | ||||||
|  |                         // in the meantime, and that could unblock things. So | ||||||
|                     _ = try scheduler.runLowPriority(); |                         // we'll just sleep for a bit, and then restart our wait | ||||||
|  |                         // loop to see what's changed | ||||||
|                     const request_intercepted = self.request_intercepted; |                         std.Thread.sleep(std.time.ns_per_ms * @as(u64, @intCast(@min(ms, 20)))); | ||||||
|  |                     } else { | ||||||
|                     // We want to prioritize processing intercepted requests |                         // We're here because we either have active HTTP | ||||||
|                     // because, the sooner they get unblocked, the sooner we |                         // connections, of exit_when_done == false (aka, there's | ||||||
|                     // can start the HTTP request. But we still want to advanced |                         // an extra_socket registered with the http client). | ||||||
|                     // existing HTTP requests, if possible. So, if we have |                         _ = try scheduler.runLowPriority(); | ||||||
|                     // intercepted requests, we'll still look at existing HTTP |                         const ms_to_wait = @min(ms_remaining, ms_to_next_task orelse 100); | ||||||
|                     // requests, but we won't block waiting for more data. |                         if (try http_client.tick(ms_to_wait) == .extra_socket) { | ||||||
|                     const ms_to_wait = |                             // data on a socket we aren't handling, return to caller | ||||||
|                         if (request_intercepted) 0 |                             return .extra_socket; | ||||||
|  |                         } | ||||||
|                         // But if we have no intercepted requests, we'll wait |  | ||||||
|                         // for as long as we can for data to our existing |  | ||||||
|                         // inflight requests |  | ||||||
|                         else @min(ms_remaining, ms_to_next_task orelse 1000); |  | ||||||
|  |  | ||||||
|                     if (try http_client.tick(ms_to_wait) == .extra_socket) { |  | ||||||
|                         // data on a socket we aren't handling, return to caller |  | ||||||
|                         return; |  | ||||||
|                     } |  | ||||||
|  |  | ||||||
|                     if (request_intercepted) { |  | ||||||
|                         // Again, proritizing intercepted requests. Exit this |  | ||||||
|                         // loop so that our caller can hopefully resolve them |  | ||||||
|                         // (i.e. continue or abort them); |  | ||||||
|                         return; |  | ||||||
|                     } |                     } | ||||||
|                 }, |                 }, | ||||||
|                 .err => |err| { |                 .err => |err| { | ||||||
|                     self.mode = .{ .raw_done = @errorName(err) }; |                     self.mode = .{ .raw_done = @errorName(err) }; | ||||||
|                     return err; |                     return err; | ||||||
|                 }, |                 }, | ||||||
|                 .raw_done => return, |                 .raw_done => { | ||||||
|  |                     if (exit_when_done) { | ||||||
|  |                         return .done; | ||||||
|  |                     } | ||||||
|  |                     // we _could_ http_client.tick(ms_to_wait), but this has | ||||||
|  |                     // the same result, and I feel is more correct. | ||||||
|  |                     return .no_page; | ||||||
|  |                 } | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             const ms_elapsed = timer.lap() / 1_000_000; |             const ms_elapsed = timer.lap() / 1_000_000; | ||||||
|             if (ms_elapsed >= ms_remaining) { |             if (ms_elapsed >= ms_remaining) { | ||||||
|                 return; |                 return .done; | ||||||
|             } |             } | ||||||
|             ms_remaining -= @intCast(ms_elapsed); |             ms_remaining -= @intCast(ms_elapsed); | ||||||
|         } |         } | ||||||
| @@ -411,48 +390,53 @@ pub const Page = struct { | |||||||
|             std.debug.print("\nactive requests: {d}\n", .{self.http_client.active}); |             std.debug.print("\nactive requests: {d}\n", .{self.http_client.active}); | ||||||
|             var n_ = self.http_client.handles.in_use.first; |             var n_ = self.http_client.handles.in_use.first; | ||||||
|             while (n_) |n| { |             while (n_) |n| { | ||||||
|                 const transfer = Http.Transfer.fromEasy(n.data.conn.easy) catch |err| { |                 const handle: *Http.Client.Handle = @fieldParentPtr("node", n); | ||||||
|  |                 const transfer = Http.Transfer.fromEasy(handle.conn.easy) catch |err| { | ||||||
|                     std.debug.print(" - failed to load transfer: {any}\n", .{err}); |                     std.debug.print(" - failed to load transfer: {any}\n", .{err}); | ||||||
|                     break; |                     break; | ||||||
|                 }; |                 }; | ||||||
|                 std.debug.print(" - {s}\n", .{transfer}); |                 std.debug.print(" - {f}\n", .{transfer}); | ||||||
|                 n_ = n.next; |                 n_ = n.next; | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         { |         { | ||||||
|             std.debug.print("\nqueued requests: {d}\n", .{self.http_client.queue.len}); |             std.debug.print("\nqueued requests: {d}\n", .{self.http_client.queue.len()}); | ||||||
|             var n_ = self.http_client.queue.first; |             var n_ = self.http_client.queue.first; | ||||||
|             while (n_) |n| { |             while (n_) |n| { | ||||||
|                 std.debug.print(" - {s}\n", .{n.data.url}); |                 const transfer: *Http.Transfer = @fieldParentPtr("_node", n); | ||||||
|  |                 std.debug.print(" - {f}\n", .{transfer.uri}); | ||||||
|                 n_ = n.next; |                 n_ = n.next; | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         { |         { | ||||||
|             std.debug.print("\nscripts: {d}\n", .{self.script_manager.scripts.len}); |             std.debug.print("\nscripts: {d}\n", .{self.script_manager.scripts.len()}); | ||||||
|             var n_ = self.script_manager.scripts.first; |             var n_ = self.script_manager.scripts.first; | ||||||
|             while (n_) |n| { |             while (n_) |n| { | ||||||
|                 std.debug.print(" - {s} complete: {any}\n", .{ n.data.script.url, n.data.complete }); |                 const ps: *ScriptManager.PendingScript = @fieldParentPtr("node", n); | ||||||
|  |                 std.debug.print(" - {s} complete: {any}\n", .{ ps.script.url, ps.complete }); | ||||||
|                 n_ = n.next; |                 n_ = n.next; | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         { |         { | ||||||
|             std.debug.print("\ndeferreds: {d}\n", .{self.script_manager.deferreds.len}); |             std.debug.print("\ndeferreds: {d}\n", .{self.script_manager.deferreds.len()}); | ||||||
|             var n_ = self.script_manager.deferreds.first; |             var n_ = self.script_manager.deferreds.first; | ||||||
|             while (n_) |n| { |             while (n_) |n| { | ||||||
|                 std.debug.print(" - {s} complete: {any}\n", .{ n.data.script.url, n.data.complete }); |                 const ps: *ScriptManager.PendingScript = @fieldParentPtr("node", n); | ||||||
|  |                 std.debug.print(" - {s} complete: {any}\n", .{ ps.script.url, ps.complete }); | ||||||
|                 n_ = n.next; |                 n_ = n.next; | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         const now = std.time.milliTimestamp(); |         const now = std.time.milliTimestamp(); | ||||||
|         { |         { | ||||||
|             std.debug.print("\nasyncs: {d}\n", .{self.script_manager.asyncs.len}); |             std.debug.print("\nasyncs: {d}\n", .{self.script_manager.asyncs.len()}); | ||||||
|             var n_ = self.script_manager.asyncs.first; |             var n_ = self.script_manager.asyncs.first; | ||||||
|             while (n_) |n| { |             while (n_) |n| { | ||||||
|                 std.debug.print(" - {s} complete: {any}\n", .{ n.data.script.url, n.data.complete }); |                 const ps: *ScriptManager.PendingScript = @fieldParentPtr("node", n); | ||||||
|  |                 std.debug.print(" - {s} complete: {any}\n", .{ ps.script.url, ps.complete }); | ||||||
|                 n_ = n.next; |                 n_ = n.next; | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -137,7 +137,13 @@ pub const Session = struct { | |||||||
|         return &(self.page orelse return null); |         return &(self.page orelse return null); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     pub fn wait(self: *Session, wait_sec: u16) void { |     pub const WaitResult = enum { | ||||||
|  |         done, | ||||||
|  |         no_page, | ||||||
|  |         extra_socket, | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     pub fn wait(self: *Session, wait_ms: i32) WaitResult { | ||||||
|         if (self.queued_navigation) |qn| { |         if (self.queued_navigation) |qn| { | ||||||
|             // This was already aborted on the page, but it would be pretty |             // This was already aborted on the page, but it would be pretty | ||||||
|             // bad if old requests went to the new page, so let's make double sure |             // bad if old requests went to the new page, so let's make double sure | ||||||
| @@ -154,21 +160,24 @@ pub const Session = struct { | |||||||
|                     .err = err, |                     .err = err, | ||||||
|                     .url = qn.url, |                     .url = qn.url, | ||||||
|                 }); |                 }); | ||||||
|                 return; |                 return .done; | ||||||
|             }; |             }; | ||||||
|  |  | ||||||
|             page.navigate(qn.url, qn.opts) catch |err| { |             page.navigate(qn.url, qn.opts) catch |err| { | ||||||
|                 log.err(.browser, "queued navigation error", .{ .err = err, .url = qn.url }); |                 log.err(.browser, "queued navigation error", .{ .err = err, .url = qn.url }); | ||||||
|                 return; |                 return .done; | ||||||
|             }; |             }; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (self.page) |*page| { |         if (self.page) |*page| { | ||||||
|             page.wait(wait_sec); |             return page.wait(wait_ms); | ||||||
|         } |         } | ||||||
|  |         return .no_page; | ||||||
|     } |     } | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| const QueuedNavigation = struct { | const QueuedNavigation = struct { | ||||||
|     url: []const u8, |     url: []const u8, | ||||||
|     opts: NavigateOpts, |     opts: NavigateOpts, | ||||||
|   | |||||||
| @@ -116,11 +116,13 @@ pub fn CDPT(comptime TypeProvider: type) type { | |||||||
|         // A bit hacky right now. The main server loop doesn't unblock for |         // A bit hacky right now. The main server loop doesn't unblock for | ||||||
|         // scheduled task. So we run this directly in order to process any |         // scheduled task. So we run this directly in order to process any | ||||||
|         // timeouts (or http events) which are ready to be processed. |         // timeouts (or http events) which are ready to be processed. | ||||||
|         pub fn pageWait(self: *Self) void { |  | ||||||
|             const session = &(self.browser.session orelse return); |         pub fn hasPage() bool { | ||||||
|             // exits early if there's nothing to do, so a large value like |  | ||||||
|             // 5 seconds should be ok |         } | ||||||
|             session.wait(5); |         pub fn pageWait(self: *Self, ms: i32) Session.WaitResult { | ||||||
|  |             const session = &(self.browser.session orelse return .no_page); | ||||||
|  |             return session.wait(ms); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         // Called from above, in processMessage which handles client messages |         // Called from above, in processMessage which handles client messages | ||||||
|   | |||||||
| @@ -182,7 +182,6 @@ pub fn requestIntercept(arena: Allocator, bc: anytype, intercept: *const Notific | |||||||
|     // unreachable because we _have_ to have a page. |     // unreachable because we _have_ to have a page. | ||||||
|     const session_id = bc.session_id orelse unreachable; |     const session_id = bc.session_id orelse unreachable; | ||||||
|     const target_id = bc.target_id orelse unreachable; |     const target_id = bc.target_id orelse unreachable; | ||||||
|     const page = bc.session.currentPage() orelse unreachable; |  | ||||||
|  |  | ||||||
|     // We keep it around to wait for modifications to the request. |     // We keep it around to wait for modifications to the request. | ||||||
|     // NOTE: we assume whomever created the request created it with a lifetime of the Page. |     // NOTE: we assume whomever created the request created it with a lifetime of the Page. | ||||||
| @@ -211,7 +210,6 @@ pub fn requestIntercept(arena: Allocator, bc: anytype, intercept: *const Notific | |||||||
|     // Await either continueRequest, failRequest or fulfillRequest |     // Await either continueRequest, failRequest or fulfillRequest | ||||||
|  |  | ||||||
|     intercept.wait_for_interception.* = true; |     intercept.wait_for_interception.* = true; | ||||||
|     page.request_intercepted = true; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| fn continueRequest(cmd: anytype) !void { | fn continueRequest(cmd: anytype) !void { | ||||||
| @@ -229,8 +227,6 @@ fn continueRequest(cmd: anytype) !void { | |||||||
|         return error.NotImplemented; |         return error.NotImplemented; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const page = bc.session.currentPage() orelse return error.PageNotLoaded; |  | ||||||
|  |  | ||||||
|     var intercept_state = &bc.intercept_state; |     var intercept_state = &bc.intercept_state; | ||||||
|     const request_id = try idFromRequestId(params.requestId); |     const request_id = try idFromRequestId(params.requestId); | ||||||
|     const transfer = intercept_state.remove(request_id) orelse return error.RequestNotFound; |     const transfer = intercept_state.remove(request_id) orelse return error.RequestNotFound; | ||||||
| @@ -266,11 +262,6 @@ fn continueRequest(cmd: anytype) !void { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     try bc.cdp.browser.http_client.process(transfer); |     try bc.cdp.browser.http_client.process(transfer); | ||||||
|  |  | ||||||
|     if (intercept_state.empty()) { |  | ||||||
|         page.request_intercepted = false; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return cmd.sendResult(null, .{}); |     return cmd.sendResult(null, .{}); | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -292,8 +283,6 @@ fn continueWithAuth(cmd: anytype) !void { | |||||||
|         }, |         }, | ||||||
|     })) orelse return error.InvalidParams; |     })) orelse return error.InvalidParams; | ||||||
|  |  | ||||||
|     const page = bc.session.currentPage() orelse return error.PageNotLoaded; |  | ||||||
|  |  | ||||||
|     var intercept_state = &bc.intercept_state; |     var intercept_state = &bc.intercept_state; | ||||||
|     const request_id = try idFromRequestId(params.requestId); |     const request_id = try idFromRequestId(params.requestId); | ||||||
|     const transfer = intercept_state.remove(request_id) orelse return error.RequestNotFound; |     const transfer = intercept_state.remove(request_id) orelse return error.RequestNotFound; | ||||||
| @@ -323,11 +312,6 @@ fn continueWithAuth(cmd: anytype) !void { | |||||||
|  |  | ||||||
|     transfer.reset(); |     transfer.reset(); | ||||||
|     try bc.cdp.browser.http_client.process(transfer); |     try bc.cdp.browser.http_client.process(transfer); | ||||||
|  |  | ||||||
|     if (intercept_state.empty()) { |  | ||||||
|         page.request_intercepted = false; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return cmd.sendResult(null, .{}); |     return cmd.sendResult(null, .{}); | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -380,8 +364,6 @@ fn failRequest(cmd: anytype) !void { | |||||||
|         errorReason: ErrorReason, |         errorReason: ErrorReason, | ||||||
|     })) orelse return error.InvalidParams; |     })) orelse return error.InvalidParams; | ||||||
|  |  | ||||||
|     const page = bc.session.currentPage() orelse return error.PageNotLoaded; |  | ||||||
|  |  | ||||||
|     var intercept_state = &bc.intercept_state; |     var intercept_state = &bc.intercept_state; | ||||||
|     const request_id = try idFromRequestId(params.requestId); |     const request_id = try idFromRequestId(params.requestId); | ||||||
|  |  | ||||||
| @@ -394,10 +376,6 @@ fn failRequest(cmd: anytype) !void { | |||||||
|         .url = transfer.uri, |         .url = transfer.uri, | ||||||
|         .reason = params.errorReason, |         .reason = params.errorReason, | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     if (intercept_state.empty()) { |  | ||||||
|         page.request_intercepted = false; |  | ||||||
|     } |  | ||||||
|     return cmd.sendResult(null, .{}); |     return cmd.sendResult(null, .{}); | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -405,7 +383,6 @@ pub fn requestAuthRequired(arena: Allocator, bc: anytype, intercept: *const Noti | |||||||
|     // unreachable because we _have_ to have a page. |     // unreachable because we _have_ to have a page. | ||||||
|     const session_id = bc.session_id orelse unreachable; |     const session_id = bc.session_id orelse unreachable; | ||||||
|     const target_id = bc.target_id orelse unreachable; |     const target_id = bc.target_id orelse unreachable; | ||||||
|     const page = bc.session.currentPage() orelse unreachable; |  | ||||||
|  |  | ||||||
|     // We keep it around to wait for modifications to the request. |     // We keep it around to wait for modifications to the request. | ||||||
|     // NOTE: we assume whomever created the request created it with a lifetime of the Page. |     // NOTE: we assume whomever created the request created it with a lifetime of the Page. | ||||||
| @@ -442,7 +419,6 @@ pub fn requestAuthRequired(arena: Allocator, bc: anytype, intercept: *const Noti | |||||||
|     // Await continueWithAuth |     // Await continueWithAuth | ||||||
|  |  | ||||||
|     intercept.wait_for_interception.* = true; |     intercept.wait_for_interception.* = true; | ||||||
|     page.request_intercepted = true; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // Get u64 from requestId which is formatted as: "INTERCEPT-{d}" | // Get u64 from requestId which is formatted as: "INTERCEPT-{d}" | ||||||
|   | |||||||
| @@ -523,7 +523,7 @@ const Handles = struct { | |||||||
| }; | }; | ||||||
|  |  | ||||||
| // wraps a c.CURL (an easy handle) | // wraps a c.CURL (an easy handle) | ||||||
| const Handle = struct { | pub const Handle = struct { | ||||||
|     client: *Client, |     client: *Client, | ||||||
|     conn: Http.Connection, |     conn: Http.Connection, | ||||||
|     node: Handles.HandleList.Node, |     node: Handles.HandleList.Node, | ||||||
|   | |||||||
| @@ -137,7 +137,7 @@ fn run(alloc: Allocator) !void { | |||||||
|             const server = &_server.?; |             const server = &_server.?; | ||||||
|             defer server.deinit(); |             defer server.deinit(); | ||||||
|  |  | ||||||
|             server.run(address, opts.timeout) catch |err| { |             server.run(address, opts.timeout * 1000) catch |err| { | ||||||
|                 log.fatal(.app, "server run error", .{ .err = err }); |                 log.fatal(.app, "server run error", .{ .err = err }); | ||||||
|                 return err; |                 return err; | ||||||
|             }; |             }; | ||||||
| @@ -166,7 +166,7 @@ fn run(alloc: Allocator) !void { | |||||||
|                 }, |                 }, | ||||||
|             }; |             }; | ||||||
|  |  | ||||||
|             session.wait(5); // 5 seconds |             _ = session.wait(5000); // 5 seconds | ||||||
|  |  | ||||||
|             // dump |             // dump | ||||||
|             if (opts.dump) { |             if (opts.dump) { | ||||||
|   | |||||||
| @@ -109,7 +109,7 @@ fn run( | |||||||
|     const url = try std.fmt.allocPrint(arena, "http://localhost:9582/{s}", .{test_file}); |     const url = try std.fmt.allocPrint(arena, "http://localhost:9582/{s}", .{test_file}); | ||||||
|     try page.navigate(url, .{}); |     try page.navigate(url, .{}); | ||||||
|  |  | ||||||
|     page.wait(2); |     _ = page.wait(2000); | ||||||
|  |  | ||||||
|     const js_context = page.main_context; |     const js_context = page.main_context; | ||||||
|     var try_catch: Env.TryCatch = undefined; |     var try_catch: Env.TryCatch = undefined; | ||||||
|   | |||||||
| @@ -124,48 +124,58 @@ pub const Server = struct { | |||||||
|         client.* = try Client.init(socket, self); |         client.* = try Client.init(socket, self); | ||||||
|         defer client.deinit(); |         defer client.deinit(); | ||||||
|  |  | ||||||
|         var last_message = timestamp(); |  | ||||||
|         var http = &self.app.http; |         var http = &self.app.http; | ||||||
|  |  | ||||||
|         http.monitorSocket(socket); |         http.monitorSocket(socket); | ||||||
|         defer http.unmonitorSocket(); |         defer http.unmonitorSocket(); | ||||||
|  |  | ||||||
|  |         std.debug.assert(client.mode == .http); | ||||||
|         while (true) { |         while (true) { | ||||||
|             if (http.poll(10) == .extra_socket) { |             if (http.poll(timeout_ms) != .extra_socket) { | ||||||
|                 const n = posix.read(socket, client.readBuf()) catch |err| { |  | ||||||
|                     log.warn(.app, "CDP read", .{ .err = err }); |  | ||||||
|                     return; |  | ||||||
|                 }; |  | ||||||
|                 if (n == 0) { |  | ||||||
|                     log.info(.app, "CDP disconnect", .{}); |  | ||||||
|                     return; |  | ||||||
|                 } |  | ||||||
|                 const more = client.processData(n) catch false; |  | ||||||
|                 if (!more) { |  | ||||||
|                     return; |  | ||||||
|                 } |  | ||||||
|                 last_message = timestamp(); |  | ||||||
|             } else if (timestamp() - last_message > timeout_ms) { |  | ||||||
|                 log.info(.app, "CDP timeout", .{}); |                 log.info(.app, "CDP timeout", .{}); | ||||||
|                 return; |                 return; | ||||||
|             } |             } | ||||||
|             // We have 3 types of "events": |  | ||||||
|             // - Incoming CDP messages |  | ||||||
|             // - Network events from the browser |  | ||||||
|             // - Timeouts from the browser |  | ||||||
|  |  | ||||||
|             // The call to http.poll above handles the first two (which is why |             if (try client.readSocket() == false) { | ||||||
|             // we pass the client socket to it). But browser timeouts aren't |                 return; | ||||||
|             // hooked into that. So we need to go to the browser page (if there |             } | ||||||
|             // is one), and ask it to process any pending events. That action |  | ||||||
|             // doesn't starve #2 (Network events from the browser), because |  | ||||||
|             // page.wait() handles that too. But it does starve #1 (Incoming CDP |  | ||||||
|             // messages). The good news is that, while the Page is mostly |  | ||||||
|             // unaware of CDP, it will only block if it actually has something to |  | ||||||
|             // do AND it knows if we're waiting on an intercept request, and will |  | ||||||
|             // eagerly return control here in those cases. |  | ||||||
|             if (client.mode == .cdp) { |             if (client.mode == .cdp) { | ||||||
|                 client.mode.cdp.pageWait(); |                 break; // switch to our CDP loop | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         var cdp = &client.mode.cdp; | ||||||
|  |         var last_message = timestamp(); | ||||||
|  |         var ms_remaining = timeout_ms; | ||||||
|  |         while (true) { | ||||||
|  |             switch (cdp.pageWait(ms_remaining)) { | ||||||
|  |                 .extra_socket => { | ||||||
|  |                     if (try client.readSocket() == false) { | ||||||
|  |                         return; | ||||||
|  |                     } | ||||||
|  |                     last_message = timestamp(); | ||||||
|  |                     ms_remaining = timeout_ms; | ||||||
|  |                 }, | ||||||
|  |                 .no_page => { | ||||||
|  |                     if (http.poll(ms_remaining) != .extra_socket) { | ||||||
|  |                         log.info(.app, "CDP timeout", .{}); | ||||||
|  |                         return; | ||||||
|  |                     } | ||||||
|  |                     if (try client.readSocket() == false) { | ||||||
|  |                         return; | ||||||
|  |                     } | ||||||
|  |                     last_message = timestamp(); | ||||||
|  |                     ms_remaining = timeout_ms; | ||||||
|  |                 }, | ||||||
|  |                 .done => { | ||||||
|  |                     std.debug.print("ok\n", .{}); | ||||||
|  |                     const elapsed = timestamp() - last_message; | ||||||
|  |                     if (elapsed > ms_remaining) { | ||||||
|  |                         log.info(.app, "CDP timeout", .{}); | ||||||
|  |                         return; | ||||||
|  |                     } | ||||||
|  |                     ms_remaining -= @as(i32, @intCast(elapsed)); | ||||||
|  |                 }, | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| @@ -222,6 +232,20 @@ pub const Client = struct { | |||||||
|         self.send_arena.deinit(); |         self.send_arena.deinit(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     fn readSocket(self: *Client) !bool { | ||||||
|  |         const n = posix.read(self.socket, self.readBuf()) catch |err| { | ||||||
|  |             log.warn(.app, "CDP read", .{ .err = err }); | ||||||
|  |             return false; | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         if (n == 0) { | ||||||
|  |             log.info(.app, "CDP disconnect", .{}); | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return self.processData(n) catch false; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     fn readBuf(self: *Client) []u8 { |     fn readBuf(self: *Client) []u8 { | ||||||
|         return self.reader.readBuf(); |         return self.reader.readBuf(); | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -424,7 +424,7 @@ pub const JsRunner = struct { | |||||||
|                 } |                 } | ||||||
|                 return err; |                 return err; | ||||||
|             }; |             }; | ||||||
|             self.page.session.wait(1); |             self.page.session.wait(1000); | ||||||
|             @import("root").js_runner_duration += std.time.Instant.since(try std.time.Instant.now(), start); |             @import("root").js_runner_duration += std.time.Instant.since(try std.time.Instant.now(), start); | ||||||
|  |  | ||||||
|             if (case.@"1") |expected| { |             if (case.@"1") |expected| { | ||||||
| @@ -518,7 +518,7 @@ pub fn htmlRunner(file: []const u8) !void { | |||||||
|  |  | ||||||
|     const url = try std.fmt.allocPrint(arena_allocator, "http://localhost:9582/src/tests/{s}", .{file}); |     const url = try std.fmt.allocPrint(arena_allocator, "http://localhost:9582/src/tests/{s}", .{file}); | ||||||
|     try page.navigate(url, .{}); |     try page.navigate(url, .{}); | ||||||
|     page.wait(2); |     _ = page.wait(2000); | ||||||
|  |  | ||||||
|     const value = js_context.exec("testing.getStatus()", "testing.getStatus()") catch |err| { |     const value = js_context.exec("testing.getStatus()", "testing.getStatus()") catch |err| { | ||||||
|         const msg = try_catch.err(arena_allocator) catch @errorName(err) orelse "unknown"; |         const msg = try_catch.err(arena_allocator) catch @errorName(err) orelse "unknown"; | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Karl Seguin
					Karl Seguin