script: Use a u16 for prototype id to speed up Castable::is<T>() (#44364)

`Castable::is<T>()` calls `get_dom_class()` in a way that incurs a
roundtrip to the JS engine and
multiple pointer indirections:

self.reflector().get_jsobject().get()  -> JSObject
Object.shape -> BaseShape.clasp    -> JSClass
DOMJSClass.dom_class              -> DOMClass
interface_chain[depth] == ID       -> bool

Since the ID doesn't change after creation of reflectors, we can instead
store the ID on the reflector
and avoid most of that cost. The trick is to use a Depth First Search to
generate IDs that allow a
range check for `Castable::is<T>`. An example of IDs in the DFS looks
like:

EventTarget = 0
  Node = 1
    CharacterData = 2
      Text = 3
      CDATASection = 4
      Comment = 5
      ProcessingInstruction = 6
    Document = 7
      HTMLDocument = 8
      XMLDocument = 9

Testing: Green try run at
https://github.com/webbeef/servo/actions/runs/24640508040 and manual
testing with general browsing.

Signed-off-by: webbeef <me@webbeef.org>
This commit is contained in:
webbeef
2026-04-25 00:54:35 -07:00
committed by GitHub
parent d0908b0342
commit 87daffa1fd
5 changed files with 88 additions and 12 deletions

View File

@@ -72,6 +72,11 @@ from configuration import (
) )
AUTOGENERATED_WARNING_COMMENT = "/* THIS FILE IS AUTOGENERATED - DO NOT EDIT */\n\n" AUTOGENERATED_WARNING_COMMENT = "/* THIS FILE IS AUTOGENERATED - DO NOT EDIT */\n\n"
# DFS-ordered prototype ID ranges, populated by GlobalGenRoots.PrototypeList()
# and consumed by CGIDLInterface to generate PROTO_FIRST/PROTO_LAST constants.
_proto_ranges: dict[str, tuple[int, int]] = {}
ALLOWED_WARNING_LIST = [ ALLOWED_WARNING_LIST = [
'non_camel_case_types', 'non_camel_case_types',
'non_upper_case_globals', 'non_upper_case_globals',
@@ -3395,6 +3400,7 @@ let val = PrivateValue(ptr::null());
JS_SetReservedSlot(obj.get(), DOM_WEAK_SLOT, &val); JS_SetReservedSlot(obj.get(), DOM_WEAK_SLOT, &val);
""" """
name = self.descriptor.name
return CGGeneric(f""" return CGGeneric(f"""
let raw = Root::new(MaybeUnreflectedDom::from_box(object)); let raw = Root::new(MaybeUnreflectedDom::from_box(object));
@@ -3409,6 +3415,7 @@ assert!(!canonical_proto.is_null());
{create} {create}
let root = raw.reflect_with(obj.get()); let root = raw.reflect_with(obj.get());
root.reflector().set_proto_id(PrototypeList::ID::{name} as u16);
{unforgeable} {unforgeable}
@@ -3441,6 +3448,7 @@ class CGWrapGlobalMethod(CGAbstractMethod):
members = [f"{function}::<D>(cx.into(), obj.handle(), {array.variableName()}.get(), obj.handle());" members = [f"{function}::<D>(cx.into(), obj.handle(), {array.variableName()}.get(), obj.handle());"
for (function, array) in pairs if array.length() > 0] for (function, array) in pairs if array.length() > 0]
membersStr = "\n".join(members) membersStr = "\n".join(members)
name = self.descriptor.name
return CGGeneric(f""" return CGGeneric(f"""
unsafe {{ unsafe {{
@@ -3459,6 +3467,7 @@ unsafe {{
assert!(!obj.is_null()); assert!(!obj.is_null());
let root = raw.reflect_with(obj.get()); let root = raw.reflect_with(obj.get());
root.reflector().set_proto_id(PrototypeList::ID::{name} as u16);
let _ac = JSAutoRealm::new(cx.raw_cx(), obj.get()); let _ac = JSAutoRealm::new(cx.raw_cx(), obj.get());
rooted!(&in(cx) let mut canonical_proto = ptr::null_mut::<JSObject>()); rooted!(&in(cx) let mut canonical_proto = ptr::null_mut::<JSObject>());
@@ -3503,12 +3512,16 @@ class CGIDLInterface(CGThing):
check = f"ptr::eq(class, unsafe {{ {bindingModule}::Class.get() }})" check = f"ptr::eq(class, unsafe {{ {bindingModule}::Class.get() }})"
else: else:
check = f"ptr::eq(class, unsafe {{ &{bindingModule}::Class.get().dom_class }})" check = f"ptr::eq(class, unsafe {{ &{bindingModule}::Class.get().dom_class }})"
# Get DFS-ordered ID range for this interface (set by PrototypeList generation).
proto_first, proto_last = _proto_ranges.get(name, (0, 65535))
return f""" return f"""
impl IDLInterface for {name} {{ impl IDLInterface for {name} {{
#[inline] #[inline]
fn derives(class: &'static DOMClass) -> bool {{ fn derives(class: &'static DOMClass) -> bool {{
{check} {check}
}} }}
const PROTO_FIRST: u16 = {proto_first};
const PROTO_LAST: u16 = {proto_last};
}} }}
""" """
@@ -9142,8 +9155,51 @@ class GlobalGenRoots():
@staticmethod @staticmethod
def PrototypeList(config: Configuration) -> CGThing: def PrototypeList(config: Configuration) -> CGThing:
# Prototype ID enum. # Prototype ID enum.
# Assign IDs in DFS preorder of the inheritance tree so that
# all descendants of a type occupy a contiguous ID range.
# This enables O(1) is::<T>() checks via range comparison.
interfaces = config.getDescriptors(isCallback=False, isNamespace=False) interfaces = config.getDescriptors(isCallback=False, isNamespace=False)
protos = [d.name for d in interfaces]
# Build a tree: parent_name -> sorted list of child names
children: dict[str | None, list[str]] = {}
name_set = set()
for d in interfaces:
name = d.name
name_set.add(name)
parent_name = d.prototypeChain[-2] if len(d.prototypeChain) >= 2 else None
children.setdefault(parent_name, []).append(name)
# Sort children alphabetically for determinism within each level
for key in children:
children[key].sort()
# DFS preorder traversal
protos: list[str] = []
# Track range for each prototype: (first_id, last_id)
proto_ranges: dict[str, tuple[int, int]] = {}
def dfs(name: str) -> None:
first_id = len(protos)
protos.append(name)
for child in children.get(name, []):
dfs(child)
proto_ranges[name] = (first_id, len(protos) - 1)
# Start from roots (interfaces with no parent in our set)
roots = sorted(children.get(None, []))
for root in roots:
dfs(root)
# Any interfaces not in the tree (shouldn't happen, but be safe)
for d in interfaces:
if d.name not in proto_ranges:
first_id = len(protos)
protos.append(d.name)
proto_ranges[d.name] = (first_id, first_id)
# Store ranges at module level for use by CGIDLInterface.
global _proto_ranges
_proto_ranges = proto_ranges
constructors = sorted([MakeNativeName(d.name) constructors = sorted([MakeNativeName(d.name)
for d in config.getDescriptors(hasInterfaceObject=True) for d in config.getDescriptors(hasInterfaceObject=True)
if d.shouldHaveGetConstructorObjectMethod()]) if d.shouldHaveGetConstructorObjectMethod()])

View File

@@ -51,6 +51,11 @@ impl<T: ToJSValConvertible + ?Sized> SafeToJSValConvertible for T {
pub trait IDLInterface { pub trait IDLInterface {
/// Returns whether the given DOM class derives that interface. /// Returns whether the given DOM class derives that interface.
fn derives(_: &'static DOMClass) -> bool; fn derives(_: &'static DOMClass) -> bool;
/// First prototype ID in the DFS-ordered range for this interface and its descendants.
const PROTO_FIRST: u16 = 0;
/// Last prototype ID in the DFS-ordered range for this interface and its descendants.
const PROTO_LAST: u16 = u16::MAX;
} }
/// A trait to mark an IDL interface as deriving from another one. /// A trait to mark an IDL interface as deriving from another one.

View File

@@ -6,7 +6,7 @@
use std::mem; use std::mem;
use crate::conversions::{DerivedFrom, IDLInterface, get_dom_class}; use crate::conversions::{DerivedFrom, IDLInterface};
use crate::reflector::DomObject; use crate::reflector::DomObject;
use crate::script_runtime::runtime_is_alive; use crate::script_runtime::runtime_is_alive;
@@ -26,8 +26,8 @@ pub trait Castable: IDLInterface + DomObject + Sized {
"Attempting to interact with DOM objects after JS runtime has shut down." "Attempting to interact with DOM objects after JS runtime has shut down."
); );
let class = unsafe { get_dom_class(self.reflector().get_jsobject().get()).unwrap() }; let id = self.reflector().proto_id();
T::derives(class) id >= T::PROTO_FIRST && id <= T::PROTO_LAST
} }
/// Cast a DOM object upwards to one of the interfaces it derives from. /// Cast a DOM object upwards to one of the interfaces it derives from.

View File

@@ -42,6 +42,8 @@ pub struct Reflector<T = ()> {
object: Heap<*mut JSObject>, object: Heap<*mut JSObject>,
/// Associated memory size (of rust side). Used for memory reporting to SM. /// Associated memory size (of rust side). Used for memory reporting to SM.
size: T, size: T,
/// Cached prototype ID for fast type checks.
proto_id: Cell<u16>,
} }
unsafe impl<T> js::gc::Traceable for Reflector<T> { unsafe impl<T> js::gc::Traceable for Reflector<T> {
@@ -62,6 +64,18 @@ impl<T> Reflector<T> {
unsafe { HandleObject::from_raw(self.object.handle()) } unsafe { HandleObject::from_raw(self.object.handle()) }
} }
/// Get the cached prototype ID.
#[inline]
pub fn proto_id(&self) -> u16 {
self.proto_id.get()
}
/// Set the cached prototype ID.
#[inline]
pub fn set_proto_id(&self, id: u16) {
self.proto_id.set(id);
}
/// Initialize the reflector. (May be called only once.) /// Initialize the reflector. (May be called only once.)
/// ///
/// # Safety /// # Safety
@@ -88,6 +102,7 @@ impl<T: AssociatedMemorySize> Reflector<T> {
pub fn new() -> Reflector<T> { pub fn new() -> Reflector<T> {
Reflector { Reflector {
object: Heap::default(), object: Heap::default(),
proto_id: Cell::new(u16::MAX),
size: T::default(), size: T::default(),
} }
} }

View File

@@ -29,11 +29,11 @@ macro_rules! sizeof_checker (
); );
// Update the sizes here // Update the sizes here
sizeof_checker!(size_event_target, EventTarget, 48); sizeof_checker!(size_event_target, EventTarget, 56);
sizeof_checker!(size_node, Node, 152); sizeof_checker!(size_node, Node, 160);
sizeof_checker!(size_element, Element, 344); sizeof_checker!(size_element, Element, 352);
sizeof_checker!(size_htmlelement, HTMLElement, 360); sizeof_checker!(size_htmlelement, HTMLElement, 368);
sizeof_checker!(size_div, HTMLDivElement, 360); sizeof_checker!(size_div, HTMLDivElement, 368);
sizeof_checker!(size_span, HTMLSpanElement, 360); sizeof_checker!(size_span, HTMLSpanElement, 368);
sizeof_checker!(size_text, Text, 184); sizeof_checker!(size_text, Text, 192);
sizeof_checker!(size_characterdata, CharacterData, 184); sizeof_checker!(size_characterdata, CharacterData, 192);