From feedback, using cachedParse in waitForSelector outside the loop risks
having the cached selector freed. For this reason, I'm reverting this part of
the previous commit. waitForSelector already pre-parsed the selector and I think
this is the better choice.
Pre-parsed means that we avoid the cache lookup in the [potentially] tight wait
loop. The advantage of hitting the cache inside the loop is only if
waitForSelector is called multiple times, which doesn't typically happen.
_insertNodeRelative weaved the from_parser comptime through a different
conditions. It made reading the code difficult, but also optimizing it. This
commit extracts the common code (notifyChildInserted) and handles from_parser
in one cohesive chunk.
One benefit of this is that isConnected() is no longer called when from_parser
is true.
We'll grow the WS read buffer up to cdp-max-message-size (1MB default), but
never reclaim that space. A lot of drivers send large message upfront and then
settle into sending smaller ones.
This commit shrinks the buffer to 256KB (or cdp-max-message-size, whichever is
smaller) after 8 consecutive small message (tracking consecutive messages to
prevent a spat of allocation -> shink -> allocation -> shrink ->...)
max http connections 10 -> 40
max http connetions per host 4 -> 6
These are just the defaults and can still be adjusted by the command line
arguments. 6 appears to be both Chrome and FireFox's default per host (which
is probably the more important of the two settings).
The limits are really use-case specific. A use case that it multi-threading
different domains can benefit from a conservative max-host with a very large
max conn. A use case that is multi-threading the same host will need to decide
if it's safe to raise max-host.
closest was already hitting the cache, but it was doing it inside the loop.
Moving it outside the loop avoids the cache lookup.
Inversely, Runner.waitForSelector was parsing outside the loop, but it wasn't
caching. Repeated calls to waitForSelector with the same selector would not
leverage the cache.
Puppeteer's networkidle0 requires the networkIdle/networkAlmostIdle
lifecycle events on every frame that started loading, like Chrome
emits them — not just the root frame. Run the idle-notification
checks recursively over the frame tree in Runner._tick.
Fixes goto({waitUntil: 'networkidle0'}) timing out on pages with
iframes (e.g. reddit.com post pages).
window.getComputedStyle allocated a CSSStyleProperties +
CSSStyleDeclaration pair per call; pages polling it grew the page
arena without bound. The computed variant is a stateless lazy view,
so hand out one per element from a frame-level map. This also matches
Chrome, where repeated calls return the identical object.
Pseudo-element requests keep the fresh-object path.
The `call_arena` is currently our shortest-lived arena. Its promise is that it
will be valid for at least 1 v8 -> zig -> v8 function call, which makes it ideal
for getting values from v8 into zig, temporary work, and getting values from
zig to v8.
VBut `call_arena`'s lifetime is actually much longer. It's only reset when the
call_depth reaches 0. And the reason for that are Zig functions that invoke
v8 callbacks. If you just do it when a function ends, then if you allocate in
ZigA and then call CallbackA which calls ZigB, then when ZigB ends, your ZigA
allocation is cleared. This is something we could solve by reaching into
Zig's ArenaAllocator to capture a position to rollback from.
So, `call_arena` can end up living relatively long and accumulating quite a bit
of memory. But most calls _don't_ invoke callbacks. Hence, `local_arena` IS
reset at the end of every function.
Using `local_arena` _is_ dangerous, mostly for the case where some of our APIs
receive a *Frame (or *Execution) and thus might use a `local_arena` because they
know they aren't invoking a JS callback. BUT, those APIs might not know that
they're also invoked by some other Zig code that _could_ be invoking JS.
Dangerous? Sure. But, github has code that looks like:
```js
function onDelegatedClick(e) {
for (const el of document.querySelectorAll('[data-action]')) {
if (el.matches('.menu > .item:not(.disabled) a[href]')) {
handle(el);
}
el.closest('.panel');
}
}
```
Anything allocated in the `call_arena` will only be freed when the caller of
`onDelegatedClick` ends. The `querySelectorAll` returns hundreds of elements
and thus builds hundreds of parsed CSS and other scrap (x2 for `matches` and
closest`). `call_arena` peaks at 15MB. With the local_arena? 1MB. If 10x more
elements were returned, the peak would be 10x higher. With local_arena, it
stays 1MB.
When I added this, I was going through the list in /dom/interface-objects.html
thinking that was exhaustive. But no, no interfaces should be enumerable and
various other WPT tests (usually the idlharness ones) assert that for their
respective types.
Make it _always_ non enumerable means we no longer need Meta.enumerable to
be declared true/false (it's always false).
std.crypto.random's default backend mmaps a thread-local 528-byte state
page on first use and never unmaps it — there is no thread-exit hook.
With one detached thread per CDP connection (Server.handleConnection),
that leaks one resident page per connection (uuidv4 in
Page.getOrCreateOrigin touches it), ~4KB/conn of unbounded RSS growth.
Route every std.crypto.random call to the getrandom syscall instead.
.crypto_always_getrandom = true,
Idea here is to skip re-parsing that happen for each connection; we already use BoringSSL, so we can take more advantage of it by directly mutating cert store of `SSL_CTX`.
Adds the infrastructure for [de]serializing Zig objects via structuredClone.
Adds support to Blob, File, FileList and ImageData. These are the easiest to
implement. Blob is used extensively by WPT IndexedDB tests, but this PR can be
merged prior to IndexedDB landing.
On for the main document parsing should a leading BOM be stripped. When setting
innerHTML, it should be preserved (and becomes a text node).
Fixes react hydration issue with theverge.com
Per review feedback: public querySelector doesn't guarantee reuse, so the cache
must be bounded regardless of the SelectorPath bypass. Move it off the Frame
(where it was wiped every navigation and unbounded) onto the Browser, since a
parsed selector references no Frame/Context — entries are now shared across the
browser's pages and survive navigation.
Selector.Cache is a StringArrayHashMap with per-entry arenas (so eviction can
free an individual entry, which a shared arena can't) and FIFO eviction of the
oldest entry past a capacity. The SelectorPath *Uncached bypass stays.