Commit Graph

8347 Commits

Author SHA1 Message Date
Karl Seguin
6708d5b961 Merge pull request #2862 from lightpanda-io/global_enumerable
webapi: Make all interfaces non-enumerable
2026-07-03 07:06:54 +08:00
Dustin Persek
f8934f575b Fix charset unicode HTML prescan fallback 2026-07-02 17:43:04 -04:00
Pierre Tachoire
ca0579413e Merge pull request #2837 from lightpanda-io/cache-simple-heuristic
Cache Simple Heuristic
2026-07-02 17:42:19 +02:00
Pierre Tachoire
656e2ee495 frame: emit networkIdle lifecycle events for child frames
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).
2026-07-02 17:06:12 +02:00
Karl Seguin
53767d2dad Merge pull request #2852 from lightpanda-io/local_arena
mem: add local_arena
2026-07-02 22:46:11 +08:00
Pierre Tachoire
7e353927b9 cache the computed-style object per element
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.
2026-07-02 16:33:58 +02:00
Karl Seguin
25d8b7d5c7 mem: add local_arena
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.
2026-07-02 21:06:51 +08:00
Karl Seguin
3fbe460b04 Merge pull request #2861 from lightpanda-io/MessageEvent_getSource
crash, worker: Fix crash when MessageEvent.source is called from worker
2026-07-02 21:04:45 +08:00
Karl Seguin
0db0b89def Merge pull request #2858 from lightpanda-io/structuredClone
webapi: structuredClone for host (aka Zig) objects
2026-07-02 21:00:57 +08:00
Karl Seguin
69547db8ba webapi: Make all interfaces non-enumerable
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).
2026-07-02 20:54:33 +08:00
Karl Seguin
8f8df00b0f crash, worker: Fix crash when MessageEvent.source is called from worker
The source for a worker is always null. The getter cannot receive a *Frame since
it can be called from a Worker's context.
2026-07-02 19:28:09 +08:00
Pierre Tachoire
5fcfb4e344 Merge pull request #2859 from lightpanda-io/crypto-random-leak
use getrandom syscall for std.crypto.random
2026-07-02 12:03:30 +02:00
Adrià Arrufat
f9c3e4bc2a ci: drop temporary push trigger, suite validated green in CI 2026-07-02 12:00:19 +02:00
Pierre Tachoire
37a7f034c8 use writerStreaming in log 2026-07-02 11:23:45 +02:00
Pierre Tachoire
e56a4f6259 use getrandom syscall for std.crypto.random
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,
2026-07-02 11:19:53 +02:00
Adrià Arrufat
e219581791 test: surface agent stderr on live-layer failures 2026-07-02 10:33:28 +02:00
Adrià Arrufat
86ef37e12b ci: temporary push trigger to validate agent-regression pre-merge 2026-07-02 10:24:59 +02:00
Adrià Arrufat
f191410773 test: add agent regression suite
Two layers driven by test/agent/run.sh:

- deterministic: replay a golden PandaScript against frozen local HN
  fixtures and diff the returned JSON exactly against a golden file.
  No API key, no network; gates every PR via e2e-test.yml.

- live (GOOGLE_API_KEY): closed-form Q&A over a local fixture page
  (substring match), and a live Hacker News scrape saved with --save,
  replayed token-free and validated against a jq shape invariant.
  Runs nightly and on demand via agent-regression.yml.

