Merge pull request #2901 from lightpanda-io/attribute_datastruct

perf, mem: Embed Element Attributes and replaced linked list with array
This commit is contained in:
Karl Seguin
2026-07-10 07:18:28 +08:00
committed by GitHub
11 changed files with 383 additions and 273 deletions

View File

@@ -194,7 +194,7 @@ fn walk(
var name = try axn.getName(self.frame, self.arena);
const has_explicit_label = if (node.is(Element)) |el|
el.getAttributeSafe(.wrap("aria-label")) != null or el.getAttributeSafe(.wrap("title")) != null
el.getAttributeSafe(comptime .wrap("aria-label")) != null or el.getAttributeSafe(comptime .wrap("title")) != null
else
false;
@@ -380,13 +380,12 @@ const JsonVisitor = struct {
try self.jw.write(value);
}
if (el._attributes) |attrs| {
if (!el._attributes.isEmpty()) {
try self.jw.objectField("attributes");
try self.jw.beginObject();
var iter = attrs.iterator();
while (iter.next()) |attr| {
try self.jw.objectField(attr._name.str());
try self.jw.write(attr._value.str());
for (el.attributeEntries()) |*attr| {
try self.jw.objectField(attr.name());
try self.jw.write(attr.value());
}
try self.jw.endObject();
}

View File

@@ -107,10 +107,17 @@ _fragment_scripts_runnable: bool = false,
// See Attribute.List for what this is. TL;DR: proper DOM Attribute Nodes are
// fat yet rarely needed. We only create them on-demand, but still need proper
// identity (a given attribute should return the same *Attribute), so we do
// a look here. We don't store this in the Element or Attribute.List.Entry
// because that would require additional space per element / Attribute.List.Entry
// even though we'll create very few (if any) actual *Attributes.
_attribute_lookup: std.AutoHashMapUnmanaged(usize, *Element.Attribute) = .empty,
// a look here, keyed by (list, name). We don't store this in the Element or
// Attribute.List.Entry because that would require additional space per
// element / Attribute.List.Entry even though we'll create very few (if any)
// actual *Attributes.
_attribute_lookup: Element.Attribute.List.Lookup = .empty,
// Canonical pool for attribute names that aren't in String.intern's.
// Every Attribute's entry's name is either a String intern or held here.
// This is both a memory optimization (deduping attribute names) and a performance
// optimization (since we can compare strings by just their pointer)
_attribute_names: std.StringHashMapUnmanaged(void) = .empty,
// Same as _atlribute_lookup, but instead of individual attributes, this is for
// the return of elements.attributes.

View File

@@ -862,18 +862,15 @@ pub fn createElementNS(frame: *Frame, namespace: Element.Namespace, name: []cons
// After constructor runs, invoke attributeChangedCallback for initial attributes
const element = node.as(Element);
if (element._attributes) |attributes| {
var it = attributes.iterator();
while (it.next()) |attr| {
Element.Html.Custom.enqueueAttributeChangedCallbackOnElement(
element,
attr._name,
null, // old_value is null for initial attributes
attr._value,
null,
frame,
);
}
for (element.attributeEntries()) |*attr| {
Element.Html.Custom.enqueueAttributeChangedCallbackOnElement(
element,
.wrap(attr.name()),
null, // old_value is null for initial attributes
.wrap(attr.value()),
null,
frame,
);
}
return node;
@@ -907,6 +904,7 @@ fn createHtmlElementT(frame: *Frame, comptime E: type, namespace: Element.Namesp
const html_element_ptr = try frame._factory.htmlElement(html_element);
const element = html_element_ptr.asElement();
element._namespace = namespace;
element._attributes.normalize = namespace == .html;
try populateElementAttributes(frame, element, attribute_iterator);
// Check for customized built-in element via "is" attribute
@@ -934,34 +932,27 @@ fn createSvgElementT(frame: *Frame, comptime E: type, tag_name: []const u8, attr
const svg_element_ptr = try frame._factory.svgElement(tag_name, svg_element);
var element = svg_element_ptr.asElement();
element._namespace = .svg;
element._attributes.normalize = false;
try populateElementAttributes(frame, element, attribute_iterator);
return element.asNode();
}
fn populateElementAttributes(frame: *Frame, element: *Element, list: anytype) !void {
if (@TypeOf(list) == ?*Element.Attribute.List) {
if (@TypeOf(list) == *Element.Attribute.List or @TypeOf(list) == *const Element.Attribute.List) {
// from cloneNode
var existing = list orelse return;
var attributes = try frame.arena.create(Element.Attribute.List);
attributes.* = .{
.normalize = existing.normalize,
};
var it = existing.iterator();
while (it.next()) |attr| {
try attributes.putNew(attr._name.str(), attr._value.str(), frame);
}
element._attributes = attributes;
return;
return element._attributes.cloneFrom(list, frame);
}
// from the parser
if (@TypeOf(list) == @TypeOf(null) or list.count() == 0) {
if (@TypeOf(list) == @TypeOf(null)) {
return;
}
var attributes = try element.createAttributeList(frame);
const count = list.count();
if (count == 0) {
return;
}
var attributes = &element._attributes;
try attributes.ensureTotalCapacity(count, frame);
while (list.next()) |attr| {
try attributes.putNew(attr.name.local.slice(), attr.value.slice(), frame);
}
@@ -988,7 +979,7 @@ pub fn constructCustomElement(frame: *Frame, new_target: JS.Function) !*Element
}
const tag_name = try String.init(frame.arena, definition.name, .{});
const node = try createHtmlElementT(frame, Element.Html.Custom, .html, @as(?*Element.Attribute.List, null), .{
const node = try createHtmlElementT(frame, Element.Html.Custom, .html, null, .{
._proto = undefined,
._tag_name = tag_name,
._definition = definition,

View File

@@ -539,7 +539,7 @@ fn _addAttrsIfMissingCallback(self: *Parser, node: *Node, attributes: h5e.Attrib
const element = node.as(Element);
const frame = self.frame;
const attr_list = try element.getOrCreateAttributeList(frame);
const attr_list = &element._attributes;
while (attributes.next()) |attr| {
const name = attr.name.local.slice();
const value = attr.value.slice();

View File

@@ -213,11 +213,10 @@ pub fn upgradeCustomElement(custom: *Custom, definition: *CustomElementDefinitio
// Enqueue attributeChangedCallback for existing observed attributes
const element = custom.asElement();
var attr_it = element.attributeIterator();
while (attr_it.next()) |attr| {
const name = attr._name;
for (element.attributeEntries()) |*attr| {
const name = lp.String.wrap(attr.name());
if (definition.isAttributeObserved(name)) {
Custom.enqueueAttributeChangedCallbackOnElement(element, name, null, attr._value, null, frame);
Custom.enqueueAttributeChangedCallbackOnElement(element, name, null, .wrap(attr.value()), null, frame);
}
}

View File

@@ -104,7 +104,7 @@ pub const Namespace = enum(u8) {
_type: Type,
_proto: *Node,
_namespace: Namespace = .html,
_attributes: ?*Attribute.List = null,
_attributes: Attribute.List = .{},
pub const Type = union(enum) {
html: *Html,
@@ -150,15 +150,6 @@ pub fn asConstNode(self: *const Element) *const Node {
return self._proto;
}
pub fn attributesEql(self: *const Element, other: *Element) bool {
if (self._attributes) |attr_list| {
const other_list = other._attributes orelse return false;
return attr_list.eql(other_list);
}
// Make sure no attrs in both sides.
return other._attributes == null;
}
/// TODO: localName and prefix comparison.
pub fn isEqualNode(self: *Element, other: *Element) bool {
const self_tag = self.getTagNameDump();
@@ -169,8 +160,7 @@ pub fn isEqualNode(self: *Element, other: *Element) bool {
return false;
}
// Compare attributes.
if (!self.attributesEql(other)) {
if (self._attributes.eql(&other._attributes) == false) {
return false;
}
@@ -382,22 +372,19 @@ pub fn lookupNamespaceURIForElement(self: *Element, prefix: ?[]const u8, frame:
}
// Step 2: search xmlns attributes
if (self._attributes) |attrs| {
var iter = attrs.iterator();
while (iter.next()) |entry| {
if (prefix == null) {
if (entry._name.eql(comptime .wrap("xmlns"))) {
const val = entry._value.str();
for (self._attributes.entries()) |*entry| {
if (prefix == null) {
if (std.mem.eql(u8, entry.name(), "xmlns")) {
const val = entry.value();
return if (val.len == 0) null else val;
}
} else {
const name = entry.name();
if (std.mem.startsWith(u8, name, "xmlns:")) {
if (std.mem.eql(u8, name["xmlns:".len..], prefix.?)) {
const val = entry.value();
return if (val.len == 0) null else val;
}
} else {
const name = entry._name.str();
if (std.mem.startsWith(u8, name, "xmlns:")) {
if (std.mem.eql(u8, name["xmlns:".len..], prefix.?)) {
const val = entry._value.str();
return if (val.len == 0) null else val;
}
}
}
}
}
@@ -420,13 +407,10 @@ pub fn lookupPrefixForElement(self: *Element, namespace: []const u8, frame: *Fra
}
// Step 2: search xmlns: attribute declarations for one whose value is the namespace
if (self._attributes) |attrs| {
var iter = attrs.iterator();
while (iter.next()) |entry| {
const name = entry._name.str();
if (std.mem.startsWith(u8, name, "xmlns:") and std.mem.eql(u8, entry._value.str(), namespace)) {
return name["xmlns:".len..];
}
for (self._attributes.entries()) |*entry| {
const name = entry.name();
if (std.mem.startsWith(u8, name, "xmlns:") and std.mem.eql(u8, entry.value(), namespace)) {
return name["xmlns:".len..];
}
}
@@ -565,14 +549,13 @@ pub fn setClassName(self: *Element, value: []const u8, frame: *Frame) !void {
return self.setAttributeSafe(comptime .wrap("class"), .wrap(value), frame);
}
pub fn attributeIterator(self: *Element) Attribute.InnerIterator {
const attributes = self._attributes orelse return .{};
return attributes.iterator();
/// DANGER: Invalidated by mutation of the attribute list.
pub fn attributeEntries(self: *const Element) []const Attribute.List.Entry {
return self._attributes.entries();
}
pub fn getAttribute(self: *const Element, name: String, frame: *Frame) !?String {
const attributes = self._attributes orelse return null;
return attributes.get(name, frame);
return self._attributes.get(name, frame);
}
/// For simplicity, the namespace is currently ignored and only the local name is used.
@@ -592,19 +575,16 @@ pub fn getAttributeNS(
}
pub fn getAttributeSafe(self: *const Element, name: String) ?[]const u8 {
const attributes = self._attributes orelse return null;
return attributes.getSafe(name);
return self._attributes.getSafe(name);
}
pub fn hasAttribute(self: *const Element, name: String, frame: *Frame) !bool {
const attributes = self._attributes orelse return false;
const value = try attributes.get(name, frame);
const value = try self._attributes.get(name, frame);
return value != null;
}
pub fn hasAttributeSafe(self: *const Element, name: String) bool {
const attributes = self._attributes orelse return false;
return attributes.hasSafe(name);
return self._attributes.hasSafe(name);
}
// Per HTML "concept-fe-disabled", only listed elements participate in the
@@ -665,19 +645,16 @@ pub fn isDisabled(self: *Element) bool {
}
pub fn hasAttributes(self: *const Element) bool {
const attributes = self._attributes orelse return false;
return attributes.isEmpty() == false;
return self._attributes.isEmpty() == false;
}
pub fn getAttributeNode(self: *Element, name: String, frame: *Frame) !?*Attribute {
const attributes = self._attributes orelse return null;
return attributes.getAttribute(name, self, frame);
return self._attributes.getAttribute(name, self, frame);
}
pub fn setAttribute(self: *Element, name: String, value: String, frame: *Frame) !void {
try Attribute.validateAttributeName(name);
const attributes = try self.getOrCreateAttributeList(frame);
_ = try attributes.put(name, value, self, frame);
_ = try self._attributes.put(name, value, self, frame);
}
pub fn setAttributeNS(
@@ -710,20 +687,7 @@ pub fn setAttributeNS(
}
pub fn setAttributeSafe(self: *Element, name: String, value: String, frame: *Frame) !void {
const attributes = try self.getOrCreateAttributeList(frame);
_ = try attributes.putSafe(name, value, self, frame);
}
pub fn getOrCreateAttributeList(self: *Element, frame: *Frame) !*Attribute.List {
return self._attributes orelse return self.createAttributeList(frame);
}
pub fn createAttributeList(self: *Element, frame: *Frame) !*Attribute.List {
lp.assert(self._attributes == null, "Element.createAttributeList non-null _attributes", .{});
const a = try frame.arena.create(Attribute.List);
a.* = .{ .normalize = self._namespace == .html };
self._attributes = a;
return a;
_ = try self._attributes.putSafe(name, value, self, frame);
}
pub fn getShadowRoot(self: *Element, frame: *Frame) ?*ShadowRoot {
@@ -811,13 +775,11 @@ pub fn setAttributeNode(self: *Element, attr: *Attribute, frame: *Frame) !?*Attr
_ = try el.removeAttributeNode(attr, frame);
}
const attributes = try self.getOrCreateAttributeList(frame);
return attributes.putAttribute(attr, self, frame);
return self._attributes.putAttribute(attr, self, frame);
}
pub fn removeAttribute(self: *Element, name: String, frame: *Frame) !void {
const attributes = self._attributes orelse return;
return attributes.delete(name, self, frame);
return self._attributes.delete(name, self, frame);
}
pub fn toggleAttribute(self: *Element, name: String, force: ?bool, frame: *Frame) !bool {
@@ -847,16 +809,13 @@ pub fn removeAttributeNode(self: *Element, attr: *Attribute, frame: *Frame) !*At
}
pub fn getAttributeNames(self: *const Element, frame: *Frame) ![][]const u8 {
const attributes = self._attributes orelse return &.{};
return attributes.getNames(frame);
return self._attributes.getNames(frame.local_arena);
}
pub fn getAttributeNamedNodeMap(self: *Element, frame: *Frame) !*Attribute.NamedNodeMap {
const gop = try frame._attribute_named_node_map_lookup.getOrPut(frame.arena, @intFromPtr(self));
if (!gop.found_existing) {
const attributes = try self.getOrCreateAttributeList(frame);
const named_node_map = try frame._factory.create(Attribute.NamedNodeMap{ ._list = attributes, ._element = self });
gop.value_ptr.* = named_node_map;
gop.value_ptr.* = try frame._factory.create(Attribute.NamedNodeMap{ ._element = self });
}
return gop.value_ptr.*;
}
@@ -1506,7 +1465,7 @@ pub fn getElementsByClassName(self: *Element, class_name: []const u8, frame: *Fr
pub fn clone(self: *Element, deep: bool, frame: *Frame) !*Node {
const tag_name = self.getTagNameDump();
const node = try Frame.node_factory.createElementNS(frame, self._namespace, tag_name, self._attributes);
const node = try Frame.node_factory.createElementNS(frame, self._namespace, tag_name, &self._attributes);
// Allow element-specific types to copy their runtime state
_ = Element.Build.call(node.as(Element), "cloned", .{ self, node.as(Element), deep, frame }) catch |err| {
@@ -1623,11 +1582,8 @@ pub fn format(self: *Element, writer: *std.Io.Writer) !void {
try writer.writeByte('<');
try writer.writeAll(self.getTagNameDump());
if (self._attributes) |attributes| {
var it = attributes.iterator();
while (it.next()) |attr| {
try writer.print(" {f}", .{attr});
}
for (self._attributes.entries()) |*attr| {
try writer.print(" {f}", .{attr});
}
try writer.writeByte('>');
}

View File

@@ -47,7 +47,7 @@ _value: String,
_element: ?*Element,
pub fn format(self: *const Attribute, writer: *std.Io.Writer) !void {
return formatAttribute(self._name, self._value, writer);
return formatAttribute(self._name.str(), self._value.str(), writer);
}
pub fn getName(self: *const Attribute) String {
@@ -128,41 +128,51 @@ pub const JsApi = struct {
// in our Entry? Because that would require an extra 8 bytes for every single
// attribute in the DOM, and, again, we expect that to almost always be null.
pub const List = struct {
normalize: bool,
/// Length of items in `_list`. Not usize to increase memory usage.
/// Honestly, this is more than enough.
_len: u32 = 0,
_list: std.DoublyLinkedList = .{},
normalize: bool = true,
_len: u16 = 0,
_cap: u16 = 0,
_entries: [*]Entry = @constCast(&empty_entries),
const empty_entries: [0]Entry = .{};
pub const Lookup = std.AutoHashMapUnmanaged(LookupKey, *Attribute);
// for Frame._attribute_lookup which is our identity map for attributes
pub const LookupKey = struct {
list: *const List,
// canonical (see canonicalizeName), so identity is the address
name: [*]const u8,
};
pub fn entries(self: *const List) []const Entry {
return self._entries[0..self._len];
}
pub fn isEmpty(self: *const List) bool {
return self._list.first == null;
return self._len == 0;
}
pub fn length(self: *const List) usize {
return self._len;
}
pub fn get(self: *const List, name: String, frame: *Frame) !?String {
const entry = (try self.getEntry(name, frame)) orelse return null;
return entry._value;
return .wrap(entry.value());
}
pub inline fn length(self: *const List) usize {
return self._len;
}
/// Compares 2 attribute lists for equality.
pub fn eql(self: *List, other: *List) bool {
if (self.length() != other.length()) {
pub fn eql(self: *const List, other: *const List) bool {
if (self._len != other._len) {
return false;
}
var iter = self.iterator();
search: while (iter.next()) |attr| {
// Iterate over all `other` attributes.
var other_iter = other.iterator();
while (other_iter.next()) |other_attr| {
search: for (self.entries()) |*attr| {
for (other.entries()) |*other_attr| {
if (attr.eql(other_attr)) {
continue :search; // Found match.
}
}
// Iterated over all `other` and not match.
// Iterated over all `other` and no match.
return false;
}
return true;
@@ -171,7 +181,7 @@ pub const List = struct {
// meant for internal usage, where the name is known to be properly cased
pub fn getSafe(self: *const List, name: String) ?[]const u8 {
const entry = self.getEntryWithNormalizedName(name) orelse return null;
return entry._value.str();
return entry.value();
}
// meant for internal usage, where the name is known to be properly cased
@@ -181,13 +191,17 @@ pub const List = struct {
pub fn getAttribute(self: *const List, name: String, element: ?*Element, frame: *Frame) !?*Attribute {
const entry = (try self.getEntry(name, frame)) orelse return null;
const gop = try frame._attribute_lookup.getOrPut(frame.arena, @intFromPtr(entry));
if (gop.found_existing) {
return gop.value_ptr.*;
return self.getOrCreateAttribute(entry, element, frame);
}
// Identity map access: a given (list, name) always yields the same
// *Attribute until the attribute is removed.
pub fn getOrCreateAttribute(self: *const List, entry: *const Entry, element: ?*Element, frame: *Frame) !*Attribute {
const gop = try frame._attribute_lookup.getOrPut(frame.arena, .{ .list = self, .name = entry._name_ptr });
if (!gop.found_existing) {
gop.value_ptr.* = try entry.toAttribute(element, frame);
}
const attribute = try entry.toAttribute(element, frame);
gop.value_ptr.* = attribute;
return attribute;
return gop.value_ptr.*;
}
pub fn put(self: *List, name: String, value: String, element: *Element, frame: *Frame) !*Entry {
@@ -200,25 +214,27 @@ pub const List = struct {
return self._put(.{ .entry = entry, .normalized = name }, value, element, frame);
}
// The returned *Entry is only valid until the next mutation of the list.
fn _put(self: *List, result: NormalizeAndEntry, value: String, element: *Element, frame: *Frame) !*Entry {
const is_id = shouldAddToIdMap(result.normalized, element);
var entry: *Entry = undefined;
var old_value: ?String = null;
if (result.entry) |e| {
old_value = try e._value.dupe(frame.call_arena);
// the old bytes are arena-owned or static; they outlive this update
old_value = String.wrap(e.value());
if (is_id) {
frame.removeElementId(element, e._value.str());
frame.removeElementId(element, e.value());
}
e._value = try value.dupe(frame.arena);
e.setValue(try frame.dupeString(value.str()));
entry = e;
} else {
entry = try frame._factory.create(Entry{
._node = .{},
._name = try result.normalized.dupe(frame.arena),
._value = try value.dupe(frame.arena),
});
self._list.append(&entry._node);
try self.ensureUnusedCapacity(1, frame);
entry = &self._entries[self._len];
entry.* = .init(
try canonicalizeName(result.normalized.str(), frame),
try frame.dupeString(value.str()),
);
self._len += 1;
}
@@ -226,23 +242,27 @@ pub const List = struct {
const parent = element.asNode()._parent orelse {
return entry;
};
try frame.addElementId(parent, element, entry._value.str());
try frame.addElementId(parent, element, entry.value());
}
frame.domChanged();
frame.attributeChange(element, result.normalized, entry._value, old_value);
frame.attributeChange(element, result.normalized, .wrap(entry.value()), old_value);
return entry;
}
// Optimized for cloning. We know `name` is already normalized. We know there isn't duplicates.
// We know the Element is detached (and thus, don't need to check for `id`).
pub fn putForCloned(self: *List, name: []const u8, value: []const u8, frame: *Frame) !void {
const entry = try frame._factory.create(Entry{
._node = .{},
._name = try String.init(frame.arena, name, .{}),
._value = try String.init(frame.arena, value, .{}),
});
self._list.append(&entry._node);
self._len += 1;
// Optimized for cloning. We know the names are already normalized and
// unique. We know the Element is detached (and thus, don't need to check
// for `id`).
pub fn cloneFrom(self: *List, other: *const List, frame: *Frame) !void {
try self.ensureTotalCapacity(other._len, frame);
for (other.entries()) |*e| {
const len = self._len;
// re-canonicalize: `other` can belong to a different frame
self._entries[len] = .init(
try canonicalizeName(e.name(), frame),
try frame.dupeString(e.value()),
);
self._len = len + 1;
}
}
// not efficient, won't be called often (if ever!)
@@ -263,7 +283,7 @@ pub const List = struct {
const entry = try self.put(attribute._name, attribute._value, element, frame);
attribute._element = element;
try frame._attribute_lookup.put(frame.arena, @intFromPtr(entry), attribute);
try frame._attribute_lookup.put(frame.arena, .{ .list = self, .name = entry._name_ptr }, attribute);
return existing_attribute;
}
@@ -274,14 +294,18 @@ pub const List = struct {
// the first is kept
return;
}
const len = self._len;
if (len == std.math.maxInt(u16) or name.len > std.math.maxInt(u32) or value.len > std.math.maxInt(u32)) {
// Bad input. Drop rather than fail the parse
return;
}
const entry = try frame._factory.create(Entry{
._node = .{},
._name = try String.init(frame.arena, name, .{}),
._value = try String.init(frame.arena, value, .{}),
});
self._list.append(&entry._node);
self._len += 1;
try self.ensureUnusedCapacity(1, frame);
self._entries[len] = .init(
try canonicalizeName(name, frame),
try frame.dupeString(value),
);
self._len = len + 1;
}
pub fn delete(self: *List, name: String, element: *Element, frame: *Frame) !void {
@@ -289,35 +313,61 @@ pub const List = struct {
const entry = result.entry orelse return;
const is_id = shouldAddToIdMap(result.normalized, element);
const old_value = entry._value;
const old_value = entry.value();
if (is_id) {
frame.removeElementId(element, entry._value.str());
frame.removeElementId(element, old_value);
}
// remove this BEFORE triggering anything, incase that re-enters delete
// or some other callback.
_ = frame._attribute_lookup.remove(@intFromPtr(entry));
self._list.remove(&entry._node);
_ = frame._attribute_lookup.remove(.{ .list = self, .name = entry._name_ptr });
const index = (@intFromPtr(entry) - @intFromPtr(self._entries)) / @sizeOf(Entry);
const list_entries = self._entries[0..self._len];
std.mem.copyForwards(Entry, list_entries[index .. list_entries.len - 1], list_entries[index + 1 ..]);
self._len -= 1;
frame.domChanged();
frame.attributeRemove(element, result.normalized, old_value);
frame._factory.destroy(entry);
frame.attributeRemove(element, result.normalized, .wrap(old_value));
}
pub fn getNames(self: *const List, frame: *Frame) ![][]const u8 {
pub fn getNames(self: *const List, allocator: Allocator) ![][]const u8 {
var arr: std.ArrayList([]const u8) = .empty;
var node = self._list.first;
while (node) |n| {
try arr.append(frame.call_arena, Entry.fromNode(n)._name.str());
node = n.next;
try arr.ensureTotalCapacity(allocator, self._len);
for (self.entries()) |*e| {
arr.appendAssumeCapacity(e.name());
}
return arr.items;
}
pub fn iterator(self: *List) InnerIterator {
return .{ ._node = self._list.first };
pub fn ensureTotalCapacity(self: *List, count: usize, frame: *Frame) !void {
const cap: u16 = @intCast(@min(count, std.math.maxInt(u16)));
return self.setCapacity(cap, frame);
}
fn ensureUnusedCapacity(self: *List, extra: u16, frame: *Frame) !void {
const needed = @as(u32, self._len) + extra;
if (needed <= self._cap) {
return;
}
if (needed > std.math.maxInt(u16)) {
return error.OutOfMemory;
}
// Lists are sized exactly at creation; dynamic additions are rare and
// small, so grow slowly.
return self.setCapacity(@intCast(@max(needed, @as(u32, self._cap) + 2)), frame);
}
fn setCapacity(self: *List, new_cap: u16, frame: *Frame) !void {
if (new_cap <= self._cap) {
return;
}
if (self._cap == 0) {
self._entries = (try frame.arena.alloc(Entry, new_cap)).ptr;
} else {
self._entries = (try frame.arena.realloc(self._entries[0..self._cap], new_cap)).ptr;
}
self._cap = new_cap;
}
fn getEntry(self: *const List, name: String, frame: *Frame) !?*Entry {
@@ -335,52 +385,89 @@ pub const List = struct {
const normalized =
if (self.normalize) try normalizeNameForLookup(name, frame) else name;
// A name that was never canonicalized can't be in any list.
const canonical = lookupCanonicalName(normalized.str(), frame) orelse {
return .{ .normalized = normalized, .entry = null };
};
return .{
.normalized = normalized,
.entry = self.getEntryWithNormalizedName(normalized),
.entry = self.getEntryWithCanonicalName(canonical.ptr),
};
}
fn getEntryWithNormalizedName(self: *const List, name: String) ?*Entry {
var node = self._list.first;
while (node) |n| {
var e = Entry.fromNode(n);
if (e._name.eql(name)) {
// This is one of the wins of the canonical names...we can compare strings
// as a single pointer comparison. By applying the same canonicalization to
// the lists attributes name and to an input, we get the same pointer for
// a given string.
fn getEntryWithCanonicalName(self: *const List, name_ptr: [*]const u8) ?*Entry {
for (self._entries[0..self._len]) |*e| {
if (e._name_ptr == name_ptr) {
return e;
}
node = n.next;
}
return null;
}
pub const Entry = struct {
_name: String,
_value: String,
_node: std.DoublyLinkedList.Node,
fn getEntryWithNormalizedName(self: *const List, name: String) ?*Entry {
const name_str = name.str();
for (self._entries[0..self._len]) |*e| {
if (std.mem.eql(u8, e.name(), name_str)) {
return e;
}
}
return null;
}
fn fromNode(n: *std.DoublyLinkedList.Node) *Entry {
return @alignCast(@fieldParentPtr("_node", n));
// Two []const u8 would be 32 bytes. Packed like this, it's 24.
pub const Entry = struct {
_name_ptr: [*]const u8,
_value_ptr: [*]const u8,
_name_len: u32,
_value_len: u32,
fn init(canonical_name: []const u8, value_: []const u8) Entry {
return .{
._name_ptr = canonical_name.ptr,
._value_ptr = value_.ptr,
._name_len = @intCast(canonical_name.len),
._value_len = @intCast(value_.len),
};
}
/// Returns true if 2 entries are equal.
/// This doesn't compare `_node` fields.
pub fn name(self: *const Entry) []const u8 {
return self._name_ptr[0..self._name_len];
}
pub fn value(self: *const Entry) []const u8 {
return self._value_ptr[0..self._value_len];
}
fn setValue(self: *Entry, value_: []const u8) void {
self._value_ptr = value_.ptr;
self._value_len = @intCast(value_.len);
}
/// Returns true if 2 entries are equal. Names can't be compared by
/// pointer alone: the entries can belong to different frames.
pub fn eql(self: *const Entry, other: *const Entry) bool {
return self._name.eql(other._name) and self._value.eql(other._value);
if (self._name_ptr != other._name_ptr and !std.mem.eql(u8, self.name(), other.name())) {
return false;
}
return std.mem.eql(u8, self.value(), other.value());
}
pub fn format(self: *const Entry, writer: *std.Io.Writer) !void {
return formatAttribute(self._name, self._value, writer);
return formatAttribute(self.name(), self.value(), writer);
}
pub fn toAttribute(self: *const Entry, element: ?*Element, frame: *Frame) !*Attribute {
return frame._factory.node(Attribute{
._proto = undefined,
._element = element,
// Cannot directly reference self._name.str() and self._value.str()
// This attribute can outlive the list entry (the node can be
// removed from the element's attribute, but still exist in the DOM)
._name = try self._name.dupe(frame.arena),
._value = try self._value.dupe(frame.arena),
// The entry's bytes outlive the entry itself, so the
// Attribute can wrap them without duping.
._name = .wrap(self.name()),
._value = .wrap(self.value()),
});
}
};
@@ -426,6 +513,35 @@ pub fn validateAttributeName(name: String) !void {
}
}
// Every stored entry name either comes from the static String.intern or from
// the frame._attribute_names. This doesn't just avoid extra dupes/allocations,
// it gives us comparable pointer.
// For the lifetime of a frame:
// canonicalizeName("attrx").ptr == canonicalizeName("attrx").ptr
fn canonicalizeName(name: []const u8, frame: *Frame) ![]const u8 {
if (String.intern(name)) |static| {
return static;
}
const gop = try frame._attribute_names.getOrPut(frame.arena, name);
if (!gop.found_existing) {
gop.key_ptr.* = try frame.arena.dupe(u8, name);
}
return gop.key_ptr.*;
}
// Let's say you want to find an attribute name "attrx". We could iterate an
// attribute list to do the string comparison. OR, we could run "attrx" through
// the same canonicalization as we applied to the attribute's names. If we don't
// find a canonical value, then it's then this attribute was never canonicalized
// before (and thus, we can shortcircuit a search). If we DO canonicalize it
// the we get do a pointer comparison rather than a full string comparison.
fn lookupCanonicalName(name: []const u8, frame: *const Frame) ?[]const u8 {
if (String.intern(name)) |static| {
return static;
}
return frame._attribute_names.getKey(name);
}
fn normalizeNameForLookup(name: String, frame: *Frame) !String {
if (!needsLowerCasing(name.str())) {
return name;
@@ -433,7 +549,7 @@ fn normalizeNameForLookup(name: String, frame: *Frame) !String {
const normalized = if (name.len < frame.buf.len)
std.ascii.lowerString(&frame.buf, name.str())
else
try std.ascii.allocLowerString(frame.call_arena, name.str());
try std.ascii.allocLowerString(frame.local_arena, name.str());
return .wrap(normalized);
}
@@ -467,50 +583,40 @@ fn needsLowerCasing(name: []const u8) bool {
}
pub const NamedNodeMap = struct {
_list: *List,
// Whenever the NamedNodeMap creates an Attribute, it needs to provide the
// "ownerElement".
// "ownerElement". The attribute list is the element's.
_element: *Element,
fn list(self: *const NamedNodeMap) *List {
return &self._element._attributes;
}
pub fn length(self: *const NamedNodeMap) u32 {
return @intCast(self._list._list.len());
return self.list()._len;
}
pub fn getAtIndex(self: *const NamedNodeMap, index: usize, frame: *Frame) !?*Attribute {
var i: usize = 0;
var node = self._list._list.first;
while (node) |n| {
if (i == index) {
var entry = List.Entry.fromNode(n);
const gop = try frame._attribute_lookup.getOrPut(frame.arena, @intFromPtr(entry));
if (gop.found_existing) {
return gop.value_ptr.*;
}
const attribute = try entry.toAttribute(self._element, frame);
gop.value_ptr.* = attribute;
return attribute;
}
node = n.next;
i += 1;
const l = self.list();
if (index >= l._len) {
return null;
}
return null;
return l.getOrCreateAttribute(&l._entries[index], self._element, frame);
}
pub fn getByName(self: *const NamedNodeMap, name: String, frame: *Frame) !?*Attribute {
return self._list.getAttribute(name, self._element, frame);
return self.list().getAttribute(name, self._element, frame);
}
pub fn set(self: *const NamedNodeMap, attribute: *Attribute, frame: *Frame) !?*Attribute {
attribute._element = null; // just a requirement of list.putAttribute, it'll re-set it.
return self._list.putAttribute(attribute, self._element, frame);
return self.list().putAttribute(attribute, self._element, frame);
}
pub fn removeByName(self: *const NamedNodeMap, name: String, frame: *Frame) !?*Attribute {
// this 2-step process (get then delete) isn't efficient. But we don't
// expect this to be called often, and this lets us keep delete straightforward.
const attr = (try self.getByName(name, frame)) orelse return null;
try self._list.delete(name, self._element, frame);
try self.list().delete(name, self._element, frame);
return attr;
}
@@ -559,26 +665,12 @@ pub const NamedNodeMap = struct {
};
};
// Not meant to be exposed. The "public" iterator is a NamedNodeMap, and it's a
// bit awkward. Having this for more straightforward key=>value is useful for
// the few internal places we need to iterate through the attributes (e.g. dump)
pub const InnerIterator = struct {
_node: ?*std.DoublyLinkedList.Node = null,
pub fn next(self: *InnerIterator) ?*List.Entry {
const node = self._node orelse return null;
self._node = node.next;
return List.Entry.fromNode(node);
}
};
fn formatAttribute(name: String, value_: String, writer: *std.Io.Writer) !void {
try writer.writeAll(name.str());
fn formatAttribute(name: []const u8, value: []const u8, writer: *std.Io.Writer) !void {
try writer.writeAll(name);
// Boolean attributes with empty values are serialized without a value
const value = value_.str();
if (value.len == 0 and boolean_attributes_lookup.has(name.str())) {
if (value.len == 0 and boolean_attributes_lookup.has(name)) {
return;
}

View File

@@ -35,12 +35,11 @@ pub fn constructor(maybe_url: ?String, frame: *Frame) !*Media {
const node = try Frame.node_factory.createElementNS(frame, .html, "audio", null);
const el = node.as(Element);
const list = try el.getOrCreateAttributeList(frame);
// Always set to "auto" initially.
_ = try list.putSafe(comptime .wrap("preload"), comptime .wrap("auto"), el, frame);
try el.setAttributeSafe(comptime .wrap("preload"), comptime .wrap("auto"), frame);
// Set URL if provided.
if (maybe_url) |url| {
_ = try list.putSafe(comptime .wrap("src"), url, el, frame);
try el.setAttributeSafe(comptime .wrap("src"), url, frame);
}
return node.as(Media);

View File

@@ -529,17 +529,12 @@ fn appendPreceding(self: *Evaluator, start: *Node, out: *std.ArrayList(*Node)) E
fn appendAttributes(self: *Evaluator, node: *Node, out: *std.ArrayList(*Node)) Error!void {
const el = node.is(Element) orelse return;
var it = el.attributeIterator();
while (it.next()) |entry| {
// Memoize via frame._attribute_lookup so repeated XPath queries
for (el.attributeEntries()) |*entry| {
// Memoized via frame._attribute_lookup so repeated XPath queries
// (Capybara/Selenium polling) reuse the same *Attribute instead
// of leaking fresh ones into page-lifetime storage on every call.
// Same pattern as Attribute.List.getAttribute / NamedNodeMap.getAtIndex.
const gop = try self.frame._attribute_lookup.getOrPut(self.frame.arena, @intFromPtr(entry));
if (!gop.found_existing) {
gop.value_ptr.* = try entry.toAttribute(el, self.frame);
}
try out.append(self.arena, gop.value_ptr.*._proto);
const attribute = try el._attributes.getOrCreateAttribute(entry, el, self.frame);
try out.append(self.arena, attribute._proto);
}
}

View File

@@ -306,10 +306,9 @@ pub const Writer = struct {
if (element.hasAttributes()) {
try w.objectField("attributes");
try w.beginArray();
var it = element.attributeIterator();
while (it.next()) |attr| {
try w.write(attr._name.str());
try w.write(attr._value.str());
for (element.attributeEntries()) |*attr| {
try w.write(attr.name());
try w.write(attr.value());
}
try w.endArray();
}

View File

@@ -212,6 +212,7 @@ pub const String = packed struct {
// This can be used outside of the small string optimization
pub fn intern(input: []const u8) ?[]const u8 {
switch (input.len) {
0 => return "",
1 => switch (input[0]) {
'\n' => return "\n",
'\r' => return "\r",
@@ -248,36 +249,102 @@ pub const String = packed struct {
3 => switch (@as(u24, @bitCast(input[0..3].*))) {
asUint(" ") => return " ",
asUint("") => return "",
asUint("src") => return "src",
asUint("rel") => return "rel",
asUint("alt") => return "alt",
asUint("dir") => return "dir",
asUint("for") => return "for",
else => {},
},
4 => switch (@as(u32, @bitCast(input[0..4].*))) {
asUint(" ") => return " ",
asUint(" to ") => return " to ",
asUint("type") => return "type",
asUint("name") => return "name",
asUint("href") => return "href",
asUint("role") => return "role",
asUint("lang") => return "lang",
asUint("true") => return "true",
asUint("text") => return "text",
asUint("none") => return "none",
asUint("lazy") => return "lazy",
else => {},
},
5 => switch (@as(u40, @bitCast(input[0..5].*))) {
asUint(" ") => return " ",
asUint(" ") => return " ",
asUint("class") => return "class",
asUint("style") => return "style",
asUint("title") => return "title",
asUint("value") => return "value",
asUint("width") => return "width",
asUint("media") => return "media",
asUint("async") => return "async",
asUint("defer") => return "defer",
asUint("false") => return "false",
else => {},
},
6 => switch (@as(u48, @bitCast(input[0..6].*))) {
asUint(" ") => return " ",
asUint("height") => return "height",
asUint("action") => return "action",
asUint("method") => return "method",
asUint("target") => return "target",
asUint("srcset") => return "srcset",
asUint("hidden") => return "hidden",
asUint("button") => return "button",
asUint("submit") => return "submit",
asUint("_blank") => return "_blank",
else => {},
},
7 => switch (@as(u56, @bitCast(input[0..7].*))) {
asUint(" ") => return " ",
asUint("content") => return "content",
asUint("charset") => return "charset",
asUint("checked") => return "checked",
asUint("loading") => return "loading",
else => {},
},
8 => switch (@as(u64, @bitCast(input[0..8].*))) {
asUint(" ") => return " ",
asUint("disabled") => return "disabled",
asUint("multiple") => return "multiple",
asUint("readonly") => return "readonly",
asUint("required") => return "required",
asUint("selected") => return "selected",
asUint("tabindex") => return "tabindex",
asUint("checkbox") => return "checkbox",
asUint("noopener") => return "noopener",
asUint("download") => return "download",
else => {},
},
9 => switch (@as(u72, @bitCast(input[0..9].*))) {
asUint(" ") => return " ",
asUint("autofocus") => return "autofocus",
asUint("maxlength") => return "maxlength",
asUint("draggable") => return "draggable",
asUint("anonymous") => return "anonymous",
else => {},
},
10 => switch (@as(u80, @bitCast(input[0..10].*))) {
asUint(" ") => return " ",
asUint("aria-label") => return "aria-label",
asUint("novalidate") => return "novalidate",
asUint("spellcheck") => return "spellcheck",
asUint("stylesheet") => return "stylesheet",
asUint("noreferrer") => return "noreferrer",
else => {},
},
11 => switch (@as(u88, @bitCast(input[0..11].*))) {
asUint("aria-hidden") => return "aria-hidden",
asUint("crossorigin") => return "crossorigin",
asUint("placeholder") => return "placeholder",
else => {},
},
12 => switch (@as(u96, @bitCast(input[0..12].*))) {
asUint("autocomplete") => return "autocomplete",
asUint("aria-current") => return "aria-current",
asUint("presentation") => return "presentation",
else => {},
},
13 => switch (@as(u104, @bitCast(input[0..13].*))) {
@@ -285,6 +352,9 @@ pub const String = packed struct {
asUint("padding-right") => return "padding-right",
asUint("margin-bottom") => return "margin-bottom",
asUint("space-between") => return "space-between",
asUint("aria-expanded") => return "aria-expanded",
asUint("aria-disabled") => return "aria-disabled",
asUint("aria-controls") => return "aria-controls",
else => {},
},
14 => switch (@as(u112, @bitCast(input[0..14].*))) {
@@ -292,15 +362,18 @@ pub const String = packed struct {
asUint("text-transform") => return "text-transform",
asUint("letter-spacing") => return "letter-spacing",
asUint("vertical-align") => return "vertical-align",
asUint("referrerpolicy") => return "referrerpolicy",
else => {},
},
15 => switch (@as(u120, @bitCast(input[0..15].*))) {
asUint("text-decoration") => return "text-decoration",
asUint("justify-content") => return "justify-content",
asUint("aria-labelledby") => return "aria-labelledby",
else => {},
},
16 => switch (@as(u128, @bitCast(input[0..16].*))) {
asUint("background-color") => return "background-color",
asUint("aria-describedby") => return "aria-describedby",
else => {},
},
else => {},