From 729051b279f2f526e22101432c53ad31a52bc798 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Mon, 14 Sep 2026 13:10:35 +0800 Subject: [PATCH] internal: run microtasks during parsing Runs microtasks during parsing, rather than waiting for parsing to complete. We see some sites that setup MutationObserver on the root, early in a document's code. These then get thousands (4K-10K) of MutationRecords for every added node. We deliver these as a single batch at the end of parsing. Chrome delivers it based on some elapsed time calculation (I think). Both are correct, as far as I can tell. And, I don't really expect this to change anything. But, because the batch size is fixed: 1 - the inflight metric should be flatter 2 - there could be some small reduction in retained memory (e.g. MO's `_pending_records` might not grow so much) 3 - `call_arena` doesn't need to dupe such a large array Related to (1), when we do special builds that log arena usage and eliminating / reducing this known OK behavior helps remove some noise (Or, put it this way: I spent some time debugging this, it's more or less nothing, but it still looks like something that needs fixing, this commit reduces that noise). --- src/browser/parser/Parser.zig | 27 ++++++++++++++ .../mutation_observer/parser_chunks.html | 36 +++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 src/browser/tests/mutation_observer/parser_chunks.html diff --git a/src/browser/parser/Parser.zig b/src/browser/parser/Parser.zig index d1c87e9b5..0bbdc97a0 100644 --- a/src/browser/parser/Parser.zig +++ b/src/browser/parser/Parser.zig @@ -20,6 +20,7 @@ const std = @import("std"); const lp = @import("lightpanda"); const h5e = @import("html5ever.zig"); +const js = @import("../js/js.zig"); const Frame = @import("../Frame.zig"); const Node = @import("../webapi/Node.zig"); const Element = @import("../webapi/Element.zig"); @@ -29,6 +30,8 @@ pub const QualName = h5e.QualName; pub const AttributeIterator = h5e.AttributeIterator; const Allocator = std.mem.Allocator; + +const CHECKPOINT_INTERVAL = 1024; const TERMINATE_CHECK_INTERVAL = 1024; pub const ParsedNode = struct { @@ -86,6 +89,7 @@ context: ?*Element = null, xml_error: bool = false, terminated: bool = false, appends_until_terminate_check: u16 = TERMINATE_CHECK_INTERVAL, +inserted_since_checkpoint: u16 = 0, pub const Options = struct { allow_declarative_shadow: bool = false, @@ -157,6 +161,7 @@ fn appendTextChunk(self: *Parser, parent: *Node, txt: []const u8) !void { // until (and unless) a second chunk arrives. const new_text = try Frame.node_factory.createTextNode(self.frame, txt); try self.frame.appendNew(parent, new_text); + self.inserted_since_checkpoint +|= 1; self.pending_text = .{ .parent = parent, .text_node = new_text.is(CData.Text).?.asCData(), @@ -713,6 +718,8 @@ fn _appendCallback(self: *Parser, parent: *Node, node_or_text: h5e.NodeOrText) ! // before the insertion so that connectedCallback (etc.) sees the // final data on the preceding text sibling. try self.flushPendingText(); + self.maybeCheckpoint(); + self.inserted_since_checkpoint +|= 1; const child = getNode(cpn); if (child._parent) |previous_parent| { // html5ever says this can't happen, but we might be screwing up @@ -845,6 +852,26 @@ fn asUint(comptime string: anytype) std.meta.Int( return @bitCast(@as(*const [byteLength]u8, string).*); } +fn maybeCheckpoint(self: *Parser) void { + if (self.inserted_since_checkpoint < CHECKPOINT_INTERVAL) { + return; + } + + // only the navigation parse, and only at an empty JS stack: a fragment + // parse, document.write or a parse run by a script must not drain the + // queue mid-task. + const frame = self.frame; + if (frame.js.call_depth != 0 or frame._load_state != .parsing or frame._parse_mode != .document) { + return; + } + self.inserted_since_checkpoint = 0; + + var ls: js.Local.Scope = undefined; + frame.js.localScope(&ls); + defer ls.deinit(); + ls.local.runMicrotasks(); +} + // v8's terminate isn't pre-emptive. A parse of unbounded input // (this.innerHTML += this.innerHTML) has to poll the terminate flag itself. // We'll poll this (atomic) variable every TERMINATE_CHECK_INTERVAL append. diff --git a/src/browser/tests/mutation_observer/parser_chunks.html b/src/browser/tests/mutation_observer/parser_chunks.html new file mode 100644 index 000000000..6bceac179 --- /dev/null +++ b/src/browser/tests/mutation_observer/parser_chunks.html @@ -0,0 +1,36 @@ + + +
+ + +