The live output contract lives in cases/hn-live.task (prompt) +
schemas/hn-live.jq (invariant). Model pinned to gemini-3.5-flash in
run.sh; per-task timeouts and a $usage token ceiling guard runaway
loops. make test-agent for local runs.
2026-07-02 09:40:22 +02:00
Halil Durak
60aacfa2b0 Merge pull request #2851 from lightpanda-io/nikneym/network-x509-store
networking: prefer `X509_STORE` instead of `ca_blob`
2026-07-02 09:10:15 +03:00
Halil Durak
c86dad5430 libcrypto: remove unused utilities 2026-07-02 08:46:22 +03:00
Halil Durak
5e567adf1b ci: run serve command with TLS host verification disabled 2026-07-02 08:46:21 +03:00
Halil Durak
fd0bff3af2 http: remove dead code 2026-07-02 08:46:21 +03:00
Halil Durak
820f5684cb networking: replace ca_blob with X509_STORE
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`.
2026-07-02 08:46:21 +03:00
Halil Durak
00123d5102 libcurl: more bindings
* Make curl_easy_setopt aware of SSL_CTX_FUNCTION and SSL_CTX_DATA,
* Add CURLE_* errors.
2026-07-02 08:46:21 +03:00
Halil Durak
6e75a0caa9 libcrypto: bind couple X509, X509_STORE and SSL_CTX helpers 2026-07-02 08:46:20 +03:00
Karl Seguin
a750a6942d webapi: structuredClone for host (aka Zig) objects
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.
2026-07-02 12:43:56 +08:00
Karl Seguin
22fe4e993a Merge pull request #2849 from lightpanda-io/perf/selector-parse-cache
perf(css): cache parsed selectors per frame
2026-07-02 12:22:13 +08:00
Karl Seguin
7377a3a211 Merge pull request #2854 from lightpanda-io/remove-obey-robots-client
Stop storing `obey_robots` on HttpClient
2026-07-02 09:14:21 +08:00
Karl Seguin
ad3cf997df v8: When re-using window, detach from original context first
Depends on: https://github.com/lightpanda-io/zig-v8-fork/pull/185

Fixes a WPT DCHECK crash in /html/browsers/the-window-object/named-access-on-the-window-object/navigated-named-objects.window.html

https://github.com/lightpanda-io/browser/pull/2742 added the re-use of the
global (window) when an iframe or popup is re-navigated. This is fine with v8,
but we need to detach it from the origin first.
2026-07-02 08:05:25 +08:00
Karl Seguin
5223d244a3 Merge pull request #2853 from lightpanda-io/BOM-parsing
webapi, fix: don't strip leading BOM for non-document parsing
2026-07-02 07:37:25 +08:00
Muki Kiboigo
b3ee83f884 remove storing obey_robots on HttpClient 2026-07-01 08:12:53 -07:00
Karl Seguin
bdb48ee02b webapi, fix: don't strip leading BOM for non-document parsing
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
2026-07-01 21:15:00 +08:00
Adrià Arrufat
6f7bde9939 perf(css): tidy uncached-variants doc comment 2026-07-01 14:05:10 +02:00
Adrià Arrufat
c04d316f8a perf(css): move selector cache to Browser with bounded eviction
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.
2026-07-01 13:44:48 +02:00
Adrià Arrufat
b90d0fb54d Merge pull request #2835 from lightpanda-io/agent-async-goto-parallel-navigation
script-runtime: support concurrent page navigations
0.3.4
2026-07-01 11:19:35 +02:00
Pierre Tachoire
0f5d47c0cc Merge pull request #2850 from staylor/feat/disable-core-dump-env
feat: add LIGHTPANDA_DISABLE_CORE_DUMP to suppress crash core dumps
2026-07-01 10:19:18 +02:00
Karl Seguin
dfe1f91deb Merge pull request #2819 from lightpanda-io/telemetry_smaller_payload
Telemetry smaller payload
2026-07-01 15:27:35 +08:00
Pierre Tachoire
b11eb35de7 add LIGHTPANDA_DISABLE_CORE_DUMP desc into README 2026-07-01 09:25:00 +02:00
Pierre Tachoire
2278616cb1 telemetry: always write headers on send messages 2026-07-01 09:16:17 +02:00
Adrià Arrufat
1ee8f8def7 perf(css): drop selector-cache cap; bypass cache for one-off selectors
Replace the arbitrary 1024-entry cap with an explicit split: the public
querySelector/querySelectorAll/matches/closest entry points cache (page scripts,
waitForSelector — selectors that recur), while SelectorPath's synthesized one-off
candidates use new *Uncached variants that parse into a transient arena. The
cache now only ever holds genuinely-reused selectors, so it needs no size bound.
2026-07-01 08:13:31 +02:00
Karl Seguin
09eb265703 Merge pull request #2843 from navidemad/fix-a10-stylesheet-deferred-flush
page: flush deferred transfers after a synchronous stylesheet fetch
2026-07-01 12:28:10 +08:00
Scott Taylor
f7f149c9c0 feat: add LIGHTPANDA_DISABLE_CORE_DUMP to suppress crash core dumps
Lightpanda installs no SIGSEGV handler, so a segfault (or the abort() in
the panic path) falls through to the kernel and writes a core dump. When
many instances run under a shared core_pattern crash reporter -- e.g. a
containerized crawl fleet -- those cores become pure storage/alert noise,
and a browser core can capture the contents of arbitrary pages.

Crashes are already reported via telemetry, so this adds an opt-in
LIGHTPANDA_DISABLE_CORE_DUMP env var (mirroring LIGHTPANDA_DISABLE_TELEMETRY)
that zeroes the soft RLIMIT_CORE at startup. Default behavior is unchanged.
2026-06-30 21:58:05 -04:00
Karl Seguin
a36874f688 Merge pull request #2848 from lightpanda-io/perf/skip-inline-style-alloc
perf(css): skip inline-style allocation for elements without inline styles
2026-07-01 08:21:21 +08:00
Karl Seguin
a255a15350 zig fmt 2026-07-01 07:22:22 +08:00
Karl Seguin
96cfd2e84f perf: shrink header fields
mode/arch/os are all now short strings
2026-07-01 07:21:02 +08:00
Karl Seguin
091fb6be28 Tweak small telemetry
Remove sid. Include iid in every message. Booleans true/false => 1/0. Constant
string values => single letter. Example:

["8800df58-a5d5-4ca5-9a06-d7691a2a3780","H","fetch",0,"macos","aarch64","1.0.0-dev.7609+88b1bc671"]
["8800df58-a5d5-4ca5-9a06-d7691a2a3780","R"]
["8800df58-a5d5-4ca5-9a06-d7691a2a3780","N",1,"P"]

H => Header
R => Run
N => Navigate
B => Buffer Overflow / dropped
L => LLM

Navigate context are
P => Page
O => Open (popup)
I => Iframe
2026-07-01 06:33:31 +08:00
Karl Seguin
50eef611f4 telemetry: Add more aggressive batching
Telemetry worker will now wait up to 5 seconds, or for 16 events before
flushing. Shutdown still causes an immediate flush.
2026-07-01 06:33:31 +08:00
Karl Seguin
47cffb8819 telemetry: Optimize the payload size, add navigation type
First, this adds 1 small piece of data to the navigate event: whether the
navigate was a page, frame or popup.

It also adds a session id, but as far as I'm concerned, this isn't "new"
information, or any new tracking/insight into users. Between the iid and the
"run" event, a "session" was always trackable. By giving it an explicit value,
we can shrink the size of all other messes.

This change reduces the telemetry payload by ~70% (despite the extra nav field).
I'm hoping this might remove a reason some people would consider turning it off.

It hits a /v2/ endpoint. The changes:

1 - A header is the first message in a session and contains all of the static
    data, as well as a session id
2 - Every event is encoded as an array, [$SID, "event-type, params...]

```
{"sid":"92e98210a141f497","iid":"$UUID","mode":"fetch","os":"macos","arch":"aarch64","version":"$VERSION","proxy":false}
["92e98210a141f497","run"]
["92e98210a141f497","nav",false,"page"]
```

(the driver=cdp field was removed from nav, because it was always cdp).

Some notes for the server:

1 - The server can tell a header from an event based on the first character.
2 - The SID is 8 bytes, enough to be unique, but not globally unique. The
    iid + sid + time window is how a events for the same SID can be grouped.
3 - A valid event is always an array of 2+ items, index 0 = SID, index 1 = type

Although positional data isn't expressive, it's still extendable.

The 4 events:

run, no parameters (mostly just used to flush the header now)
["sid", "run"]

// nav, tls, page/popup/iframe
["sid","nav",true,"page"]

// bof, # of lost telemetry events
["sid","bof",42]

// llm, provider, model (nullable)
["sid","llm","anthropic","claude"]
2026-07-01 06:33:30 +08:00
Adrià Arrufat
7a2f4ff2da Merge branch 'main' into perf/skip-inline-style-alloc 2026-07-01 00:23:11 +02:00
Muki Kiboigo
65cf678822 heuristically cache only with Last-Modified header 2026-06-30 09:49:01 -07:00