keep the existing page active until the pending one is loaded#2297
Conversation
|
I have to add a new e2e test |
c54e9e8 to
03ea72c
Compare
|
The branch works on my own test case, but fails on #2187 (comment) script. EDIT: it works now ✔️ |
50b9ef1 to
5381eb6
Compare
7a59be4 to
b730739
Compare
| // Refresh self.frame from session. In case of pending page, we want to | ||
| // take its state while loading. If we use only the current frame, we will | ||
| // return a .done result immediately. | ||
| self.frame = self.session.currentOrPendingFrame() orelse return .done; |
There was a problem hiding this comment.
I'm not sure about that... But I don't really have a better solution :/
55efd0e to
d4765fc
Compare
|
|
||
| self.* = .{ | ||
| .page = null, | ||
| ._pages = .{ null, null }, |
There was a problem hiding this comment.
I'm guilty of doing this too, but you both set default value sin the fields, then set those same defaults on init...
There was a problem hiding this comment.
I removed from init, but I'm not sure it it's better this way or to remove default from fields?
There was a problem hiding this comment.
I think either is better than both :)
field defaults have the advantage of letting you create an empty struct without an init. But, when that's not an option (like here), the practical different is small. I do think field defaults might better improve documentation / discoverability. If I want to understand a field, I'll go scroll up to where it's defined, hope for a comment, and the default might provide more insight too.
| // available for the next pending allocation. | ||
| // | ||
| // Convention: a slot is "occupied" iff its `?Page` is non-null. | ||
| _pages: [2]?Page = .{ null, null }, |
There was a problem hiding this comment.
did you consider going with two fields and allocating page?
active: ?*Page,
pending: ?*Page,There was a problem hiding this comment.
I did, and I disliked it 🤔 But I didn't remember exactly why. I'm trying again.
You think it's better to allocate to avoid these empty structs in the stack?
There was a problem hiding this comment.
I think it's minor. A few places that are currently 2 lines ((1) get the index (2) get the page) become 1. That's pretty much it.
pub fn currentPage(self: *Session) ?*Page {
const idx = self._active_idx orelse return null;
return &self._pages[idx].?;
}becomes
pub fn currentPage(self: *Session) ?*Page {
return self.active;
}Or, if you look at the start ofinitRootNavigation, it becomes:
self.discardPendingPage(); // can do const page = self.pending orelse return;
const page = try self.pageInit(frame_id);
errdefer page.deinit();
page._state = .pending;
self.pending = page;
errdefer self.pending = null
..
(again, quite minor)
There was a problem hiding this comment.
ok, it's simpler after all. But where should I allocate the pages? Claude suggests to use app.allocator directly, but not sure about that. WDYT?
There was a problem hiding this comment.
Ya, or a dedicated std.heap.MemoryPool(Page) on the session if you think we'll create a lot. Or on the Browser if you think we'll create a lot of Sessions...
| current_frame._queued_navigation = null; | ||
|
|
||
| // Synthetic navigations (about:blank, blob:) commit instantly — no HTTP, | ||
| // so there is no in-flight window to worry about. Use the legacy |
There was a problem hiding this comment.
The fact that it's "legacy" isn't relevant. We normally say something is legacy to explain why a behavior is the way it is. But here, replaceRootImmediate doesn't exist for some "legacy" reason..it's just a more optimized / simpler (maybe more correct?) path to take for these cases. Or am I misunderstanding and replaceRootImmediate exists only to keep an pre-existing behavior?
| // Synthetic navigations (about:blank, blob:) commit instantly — no HTTP, | ||
| // so there is no in-flight window to worry about. Use the legacy | ||
| // immediate-swap path for them. | ||
| const is_synthetic = std.mem.eql(u8, qn.url, "about:blank") or |
There was a problem hiding this comment.
qn.is_about_blank
We need to check this when QueuedNavigation is created (in Frame.scheduleNavigationWithArena) and I know it was checked again during actual navigation, so I figured might as well save the boolean.
| pub fn commitPendingPage(self: *Session) !void { | ||
| const pending_idx = self._pending_idx orelse { | ||
| lp.assert(false, "Session.commitPendingPage - no pending page", .{}); | ||
| return error.NoPendingPage; |
There was a problem hiding this comment.
unreachable.
The intent here is unclear. Do you want the assert only in debug mode? (this pattern happens 3 or so times in this file, I think they should all be cleaned up).
There was a problem hiding this comment.
good, point, Claude did see that too and I didn't fix it 🤦
| lp.assert(false, "Session.commitPendingPage - no pending page", .{}); | ||
| return error.NoPendingPage; | ||
| }; | ||
| const old_idx = self._active_idx orelse { |
There was a problem hiding this comment.
old_active_idx? just a big more clear later on when you do:
self._pages[old_active_idx].?.deinit();
self.freeSlot(old_active_idx);(pretty minor, I'd probably have named it old_idx too!)
There was a problem hiding this comment.
good catch, I did move that after a rebase... It was closer to the deinit at first.
| // with this flag so the callback chain we are sitting inside isn't killed | ||
| // mid-flight. Session.discardPendingPage uses .full scope to override | ||
| // the flag in failure paths. | ||
| protect_from_abort: bool = false, |
There was a problem hiding this comment.
This is clever, and it makes sense. BUT, I think if we do give HttpClient a major cleanup (e.g. merging Request and Transfer), I'd try to come up with a better way to track ownership of frame <-> request.
There was a problem hiding this comment.
Yeah, this trick is only because I must share the same frame_id between actually 2 different frames...
And I dislike it...
b8ed5b3 to
0b374b0
Compare
During a root navigation, we keep the existing page active until we get the headers callback from the pending page. Then Session.commitPendingPage makes the switch. It delays the deinit of CPD execution context to handle JS execution in the meantime. Now session has an array of two pages, _active_idx points to the main page. Both active and pending pages share the same frame_id, it must remains stable. So this PR adds a Request.protect_from_abort to avoid removing the request form the pending page when deinit the previous active page.
0b374b0 to
7fabccb
Compare
So the CDP can remove/reset context and re-create isolated world accordingly
We want to take in account the pending frame in Runner._tick to continue to process. If we use only the current frame, we will immediately return in case of navigation.
Use distinct key for laoder id and request id based captured response.
Co-authored-by: Karl Seguin <karlseguin@users.noreply.github.com>
Remove unreachable code and use `comptime IS_DEBUG` check when need.
And fix comment
7fabccb to
14743e4
Compare
During a root navigation, we keep the existing page active until we get
the headers callback from the pending page. Then
Session.commitPendingPage makes the switch.
It delays the deinit of CPD execution context to handle JS execution in
the meantime.
Now session has an array of two pages, _active_idx points to the main
page.
Both active and pending pages share the same frame_id, it must remains
stable. So this PR adds a Request.protect_from_abort to avoid removing
the request form the pending page when deinit the previous active page.
fix #2187