StyleManager: optimize rule evaluation using SoA and early rejection

This commit is contained in:
Adrià Arrufat
2026-03-20 12:02:48 +09:00
parent 5404ca723c
commit 448eca0c32

View File

@@ -45,7 +45,7 @@ pub const PointerEventsCache = std.AutoHashMapUnmanaged(*Element, bool);
const StyleManager = @This(); const StyleManager = @This();
const Tag = Element.Tag; const Tag = Element.Tag;
const RuleList = std.ArrayList(VisibilityRule); const RuleList = std.MultiArrayList(VisibilityRule);
page: *Page, page: *Page,
@@ -103,22 +103,23 @@ fn rebuildIfDirty(self: *StyleManager) !void {
const id_rules_count = self.id_rules.count(); const id_rules_count = self.id_rules.count();
const class_rules_count = self.class_rules.count(); const class_rules_count = self.class_rules.count();
const tag_rules_count = self.tag_rules.count(); const tag_rules_count = self.tag_rules.count();
const other_rules_count = self.other_rules.items.len; const other_rules_count = self.other_rules.len;
self.page._session.arena_pool.resetRetain(self.arena); self.page._session.arena_pool.resetRetain(self.arena);
self.next_doc_order = 0; self.next_doc_order = 0;
self.id_rules = .empty; self.id_rules = .empty;
try self.id_rules.ensureUnusedCapacity(self.arena, id_rules_count); try self.id_rules.ensureTotalCapacity(self.arena, id_rules_count);
self.class_rules = .empty; self.class_rules = .empty;
try self.class_rules.ensureUnusedCapacity(self.arena, class_rules_count); try self.class_rules.ensureTotalCapacity(self.arena, class_rules_count);
self.tag_rules = .empty; self.tag_rules = .empty;
try self.tag_rules.ensureUnusedCapacity(self.arena, tag_rules_count); try self.tag_rules.ensureTotalCapacity(self.arena, tag_rules_count);
self.other_rules = try .initCapacity(self.arena, other_rules_count); self.other_rules = .{};
try self.other_rules.ensureTotalCapacity(self.arena, other_rules_count);
const sheets = self.page.document._style_sheets orelse return; const sheets = self.page.document._style_sheets orelse return;
for (sheets._sheets.items) |sheet| { for (sheets._sheets.items) |sheet| {
@@ -243,28 +244,52 @@ fn isElementHidden(self: *StyleManager, el: *Element, options: CheckVisibilityOp
return spec > best_spec or (spec == best_spec and doc_order > best_doc_order); return spec > best_spec or (spec == best_spec and doc_order > best_doc_order);
} }
fn checkRule(ctx: @This(), rule: VisibilityRule) void { fn checkRules(ctx: @This(), rules: *const RuleList) void {
// Skip rules that can't possibly beat current best for any property if (ctx.display_spec.* == INLINE_SPECIFICITY and
const dominated = (rule.props.display_none == null or !beats(rule.specificity, rule.doc_order, ctx.display_spec.*, ctx.display_doc_order.*)) and ctx.visibility_spec.* == INLINE_SPECIFICITY and
(rule.props.visibility_hidden == null or !beats(rule.specificity, rule.doc_order, ctx.visibility_spec.*, ctx.visibility_doc_order.*)) and ctx.opacity_spec.* == INLINE_SPECIFICITY)
(rule.props.opacity_zero == null or !beats(rule.specificity, rule.doc_order, ctx.opacity_spec.*, ctx.opacity_doc_order.*)); {
if (dominated) return; return;
}
if (matchesSelector(ctx.el, rule.selector, ctx.page)) { const len = rules.len;
if (rule.props.display_none != null and beats(rule.specificity, rule.doc_order, ctx.display_spec.*, ctx.display_doc_order.*)) { const specificities = rules.items(.specificity);
ctx.display_none.* = rule.props.display_none; const doc_orders = rules.items(.doc_order);
ctx.display_spec.* = rule.specificity;
ctx.display_doc_order.* = rule.doc_order; for (0..len) |i| {
const spec = specificities[i];
const doc_order = doc_orders[i];
if (!beats(spec, doc_order, ctx.display_spec.*, ctx.display_doc_order.*) and
!beats(spec, doc_order, ctx.visibility_spec.*, ctx.visibility_doc_order.*) and
!beats(spec, doc_order, ctx.opacity_spec.*, ctx.opacity_doc_order.*))
{
continue;
} }
if (rule.props.visibility_hidden != null and beats(rule.specificity, rule.doc_order, ctx.visibility_spec.*, ctx.visibility_doc_order.*)) {
ctx.visibility_hidden.* = rule.props.visibility_hidden; const props = rules.items(.props)[i];
ctx.visibility_spec.* = rule.specificity; const dominated = (props.display_none == null or !beats(spec, doc_order, ctx.display_spec.*, ctx.display_doc_order.*)) and
ctx.visibility_doc_order.* = rule.doc_order; (props.visibility_hidden == null or !beats(spec, doc_order, ctx.visibility_spec.*, ctx.visibility_doc_order.*)) and
(props.opacity_zero == null or !beats(spec, doc_order, ctx.opacity_spec.*, ctx.opacity_doc_order.*));
if (dominated) continue;
const selector = rules.items(.selector)[i];
if (matchesSelector(ctx.el, selector, ctx.page)) {
if (props.display_none != null and beats(spec, doc_order, ctx.display_spec.*, ctx.display_doc_order.*)) {
ctx.display_none.* = props.display_none;
ctx.display_spec.* = spec;
ctx.display_doc_order.* = doc_order;
}
if (props.visibility_hidden != null and beats(spec, doc_order, ctx.visibility_spec.*, ctx.visibility_doc_order.*)) {
ctx.visibility_hidden.* = props.visibility_hidden;
ctx.visibility_spec.* = spec;
ctx.visibility_doc_order.* = doc_order;
}
if (props.opacity_zero != null and beats(spec, doc_order, ctx.opacity_spec.*, ctx.opacity_doc_order.*)) {
ctx.opacity_zero.* = props.opacity_zero;
ctx.opacity_spec.* = spec;
ctx.opacity_doc_order.* = doc_order;
} }
if (rule.props.opacity_zero != null and beats(rule.specificity, rule.doc_order, ctx.opacity_spec.*, ctx.opacity_doc_order.*)) {
ctx.opacity_zero.* = rule.props.opacity_zero;
ctx.opacity_spec.* = rule.specificity;
ctx.opacity_doc_order.* = rule.doc_order;
} }
} }
} }
@@ -285,9 +310,7 @@ fn isElementHidden(self: *StyleManager, el: *Element, options: CheckVisibilityOp
if (el.getAttributeSafe(comptime .wrap("id"))) |id| { if (el.getAttributeSafe(comptime .wrap("id"))) |id| {
if (self.id_rules.get(id)) |rules| { if (self.id_rules.get(id)) |rules| {
for (rules.items) |rule| { ctx.checkRules(&rules);
ctx.checkRule(rule);
}
} }
} }
@@ -295,22 +318,16 @@ fn isElementHidden(self: *StyleManager, el: *Element, options: CheckVisibilityOp
var it = std.mem.tokenizeAny(u8, class_attr, &std.ascii.whitespace); var it = std.mem.tokenizeAny(u8, class_attr, &std.ascii.whitespace);
while (it.next()) |class| { while (it.next()) |class| {
if (self.class_rules.get(class)) |rules| { if (self.class_rules.get(class)) |rules| {
for (rules.items) |rule| { ctx.checkRules(&rules);
ctx.checkRule(rule);
}
} }
} }
} }
if (self.tag_rules.get(el.getTag())) |rules| { if (self.tag_rules.get(el.getTag())) |rules| {
for (rules.items) |rule| { ctx.checkRules(&rules);
ctx.checkRule(rule);
}
} }
for (self.other_rules.items) |rule| { ctx.checkRules(&self.other_rules);
ctx.checkRule(rule);
}
return (display_none orelse false) or (visibility_hidden orelse false) or (opacity_zero orelse false); return (display_none orelse false) or (visibility_hidden orelse false) or (opacity_zero orelse false);
} }
@@ -366,28 +383,44 @@ fn elementHasPointerEventsNone(self: *StyleManager, el: *Element) bool {
var best_doc_order: u32 = 0; var best_doc_order: u32 = 0;
// Helper to check a single rule // Helper to check a single rule
const checkRule = struct { const checkRules = struct {
fn beats(spec: u32, doc_order: u32, b_spec: u32, b_doc_order: u32) bool { fn beats(spec: u32, doc_order: u32, b_spec: u32, b_doc_order: u32) bool {
return spec > b_spec or (spec == b_spec and doc_order > b_doc_order); return spec > b_spec or (spec == b_spec and doc_order > b_doc_order);
} }
fn check(rule: VisibilityRule, res: *?bool, spec: *u32, doc_order: *u32, elem: *Element, p: *Page) void { fn check(rules: *const RuleList, res: *?bool, spec: *u32, doc_order: *u32, elem: *Element, p: *Page) void {
if (rule.props.pointer_events_none == null or !beats(rule.specificity, rule.doc_order, spec.*, doc_order.*)) { if (spec.* == INLINE_SPECIFICITY) return;
return;
const len = rules.len;
const specificities = rules.items(.specificity);
const doc_orders = rules.items(.doc_order);
for (0..len) |i| {
const rule_spec = specificities[i];
const rule_doc_order = doc_orders[i];
if (!beats(rule_spec, rule_doc_order, spec.*, doc_order.*)) {
continue;
}
const props = rules.items(.props)[i];
if (props.pointer_events_none == null) {
continue;
}
const selector = rules.items(.selector)[i];
if (matchesSelector(elem, selector, p)) {
res.* = props.pointer_events_none;
spec.* = rule_spec;
doc_order.* = rule_doc_order;
} }
if (matchesSelector(elem, rule.selector, p)) {
res.* = rule.props.pointer_events_none;
spec.* = rule.specificity;
doc_order.* = rule.doc_order;
} }
} }
}.check; }.check;
if (el.getAttributeSafe(comptime .wrap("id"))) |id| { if (el.getAttributeSafe(comptime .wrap("id"))) |id| {
if (self.id_rules.get(id)) |rules| { if (self.id_rules.get(id)) |rules| {
for (rules.items) |rule| { checkRules(&rules, &result, &best_spec, &best_doc_order, el, page);
checkRule(rule, &result, &best_spec, &best_doc_order, el, page);
}
} }
} }
@@ -395,22 +428,16 @@ fn elementHasPointerEventsNone(self: *StyleManager, el: *Element) bool {
var it = std.mem.tokenizeAny(u8, class_attr, &std.ascii.whitespace); var it = std.mem.tokenizeAny(u8, class_attr, &std.ascii.whitespace);
while (it.next()) |class| { while (it.next()) |class| {
if (self.class_rules.get(class)) |rules| { if (self.class_rules.get(class)) |rules| {
for (rules.items) |rule| { checkRules(&rules, &result, &best_spec, &best_doc_order, el, page);
checkRule(rule, &result, &best_spec, &best_doc_order, el, page);
}
} }
} }
} }
if (self.tag_rules.get(el.getTag())) |rules| { if (self.tag_rules.get(el.getTag())) |rules| {
for (rules.items) |rule| { checkRules(&rules, &result, &best_spec, &best_doc_order, el, page);
checkRule(rule, &result, &best_spec, &best_doc_order, el, page);
}
} }
for (self.other_rules.items) |rule| { checkRules(&self.other_rules, &result, &best_spec, &best_doc_order, el, page);
checkRule(rule, &result, &best_spec, &best_doc_order, el, page);
}
return result orelse false; return result orelse false;
} }
@@ -461,17 +488,17 @@ fn addRule(self: *StyleManager, style_rule: *CSSStyleRule) !void {
switch (bucket_key) { switch (bucket_key) {
.id => |id| { .id => |id| {
const gop = try self.id_rules.getOrPut(self.arena, id); const gop = try self.id_rules.getOrPut(self.arena, id);
if (!gop.found_existing) gop.value_ptr.* = .empty; if (!gop.found_existing) gop.value_ptr.* = .{};
try gop.value_ptr.append(self.arena, rule); try gop.value_ptr.append(self.arena, rule);
}, },
.class => |class| { .class => |class| {
const gop = try self.class_rules.getOrPut(self.arena, class); const gop = try self.class_rules.getOrPut(self.arena, class);
if (!gop.found_existing) gop.value_ptr.* = .empty; if (!gop.found_existing) gop.value_ptr.* = .{};
try gop.value_ptr.append(self.arena, rule); try gop.value_ptr.append(self.arena, rule);
}, },
.tag => |tag| { .tag => |tag| {
const gop = try self.tag_rules.getOrPut(self.arena, tag); const gop = try self.tag_rules.getOrPut(self.arena, tag);
if (!gop.found_existing) gop.value_ptr.* = .empty; if (!gop.found_existing) gop.value_ptr.* = .{};
try gop.value_ptr.append(self.arena, rule); try gop.value_ptr.append(self.arena, rule);
}, },
.other => { .other => {