Commit Graph
44 Commits
Author SHA1 Message Date
Karl Seguin b691603823 Merge pull request #3135 from lightpanda-io/ensureTotalCapacityPrecise
mem: prefer ensureTotalCapacityPrecise to reduce memory usage
2026-08-05 08:44:32 +08:00
Karl Seguin c86d969ce0 mem: prefer ensureTotalCapacityPrecise to reduce memory usage
When we know the precise final length, prefer ensureTotalCapacityPrecise over
ensureTotalCapacity. The latter goes through `growCapacity` which will allocate
~1.5x padding.
2026-08-04 17:44:05 +08:00
Karl Seguin d4642d8afc http: redirect even on missing close_notify
We already had special handling for BoringSSL's RecvError on improperly closed
TLS connection. This moves the handling up, so that redirect handling is
covered by it too.
2026-08-04 17:37:07 +08:00
Muki Kiboigo 7d6299895f fix minor compilation issues 2026-08-03 18:26:58 -07:00
Muki Kiboigo 0b74224b15 use CachePutRequest for puts instead of CacheMetadata 2026-08-03 18:26:57 -07:00
Muki Kiboigo 6c56144ef9 rework get to be strictly deferred 2026-08-03 18:26:12 -07:00
Muki Kiboigo 84405ad930 remove cache eviction path from HttpClient 2026-08-03 18:17:44 -07:00
Muki Kiboigo 94c029ff32 get rid of file variant in CachedData 2026-08-03 18:17:43 -07:00
Muki Kiboigo 0f2957cda2 add SqliteCache as default local impl 2026-08-03 18:17:40 -07:00
Karl Seguin fd2e6a8512 refactor: rework how httpclient headers work
Adding headers to an HTTP request was a bit awkward due to my desire to avoid
having an intermediate representation (e.g. an ArrayList(Header)). Going
straight to a curl slist avoids double-copying the headers (first to Zig, then
to curl).

But the CORS work (https://github.com/lightpanda-io/browser/pull/3002) showcases
that this micro-optimization simply isn't worth it, since it needs that
intermediate representation anyways.

And, this change isn't just for CORS. Headers have been a silly pain in the past
like unclear ownership, and messy APIs used in _a lot_ of places (WebBotAuth,
WebSocket, Fetch, ...)

This new approach stores headers on the transfer in an ArrayList. The API is:

```
const transfer = try client.newRequest(.{...}, owner);
{
    errdefer transfer.deinit();
    try transfer.addHeader("Over", "9000", .{});
}
try transfer.submit();
```

This:
1 - Eliminates ambiguity about errdefer cleanup responsibility
2 - Eliminates a bunch of stringZ concat that Frame, Config, CDP were doing
3 - Transfer.arena is now available for headers
2026-08-03 18:26:32 +08:00
Karl Seguin 6a83881634 chore: add lp.IS_DEBUG and lp.IS_TEST
Change all users to builtin.mode and built.is_test
2026-08-01 09:15:18 +08:00
Karl Seguin 2bd9848c07 mem: improve ArenaPool usage
1 - Add a metric to track the number of inlight arenas from the pool
2 - Script now use 2 arenas:
    - An initial (small) one for the script
    - A sized one for the body
    Should result in less pressure on our limited .large arenas
3 - DOMPoint and DOMPointRO are now arena free (they live on the slab only)
4 - TextDecoder no longer accumulate garbage in its arena
5 - Response object is much better at picking its arena size, rather than just
    using a .large
2026-07-31 14:36:22 +08:00
Karl Seguin bdce49a4b5 refactor: Introduce Arena wrapper
Introduces a ArenaAllocator wrappre (lp.Arena). This is groundwork for better
memory tracking and reporting memory usage to v8. This is almost purely a
mechanical change to lay a foundation for a follow up PR that will address
https://github.com/lightpanda-io/browser/issues/3027

Some code became a bit leaner: a pooled arena can release itself (it has a
reference to the ArenaPool).

Some code became uglier: The Frame has a `_local_arena: *lp.Arena` and a
`local_arena: Allocator` (same with call_arena, and same with a few other types)
so that consumers aren't impacted (they continue to use `frame.local_arena`).
2026-07-30 13:02:13 +08:00
Karl Seguin dcb98853b9 chore: make our use of clock/timestamp more consistent
Zig 0.16 changed clocks/timestamps. Our migration to Zig 0.16 took the path of
least resistance. For example, we kept 'monotonic' and 'real' even though Zig
0.16 renamed them to 'boot' and 'clock'

This commit tries to standardize all timestamp usage to: lp.datetime.timestamp
and lp.datetime.milliTimestamp using the new zig 0.16 names ('boot' and 'clock')
2026-07-27 09:28:48 +08:00
Karl Seguin 5acfd38250 Improve CDP response event data
Give accurate connectionId, connectionReused, initialPriority and securityState
values.

Always set `referrerPolicy` to `unsafe-url` as the most honest answer (we should
implement proper referrer policy!).

For workers, track the underlying frame_id so that it can be used for the
`documentURL` field.
2026-07-25 11:36:13 +08:00
Pierre Tachoire 303a3666b6 don't count blocked redirects as followed
Move the http_redirects metric increment after the URL block check so a
redirect that gets blocked is recorded as an error, not a redirect.
2026-07-23 12:15:50 +02:00
Matt Van Horn dee40075a0 feat(cdp): implement Network.setBlockedURLs via urlPatterns
Reimplement request blocking on the non-deprecated urlPatterns shape:
each pattern carries an explicit block/allow flag (first match wins),
UrlBlocklist owns the compiled patterns plus their block flags, and the
legacy setBlockedUrls path stays for back-compat. Tests updated.
2026-07-23 12:15:49 +02:00
Matt Van Horn 311ab3aaae feat(cdp): add Network.setBlockedURLs and --block-urls request blocking 2026-07-23 12:15:47 +02:00
Karl Seguin 8f562330c8 perf: Improve Runner tick signaling on macrotask-only wait
Currently, Runner assumes that HttpClient.tick did something. But it's possible
that HttpClient had nothing to do, and thus didn't poll. In that case, Runner
would return .{.ok = 0} and Runner's caller would also certainly call
Runner.tick again, resulting in a spin-loop.

The reason Runner allows this to happen is because it can still have macrotasks
to run. So now, when HttpClient.tick has done nothing, Runner will return its
ms_to_next_task, rather than 0.

On sites where all i/o is completed, and only macrotasks are waiting, this
significantly reduces CPU usage.
2026-07-23 07:28:32 +08:00
Karl Seguin e979a317ad Merge pull request #3007 from lightpanda-io/zig-0.16
zig: Zig 0.16
2026-07-22 21:43:39 +08:00
Karl Seguin 0cb9c3aebb dedupe check and centralize it in HttpClient's tickSync 2026-07-22 16:41:33 +08:00
Karl Seguin 8e42d63c1c zig: Zig 0.16
Built against https://github.com/lightpanda-io/zig-v8-fork/tree/zig-0.16 but
it doesn't require a new v8 build.

Built against https://github.com/lightpanda-io/boringssl-zig/tree/zig-0.16
since the current fork we point to isn't updated.

A global std.Io instance, lp.io. Way easier this way and requires 0 changes to
our libcurl integration / event loop.

Network code uses a new layer that does what Zig 0.15's posix package used to
do. Again, quicker migration that way. But, as long as we have the global IO,
and given the half-baked nature of networking in std.Io 0.16, this just makes
sense. Things can be migrated as needed.

The std.time.* -> std.Io.Timestamp/Clock/Duration resulted in _a lot_ of
changes. ArrayList = .{} -> ArrayList -> .empty also resulted in a lot of
changes, but that's obviously superficial. As is the trimLeft/trimRight ->
trimStart/trimEnd rename.

Locking adopt the `Uncancelable` variants, e.g. mutex.lockUncancelable() to
preserve the error-free signature (and, because cancellation would be something
we'd have to put more thought into).

std.json.ObjectMap is now unmanaged, so the allocator had to be passed along.
However, there's still a deprecated managed variant of MemoryPool, so I switched
to it (we can do a small follow up PR to move to the unmanaged after).

I tried use_llvm = false, but it locks my computer, consuming RAM until MacOS
gives me a popup I've never seen before, begging me to start killing processes.

Agent and the networking stuff saw the most significant changes.
2026-07-22 13:26:03 +08:00
Scott Taylor a12032cd0e Interrupt nested script waits during teardown
Assisted-By: devx/8bea7794-aef3-4ef5-a06d-fd1f1f3aafd0
2026-07-21 18:07:15 -04:00
Karl Seguin 9090869420 Merge pull request #2991 from lightpanda-io/blob-origin
webapi: Blob shared per origin
2026-07-20 18:46:34 +08:00
Karl Seguin 0cffa933d4 Merge pull request #2996 from lightpanda-io/root-page-no-cache
cache: Never cache the top-level root page
2026-07-20 14:58:52 +08:00
Karl Seguin 86bf28216a webapi: Blob shared per origin
This reworks blob URLs from being owned by the Frame to being owned by the Page
and shared / protected by origin. This allows, for example, a worker/popup/
iframe from accessing blobs created by its page, provided they share the same
origin.

This was already working on a small (but common) set of cases, but has now been
reworked (by moving the blob registry up to the Page, where it can be shared) to
cover every case.
2026-07-18 14:24:39 +08:00
Karl Seguin 20cde7a6f5 perf: If a message is dispatched, don't poll
Polling delays processing of messages. So don't poll after we know we've
dispatched. Largely impacts CDP which is more likely to poll (for data on the
CDP connection).
2026-07-18 09:54:41 +08:00
Karl Seguin 11f9777282 cache: Never cache the top-level root page
The idea is that, for most cases, the top level page isn't revisited, so
caching is just overhead + wasted space. Plus, if someone does revisit it,
chances are they want a fresh copy.

Imagine someone scraping a typical ecommerce site. Sure, they want to cache
the .js files which get re-used from product to product. But do they want to
cache each individual product page?
2026-07-17 18:55:31 +08:00
Karl Seguin 914bf027be Merge pull request #2941 from lightpanda-io/websocket-delivery
websocket: Process WebSocket message through delivery query
2026-07-16 13:17:18 +08:00
Karl Seguin fbe229c4bd uaf: Dupe blobs before using them, or else risk revokeObjectURL
When using a blob that might live through a JS call, dupe it so that any
subsequent revokeObjectURL doesn't invalidate the memory.
2026-07-15 17:24:35 +08:00
Karl Seguin 4d433ad028 websocket: Process WebSocket message through delivery query
This piggybacks on the HttpClient rework (1) and makes incoming WebSocket
messages get delivered in a similar fashion. Namely, WebSocket events (connect,
data, close) are queued and only delivered at safe points. The goal is to make
sure JS is never run during a libcurl callback. This has historically caused
issues and currently requires the HttpClient to have various guards (e.g.
performing flag, dirty queue, etc..).

This change removes the last cases where JS could be executed within a libcurl
callback. Consequently, HttpClient is simplified. It no longer has a perfoming
flag nor a dirty queue nor ready queue.

(1) https://github.com/lightpanda-io/browser/pull/2889
2026-07-14 17:25:52 +08:00
Karl Seguin 3b266359d2 Merge pull request #2937 from lightpanda-io/support-large-content-length
http: u32 -> usize for [very] large content length
2026-07-14 15:10:21 +08:00
Karl Seguin 8ad4bd9015 webapi: Add EventSource
The EventSource API only requires a simple line-based parser. It's pretty
straightforward. It's RC'd by v8 and itself, but also by scheduled tasks (e.g.
to reconnect).

The bigger change is to the HttpClient to support streaming requests. This
changes to things:
1 - A transfer doesn't only deliver() once
2 - A transfer can be in the multi while delivering

Neither of these are surprising, but they both add complexity.
2026-07-14 14:19:27 +08:00
Karl Seguin a0549b072d http: u32 -> usize for [very] large content length
There's 1 WPT test that returns a 8GB value content-length (testing failures)
and the u32 limit fails to parse it and makes it as though no content-length
header exists.
2026-07-14 14:09:01 +08:00
Karl Seguin f0784de015 add http metrics 2026-07-14 11:32:52 +08:00
Karl Seguin 56c5701b7b Merge pull request #2925 from lightpanda-io/default-max-response-size
http: Change default max http size to 1GB (from unlimited)
2026-07-14 09:08:50 +08:00
Karl Seguin 70278c0091 'GB' -> 'GiB' 2026-07-14 07:57:11 +08:00
Karl Seguin ca1e80a397 fix: Custom-element constructor parser endless recursion
In a custom element, when this.innerHTML = '....' is called, we need to be
careful to prevent endless recursion. The html5ever callback used to determine
the context element should not invoke the custom-element constructor, else we'll
enter an endless loop.

This also fixes an ungating problem added with the new HttpClient when a
waitForImport can block forever.

Both issues were see on a WooCommerce site - though the HttpClient is only
due to an earlier HttpClient refactor.
2026-07-13 23:23:00 +08:00
Karl Seguin 43b55b8013 http: Change default max http size to 1GB (from unlimited) 2026-07-13 18:55:05 +08:00
Karl Seguin 86c8b88328 httpclient: eliminate some uaf on transfer abort
Commit eliminates a class of UAF on transfer abort due to the abort potentially
JS callbacks within a libcurl callback.

It also introduces a graveyard queue to the HttpClient so that, on deinit,
Transfers become detached but their memory is still valid. In debug, the
transfer is poisoned so that any uaf fails loudly. In release, the graveyard
is simply cleaned up.
2026-07-10 12:34:45 +08:00
Karl Seguin 377de019b8 fix uaf when aborting a robots-parked transfer 2026-07-10 08:22:28 +08:00
Karl Seguin cca9bddead fix typos in comments 2026-07-10 07:51:33 +08:00
Karl Seguin 2d88c4a907 Protect against v8 stackoverflow when with sibling blocking request
A NSFW site was reported as crashing. It had 21 sibling iframes all loading
the same synchronous script. This would overflow the v8 stack. The issue is
that, on a blocking request, we gate other requests from the same frame, but not
from other frames. Typically, that's ok / what we want. BUT, for a document
request, we need to be more careful:

1- We're inside a v8 callback (so some v8 stack is active)
2- A sync request is made
3- while pumping the http client, a document for another frame completes
4- Ok, it's on another frame, we don't block it
5- We start the parser
6- The parser encounters iframs
7- Each of those iframes have a blocking script
8- Those blocking scripts load their own document

See what's happening? We're still in the v8 callback of #1, still on that same
stack, and we're loading more and more heavy parsers and triggering more and
more http callbacks.

It's possible this problem extends beyond document parsing. But this is the
one we observed and it's also the most likely to cause recursive blocking
requests.
2026-07-10 07:35:37 +08:00
Karl Seguin 2eab4d2630 refactor: HttpClient
Replaces layering with an inline request pipeline, and transfer queue. This is
meant to simplify the code, reduce footguns, and make future enhancements easier
to implement (e.g. speculative parsing (which requires streaming to fully
leverage)).

Previously, HttpClient implemented deferring as a layer which required special
pumping at various callsites (https://github.com/lightpanda-io/browser/pull/2855,
https://github.com/lightpanda-io/browser/pull/2843, ...). In this new approach,
deferring is built-into the HttpClient/Transfer's flow. Specifically, Transfers
now maintain a queue of events (start, header, data, end, err) which are
dispatched in HttpClient.tick. The result is that JS callbacks are never
executed in the same stack that initiated the I/O, without needing guards or any
external intervention.

tTwo other benefits come from this. The first is that reentrant libcurl is
eliminated. Instead of "libcurl -> callback", it's now "libcurl -> transfer
event queue THEN  tick -> callback" (we don't have to wait until the NEXT tick, we
can just do it later in the tick). HttpClient still has to guard against libcurl
reentrancy, but only because of how WebSocket is implemented, and we should be
able to unify WebSockets to use an event queue too in a follow up PR (which will
eliminate a bunch of guard code).

The transfer queue should also be useful to re-implement streaming, since a
data chunk is just an event in the transfer's event queue. For now, I kept it
as a single buffered event to minimize the change. But since speculative parsing
depends on this, and speculative parsing seems to be the next major performance
tweak we can make, we need to re-introduce streaming.

The other change is the removal of all other layers in favor of a pipeline. This
works well with the existing Transfer.park mechanism, where a parked Transfer
can restart the pipeline for a transfer in an arbitrary point (not as fancy as
it sounds given how simple the flow is). The fallout from this is that we're no
longer creating/wrapping contexts and callbacks: whatever the request was
configured with is all we need.

Because of this, HttpClient.Response is removed. There are no intermediary
responses and no changing context, everything is just the Transfer.

A smaller change is the addition of newRequest + transfer.submit(). The one-shot
HttpClient.request and HttpClient.requestT still exist, but this explicit create
+ submit has some advantage. First, callers can use the transfer.arena (e.g.
Frame using the transfer's arena to set the Referrer header). Second, callers
can holds Transfer immediately, rather than waiting for their startCallback to
be fired. An abort on an XMLHttpRequest called before the start of the transfer
no longer silently fails.
2026-07-10 07:35:36 +08:00