Removed FnvHash and transformed the rest to FxHashmap (#39233)

This should be the final PR for the Hash Function series that is
trivial.

Of note: I decided to transform `HashMapTracedValues<Atom,..>` to use
FxBuildHasher. This is likely not going to improve performance as Atom's
already have a unique u32 that is used as the Hash but it safes a few
bytes for the RandomState that is normally in the HashMap.

Signed-off-by: Narfinger <Narfinger@users.noreply.github.com>

Testing: Hash function changes should not change functionality, we
slightly decrease the size and unit tests still work.

Signed-off-by: Narfinger <Narfinger@users.noreply.github.com>
This commit is contained in:
Narfinger 2025-09-10 15:34:54 +02:00 committed by GitHub
parent 726b456120
commit 84465e7768
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
55 changed files with 211 additions and 202 deletions

View file

@ -29,8 +29,8 @@ use std::marker::PhantomData;
use std::rc::Rc;
use std::sync::{Arc, Weak};
use fnv::FnvHashMap;
use js::jsapi::JSTracer;
use rustc_hash::FxHashMap;
use script_bindings::script_runtime::CanGc;
use crate::dom::bindings::conversions::ToJSValConvertible;
@ -47,14 +47,14 @@ mod dummy {
use std::cell::RefCell;
use std::rc::Rc;
use fnv::{FnvBuildHasher, FnvHashMap};
use rustc_hash::FxHashMap;
use super::LiveDOMReferences;
thread_local!(pub(crate) static LIVE_REFERENCES: Rc<RefCell<LiveDOMReferences>> =
Rc::new(RefCell::new(
LiveDOMReferences {
reflectable_table: RefCell::new(FnvHashMap::with_hasher(FnvBuildHasher::new())),
promise_table: RefCell::new(FnvHashMap::with_hasher(FnvBuildHasher::new())),
reflectable_table: RefCell::new(FxHashMap::default()),
promise_table: RefCell::new(FxHashMap::default()),
}
)));
}
@ -232,8 +232,8 @@ impl<T: DomObject> Clone for Trusted<T> {
#[cfg_attr(crown, allow(crown::unrooted_must_root))]
pub(crate) struct LiveDOMReferences {
// keyed on pointer to Rust DOM object
reflectable_table: RefCell<FnvHashMap<*const libc::c_void, Weak<TrustedReference>>>,
promise_table: RefCell<FnvHashMap<*const Promise, Vec<Rc<Promise>>>>,
reflectable_table: RefCell<FxHashMap<*const libc::c_void, Weak<TrustedReference>>>,
promise_table: RefCell<FxHashMap<*const Promise, Vec<Rc<Promise>>>>,
}
impl LiveDOMReferences {
@ -283,7 +283,7 @@ impl LiveDOMReferences {
}
/// Remove null entries from the live references table
fn remove_nulls<K: Eq + Hash + Clone, V>(table: &mut FnvHashMap<K, Weak<V>>) {
fn remove_nulls<K: Eq + Hash + Clone, V>(table: &mut FxHashMap<K, Weak<V>>) {
let to_remove: Vec<K> = table
.iter()
.filter(|&(_, value)| Weak::upgrade(value).is_none())

View file

@ -15,6 +15,7 @@ use js::jsapi::{HandleValueArray, Heap, IsCallable, IsConstructor, JSAutoRealm,
use js::jsval::{BooleanValue, JSVal, NullValue, ObjectValue, UndefinedValue};
use js::rust::wrappers::{Construct1, JS_GetProperty, SameValue};
use js::rust::{HandleObject, MutableHandleValue};
use rustc_hash::FxBuildHasher;
use script_bindings::conversions::{SafeFromJSValConvertible, SafeToJSValConvertible};
use super::bindings::trace::HashMapTracedValues;
@ -68,12 +69,15 @@ pub(crate) struct CustomElementRegistry {
window: Dom<Window>,
#[ignore_malloc_size_of = "Rc"]
when_defined: DomRefCell<HashMapTracedValues<LocalName, Rc<Promise>>>,
/// It is safe to use FxBuildHasher here as `LocalName` is an `Atom` in the string_cache.
/// These get a u32 hashed instead of a string.
when_defined: DomRefCell<HashMapTracedValues<LocalName, Rc<Promise>, FxBuildHasher>>,
element_definition_is_running: Cell<bool>,
#[ignore_malloc_size_of = "Rc"]
definitions: DomRefCell<HashMapTracedValues<LocalName, Rc<CustomElementDefinition>>>,
definitions:
DomRefCell<HashMapTracedValues<LocalName, Rc<CustomElementDefinition>, FxBuildHasher>>,
}
impl CustomElementRegistry {
@ -81,9 +85,9 @@ impl CustomElementRegistry {
CustomElementRegistry {
reflector_: Reflector::new(),
window: Dom::from_ref(window),
when_defined: DomRefCell::new(HashMapTracedValues::new()),
when_defined: DomRefCell::new(HashMapTracedValues::new_fx()),
element_definition_is_running: Cell::new(false),
definitions: DomRefCell::new(HashMapTracedValues::new()),
definitions: DomRefCell::new(HashMapTracedValues::new_fx()),
}
}

View file

@ -30,7 +30,6 @@ use embedder_traits::{AllowOrDeny, AnimationState, EmbedderMsg, FocusSequenceNum
use encoding_rs::{Encoding, UTF_8};
use euclid::Point2D;
use euclid::default::{Rect, Size2D};
use fnv::FnvHashMap;
use html5ever::{LocalName, Namespace, QualName, local_name, ns};
use hyper_serde::Serde;
use js::rust::{HandleObject, HandleValue, MutableHandleValue};
@ -47,7 +46,7 @@ use percent_encoding::percent_decode;
use profile_traits::ipc as profile_ipc;
use profile_traits::time::TimerMetadataFrameType;
use regex::bytes::Regex;
use rustc_hash::FxBuildHasher;
use rustc_hash::{FxBuildHasher, FxHashMap};
use script_bindings::codegen::GenericBindings::ElementBinding::ElementMethods;
use script_bindings::interfaces::DocumentHelpers;
use script_bindings::script_runtime::JSContext;
@ -304,11 +303,12 @@ pub(crate) struct Document {
quirks_mode: Cell<QuirksMode>,
/// A helper used to process and store data related to input event handling.
event_handler: DocumentEventHandler,
/// Caches for the getElement methods
id_map: DomRefCell<HashMapTracedValues<Atom, Vec<Dom<Element>>>>,
name_map: DomRefCell<HashMapTracedValues<Atom, Vec<Dom<Element>>>>,
tag_map: DomRefCell<HashMapTracedValues<LocalName, Dom<HTMLCollection>>>,
tagns_map: DomRefCell<HashMapTracedValues<QualName, Dom<HTMLCollection>>>,
/// Caches for the getElement methods. It is safe to use FxHash for these maps
/// as Atoms are `string_cache` items that will have the hash computed from a u32.
id_map: DomRefCell<HashMapTracedValues<Atom, Vec<Dom<Element>>, FxBuildHasher>>,
name_map: DomRefCell<HashMapTracedValues<Atom, Vec<Dom<Element>>, FxBuildHasher>>,
tag_map: DomRefCell<HashMapTracedValues<LocalName, Dom<HTMLCollection>, FxBuildHasher>>,
tagns_map: DomRefCell<HashMapTracedValues<QualName, Dom<HTMLCollection>, FxBuildHasher>>,
classes_map: DomRefCell<HashMapTracedValues<Vec<Atom>, Dom<HTMLCollection>>>,
images: MutNullableDom<HTMLCollection>,
embeds: MutNullableDom<HTMLCollection>,
@ -375,7 +375,7 @@ pub(crate) struct Document {
appropriate_template_contents_owner_document: MutNullableDom<Document>,
/// Information on elements needing restyle to ship over to layout when the
/// time comes.
pending_restyles: DomRefCell<FnvHashMap<Dom<Element>, NoTrace<PendingRestyle>>>,
pending_restyles: DomRefCell<FxHashMap<Dom<Element>, NoTrace<PendingRestyle>>>,
/// A collection of reasons that the [`Document`] needs to be restyled at the next
/// opportunity for a reflow. If this is empty, then the [`Document`] does not need to
/// be restyled.
@ -436,7 +436,9 @@ pub(crate) struct Document {
/// whenever any element with the same ID as the form attribute
/// is inserted or removed from the document.
/// See <https://html.spec.whatwg.org/multipage/#form-owner>
form_id_listener_map: DomRefCell<HashMapTracedValues<Atom, HashSet<Dom<Element>>>>,
/// It is safe to use FxBuildHasher here as Atoms are in the string_cache
form_id_listener_map:
DomRefCell<HashMapTracedValues<Atom, HashSet<Dom<Element>>, FxBuildHasher>>,
#[no_trace]
interactive_time: DomRefCell<ProgressiveWebMetrics>,
#[no_trace]
@ -2796,11 +2798,15 @@ impl Document {
fonts.fulfill_ready_promise_if_needed(can_gc)
}
pub(crate) fn id_map(&self) -> Ref<'_, HashMapTracedValues<Atom, Vec<Dom<Element>>>> {
pub(crate) fn id_map(
&self,
) -> Ref<'_, HashMapTracedValues<Atom, Vec<Dom<Element>>, FxBuildHasher>> {
self.id_map.borrow()
}
pub(crate) fn name_map(&self) -> Ref<'_, HashMapTracedValues<Atom, Vec<Dom<Element>>>> {
pub(crate) fn name_map(
&self,
) -> Ref<'_, HashMapTracedValues<Atom, Vec<Dom<Element>>, FxBuildHasher>> {
self.name_map.borrow()
}
@ -3332,14 +3338,14 @@ impl Document {
// https://dom.spec.whatwg.org/#concept-document-quirks
quirks_mode: Cell::new(QuirksMode::NoQuirks),
event_handler: DocumentEventHandler::new(window),
id_map: DomRefCell::new(HashMapTracedValues::new()),
name_map: DomRefCell::new(HashMapTracedValues::new()),
id_map: DomRefCell::new(HashMapTracedValues::new_fx()),
name_map: DomRefCell::new(HashMapTracedValues::new_fx()),
// https://dom.spec.whatwg.org/#concept-document-encoding
encoding: Cell::new(encoding),
is_html_document: is_html_document == IsHTMLDocument::HTMLDocument,
activity: Cell::new(activity),
tag_map: DomRefCell::new(HashMapTracedValues::new()),
tagns_map: DomRefCell::new(HashMapTracedValues::new()),
tag_map: DomRefCell::new(HashMapTracedValues::new_fx()),
tagns_map: DomRefCell::new(HashMapTracedValues::new_fx()),
classes_map: DomRefCell::new(HashMapTracedValues::new()),
images: Default::default(),
embeds: Default::default(),
@ -3383,7 +3389,7 @@ impl Document {
current_parser: Default::default(),
base_element: Default::default(),
appropriate_template_contents_owner_document: Default::default(),
pending_restyles: DomRefCell::new(FnvHashMap::default()),
pending_restyles: DomRefCell::new(FxHashMap::default()),
needs_restyle: Cell::new(RestyleReason::DOMChanged),
dom_interactive: Cell::new(Default::default()),
dom_content_loaded_event_start: Cell::new(Default::default()),

View file

@ -4,6 +4,7 @@
use dom_struct::dom_struct;
use js::rust::HandleObject;
use rustc_hash::FxBuildHasher;
use stylo_atoms::Atom;
use super::bindings::trace::HashMapTracedValues;
@ -29,7 +30,7 @@ use crate::script_runtime::CanGc;
pub(crate) struct DocumentFragment {
node: Node,
/// Caches for the getElement methods
id_map: DomRefCell<HashMapTracedValues<Atom, Vec<Dom<Element>>>>,
id_map: DomRefCell<HashMapTracedValues<Atom, Vec<Dom<Element>>, FxBuildHasher>>,
}
impl DocumentFragment {
@ -37,7 +38,7 @@ impl DocumentFragment {
pub(crate) fn new_inherited(document: &Document) -> DocumentFragment {
DocumentFragment {
node: Node::new_inherited(document),
id_map: DomRefCell::new(HashMapTracedValues::new()),
id_map: DomRefCell::new(HashMapTracedValues::new_fx()),
}
}
@ -58,7 +59,9 @@ impl DocumentFragment {
)
}
pub(crate) fn id_map(&self) -> &DomRefCell<HashMapTracedValues<Atom, Vec<Dom<Element>>>> {
pub(crate) fn id_map(
&self,
) -> &DomRefCell<HashMapTracedValues<Atom, Vec<Dom<Element>>, FxBuildHasher>> {
&self.id_map
}
}

View file

@ -9,6 +9,7 @@ use std::fmt;
use embedder_traits::UntrustedNodeAddress;
use js::rust::HandleValue;
use layout_api::ElementsFromPointFlags;
use rustc_hash::FxBuildHasher;
use script_bindings::error::{Error, ErrorResult};
use script_bindings::script_runtime::JSContext;
use servo_arc::Arc;
@ -307,7 +308,7 @@ impl DocumentOrShadowRoot {
/// Remove any existing association between the provided id/name and any elements in this document.
pub(crate) fn unregister_named_element(
&self,
id_map: &DomRefCell<HashMapTracedValues<Atom, Vec<Dom<Element>>>>,
id_map: &DomRefCell<HashMapTracedValues<Atom, Vec<Dom<Element>>, FxBuildHasher>>,
to_unregister: &Element,
id: &Atom,
) {
@ -335,7 +336,7 @@ impl DocumentOrShadowRoot {
/// Associate an element present in this document with the provided id/name.
pub(crate) fn register_named_element(
&self,
id_map: &DomRefCell<HashMapTracedValues<Atom, Vec<Dom<Element>>>>,
id_map: &DomRefCell<HashMapTracedValues<Atom, Vec<Dom<Element>>, FxBuildHasher>>,
element: &Element,
id: &Atom,
root: DomRoot<Node>,

View file

@ -6,19 +6,18 @@ use std::cell::RefCell;
use std::collections::hash_map::Entry::{Occupied, Vacant};
use std::default::Default;
use std::ffi::CString;
use std::hash::BuildHasherDefault;
use std::mem;
use std::ops::{Deref, DerefMut};
use std::rc::Rc;
use deny_public_fields::DenyPublicFields;
use dom_struct::dom_struct;
use fnv::FnvHasher;
use js::jsapi::JS::CompileFunction;
use js::jsapi::{JS_GetFunctionObject, SupportUnscopables};
use js::jsval::JSVal;
use js::rust::{CompileOptionsWrapper, HandleObject, transform_u16_to_source_text};
use libc::c_char;
use rustc_hash::FxBuildHasher;
use servo_url::ServoUrl;
use style::str::HTML_SPACE_CHARACTERS;
use stylo_atoms::Atom;
@ -512,7 +511,7 @@ impl EventListeners {
#[dom_struct]
pub struct EventTarget {
reflector_: Reflector,
handlers: DomRefCell<HashMapTracedValues<Atom, EventListeners, BuildHasherDefault<FnvHasher>>>,
handlers: DomRefCell<HashMapTracedValues<Atom, EventListeners, FxBuildHasher>>,
}
impl EventTarget {

View file

@ -27,7 +27,6 @@ use crossbeam_channel::Sender;
use devtools_traits::{PageError, ScriptToDevtoolsControlMsg};
use dom_struct::dom_struct;
use embedder_traits::{EmbedderMsg, JavaScriptEvaluationError, ScriptToEmbedderChan};
use fnv::FnvHashMap;
use fonts::FontContext;
use ipc_channel::ipc::{self, IpcSender};
use ipc_channel::router::ROUTER;
@ -567,7 +566,7 @@ impl MessageListener {
};
let mut succeeded = vec![];
let mut failed = FnvHashMap::default();
let mut failed = FxHashMap::default();
for (id, info) in ports.into_iter() {
if global.is_managing_port(&id) {

View file

@ -16,6 +16,7 @@ use js::rust::HandleObject;
use mime::{self, Mime};
use net_traits::http_percent_encode;
use net_traits::request::Referrer;
use rustc_hash::FxBuildHasher;
use servo_rand::random;
use style::attr::AttrValue;
use style::str::split_html_space_chars;
@ -93,8 +94,10 @@ pub(crate) struct HTMLFormElement {
elements: DomOnceCell<HTMLFormControlsCollection>,
controls: DomRefCell<Vec<Dom<Element>>>,
/// It is safe to use FxBuildHasher here as `Atom` is in the string_cache.
#[allow(clippy::type_complexity)]
past_names_map: DomRefCell<HashMapTracedValues<Atom, (Dom<Element>, NoTrace<usize>)>>,
past_names_map:
DomRefCell<HashMapTracedValues<Atom, (Dom<Element>, NoTrace<usize>), FxBuildHasher>>,
/// The current generation of past names, i.e., the number of name changes to the name.
current_name_generation: Cell<usize>,
@ -126,7 +129,7 @@ impl HTMLFormElement {
constructing_entry_list: Cell::new(false),
elements: Default::default(),
controls: DomRefCell::new(Vec::new()),
past_names_map: DomRefCell::new(HashMapTracedValues::new()),
past_names_map: DomRefCell::new(HashMapTracedValues::new_fx()),
current_name_generation: Cell::new(0),
firing_submission_events: Cell::new(false),
rel_list: Default::default(),

View file

@ -6,7 +6,6 @@
use std::borrow::Cow;
use std::cell::{Cell, Ref, RefCell, RefMut};
use std::collections::HashMap;
use std::collections::vec_deque::VecDeque;
use std::rc::Rc;
use std::thread;
@ -226,8 +225,8 @@ pub(crate) struct Tokenizer {
#[ignore_malloc_size_of = "Defined in std"]
#[no_trace]
html_tokenizer_sender: Sender<ToHtmlTokenizerMsg>,
#[ignore_malloc_size_of = "Defined in std"]
nodes: RefCell<HashMap<ParseNodeId, Dom<Node>>>,
//#[ignore_malloc_size_of = "Defined in std"]
nodes: RefCell<FxHashMap<ParseNodeId, Dom<Node>>>,
#[no_trace]
url: ServoUrl,
parsing_algorithm: ParsingAlgorithm,
@ -256,7 +255,7 @@ impl Tokenizer {
document: Dom::from_ref(document),
receiver: tokenizer_receiver,
html_tokenizer_sender: to_html_tokenizer_sender,
nodes: RefCell::new(HashMap::new()),
nodes: RefCell::new(FxHashMap::default()),
url,
parsing_algorithm: algorithm,
custom_element_reaction_stack,

View file

@ -6,6 +6,7 @@ use std::cmp::Ordering;
use std::iter::Iterator;
use dom_struct::dom_struct;
use rustc_hash::FxBuildHasher;
use style::custom_properties;
use stylo_atoms::Atom;
@ -21,7 +22,7 @@ use crate::script_runtime::CanGc;
#[dom_struct]
pub(crate) struct StylePropertyMapReadOnly {
reflector: Reflector,
entries: HashMapTracedValues<Atom, Dom<CSSStyleValue>>,
entries: HashMapTracedValues<Atom, Dom<CSSStyleValue>, FxBuildHasher>,
}
impl StylePropertyMapReadOnly {

View file

@ -7,9 +7,9 @@ use std::iter::FromIterator;
use std::ptr::NonNull;
use canvas_traits::webgl::{GlType, TexFormat, WebGLSLVersion, WebGLVersion};
use fnv::{FnvHashMap, FnvHashSet};
use js::jsapi::JSObject;
use malloc_size_of::MallocSizeOf;
use rustc_hash::{FxHashMap, FxHashSet};
type GLenum = u32;
use super::wrapper::{TypedWebGLExtensionWrapper, WebGLExtensionWrapper};
@ -81,25 +81,25 @@ const DEFAULT_DISABLED_GET_VERTEX_ATTRIB_NAMES_WEBGL1: [GLenum; 1] =
/// WebGL features that are enabled/disabled by WebGL Extensions.
#[derive(JSTraceable, MallocSizeOf)]
struct WebGLExtensionFeatures {
gl_extensions: FnvHashSet<String>,
disabled_tex_types: FnvHashSet<GLenum>,
not_filterable_tex_types: FnvHashSet<GLenum>,
gl_extensions: FxHashSet<String>,
disabled_tex_types: FxHashSet<GLenum>,
not_filterable_tex_types: FxHashSet<GLenum>,
#[no_trace]
effective_tex_internal_formats: FnvHashMap<TexFormatType, TexFormat>,
effective_tex_internal_formats: FxHashMap<TexFormatType, TexFormat>,
/// WebGL Hint() targets enabled by extensions.
hint_targets: FnvHashSet<GLenum>,
hint_targets: FxHashSet<GLenum>,
/// WebGL GetParameter() names enabled by extensions.
disabled_get_parameter_names: FnvHashSet<GLenum>,
disabled_get_parameter_names: FxHashSet<GLenum>,
/// WebGL GetTexParameter() names enabled by extensions.
disabled_get_tex_parameter_names: FnvHashSet<GLenum>,
disabled_get_tex_parameter_names: FxHashSet<GLenum>,
/// WebGL GetAttribVertex() names enabled by extensions.
disabled_get_vertex_attrib_names: FnvHashSet<GLenum>,
disabled_get_vertex_attrib_names: FxHashSet<GLenum>,
/// WebGL OES_element_index_uint extension.
element_index_uint_enabled: bool,
/// WebGL EXT_blend_minmax extension.
blend_minmax_enabled: bool,
/// WebGL supported texture compression formats enabled by extensions.
tex_compression_formats: FnvHashMap<GLenum, TexCompression>,
tex_compression_formats: FxHashMap<GLenum, TexCompression>,
}
impl WebGLExtensionFeatures {
@ -199,7 +199,7 @@ impl WebGLExtensions {
if self.extensions.borrow().is_empty() {
let gl_str = cb();
self.features.borrow_mut().gl_extensions =
FnvHashSet::from_iter(gl_str.split(&[',', ' '][..]).map(|s| s.into()));
FxHashSet::from_iter(gl_str.split(&[',', ' '][..]).map(|s| s.into()));
self.register_all_extensions();
}
}
@ -382,7 +382,7 @@ impl WebGLExtensions {
}
pub(crate) fn add_tex_compression_formats(&self, formats: &[TexCompression]) {
let formats: FnvHashMap<GLenum, TexCompression> = formats
let formats: FxHashMap<GLenum, TexCompression> = formats
.iter()
.map(|&compression| (compression.format.as_gl_constant(), compression))
.collect();

View file

@ -4,13 +4,13 @@
// https://www.khronos.org/registry/webgl/specs/latest/1.0/webgl.idl
use std::cell::Cell;
use std::collections::HashSet;
use canvas_traits::webgl::{
ActiveAttribInfo, ActiveUniformBlockInfo, ActiveUniformInfo, WebGLCommand, WebGLError,
WebGLProgramId, WebGLResult, webgl_channel,
};
use dom_struct::dom_struct;
use fnv::FnvHashSet;
use crate::canvas_context::CanvasContext;
use crate::dom::bindings::cell::{DomRefCell, Ref};
@ -180,8 +180,8 @@ impl WebGLProgram {
let link_info = receiver.recv().unwrap();
{
let mut used_locs = FnvHashSet::default();
let mut used_names = FnvHashSet::default();
let mut used_locs = HashSet::new();
let mut used_names = HashSet::new();
for active_attrib in &*link_info.active_attribs {
let Some(location) = active_attrib.location else {
continue;

View file

@ -17,6 +17,7 @@ use js::jsapi::JSObject;
use js::rust::MutableHandleValue;
use js::typedarray::Float32Array;
use profile_traits::ipc;
use rustc_hash::FxBuildHasher;
use stylo_atoms::Atom;
use webxr_api::{
self, ApiSpace, ContextId as WebXRContextId, Display, EntityTypes, EnvironmentBlendMode,
@ -103,7 +104,8 @@ pub(crate) struct XRSession {
#[no_trace]
next_hit_test_id: Cell<HitTestId>,
#[ignore_malloc_size_of = "defined in webxr"]
pending_hit_test_promises: DomRefCell<HashMapTracedValues<HitTestId, Rc<Promise>>>,
pending_hit_test_promises:
DomRefCell<HashMapTracedValues<HitTestId, Rc<Promise>, FxBuildHasher>>,
/// Opaque framebuffers need to know the session is "outside of a requestAnimationFrame"
/// <https://immersive-web.github.io/webxr/#opaque-framebuffer>
outside_raf: Cell<bool>,
@ -142,7 +144,7 @@ impl XRSession {
end_promises: DomRefCell::new(vec![]),
ended: Cell::new(false),
next_hit_test_id: Cell::new(HitTestId(0)),
pending_hit_test_promises: DomRefCell::new(HashMapTracedValues::new()),
pending_hit_test_promises: DomRefCell::new(HashMapTracedValues::new_fx()),
outside_raf: Cell::new(true),
input_frames: DomRefCell::new(HashMap::new()),
framerate: Cell::new(0.0),