mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-07-30 17:25:58 -04:00
mem: optimize node child list
Reworks how a Node's child list is modeled. This used to be a optional pointer to the child list, along with a Node representing this node's linkedlist node in the parent's child list. The new model is the same 24 bytes, but it's fully embedded: nodes with children don't require another 16 byte allocation. Generally speaking, this is more linked-list-ish, so it does simplify the code. `linkToNode` is removed BUT, the first child's `_prev` points to the last child, which is a bit unusual but necessary so that `lastChild` remains O(1).
This commit is contained in:
@@ -2458,20 +2458,13 @@ pub fn removeNode(self: *Frame, parent: *Node, child: *Node, opts: RemoveNodeOpt
|
||||
else
|
||||
null;
|
||||
|
||||
const children = parent._children.?;
|
||||
children.remove(&child._child_link);
|
||||
if (children.first == null) {
|
||||
// last child removed; drop the list so a childless node holds no allocation
|
||||
parent._children = null;
|
||||
self._factory.destroy(children);
|
||||
}
|
||||
parent.unlink(child);
|
||||
// grab this before we null the parent
|
||||
const was_connected = child.isConnected();
|
||||
// Capture the ID map before disconnecting, so we can remove IDs from the correct document
|
||||
const id_maps = if (was_connected) self.getElementIdMap(child) else null;
|
||||
|
||||
child._parent = null;
|
||||
child._child_link = .{};
|
||||
|
||||
// Update live ranges for removal (DOM spec remove steps 4-7)
|
||||
if (child_index_for_ranges) |idx| {
|
||||
@@ -2631,23 +2624,17 @@ pub fn _insertNodeRelative(self: *Frame, comptime from_parser: bool, parent: *No
|
||||
|
||||
lp.assert(child._parent == null, "Frame.insertNodeRelative parent", .{});
|
||||
|
||||
const children = parent._children orelse blk: {
|
||||
const list = try self._factory.create(std.DoublyLinkedList{});
|
||||
parent._children = list;
|
||||
break :blk list;
|
||||
};
|
||||
|
||||
switch (relative) {
|
||||
.append => children.append(&child._child_link),
|
||||
.append => parent.linkLast(child),
|
||||
.after => |ref_node| {
|
||||
// caller should have made sure this was the case
|
||||
lp.assert(ref_node._parent.? == parent, "Frame.insertNodeRelative after", .{ .url = self.url });
|
||||
children.insertAfter(&ref_node._child_link, &child._child_link);
|
||||
parent.linkAfter(ref_node, child);
|
||||
},
|
||||
.before => |ref_node| {
|
||||
// caller should have made sure this was the case
|
||||
lp.assert(ref_node._parent.? == parent, "Frame.insertNodeRelative before", .{ .url = self.url });
|
||||
children.insertBefore(&ref_node._child_link, &child._child_link);
|
||||
parent.linkBefore(ref_node, child);
|
||||
},
|
||||
}
|
||||
child._parent = parent;
|
||||
|
||||
@@ -80,12 +80,15 @@ fn htmlAsChildrenInner(frame: *Frame, node: *Node, html: []const u8, opts: Fragm
|
||||
// Because of custom element callbacks, the structure might not be what
|
||||
// we expect, and nodes might be altogether removed. We deal with this in a
|
||||
// few different places, but always the same way: leave it as-is.
|
||||
const children = node._children orelse return;
|
||||
const first = Node.linkToNode(children.first.?);
|
||||
const first = node.firstChild() orelse return;
|
||||
if (first.is(Element.Html.Html) == null) {
|
||||
return;
|
||||
}
|
||||
node._children = first._children;
|
||||
node._first_child = first._first_child;
|
||||
first._first_child = null;
|
||||
first._parent = null;
|
||||
first._prev = null;
|
||||
first._next = null;
|
||||
|
||||
// No mutation records for the unwrapped children either; see the comment
|
||||
// about fragment parses in _insertNodeRelative.
|
||||
|
||||
@@ -215,8 +215,8 @@ fn filterNode(self: *const DOMNodeIterator, node: *Node, frame: *Frame) !i32 {
|
||||
|
||||
fn getNextInTree(self: *const DOMNodeIterator, node: *Node) ?*Node {
|
||||
// Depth-first traversal within the root subtree
|
||||
if (node._children) |children| {
|
||||
return Node.linkToNode(children.first.?);
|
||||
if (node.firstChild()) |first| {
|
||||
return first;
|
||||
}
|
||||
|
||||
var current = node;
|
||||
|
||||
@@ -1061,8 +1061,7 @@ fn writeInternal(self: *Document, text: []const []const u8, append_newline: bool
|
||||
|
||||
// Extract children from wrapper HTML element (html5ever wraps fragments)
|
||||
// https://github.com/servo/html5ever/issues/583
|
||||
const children = fragment_node._children orelse return;
|
||||
const first = Node.linkToNode(children.first.?);
|
||||
const first = fragment_node.firstChild() orelse return;
|
||||
|
||||
// Collect all children to insert (to avoid iterator invalidation)
|
||||
var children_to_insert: std.ArrayList(*Node) = .empty;
|
||||
@@ -1472,7 +1471,7 @@ pub fn injectBlank(self: *Document, frame: *Frame) error{InjectBlankError}!void
|
||||
fn _injectBlank(self: *Document, frame: *Frame) !void {
|
||||
if (comptime IS_DEBUG) {
|
||||
// should only be called on an empty document
|
||||
std.debug.assert(self.asNode()._children == null);
|
||||
std.debug.assert(self.asNode()._first_child == null);
|
||||
}
|
||||
|
||||
const html = try Frame.node_factory.createElementNS(frame, .html, "html", null);
|
||||
|
||||
@@ -37,7 +37,6 @@ pub const ShadowRoot = @import("ShadowRoot.zig");
|
||||
|
||||
const String = lp.String;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const LinkedList = std.DoublyLinkedList;
|
||||
|
||||
pub const AssignedSlotLookup = std.AutoHashMapUnmanaged(*Node, *Element.Html.Slot);
|
||||
|
||||
@@ -46,10 +45,11 @@ const Node = @This();
|
||||
_type: Type,
|
||||
_proto: *EventTarget,
|
||||
_parent: ?*Node = null,
|
||||
// A node with no children leaves this null (no allocation). Otherwise it
|
||||
// points to a heap-allocated intrusive list of the node's `_child_link`s.
|
||||
_children: ?*LinkedList = null,
|
||||
_child_link: LinkedList.Node = .{},
|
||||
// The first child's `_prev` points at the LAST child (keeping lastChild
|
||||
// O(1)); the last child's `_next` is null. A detached node has null links.
|
||||
_first_child: ?*Node = null,
|
||||
_next: ?*Node = null,
|
||||
_prev: ?*Node = null,
|
||||
|
||||
// Lookup for nodes that have a different owner document than frame.document
|
||||
pub const OwnerDocumentLookup = std.AutoHashMapUnmanaged(*Node, *Document);
|
||||
@@ -178,21 +178,25 @@ fn adjacentParent(self: *Node, variant: AdjacentVariant) !*Node {
|
||||
}
|
||||
|
||||
pub fn firstChild(self: *const Node) ?*Node {
|
||||
const children = self._children orelse return null;
|
||||
return linkToNodeOrNull(children.first);
|
||||
return self._first_child;
|
||||
}
|
||||
|
||||
pub fn lastChild(self: *const Node) ?*Node {
|
||||
const children = self._children orelse return null;
|
||||
return linkToNodeOrNull(children.last);
|
||||
const first = self._first_child orelse return null;
|
||||
return first._prev;
|
||||
}
|
||||
|
||||
pub fn nextSibling(self: *const Node) ?*Node {
|
||||
return linkToNodeOrNull(self._child_link.next);
|
||||
return self._next;
|
||||
}
|
||||
|
||||
pub fn previousSibling(self: *const Node) ?*Node {
|
||||
return linkToNodeOrNull(self._child_link.prev);
|
||||
const parent = self._parent orelse return null;
|
||||
if (parent._first_child == self) {
|
||||
// our _prev points at the parent's last child
|
||||
return null;
|
||||
}
|
||||
return self._prev;
|
||||
}
|
||||
|
||||
pub fn parentNode(self: *const Node) ?*Node {
|
||||
@@ -1074,13 +1078,7 @@ pub fn format(self: *Node, writer: *std.Io.Writer) !void {
|
||||
// Returns an iterator the can be used to iterate through the node's children
|
||||
// For internal use.
|
||||
pub fn childrenIterator(self: *Node) NodeIterator {
|
||||
const children = self._children orelse {
|
||||
return .{ .node = null };
|
||||
};
|
||||
|
||||
return .{
|
||||
.node = linkToNodeOrNull(children.first),
|
||||
};
|
||||
return .{ .node = self._first_child };
|
||||
}
|
||||
|
||||
pub fn getChildrenCount(self: *Node) usize {
|
||||
@@ -1322,13 +1320,11 @@ pub fn compareDocumentPosition(self: *Node, other: *Node) u16 {
|
||||
return DISCONNECTED;
|
||||
}
|
||||
|
||||
// faster to compare the linked list node links directly
|
||||
fn isNodeBefore(node1: *const Node, node2: *const Node) bool {
|
||||
var current = node1._child_link.next;
|
||||
const target = &node2._child_link;
|
||||
while (current) |link| {
|
||||
if (link == target) return true;
|
||||
current = link.next;
|
||||
var current = node1._next;
|
||||
while (current) |n| {
|
||||
if (n == node2) return true;
|
||||
current = n._next;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -1587,18 +1583,70 @@ const NodeIterator = struct {
|
||||
node: ?*Node,
|
||||
pub fn next(self: *NodeIterator) ?*Node {
|
||||
const node = self.node orelse return null;
|
||||
self.node = linkToNodeOrNull(node._child_link.next);
|
||||
self.node = node._next;
|
||||
return node;
|
||||
}
|
||||
};
|
||||
|
||||
// Turns a linked list node into a Node
|
||||
pub fn linkToNode(n: *LinkedList.Node) *Node {
|
||||
return @fieldParentPtr("_child_link", n);
|
||||
// Low-level sibling-list splicing, used by Frame's insert/remove. These only
|
||||
// maintain the link invariants (first child's `_prev` is the last child, last
|
||||
// child's `_next` is null); the caller handles `_parent` and all DOM
|
||||
// bookkeeping.
|
||||
pub fn linkLast(self: *Node, child: *Node) void {
|
||||
child._next = null;
|
||||
if (self._first_child) |first| {
|
||||
const last = first._prev.?;
|
||||
last._next = child;
|
||||
child._prev = last;
|
||||
first._prev = child;
|
||||
} else {
|
||||
self._first_child = child;
|
||||
child._prev = child;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn linkToNodeOrNull(n_: ?*LinkedList.Node) ?*Node {
|
||||
return if (n_) |n| linkToNode(n) else null;
|
||||
pub fn linkBefore(self: *Node, ref: *Node, child: *Node) void {
|
||||
child._next = ref;
|
||||
child._prev = ref._prev;
|
||||
if (self._first_child == ref) {
|
||||
self._first_child = child;
|
||||
} else {
|
||||
ref._prev.?._next = child;
|
||||
}
|
||||
ref._prev = child;
|
||||
}
|
||||
|
||||
pub fn linkAfter(self: *Node, ref: *Node, child: *Node) void {
|
||||
child._prev = ref;
|
||||
child._next = ref._next;
|
||||
if (ref._next) |next| {
|
||||
next._prev = child;
|
||||
} else {
|
||||
// ref was the last child
|
||||
self._first_child.?._prev = child;
|
||||
}
|
||||
ref._next = child;
|
||||
}
|
||||
|
||||
pub fn unlink(self: *Node, child: *Node) void {
|
||||
const first = self._first_child.?;
|
||||
if (child == first) {
|
||||
if (child._next) |next| {
|
||||
next._prev = child._prev;
|
||||
self._first_child = next;
|
||||
} else {
|
||||
self._first_child = null;
|
||||
}
|
||||
} else {
|
||||
child._prev.?._next = child._next;
|
||||
if (child._next) |next| {
|
||||
next._prev = child._prev;
|
||||
} else {
|
||||
first._prev = child._prev;
|
||||
}
|
||||
}
|
||||
child._next = null;
|
||||
child._prev = null;
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
|
||||
@@ -32,7 +32,7 @@ const ChildNodes = @This();
|
||||
_arena: std.mem.Allocator,
|
||||
_last_index: usize,
|
||||
_last_length: ?u32,
|
||||
_last_node: ?*std.DoublyLinkedList.Node,
|
||||
_last_node: ?*Node,
|
||||
_cached_version: usize,
|
||||
_node: *Node,
|
||||
|
||||
@@ -66,10 +66,12 @@ pub fn length(self: *ChildNodes, frame: *const Frame) !u32 {
|
||||
return cached_length;
|
||||
}
|
||||
}
|
||||
const children = self._node._children orelse return 0;
|
||||
|
||||
// O(N)
|
||||
const len: u32 = @intCast(children.len());
|
||||
var len: u32 = 0;
|
||||
var it = self._node.childrenIterator();
|
||||
while (it.next()) |_| {
|
||||
len += 1;
|
||||
}
|
||||
self._last_length = len;
|
||||
return len;
|
||||
}
|
||||
@@ -78,7 +80,7 @@ pub fn getAtIndex(self: *ChildNodes, index: usize, frame: *const Frame) !?*Node
|
||||
_ = self.versionCheck(frame);
|
||||
|
||||
var current = self._last_index;
|
||||
var node: ?*std.DoublyLinkedList.Node = null;
|
||||
var node: ?*Node = null;
|
||||
if (index < current or self._last_node == null) {
|
||||
current = 0;
|
||||
node = self.first() orelse return null;
|
||||
@@ -90,17 +92,17 @@ pub fn getAtIndex(self: *ChildNodes, index: usize, frame: *const Frame) !?*Node
|
||||
while (node) |n| {
|
||||
if (index == current) {
|
||||
self._last_node = n;
|
||||
return Node.linkToNode(n);
|
||||
return n;
|
||||
}
|
||||
current += 1;
|
||||
node = n.next;
|
||||
node = n.nextSibling();
|
||||
}
|
||||
self._last_node = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn first(self: *const ChildNodes) ?*std.DoublyLinkedList.Node {
|
||||
return (self._node._children orelse return null).first;
|
||||
pub fn first(self: *const ChildNodes) ?*Node {
|
||||
return self._node.firstChild();
|
||||
}
|
||||
|
||||
pub fn keys(self: *ChildNodes, frame: *Frame) !*KeyIterator {
|
||||
|
||||
Reference in New Issue
Block a user