Skip to content

Conversation

@karlseguin
Copy link
Collaborator

@karlseguin karlseguin commented Nov 3, 2025

This PR introduces two major changes to the ScriptManager.

1 - Simplification.
Rather than having a Script, PendingScript, AsyncModule and SyncModule, there is only a Script, with an added mode union. All of the previous objects had the same behavior (collect the response in a buffer), up to the point of execution, which is where the mode comes in.

2 - Correctness
Whether or not the previous version was "incorrect", it was difficult to use correctly. Specifically, the previous version would execute async scripts and async modules as soon as they're done. That seems allowed, but it caused issues with module loading in Context.zig. Specifically, between compiling and instantiating a module, or between instantiation and evaluation, an async script or module could be evaluated. It isn't clear whether v8 allows that, but if it does, it introduces a lot of new potential states (specifically, unexpected changes to the v8.Module's status) that we have to handle.

This version only evaluate scripts in the evaluate, which doesn't allow recursive calls (so a waitForImport, which continues to pump the HTTP loop, can never result in evaluate being called again).

This undoes the change made in #1158 because I do not think it's possible to have multiple waiters waiting for the same (or even different) modules. The linked issue points to a crash in https://www.nytimes.com which doesn't crash with this version.

This PR introduces two major changes to the ScriptManager.

1 - Simplification.
Rather than having a `Script`, `PendingScript`, `AsyncModule` and `SyncModule`,
there is only a `Script`, with an added `mode` union. All of the previous
objects had the same behavior (collect the response in a buffer), up to the
point of execution, which is where the mode comes in.

2 - Correctness
Whether or not the previous version was "incorrect", it was difficult to use
correctly. Specifically, the previous version would execute async scripts and
async modules as soon as they're done. That seems allowed, but it caused issues
with module loading in Context.js. Specifically, between compiling and
instantiating a module, or between instantiation and evaluation, an async script
or module could be evaluated. It isn't clear whether v8 allows that, but if it
does, it introduces a lot of new potential states (specifically, unexpected
changes to the v8.Module's status) that we have to handle.

This version only evaluate scripts in the `evaluate`, which doesn't allow
recursive calls (so a waitForImport, which continues to pump the HTTP loop, can
never result in `evaluate` being called again).

This undoes the change made in #1158
because I do not think it's possible to have multiple waiters waiting for the
same (or even different) modules. The linked issue points to a crash in
https://www.nytimes.com which doesn't crash with this version.
// So this is more like a cache. When an imported module is completed, its
// source is placed here (keyed by the full url) for some point in the future
// when v8 asks for it.
// The type is confusing (too confusing? move to a union). Starts of as `null`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the type seems OK for me :)

@krichprollsch
Copy link
Member

krichprollsch commented Nov 3, 2025

Well, I have a crash with https://www.xange.vc/

(not using fetch, but with cdp + cdpcli)

EDIT: I reporduce after fetching https://www.nytimes.com/ and https://www.xange.vc/

WARN  http : script fetch error . . . . . . . . . . . . . . . [+604.771s]
      req = https://www.googletagmanager.com/gtm.js?id=GTM-TCKXBQ9
      err = Abort

General protection exception (no address available)
/home/pierre/wrk/browser/src/browser/ScriptManager.zig:417:13: 0x3415564 in evaluate (lightpanda)
    if (self.is_evaluating) {
            ^
/home/pierre/wrk/browser/src/browser/ScriptManager.zig:649:30: 0x334f8bc in errorCallback (lightpanda)
        self.manager.evaluate();
                             ^
/home/pierre/wrk/browser/src/http/Client.zig:306:32: 0x3316a99 in requestFailed (lightpanda)
    transfer.req.error_callback(transfer.ctx, err);
                               ^
/home/pierre/wrk/browser/src/http/Client.zig:842:34: 0x3383b06 in abort (lightpanda)
        self.client.requestFailed(self, error.Abort);
                                 ^
/home/pierre/wrk/browser/src/http/Client.zig:163:23: 0x33040eb in abort (lightpanda)
        transfer.abort();
                      ^
/home/pierre/wrk/browser/src/browser/page.zig:159:31: 0x34a41ce in deinit (lightpanda)
        self.http_client.abort();
                              ^
/home/pierre/wrk/browser/src/browser/session.zig:135:27: 0x33947ef in removePage (lightpanda)
        self.page.?.deinit();
                          ^
/home/pierre/wrk/browser/src/cdp/domains/target.zig:247:26: 0x33cc6aa in closeTarget__anon_137139 (lightpanda)
    bc.session.removePage();
                         ^
/home/pierre/wrk/browser/src/cdp/domains/target.zig:42:43: 0x3346022 in processMessage__anon_120384 (lightpanda)
        .closeTarget => return closeTarget(cmd),
                                          ^
/home/pierre/wrk/browser/src/cdp/cdp.zig:210:97: 0x3344ef4 in dispatchCommand__anon_120227 (lightpanda)
                    asUint(u48, "Target") => return @import("domains/target.zig").processMessage(command),
                                                                                                ^
/home/pierre/wrk/browser/src/cdp/cdp.zig:163:32: 0x33477fe in dispatch__anon_120034 (lightpanda)
                dispatchCommand(&command, input.method) catch |err| {
                               ^
/home/pierre/wrk/browser/src/cdp/cdp.zig:111:33: 0x3347c57 in processMessage (lightpanda)
            return self.dispatch(arena.allocator(), self, msg);
                                ^
/home/pierre/wrk/browser/src/cdp/cdp.zig:104:32: 0x32cb047 in handleMessage (lightpanda)
            self.processMessage(msg) catch return false;
                               ^
/home/pierre/wrk/browser/src/server.zig:460:56: 0x32caf1d in processWebsocketMessage (lightpanda)
                .text, .binary => if (cdp.handleMessage(msg.data) == false) {
                                                       ^
/home/pierre/wrk/browser/src/server.zig:254:63: 0x32e0761 in processData (lightpanda)
            .cdp => |*cdp| return self.processWebsocketMessage(cdp),
                                                              ^
/home/pierre/wrk/browser/src/server.zig:243:32: 0x32e092f in readSocket (lightpanda)
        return self.processData(n) catch false;
                               ^
/home/pierre/wrk/browser/src/server.zig:151:46: 0x32e0e53 in readLoop (lightpanda)
                    if (try client.readSocket() == false) {
                                             ^
/home/pierre/wrk/browser/src/server.zig:109:26: 0x32e1acc in run (lightpanda)
            self.readLoop(socket, timeout_ms) catch |err| {
            ```

@karlseguin
Copy link
Collaborator Author

@krichprollsch I was able to reproduce and fix the crash.

@krichprollsch
Copy link
Member

I have another crash:

$ zig build run -- fetch 'https://www.etro.com/us-en/cotton-jacquard-waistcoat-MRKF0100AQ197S9820.html'
General protection exception (no address available)
/usr/local/zig-0.15.2/lib/std/DoublyLinkedList.zig:114:30: 0x33b9093 in remove (lightpanda)
        prev_node.next = node.next;
                             ^
/usr/local/zig-0.15.2/lib/std/DoublyLinkedList.zig:145:16: 0x33330db in popFirst (lightpanda)
    list.remove(first);
               ^
/home/pierre/wrk/browser-review/src/browser/ScriptManager.zig:139:25: 0x3384008 in clearList (lightpanda)
    while (list.popFirst()) |n| {
                        ^
/home/pierre/wrk/browser-review/src/browser/ScriptManager.zig:131:14: 0x3304363 in reset (lightpanda)
    clearList(&self.normal_scripts);
             ^
/home/pierre/wrk/browser-review/src/browser/ScriptManager.zig:106:15: 0x35ff500 in deinit (lightpanda)
    self.reset();
              ^
/home/pierre/wrk/browser-review/src/browser/page.zig:160:35: 0x34a41e2 in deinit (lightpanda)
        self.script_manager.deinit();
                                  ^
/home/pierre/wrk/browser-review/src/browser/session.zig:135:27: 0x33947ef in removePage (lightpanda)
        self.page.?.deinit();
                          ^
/home/pierre/wrk/browser-review/src/browser/session.zig:91:28: 0x337650a in deinit (lightpanda)
            self.removePage();
                           ^
/home/pierre/wrk/browser-review/src/browser/browser.zig:97:27: 0x32e383c in closeSession (lightpanda)
            session.deinit();
                          ^
/home/pierre/wrk/browser-review/src/browser/browser.zig:76:26: 0x3320c5c in deinit (lightpanda)
        self.closeSession();
                         ^
/home/pierre/wrk/browser-review/src/main.zig:161:33: 0x331f4f6 in run (lightpanda)
            defer browser.deinit();
                                ^
/home/pierre/wrk/browser-review/src/main.zig:45:8: 0x3320f53 in main (lightpanda)
    run(alloc) catch |err| {
       ^
/usr/local/zig-0.15.2/lib/std/start.zig:627:37: 0x332172d in main (lightpanda)
            const result = root.main() catch |err| {
                                    ^
../sysdeps/nptl/libc_start_call_main.h:58:16: 0x7d5b2242a1c9 in __libc_start_call_main (../sysdeps/x86/libc-start.c)
../csu/libc-start.c:360:3: 0x7d5b2242a28a in __libc_start_main_impl (../sysdeps/x86/libc-start.c)
???:?:?: 0x29f1024 in ??? (???)
???:?:?: 0x0 in ??? (???)

karlseguin added a commit that referenced this pull request Nov 4, 2025
This does two changes to module loading. First, for normal imports, it only
instantiates and evaluates the top-level module. This ensures that circular
dependencies can be resolved. This bug was introduced when I tried to
deduplicate code between dynamic and normal modules - but it turns out that
non-top-level normal modules do have a simpler flow (they just need to be
compiled, and we let v8 deal with the rest).

The other change is to handle more edge cases. Code like this should now be ok:

```
<script type=module>
  var a = await import('a.js');
</script>
<script type=module>
  import a from a.js
</script>
```

Previously, the dynamic import of a.js (first block) could interact badly with
the normal import of a.js in the 2nd block.

This change is built on top of #1191
which also helps reduce the number of cases by ensure that a script isn't
evaluated while we're trying to evaluate a script.
@karlseguin karlseguin mentioned this pull request Nov 4, 2025
Co-authored-by: Pierre Tachoire <pierre@lightpanda.io>
@karlseguin karlseguin merged commit dc4927d into main Nov 4, 2025
10 checks passed
@karlseguin karlseguin deleted the refactor_script_manager branch November 4, 2025 14:35
@github-actions github-actions bot locked and limited conversation to collaborators Nov 4, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants