The interval between two top-level navigations to the same host now
grows with the number of navigations sent to it: one extra base interval
every burst*10 navigations, capped at 60x. Pressure cools down by one
unit for every 10 base intervals the host is left alone.
--http-nav-delay is now a minimum, not a fixed spacing.
Attempt to improve https://github.com/lightpanda-io/browser/issues/3199
Meant an leaner alternative to https://github.com/lightpanda-io/browser/pull/3440
3440 is close, but it adds 16 bytes to every VisibilityRule which isn't ideal
especially since the majority of these rules have no custom properties.
Let's make a few reasonable assumptions:
1 - Most custom properties don't have many distinct selectors
(--background-color might be defined on, :root, and .card and a few others)
2 - Most custom properties aren't queried from JS (Tailwind can define thousands
of custom properties, but they're used by the rendering engine, not from JS)
3 - Most selectors don't have custom properties
The design is to inverse what we do for visibility: lookup per property. We end
up with a property-name -> [(selector, value), (selector, value)] lookup. If
there's a query for --foreground, we O(1) to get the list of (selector, value)
and then iterate through this (hopefully) short list to get the value.
PLUS, we only parse the selector on the first query. So for those sites with
thousands of custom-properties, we're not wastefully storing / parsing the
selector on every build.
With the StyleManager memo, a VisibilityCache/PointerEventsCache hit
costs the same hash probe as a memo hit, so the caches only added a
second probe per ancestor, a call_arena allocation per element, and
plumbing through SemanticTree, AXNode, CDP, links, ResizeObserver and
elementFromPoint. checkVisibilityCached becomes isVisible.
Every ancestor-aware visibility query (checkVisibility, offsetWidth,
getBoundingClientRect, getComputedStyle().visibility, pointer-events, and
the Zig tree walkers) re-resolved each ancestor from scratch. Resolve an
element's display/visibility/opacity/pointer-events once and reuse it
while Page.style_version is unchanged.
The inline style attribute is scanned once for all four properties, the
pointer-events scan shares the same path, and a materialize-mode hit still
creates the inline style object that layout reads.
Bumped through Frame.styleChanged: with dom_version, and by the state
changes that alter a selector match without changing what live
collections see: checked/indeterminate, input values, custom element
definitions, character data (:empty), parser insertions and stylesheet
changes. Stamps the visibility memo added next.
navigator.languages now lists the Accept-Language tags in order, which is
Chrome's contract, instead of a second derivation from the locale tag that
disagreed with the header (--locale de-DE sent de-DE,de,en but reported
["de-DE","de"]). HttpHeaders.AcceptLanguage owns both shapes and is also
the CDP override type.
ICU canonicalizes a BCP 47 tag read from LC_ALL itself, script subtag
included, so the POSIX id conversion is gone; it dropped the script and
turned zh-Hans-TW into Traditional Chinese.
Also: the CDP handler keeps validateUserAgent's verdict instead of scanning
for Mozilla twice, the override is cleared unconditionally on context
teardown instead of through a flag, and the flags are sentinel strings so
Platform passes them to setenv without copying.
navigator.language was hard-coded to en-US and Accept-Language was a
constant, while Intl, toLocaleString and Date followed the host process
environment. On a de_DE host a page saw navigator.language === "en-US"
next to German number formatting, a mismatch fingerprinting scripts look
for, and the same page rendered differently across machines.
Follow Chrome's --lang rule: one configured tag drives navigator.language(s),
the Accept-Language header and ICU's default locale. --locale defaults to
en-US, so Intl is now en-US on every host instead of whatever LANG says.
--timezone sets the IANA zone Date and Intl use; absent, the host zone stays.
Both are applied by writing LC_ALL and TZ before V8 initializes ICU, which
reads them lazily. Platform.init is the first call in App.init, before any
thread exists, so setenv is safe there.
CDP Emulation.setUserAgentOverride.acceptLanguage, which Playwright sends
for its locale option, now overrides the header and navigator.languages
for the browser context's lifetime, mirroring the user agent override, and
applies even when the Mozilla user agent is refused.
Emulation.setLocaleOverride and setTimezoneOverride stay no-ops: changing
ICU's defaults at runtime needs new zig-v8-fork bindings.
Attr.cloneNode() copied _element, so the clone still claimed the original
owner. Element.setAttributeNode() then tried to remove it from that owner,
which never held it, and threw NotFoundError. Per the DOM spec a cloned Attr
has a null ownerElement.
While there, make setAttributeNode() throw InUseAttributeError when the Attr
belongs to another element instead of silently moving it, matching the spec
and both Chrome and Firefox.
Mozilla's Readability.js hits this on every page (_setNodeTag,
_simplifyNestedElements), as does any code that copies attributes with
el.setAttributeNode(attr.cloneNode()).
Driven by a handful of /fetch/ WPT tests, three changes:
1 - Prevent libcurl from auto-inserting a 'application/x-www-form-urlencoded"
content type for types we really have no content-type for.
2 - Include origin header in all requests that should have it. This is something
CorsGate was doing in most cases, but cors can be disabled, so the logic
is now moved to HttpClient.
3 - Expands on the header guard added in https://github.com/lightpanda-io/browser/pull/3374/
Adds more modes and more header check. Request.init also uses the header
guard now
Inspired by https://github.com/lightpanda-io/browser/pull/3454 which added
the presence check for DOMStringMap, this adds the check to a number of other
collections (PluginArray, HTMLAllCollection, ...).
It also adds custom element reactions on indexed properties (previously only
existing for named index properties).
Finally, DOMStringMap now correctly handle numeric (or number-looking) indexes.
In main, there's a `getId`, and `getClassName` (etc...) getter on Element. But
these all `orelse ""`, because that's what the WebAPI wants. Internally though,
most code want the optional. The result is that _many_ places do:
```zig
el.getAttributeSafe(comptime .wrap("id"))
```
instead of:
```zig
el.getId()
```
This is a bit tedious AND, it means that when we improve `Element.getId` (1) no
internal caller benefits from it. This commit makes the element getters return
the optional (`?[]const u8`) and updates every callsite to use the new getter.
The `orelse ""` needed by the WebAPI is moved to the JsApi bridge.
(1) https://github.com/lightpanda-io/browser/pull/3457
Follow up to https://github.com/lightpanda-io/browser/pull/3449 which improved
nth-* type CSS selectors, this improves common attribute lookup and CSS selector
class matching.
Two distinct changes:
1 - We already have a `getAttributeSafe` for string literals. Now we have a
`getAttributeInterned` for known interned value (compile error when used with
a non-interned string, so safe). This relies on attribute names always going
through String.intern (which they do). Turns an equality check into a single
pointer comparison
2 - Improve how an element's class list is matches against a specific class.
Rather than tokenize + compare, this looks for the first character of the class
then does a boundary check.
In no case is this slower, but the gain depends on a number of factors,
including how many attributes an element has, and how many classes a class list
has. But a class selector goes from ~60us/query to ~40us/query compared to
Firefox's 47us/query.
Without a query callback on DOMStringMap's named property interceptor,
'name' in element.dataset and Object.hasOwn(element.dataset, name)
returned true for every name, because the getter intercepts absent
keys as undefined. Pages that feature-detect data-* attributes in
custom element constructors took the wrong branch; one observed
pattern (a constructor that reparents itself when a data attribute is
present) turned cloneNode(true) into an unbounded loop over a growing
live child list, ending in a watchdog termination.
Add a query callback that maps the camel-case name to its data-*
attribute and reports actual presence, matching spec and other
engines.
A misspelt value for an enum-typed flag now names the nearest tag as
did_you_mean. --dump's peeking validator treats a word within two
edits of a format name as that mistake rather than as the url, so
--dump htmx no longer becomes a second url.
tagNames moves from Config into cli so both can use it.
An unknown --flag now logs the nearest accepted spelling within two
edits as did_you_mean, painted green next to the red typo in the pretty
log format; logfmt stays plain. A bare first argument within two edits
of a command name is rejected with the same hint instead of being
fetched as a url.
The Levenshtein helper moves from SlashCommand into string.zig so the
agent REPL and the CLI share it, with the table widened to fit the
longest flag name.
This adds an NthCache which will cache the ordinal position of elements. Only
nodes which have >= 32 sibblings are cached - the cache has performance/memory
overhead and for smaller nodes, walking is cheaper.
Using the new benchmark: https://github.com/lightpanda-io/demo/pull/245 under
particularly bad conditions (W=4000):
main:
case match iters us/query
:nth-child(2n) 2000 2 55862.5
:nth-of-type(2n) 1999 2 57675.0
:nth-last-of-type(2n) 1999 2 53935.0
ancestor :nth-child(2n) 4000 1 111855.0
list of two nth 2667 1 111180.0
PR:
case match iters us/query
:nth-child(2n) 2000 258 388.1
:nth-of-type(2n) 1999 245 409.6
:nth-last-of-type(2n) 1999 245 409.0
ancestor :nth-child(2n) 4000 168 596.1
list of two nth 2667 160 626.8
Firefox
case match iters us/query
:nth-child(2n) 2000 460 217.4
:nth-of-type(2n) 1999 445 224.7
:nth-last-of-type(2n) 1999 260 384.6
ancestor :nth-child(2n) 4000 246 406.5
list of two nth 2667 294 340.1
With no regressions in the other cases.
The js.Execution is the API behind the Frame/WGS split, but the split is
actually held by the underlying js.GlobalScope. Most Execution methods are:
```zig
return switch (self.js.global) {
inline else => |g| g.isSameOrigin(url),
};
```
And that works well, except that in some cases, code has a js.Context, not an
js.Execution, and they need to do the same inline switch.
This commit moves GlobalScope from src/browser/js to src/browser (there's
nothing JS/v8 about it), and moves all those inline switches into it. The
js.Execution API stays the same (it forwards the call to js.global)
but all callers that directly inlined switched the js.global no longer do.
A paginated storefront listing holds ~2.7k pending timers, which crosses
the 2048 table cap. Past it setTimeout throws TooManyTimeout into the
framework's scheduler, which turns a busy page into a retry storm. No
browser throws there: raise the combined cap to 8192 and repeating timers
to 2048 so it stays a runaway backstop, not a budget.
Tried to not to change function signature by relying on saturating addition; could've implemented differently, though I'm not sure if returning an error here would make a huge difference.
Headers strip whitespace and guard against invalid characters
Headers iterator sorts and combines PER step, so that mutations are picked up.
Not the most efficient, but this is a short list, and how often are these being
iterated?
XMLHttpRequest: has its own extra header validation
Mime support for multiple Content-Type headers (or a header with multiple values)
last value wins.
Add BufferSource js bridge type that accepts various types -> []const u8 (at the
cost of losing the actual type). Useful in fetch, where various types can be a
body, but we only care about the underlying bytes (e.g. we didn't support A
rrayBufferView before this)
Refactored response body getters so that they all go through the same consume
and resolve logic