From 923a270d9c374b997c76eda7e3f07180e6ea7697 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Sat, 19 Sep 2026 07:16:59 +0800 Subject: [PATCH] mem: Make dump iterative Inspired by https://github.com/lightpanda-io/browser/pull/3500 this switches the dump from recursive to iterative. Huge DOMs no longer stack overflow. --- src/browser/dump.zig | 550 +++++++++++++++++++---------- src/browser/tests/dump_frames.html | 1 + 2 files changed, 369 insertions(+), 182 deletions(-) create mode 100644 src/browser/tests/dump_frames.html diff --git a/src/browser/dump.zig b/src/browser/dump.zig index b487f5dc9..30eca503e 100644 --- a/src/browser/dump.zig +++ b/src/browser/dump.zig @@ -101,19 +101,28 @@ fn rootUncapped(doc: *Node.Document, opts: Opts, writer: *std.Io.Writer, frame: } } - return _deep(doc.asNode(), opts, false, writer, frame); + return _deep(doc.asNode(), opts, writer, frame); } -pub fn deep(node: *Node, opts: Opts, writer: *std.Io.Writer, frame: *Frame) error{WriteFailed}!void { - if (opts.max_bytes == null) return _deep(node, opts, false, writer, frame); +pub fn deep(node: *Node, opts: Opts, writer: *std.Io.Writer, frame: *Frame) Error!void { + if (opts.max_bytes == null) { + return _deep(node, opts, writer, frame); + } var lw: LimitedWriter = .init(writer, opts.max_bytes); - _deep(node, opts, false, &lw.writer, frame) catch |err| { + _deep(node, opts, &lw.writer, frame) catch |err| { if (!lw.truncated) return err; try writer.writeAll(LimitedWriter.truncation_marker); }; } +fn _deep(node: *Node, opts: Opts, writer: *std.Io.Writer, frame: *Frame) Error!void { + var walk: Walk = .init(opts, writer, frame); + defer walk.deinit(); + try walk.visit(node, false); + return walk.run(); +} + pub fn render(state: RenderTree.State, opts: Opts, writer: *std.Io.Writer, frame: *Frame) !void { var o = opts; o.strip = state.strip; @@ -122,162 +131,265 @@ pub fn render(state: RenderTree.State, opts: Opts, writer: *std.Io.Writer, frame return deep(state.root, o, writer, frame); } -fn _deep(node: *Node, opts: Opts, comptime force_slot: bool, writer: *std.Io.Writer, frame: *Frame) error{WriteFailed}!void { - switch (node._type) { - .cdata => { - if (opts.pruned) |set| { - if (set.contains(node)) return; - } - const cd = node.subtype(Node.CData); - if (node.is(Node.CData.Comment)) |_| { - try writer.writeAll(""); - } else if (node.is(Node.CData.ProcessingInstruction)) |pi| { - try writer.writeAll(""); - } else { - if (shouldEscapeText(node._parent)) { - try writeEscapedText(cd.getData().str(), writer); - } else { - try writer.writeAll(cd.getData().str()); - } - } - }, - .element => { - const el = node.subtype(Node.Element); - if (shouldStripElement(el, opts.strip, opts.pruned, frame)) { - return; - } +const Error = error{ WriteFailed, OutOfMemory }; - // When opts.shadow == .rendered, we normally skip any element with - // a slot attribute. Only the "active" element will get rendered into - // the . However, the `deep` function is itself used - // to render that "active" content, so when we're trying to render - // it, we don't want to skip it. - if ((comptime force_slot == false) and opts.shadow == .rendered) { - if (el.getSlot()) |_| { - // Skip - will be rendered by the Slot if it's the active container - return; - } - } +// Very large trees can stackoverflow, hence we switch to an iterative walk +const Walk = struct { + opts: Opts, + writer: *std.Io.Writer, + frame: *Frame, + stack: std.ArrayList(Open) = .empty, - try el.format(writer); + // Content still to write, followed by the end tag + const Open = struct { + end_tag: ?[]const u8, + rest: Rest, + }; - if (opts.shadow == .rendered) { - if (el.is(Slot)) |slot| { - try dumpSlotContent(slot, opts, writer, frame); - return writer.writeAll(""); - } - } - switch (opts.shadow) { - .skip => {}, - .complete, .rendered => { - if (el.hostedShadowRoot(frame)) |shadow| { - try children(shadow.asNode(), opts, writer, frame); - // In rendered mode, light DOM is only shown through slots, not directly - if (opts.shadow == .rendered) { - // Skip rendering light DOM children - if (!isVoidElement(el)) { - try writer.writeAll("'); - } - return; - } - } - }, - .declarative => |declarative| { - if (el.hostedShadowRoot(frame)) |shadow| { - if (shouldSerializeShadow(shadow, declarative)) { - try writeDeclarativeShadow(shadow, opts, writer, frame); - } - } - }, - } + const Rest = union(enum) { + siblings: ?*Node, + // Nodes assigned to a slot. Rendered despite their slot attribute. + assigned: []const *Node, + // An iframe's content document (opts.with_frames) + document: ?*Node.Document, + }; - if (opts.with_frames and el.is(IFrame) != null) { - const iframe = el.as(IFrame); - if (iframe.getContentDocument()) |doc| { - // A frame's document should always ahave a frame, but - // I'm not willing to crash a release build on that assertion. - if (comptime lp.IS_DEBUG) { - std.debug.assert(doc._frame != null); - } - if (doc._frame) |f| { - try writer.writeByte('\n'); - root(doc, opts, writer, f) catch return error.WriteFailed; - try writer.writeByte('\n'); - } - } - } else { - try children(node, opts, writer, frame); - } - - if (!isVoidElement(el)) { - try writer.writeAll("'); - } - }, - .document => try children(node, opts, writer, frame), - .document_type => { - const dt = node.subtype(Node.DocumentType); - try writer.writeAll("\n"); - }, - .document_fragment => try children(node, opts, writer, frame), - .attribute => { - // Not called normally, but can be called via XMLSerializer.serializeToString - // in which case it should return an empty string - try writer.writeAll(""); - }, + fn init(opts: Opts, writer: *std.Io.Writer, frame: *Frame) Walk { + return .{ .opts = opts, .writer = writer, .frame = frame }; } -} -// Element.getHTML / ShadowRoot.getHTML -pub fn getHTML(node: *Node, declarative: Opts.Shadow.Declarative, writer: *std.Io.Writer, frame: *Frame) !void { - const opts = Opts{ .shadow = .{ .declarative = declarative } }; - if (node.is(Node.Element)) |el| { - if (el.hostedShadowRoot(frame)) |shadow| { - if (shouldSerializeShadow(shadow, declarative)) { - // if the element's shadowroot tree is rendered before its - // children (assume the opts say that it should serialize the - // shadowroot at all (i.e. shouldSerializeShadow). - try writeDeclarativeShadow(shadow, opts, writer, frame); + // No JS runs during a walk, so nothing resets local_arena under the stack. + // The stack is normally its latest allocation: it grows in place, and + // freeing it here hands the bytes back even when no Caller ever resets + // the arena (CDP, fetch). + fn deinit(self: *Walk) void { + self.stack.deinit(self.frame.local_arena); + } + + fn run(self: *Walk) Error!void { + while (self.stack.items.len > 0) { + // Cursors advance before visit(), which can grow (move) the stack. + const top = &self.stack.items[self.stack.items.len - 1]; + switch (top.rest) { + .siblings => |*cursor| if (cursor.*) |n| { + cursor.* = n.nextSibling(); + try self.visit(n, false); + continue; + }, + .assigned => |*nodes| if (nodes.len > 0) { + const n = nodes.*[0]; + nodes.* = nodes.*[1..]; + try self.visit(n, true); + continue; + }, + .document => |*doc| if (doc.*) |d| { + doc.* = null; + try self.contentDocument(d); + continue; + }, + } + const done = self.stack.pop().?; + if (done.end_tag) |name| { + try self.writeEndTag(name); } } } - return children(node, opts, writer, frame); + + fn open(self: *Walk, end_tag: ?[]const u8, rest: Rest) !void { + return self.stack.append(self.frame.local_arena, .{ .end_tag = end_tag, .rest = rest }); + } + + fn writeEndTag(self: *Walk, name: []const u8) !void { + try self.writer.writeAll("'); + } + + fn contentDocument(self: *Walk, doc: *Node.Document) !void { + if (comptime lp.IS_DEBUG) { + // A frame's document should always have a frame, but + // I'm not willing to crash a release build on that assertion. + std.debug.assert(doc._frame != null); + } + if (doc._frame) |f| { + try self.writer.writeByte('\n'); + root(doc, self.opts, self.writer, f) catch return error.WriteFailed; + try self.writer.writeByte('\n'); + } + } + + // The spec's "attach a declarative shadow root" serialization: attribute order + // is fixed, and boolean attributes serialize with an explicit ="". + fn declarativeShadow(self: *Walk, shadow: *Node.ShadowRoot) !void { + const writer = self.writer; + try writer.writeAll("