Skip to content

Conversation

@karlseguin
Copy link
Collaborator

@karlseguin karlseguin commented Apr 6, 2025

Big PR description, but 2 important things to highlight:

1 -
The build process has changed a lot. You'll need to run zig build get-v8
followed by zig build build-v8 first. It might be better to test this branch
in a separate / clean folder (it'll leave a large v8 folder in the root of the
project).

2 -
If you run this and notice that memory usage is high, try running it with the new
--gc_hints command line argument. Note that without the --gc_hints memory usage
does level off at some point, and it's faster. With --gc_hints, the memory
consumption should be lower than before and it might still be a bit faster.

Overview

This PR aims to replace zig-js-runtime with the main goal of:

  1. Removing the global objects (Types and UserContext). (IO was largely removed
    by the http client rewrite).
  2. Reworking problematic and unsafe proto implementation (i.e.
    No memory layout guarantee with zig structs zig-js-runtime#205)

The intention wasn't to improve performance or memory usage, but, at least on
my tests, both have been improved.

The changes can be broken down into two parts, changes to Browser and reworking
of zig-js-runtime.

Browser Changes

Most of the changes to browser were made to accommodate the new js runtime.
However, other changes were also made:

  • All browser-related code was moved under the browser folder. For example,
    src/css/ -> src/browser/css/

  • zig-v8-fork and tigerbeetle-io are now Zig packages. Both projects were
    changed slightly to be proper Zig libraries. They both have a jsruntime branch
    which this branch references.

  • netsurf.zig and mimalloc.zig are no longer separate modules and can be found in: src/browser/netsurf.zig and src/browser/mimalloc.zig.

  • zig build unittest has been removed. zig build test is now more idiomatic:
    the root source file is src/main.zig and tests are automatically discovered.
    zig build test is, as before, slow and something to be looked into.

  • All existing web API tests are passing, but the test runner has changed a little
    so every single line of test has been superficially touched. Also,
    getter can't return a Callback zig-js-runtime#200 is now fixed, so
    the XHR test no long has commented out tests cases.

  • The rest of the changes exist specifically to accommodate the new jsruntime,
    which will be covered next, but briefly:

    1. Browser / Session / Page, the main integration points with the js runtime,
      have changed slightly.
    2. UserContext has been renamed to SessionState and, while the same in spirit,
      is also quite different.
    3. Loop and Allocator are no longer "internal" or special types. These are now
      accessed through the SessionState.
    4. Callback API changed a little
    5. Variadic no longer exists, they map directly to plain Zig slices
    6. postAttach has been replaced with indexed_get and named_get which
      capture live changes.
    7. mem_guarantied is no longer needed and has been removed where declared.
    8. 2 new build targets: get-v8 which gets the tools to build v8
      as well as the v8 source, and build-v8 which builds v8. All of this is placed
      in a v8 folder at the root of the project. Also, on mac, we always build a release
      build (since the dev build segfaults).

js runtime

The zig-js-runtime project has been replaced with two files
src/runtime/loop.zig and src/runtime/js.zig.

The loop is unchanged except that cancel on Mac no longer allocates
(and leaks) memory (it's a complete no-op, versus the current partial no-op).

Architecture

The js runtime architecture matches the Browser structure:

Browser -> js.Env -> v8.Isolate
Session -> js.Executor -> v8.Context
Page -> js.Scope -> v8.HandleScope

As you can see, the v8.Isolate has been elevated to the Browser and is re-used
across multiple sessions. To make this possible, most (maybe all??) leaks have
been fixed. However, v8.Context are garbage collected by v8 at its own discretion.
Thus, this new approach, while faster, is likely to use more memory. With the new
--gc_hints command line argument, we call isolate.lowMemoryNotification() when
each executor ends - encouraging v8 to garbage collect the just-released
context. Note that this is still just a hint. I think it's safe to say that this
approach gives more control to the v8 GC, making memory usage less predictable.

Code Generation

The v8 bindings are created in a single-pass, directly from Zig's type system.
There is no longer a reflect stage nor an internal type representation. The
main advantage is that you don't need to learn two type systems.

Generics

The global types have been replaced by making most js.* types (i.e. js.Env)
generics. I believe this significantly simplifies our build process, thus the
ability to have the tests' root source file be src/main.zig.

The only downside that I've seen is that you will get an awful type description
in an error message involving the js.Env(T) or any of its child types.

UserContext / SessionState / State

UserContext has been reworked. From the js runtime's point of view, i.e.
js.Env(T), this is State generic. From the rest of Browser's point of view,
it is now called SessionState.

The SessionState belongs to the Session and is referenced by the Executor.
Rather than calling setUserContext, the session.state object is
mutated directly.

Most importantly, the js runtime no longer has the concept of special/internal
types. Thus, *Loop and Allocator have been added to the SessionState.
For example, the XMLHttpRequest constructor was:

pub fn constructor(alloc: std.mem.Allocator, userctx: UserContext) !XMLHttpRequest {
  ...
}

And is now:

pub fn constructor(session_state: *SessionState) !XMLHttpRequest {
  const arena = session_state.arena;
  ...
}

Compatibility with other engines

Without knowing anything about other JS engines, I feel that some of the changes
could make integrating a different JS engine harder and some of the changes
could make it simpler. I think this new approaches exposes a few less types and
presents more of a self-contained, but far from perfect, interface.

Personally, I think it would be great if the abstraction could be built under this layer, so that instead of having a v8.Context, v8.Value and v8.Object, we have a js.Context, js.Value and js.Object, so that binding code can be re-used, but that might be a stretch.

@karlseguin karlseguin force-pushed the jsruntime branch 2 times, most recently from 18f9d9f to b8c7c95 Compare April 6, 2025 11:11
@karlseguin karlseguin marked this pull request as draft April 6, 2025 11:14
@karlseguin karlseguin force-pushed the jsruntime branch 2 times, most recently from 6a254e4 to 089a641 Compare April 10, 2025 13:37
@krichprollsch krichprollsch self-requested a review April 11, 2025 07:41
@krichprollsch
Copy link
Member

FYI I get a non-blocking warning when running build-v8

$ zig build build-v8
WARNING at the command-line "--args":1:76: Build argument has no effect.
target_os="linux" target_cpu="x64" is_debug=true symbol_level=1 cxx_use_ld="lld"
                                                                           ^----
The variable "cxx_use_ld" was set as a build argument
but never appeared in a declare_args() block in any buildfile.

To view all possible args, run "gn args --list <out_dir>"

The build continued as if that argument was unspecified.

Done. Made 183 targets from 100 files in 99ms
ninja: Entering directory `v8/build/x86_64-linux/debug/ninja'
[882/1972] CXX obj/torque_generated_definitions/feedback-vector-tq.o

@karlseguin
Copy link
Collaborator Author

I saw this in the original too.

try gn_args.append("cxx_use_ld="lld"");

@krichprollsch
Copy link
Member

I tried the puppeteer cdp.js script with a release build and I get this error randomly on puppeteer:
(but nothing at lightpanda)

$ node ../demo/puppeteer/cdp.js
......................................................................file:///home/pierre/wrk/demo/node_modules/puppeteer-core/lib/esm/puppeteer/common/CallbackRegistry.js:69
            this._reject(callback, new TargetCloseError('Target closed'));
                                   ^

TargetCloseError: Protocol error (Runtime.callFunctionOn): Target closed
    at CallbackRegistry.clear (file:///home/pierre/wrk/demo/node_modules/puppeteer-core/lib/esm/puppeteer/common/CallbackRegistry.js:69:36)
    at CdpCDPSession._onClosed (file:///home/pierre/wrk/demo/node_modules/puppeteer-core/lib/esm/puppeteer/cdp/CDPSession.js:98:25)
    at #onClose (file:///home/pierre/wrk/demo/node_modules/puppeteer-core/lib/esm/puppeteer/cdp/Connection.js:163:21)
    at WebSocket.<anonymous> (file:///home/pierre/wrk/demo/node_modules/puppeteer-core/lib/esm/puppeteer/node/NodeWebSocketTransport.js:43:30)
    at callListener (/home/pierre/wrk/demo/node_modules/ws/lib/event-target.js:290:14)
    at WebSocket.onClose (/home/pierre/wrk/demo/node_modules/ws/lib/event-target.js:220:9)
    at WebSocket.emit (node:events:518:28)
    at WebSocket.emitClose (/home/pierre/wrk/demo/node_modules/ws/lib/websocket.js:265:10)
    at Socket.socketOnClose (/home/pierre/wrk/demo/node_modules/ws/lib/websocket.js:1291:15)
    at Socket.emit (node:events:518:28) {
  cause: ProtocolError
      at <instance_members_initializer> (file:///home/pierre/wrk/demo/node_modules/puppeteer-core/lib/esm/puppeteer/common/CallbackRegistry.js:89:14)
      at new Callback (file:///home/pierre/wrk/demo/node_modules/puppeteer-core/lib/esm/puppeteer/common/CallbackRegistry.js:93:16)
      at CallbackRegistry.create (file:///home/pierre/wrk/demo/node_modules/puppeteer-core/lib/esm/puppeteer/common/CallbackRegistry.js:19:26)
      at Connection._rawSend (file:///home/pierre/wrk/demo/node_modules/puppeteer-core/lib/esm/puppeteer/cdp/Connection.js:86:26)
      at CdpCDPSession.send (file:///home/pierre/wrk/demo/node_modules/puppeteer-core/lib/esm/puppeteer/cdp/CDPSession.js:63:33)
      at #evaluate (file:///home/pierre/wrk/demo/node_modules/puppeteer-core/lib/esm/puppeteer/cdp/ExecutionContext.js:353:50)
      at async ExecutionContext.evaluateHandle (file:///home/pierre/wrk/demo/node_modules/puppeteer-core/lib/esm/puppeteer/cdp/ExecutionContext.js:320:16)
      at async IsolatedWorld.evaluateHandle (file:///home/pierre/wrk/demo/node_modules/puppeteer-core/lib/esm/puppeteer/cdp/IsolatedWorld.js:85:16)
      at async CdpJSHandle.evaluateHandle (file:///home/pierre/wrk/demo/node_modules/puppeteer-core/lib/esm/puppeteer/api/JSHandle.js:147:20)
      at async WaitTask.rerun (file:///home/pierre/wrk/demo/node_modules/puppeteer-core/lib/esm/puppeteer/common/WaitTask.js:100:28)
}

Node.js v22.14.0

@krichprollsch
Copy link
Member

We had a benchmark test to track zig-js-runtime perf (https://perf.lightpanda.io/benchmark/jsruntime)
But idk if we can run it anymore with the new code and if it makes any sense now.
https://github.com/lightpanda-io/zig-js-runtime/blob/main/src/main_bench.zig
https://github.com/lightpanda-io/zig-js-runtime/blob/main/src/bench.zig

But it coud be useful to track the performance someway to detect regressions.

[
  {
    "name": "With Isolate",
    "bench": {
      "duration": 17579169,
      "alloc_nb": 6,
      "realloc_nb": 484,
      "alloc_size": 154804
    }
  },
  {
    "name": "Without Isolate",
    "bench": {
      "duration": 1996698,
      "alloc_nb": 6,
      "realloc_nb": 558,
      "alloc_size": 155091
    }
  }
]

@krichprollsch
Copy link
Member

This PR is huge, but well explained. The comments in runtime/ are useful and the code is easy to read, I love it 😍 well done and thanks @karlseguin 👏

@karlseguin karlseguin marked this pull request as ready for review April 15, 2025 07:07
Remove Env from caller, and don't store Env in isolate. We don't need it, we
can execute all runtime code from the Executor (which we store a pointer to
in the v8.Context)
- Fix passing nothing into variadic (i.e. slice) parameter
- Optimize @sizeof(T) == 0 types by avoiding uncessary allocations
  (something zig-js-runtime is doing)
Add dummy --json stats output to tests

Comment typos fixed

Add make get-v8, build-v8 and build-v8-dev make targets
karlseguin and others added 9 commits April 15, 2025 15:42
Remove index and named setters, since they aren't working, and they aren't
currently needed.
index_get seems to be ~1000x slower than setting the value directly on the
v8.Object. There's a lot of information on "v8 fast properties", and values
set directly on objects seem to be heavily optimized. Still, I can't imagine
indexed properties are always _that_ slow, so I must be doing something wrong.
Still, for now, this brings back the original functionality / behavior / perf.

Introduce the ability for Zig functions to take a Env.JsObject parameter. While
this isn't currently being used, it aligns with bringing back the postAttach /
JSObject functionality in main.

Moved function *State to the end of the function list (making it consistent with
getters and setters). The optional Env.JsObject parameter comes after the
optional state.

Removed some uncessary arena deinits from a few webapis.
Having a js source name is useful to detect where the error comes from.

Using `null` generates messages with `<anonymous>` source name.
eg. `ReferenceError: report is not defined\n    at <anonymous>:1:1`
vs. `ReferenceError: report is not defined\n    at teststatus:1:1`
karlseguin and others added 2 commits April 17, 2025 16:18
test: re-introduce js source name
The call arena doesn't consider nested calls (like, from callbacks). Currently
when a "call" ends, the arena is cleared. But in a callback, if we do that,
the memory for the containing code is no longer valid, even though it's still
executing.

For now, use the existing scope_arena, instead of the call_arena. In the future
we need to track the call-depth, and only reset the call_arena when we're done
with a top-level statement.

Also:
-Properly handle callback errors
-Increase wpt file size
-Merge latest loop.zig from zig-js-runtime.
@krichprollsch krichprollsch merged commit 8c489c2 into main Apr 17, 2025
12 checks passed
@krichprollsch krichprollsch deleted the jsruntime branch April 17, 2025 11:09
@github-actions github-actions bot locked and limited conversation to collaborators Apr 17, 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