Skip to main content

script/runtime/
script_runtime.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5//! The script runtime contains common traits and structs commonly used by the
6//! script thread, the dom, and the worker threads.
7
8#![expect(dead_code)]
9
10use core::ffi::c_char;
11use std::cell::{Cell, LazyCell, RefCell};
12use std::collections::{HashMap, HashSet};
13use std::ffi::{CStr, CString};
14use std::io::{Write, stdout};
15use std::ops::{Deref, DerefMut};
16use std::os::raw::c_void;
17use std::ptr::NonNull;
18use std::rc::Weak;
19use std::time::Instant;
20use std::{os, ptr};
21
22use background_hang_monitor_api::ScriptHangAnnotation;
23use js::context::JSContext;
24use js::conversions::jsstr_to_string;
25use js::gc::StackGCVector;
26use js::glue::{
27    DispatchablePointer, RUST_js_GetErrorMessage, RegisterScriptEnvironmentPreparer,
28    RunScriptEnvironmentPreparerClosure, SetBuildId, StreamConsumerConsumeChunk,
29    StreamConsumerNoteResponseURLs, StreamConsumerStreamEnd, StreamConsumerStreamError,
30};
31use js::jsapi::{
32    AsmJSOption, BuildIdCharVector, CompilationType, Dispatchable_MaybeShuttingDown, GCDescription,
33    GCOptions, GCProgress, GCReason, Handle as RawHandle, HandleObject, HandleString,
34    HandleValue as RawHandleValue, Heap, JSContext as RawJSContext, JSGCParamKey, JSGCStatus,
35    JSJitCompilerOption, JSObject, JSSecurityCallbacks, JSString, JSTracer, MimeType,
36    MutableHandleString, PromiseRejectionHandlingState, RuntimeCode,
37    ScriptEnvironmentPreparer_Closure, SetProcessBuildIdOp, StreamConsumer as JSStreamConsumer,
38};
39use js::jsval::{JSVal, UndefinedValue};
40use js::panic::wrap_panic;
41use js::realm::CurrentRealm;
42pub(crate) use js::rust::ThreadSafeJSContext;
43use js::rust::wrappers2::{
44    CollectServoSizes, ContextOptionsRef, DispatchableRun, InitConsumeStreamCallback,
45    JS_AddExtraGCRootsTracer, JS_GetPromiseResult, JS_InitDestroyPrincipalsCallback,
46    JS_InitReadPrincipalsCallback, JS_NewStringCopyUTF8N, JS_SetGCCallback, JS_SetGCParameter,
47    JS_SetGlobalJitCompilerOption, JS_SetOffthreadIonCompilationEnabled, JS_SetSecurityCallbacks,
48    SetDOMCallbacks, SetGCSliceCallback, SetPreserveWrapperCallbacks,
49    SetPromiseRejectionTrackerCallback, SetUpEventLoopDispatch,
50};
51use js::rust::{
52    Handle, HandleObject as RustHandleObject, HandleValue, IntoHandle, ParentRuntime,
53    Runtime as RustRuntime, Trace,
54};
55use malloc_size_of::MallocSizeOfOps;
56use malloc_size_of_derive::MallocSizeOf;
57use profile_traits::mem::{Report, ReportKind};
58use profile_traits::path;
59use profile_traits::time::ProfilerCategory;
60use script_bindings::reflector::DomObject;
61use script_bindings::script_runtime::{mark_runtime_dead, runtime_is_alive, temp_cx};
62use script_bindings::settings_stack::run_a_script;
63use servo_config::opts::{self, DiagnosticsLoggingOption};
64use servo_config::pref;
65use servo_url::ServoUrl;
66use style::thread_state::{self, ThreadState};
67
68use crate::dom::bindings::codegen::Bindings::ResponseBinding::Response_Binding::ResponseMethods;
69use crate::dom::bindings::codegen::Bindings::ResponseBinding::ResponseType as DOMResponseType;
70use crate::dom::bindings::codegen::UnionTypes::TrustedScriptOrString;
71use crate::dom::bindings::conversions::{
72    get_dom_class, private_from_object, root_from_handleobject, root_from_object,
73};
74use crate::dom::bindings::error::{Error, report_pending_exception, throw_dom_exception};
75use crate::dom::bindings::inheritance::Castable;
76use crate::dom::bindings::refcounted::{
77    LivePromiseReferences, Trusted, TrustedPromise, trace_refcounted_objects,
78};
79use crate::dom::bindings::reflector::DomGlobal;
80use crate::dom::bindings::root::trace_roots;
81use crate::dom::bindings::str::DOMString;
82use crate::dom::bindings::utils::DOM_CALLBACKS;
83use crate::dom::bindings::{principals, settings_stack};
84use crate::dom::console::stringify_handle_value;
85use crate::dom::csp::CspReporting;
86use crate::dom::event::{Event, EventBubbles, EventCancelable};
87use crate::dom::eventtarget::EventTarget;
88use crate::dom::globalscope::GlobalScope;
89use crate::dom::promise::Promise;
90use crate::dom::promiserejectionevent::PromiseRejectionEvent;
91use crate::dom::response::Response;
92use crate::dom::trustedtypes::trustedscript::TrustedScript;
93use crate::engine::handle::current_js_engine_handle;
94use crate::messaging::{CommonScriptMsg, ScriptEventLoopSender};
95use crate::modules::script_module::EnsureModuleHooksInitialized;
96use crate::realms::enter_auto_realm;
97use crate::runtime::job_queue::{JobQueue, job_queue_clear};
98use crate::tasks::task_source::TaskSourceName;
99use crate::{DomTypeHolder, ScriptThread};
100
101static SECURITY_CALLBACKS: JSSecurityCallbacks = JSSecurityCallbacks {
102    contentSecurityPolicyAllows: Some(content_security_policy_allows),
103    codeForEvalGets: Some(code_for_eval_gets),
104    subsumes: Some(principals::subsumes),
105};
106
107#[derive(Clone, Copy, Debug, Eq, Hash, JSTraceable, MallocSizeOf, PartialEq)]
108pub(crate) enum ScriptThreadEventCategory {
109    SpawnPipeline,
110    ConstellationMsg,
111    DatabaseAccessEvent,
112    DevtoolsMsg,
113    DocumentEvent,
114    FileRead,
115    FontLoading,
116    FormPlannedNavigation,
117    GeolocationEvent,
118    ImageCacheMsg,
119    InputEvent,
120    NavigationAndTraversalEvent,
121    NetworkEvent,
122    PortMessage,
123    Rendering,
124    Resize,
125    ScriptEvent,
126    SetScrollState,
127    SetViewport,
128    StylesheetLoad,
129    TimerEvent,
130    UpdateReplacedElement,
131    WebSocketEvent,
132    WorkerEvent,
133    WorkletEvent,
134    ServiceWorkerEvent,
135    EnterFullscreen,
136    ExitFullscreen,
137    PerformanceTimelineTask,
138    #[cfg(feature = "webgpu")]
139    WebGPUMsg,
140}
141
142impl From<ScriptThreadEventCategory> for ProfilerCategory {
143    fn from(category: ScriptThreadEventCategory) -> Self {
144        match category {
145            ScriptThreadEventCategory::SpawnPipeline => ProfilerCategory::ScriptSpawnPipeline,
146            ScriptThreadEventCategory::ConstellationMsg => ProfilerCategory::ScriptConstellationMsg,
147            ScriptThreadEventCategory::DatabaseAccessEvent => {
148                ProfilerCategory::ScriptDatabaseAccessEvent
149            },
150            ScriptThreadEventCategory::DevtoolsMsg => ProfilerCategory::ScriptDevtoolsMsg,
151            ScriptThreadEventCategory::DocumentEvent => ProfilerCategory::ScriptDocumentEvent,
152            ScriptThreadEventCategory::EnterFullscreen => ProfilerCategory::ScriptEnterFullscreen,
153            ScriptThreadEventCategory::ExitFullscreen => ProfilerCategory::ScriptExitFullscreen,
154            ScriptThreadEventCategory::FileRead => ProfilerCategory::ScriptFileRead,
155            ScriptThreadEventCategory::FontLoading => ProfilerCategory::ScriptFontLoading,
156            ScriptThreadEventCategory::FormPlannedNavigation => {
157                ProfilerCategory::ScriptPlannedNavigation
158            },
159            ScriptThreadEventCategory::GeolocationEvent => ProfilerCategory::ScriptGeolocationEvent,
160            ScriptThreadEventCategory::NavigationAndTraversalEvent => {
161                ProfilerCategory::ScriptNavigationAndTraversalEvent
162            },
163            ScriptThreadEventCategory::ImageCacheMsg => ProfilerCategory::ScriptImageCacheMsg,
164            ScriptThreadEventCategory::InputEvent => ProfilerCategory::ScriptInputEvent,
165            ScriptThreadEventCategory::NetworkEvent => ProfilerCategory::ScriptNetworkEvent,
166            ScriptThreadEventCategory::PerformanceTimelineTask => {
167                ProfilerCategory::ScriptPerformanceEvent
168            },
169            ScriptThreadEventCategory::PortMessage => ProfilerCategory::ScriptPortMessage,
170            ScriptThreadEventCategory::Resize => ProfilerCategory::ScriptResize,
171            ScriptThreadEventCategory::Rendering => ProfilerCategory::ScriptRendering,
172            ScriptThreadEventCategory::ScriptEvent => ProfilerCategory::ScriptEvent,
173            ScriptThreadEventCategory::ServiceWorkerEvent => {
174                ProfilerCategory::ScriptServiceWorkerEvent
175            },
176            ScriptThreadEventCategory::SetScrollState => ProfilerCategory::ScriptSetScrollState,
177            ScriptThreadEventCategory::SetViewport => ProfilerCategory::ScriptSetViewport,
178            ScriptThreadEventCategory::StylesheetLoad => ProfilerCategory::ScriptStylesheetLoad,
179            ScriptThreadEventCategory::TimerEvent => ProfilerCategory::ScriptTimerEvent,
180            ScriptThreadEventCategory::UpdateReplacedElement => {
181                ProfilerCategory::ScriptUpdateReplacedElement
182            },
183            ScriptThreadEventCategory::WebSocketEvent => ProfilerCategory::ScriptWebSocketEvent,
184            ScriptThreadEventCategory::WorkerEvent => ProfilerCategory::ScriptWorkerEvent,
185            ScriptThreadEventCategory::WorkletEvent => ProfilerCategory::ScriptWorkletEvent,
186            #[cfg(feature = "webgpu")]
187            ScriptThreadEventCategory::WebGPUMsg => ProfilerCategory::ScriptWebGPUMsg,
188        }
189    }
190}
191
192impl From<ScriptThreadEventCategory> for ScriptHangAnnotation {
193    fn from(category: ScriptThreadEventCategory) -> Self {
194        match category {
195            ScriptThreadEventCategory::SpawnPipeline => ScriptHangAnnotation::SpawnPipeline,
196            ScriptThreadEventCategory::ConstellationMsg => ScriptHangAnnotation::ConstellationMsg,
197            ScriptThreadEventCategory::DatabaseAccessEvent => {
198                ScriptHangAnnotation::DatabaseAccessEvent
199            },
200            ScriptThreadEventCategory::DevtoolsMsg => ScriptHangAnnotation::DevtoolsMsg,
201            ScriptThreadEventCategory::DocumentEvent => ScriptHangAnnotation::DocumentEvent,
202            ScriptThreadEventCategory::InputEvent => ScriptHangAnnotation::InputEvent,
203            ScriptThreadEventCategory::FileRead => ScriptHangAnnotation::FileRead,
204            ScriptThreadEventCategory::FontLoading => ScriptHangAnnotation::FontLoading,
205            ScriptThreadEventCategory::FormPlannedNavigation => {
206                ScriptHangAnnotation::FormPlannedNavigation
207            },
208            ScriptThreadEventCategory::GeolocationEvent => ScriptHangAnnotation::GeolocationEvent,
209            ScriptThreadEventCategory::NavigationAndTraversalEvent => {
210                ScriptHangAnnotation::NavigationAndTraversalEvent
211            },
212            ScriptThreadEventCategory::ImageCacheMsg => ScriptHangAnnotation::ImageCacheMsg,
213            ScriptThreadEventCategory::NetworkEvent => ScriptHangAnnotation::NetworkEvent,
214            ScriptThreadEventCategory::Rendering => ScriptHangAnnotation::Rendering,
215            ScriptThreadEventCategory::Resize => ScriptHangAnnotation::Resize,
216            ScriptThreadEventCategory::ScriptEvent => ScriptHangAnnotation::ScriptEvent,
217            ScriptThreadEventCategory::SetScrollState => ScriptHangAnnotation::SetScrollState,
218            ScriptThreadEventCategory::SetViewport => ScriptHangAnnotation::SetViewport,
219            ScriptThreadEventCategory::StylesheetLoad => ScriptHangAnnotation::StylesheetLoad,
220            ScriptThreadEventCategory::TimerEvent => ScriptHangAnnotation::TimerEvent,
221            ScriptThreadEventCategory::UpdateReplacedElement => {
222                ScriptHangAnnotation::UpdateReplacedElement
223            },
224            ScriptThreadEventCategory::WebSocketEvent => ScriptHangAnnotation::WebSocketEvent,
225            ScriptThreadEventCategory::WorkerEvent => ScriptHangAnnotation::WorkerEvent,
226            ScriptThreadEventCategory::WorkletEvent => ScriptHangAnnotation::WorkletEvent,
227            ScriptThreadEventCategory::ServiceWorkerEvent => {
228                ScriptHangAnnotation::ServiceWorkerEvent
229            },
230            ScriptThreadEventCategory::EnterFullscreen => ScriptHangAnnotation::EnterFullscreen,
231            ScriptThreadEventCategory::ExitFullscreen => ScriptHangAnnotation::ExitFullscreen,
232            ScriptThreadEventCategory::PerformanceTimelineTask => {
233                ScriptHangAnnotation::PerformanceTimelineTask
234            },
235            ScriptThreadEventCategory::PortMessage => ScriptHangAnnotation::PortMessage,
236            #[cfg(feature = "webgpu")]
237            ScriptThreadEventCategory::WebGPUMsg => ScriptHangAnnotation::WebGPUMsg,
238        }
239    }
240}
241
242#[expect(unsafe_code)]
243/// <https://html.spec.whatwg.org/multipage/#the-hostpromiserejectiontracker-implementation>
244unsafe extern "C" fn promise_rejection_tracker(
245    cx: *mut RawJSContext,
246    muted_errors: bool,
247    promise: HandleObject,
248    state: PromiseRejectionHandlingState,
249    _data: *mut c_void,
250) {
251    // Step 1. Let script be the running script.
252    // Step 2. If script is a classic script and script's muted errors is true, then return.
253    if muted_errors {
254        return;
255    }
256
257    // Step 3.
258    // SAFETY: it is safe to construct a JSContext from engine hook.
259    let mut cx = unsafe { JSContext::from_ptr(NonNull::new(cx).unwrap()) };
260    let mut realm = CurrentRealm::assert(&mut cx);
261
262    let global = GlobalScope::from_current_realm(&mut realm);
263    let cx = &mut realm;
264
265    wrap_panic(&mut || {
266        match state {
267            // Step 4.
268            PromiseRejectionHandlingState::Unhandled => {
269                global.add_uncaught_rejection(promise);
270            },
271            // Step 5.
272            PromiseRejectionHandlingState::Handled => {
273                // Step 5-1.
274                if global
275                    .get_uncaught_rejections()
276                    .borrow()
277                    .contains(&Heap::boxed(promise.get()))
278                {
279                    global.remove_uncaught_rejection(promise);
280                    return;
281                }
282
283                // Step 5-2.
284                if !global
285                    .get_consumed_rejections()
286                    .borrow()
287                    .contains(&Heap::boxed(promise.get()))
288                {
289                    return;
290                }
291
292                // Step 5-3.
293                global.remove_consumed_rejection(promise);
294
295                let target = Trusted::new(global.upcast::<EventTarget>());
296                let promise =
297                    Promise::new_with_js_promise(cx, unsafe { Handle::from_raw(promise) });
298                let trusted_promise = TrustedPromise::new(promise);
299
300                // Step 5-4.
301                global.task_manager().dom_manipulation_task_source().queue(
302                task!(rejection_handled_event: move |cx| {
303                    let target = target.root();
304                    let root_promise = trusted_promise.root(cx);
305
306                    rooted!(&in(cx) let mut reason = UndefinedValue());
307                    unsafe {
308                        JS_GetPromiseResult(root_promise.reflector().get_jsobject(), reason.handle_mut());
309                    }
310
311                    let event = PromiseRejectionEvent::new(
312                        cx,
313                        &target.global(),
314                        atom!("rejectionhandled"),
315                        EventBubbles::DoesNotBubble,
316                        EventCancelable::Cancelable,
317                        &root_promise,
318                        reason.handle(),
319                    );
320
321                    event.upcast::<Event>().fire(cx, &target);
322                })
323                );
324            },
325        };
326    })
327}
328
329#[expect(unsafe_code)]
330fn safely_convert_null_to_string(cx: &JSContext, str_: HandleString) -> DOMString {
331    DOMString::from(match std::ptr::NonNull::new(*str_) {
332        None => String::new(),
333        Some(str_) => unsafe { jsstr_to_string(cx, str_) },
334    })
335}
336
337#[expect(unsafe_code)]
338unsafe extern "C" fn code_for_eval_gets(
339    cx: *mut RawJSContext,
340    code: HandleObject,
341    code_for_eval: MutableHandleString,
342) -> bool {
343    // SAFETY: We are in SM hook
344    let mut cx = unsafe { JSContext::from_ptr(NonNull::new(cx).unwrap()) };
345    let cx = &mut cx;
346    if let Ok(trusted_script) = unsafe { root_from_object::<TrustedScript>(cx, code.get()) } {
347        let script_str = trusted_script.data().str();
348        let s = js::conversions::Utf8Chars::from(&*script_str);
349        let new_string = unsafe { JS_NewStringCopyUTF8N(cx, &*s as *const _) };
350        code_for_eval.set(new_string);
351    }
352    true
353}
354
355#[expect(unsafe_code)]
356unsafe extern "C" fn content_security_policy_allows(
357    cx: *mut RawJSContext,
358    runtime_code: RuntimeCode,
359    code_string: HandleString,
360    compilation_type: CompilationType,
361    parameter_strings: RawHandle<StackGCVector<*mut JSString>>,
362    body_string: HandleString,
363    parameter_args: RawHandle<StackGCVector<JSVal>>,
364    body_arg: RawHandleValue,
365    can_compile_strings: *mut bool,
366) -> bool {
367    let mut allowed = false;
368    // SAFETY: We are in SM hook
369    let mut cx = unsafe { JSContext::from_ptr(NonNull::new(cx).unwrap()) };
370    let cx = &mut cx;
371    wrap_panic(&mut || {
372        // SpiderMonkey provides null pointer when executing webassembly.
373        let mut realm = CurrentRealm::assert(cx);
374        let global = GlobalScope::from_current_realm(&mut realm);
375        let csp_list = global.get_csp_list();
376
377        // If we don't have any CSP checks to run, short-circuit all logic here
378        allowed = csp_list.is_none() ||
379            match runtime_code {
380                RuntimeCode::JS => {
381                    let parameter_strings = unsafe { Handle::from_raw(parameter_strings) };
382                    let parameter_strings_length = parameter_strings.len();
383                    let mut parameter_strings_vec =
384                        Vec::with_capacity(parameter_strings_length as usize);
385
386                    for i in 0..parameter_strings_length {
387                        let Some(str_) = parameter_strings.at(i) else {
388                            unreachable!();
389                        };
390                        parameter_strings_vec.push(safely_convert_null_to_string(cx, str_.into()));
391                    }
392
393                    let parameter_args = unsafe { Handle::from_raw(parameter_args) };
394                    let parameter_args_length = parameter_args.len();
395                    let mut parameter_args_vec = Vec::with_capacity(parameter_args_length as usize);
396
397                    for i in 0..parameter_args_length {
398                        let Some(arg) = parameter_args.at(i) else {
399                            unreachable!();
400                        };
401                        let value = arg.into_handle().get();
402                        if value.is_object() {
403                            if let Ok(trusted_script) =
404                                unsafe { root_from_object::<TrustedScript>(cx, value.to_object()) }
405                            {
406                                parameter_args_vec
407                                    .push(TrustedScriptOrString::TrustedScript(trusted_script));
408                            } else {
409                                // It's not a trusted script but a different object. Treat it
410                                // as if it is a string, since we don't need the actual contents
411                                // of the object.
412                                parameter_args_vec
413                                    .push(TrustedScriptOrString::String(DOMString::new()));
414                            }
415                        } else if value.is_string() {
416                            // We don't need to know the specific string, only that it is untrusted
417                            parameter_args_vec
418                                .push(TrustedScriptOrString::String(DOMString::new()));
419                        } else {
420                            unreachable!();
421                        }
422                    }
423
424                    let code_string = safely_convert_null_to_string(cx, code_string);
425                    let body_string = safely_convert_null_to_string(cx, body_string);
426
427                    TrustedScript::can_compile_string_with_trusted_type(
428                        cx,
429                        &global,
430                        code_string,
431                        compilation_type,
432                        parameter_strings_vec,
433                        body_string,
434                        parameter_args_vec,
435                        unsafe { HandleValue::from_raw(body_arg) },
436                    )
437                },
438                RuntimeCode::WASM => global
439                    .get_csp_list()
440                    .is_wasm_evaluation_allowed(cx, &global),
441            };
442    });
443    unsafe { *can_compile_strings = allowed };
444    true
445}
446
447#[expect(unsafe_code)]
448/// <https://html.spec.whatwg.org/multipage/#notify-about-rejected-promises>
449pub(crate) fn notify_about_rejected_promises(cx: &mut JSContext, global: &GlobalScope) {
450    // Step 1. Let list be a clone of global's about-to-be-notified rejected promises list.
451    let uncaught_rejections: Vec<TrustedPromise> = {
452        global
453            .get_uncaught_rejections()
454            .borrow()
455            .iter()
456            .map(|promise| {
457                let promise =
458                    Promise::new_with_js_promise(cx, unsafe { Handle::from_raw(promise.handle()) });
459
460                TrustedPromise::new(promise)
461            })
462            .collect()
463    };
464    global.get_uncaught_rejections().safe_borrow_mut(cx).clear();
465
466    // Step 2. If list is empty, then return.
467    if uncaught_rejections.is_empty() {
468        return;
469    }
470
471    // Step 3. Empty global's about-to-be-notified rejected promises list.
472    // NOTE: We did this as part of Step 1. using the "drain(..)" call.
473
474    // Step 4. Queue a global task on the DOM manipulation task source given global to run the following step:
475    let target = Trusted::new(global.upcast::<EventTarget>());
476    global.task_manager().dom_manipulation_task_source().queue(
477        task!(unhandled_rejection_event: move |cx| {
478            let target = target.root();
479
480            // Step 4.1 For each promise p of list:
481            for promise in uncaught_rejections {
482                let promise = promise.root(cx);
483
484                // 4.1.1 If p.[[PromiseIsHandled]] is true, then continue.
485                if promise.get_promise_is_handled() {
486                    continue;
487                }
488
489                // Step 4.1.2 Let notCanceled be the result of firing an event named unhandledrejection at global,
490                // using PromiseRejectionEvent, with the cancelable attribute initialized to true,
491                // the promise attribute initialized to p, and the reason attribute initialized to p.[[PromiseResult]].
492                rooted!(&in(cx) let mut reason = UndefinedValue());
493                unsafe {
494                    JS_GetPromiseResult(promise.reflector().get_jsobject(), reason.handle_mut());
495                }
496
497                log::error!(
498                    "Unhandled promise rejection: {}",
499                    stringify_handle_value( cx, reason.handle())
500                );
501
502                let event = PromiseRejectionEvent::new(
503                    cx,
504                    &target.global(),
505                    atom!("unhandledrejection"),
506                    EventBubbles::DoesNotBubble,
507                    EventCancelable::Cancelable,
508                    &promise,
509                    reason.handle(),
510                );
511                event.upcast::<Event>().fire(cx, &target);
512
513                // TODO: Step 4.1.3 If notCanceled is true, then the user agent may report
514                // p.[[PromiseResult]] to a developer console.
515
516                // Step 4.1.4 If p.[[PromiseIsHandled]] is false, then append p to global's outstanding
517                // rejected promises weak set.
518                if !promise.get_promise_is_handled() {
519                    target.global().add_consumed_rejection(promise.reflector().get_jsobject().into_handle());
520                }
521            }
522        })
523    );
524}
525
526/// Data that is sent to SpiderMonkey runtime callbacks as a pointer, which allows access
527/// to the `Runtime` state.
528#[derive(Default, JSTraceable, MallocSizeOf)]
529struct RuntimeCallbackData {
530    script_event_loop_sender: Option<ScriptEventLoopSender>,
531    #[no_trace]
532    #[ignore_malloc_size_of = "ScriptThread measures its own memory itself."]
533    script_thread: Option<Weak<ScriptThread>>,
534}
535
536#[derive(JSTraceable, MallocSizeOf)]
537pub(crate) struct Runtime {
538    #[ignore_malloc_size_of = "Type from mozjs"]
539    rt: RustRuntime,
540    job_queue: JobQueue,
541    /// The data that is set on the SpiderMonkey runtime callbacks as a pointer.
542    runtime_callback_data: Box<RuntimeCallbackData>,
543}
544
545impl Runtime {
546    /// Create a new runtime, optionally with the given [`SendableTaskSource`] for networking.
547    ///
548    /// # Safety
549    ///
550    /// If panicking does not abort the program, any threads with child runtimes will continue
551    /// executing after the thread with the parent runtime panics, but they will be in an
552    /// invalid and undefined state.
553    ///
554    /// This, like many calls to SpiderMoney API, is unsafe.
555    #[expect(unsafe_code)]
556    pub(crate) fn new(main_thread_sender: Option<ScriptEventLoopSender>) -> Runtime {
557        unsafe { Self::new_with_parent(None, main_thread_sender) }
558    }
559
560    #[allow(unsafe_code)]
561    /// ## Safety
562    /// - only one `JSContext` can exist on the thread at a time (see note in [JSContext::from_ptr])
563    /// - the `JSContext` must not outlive the `Runtime`
564    pub(crate) unsafe fn cx(&self) -> JSContext {
565        unsafe { JSContext::from_ptr(RustRuntime::get().unwrap()) }
566    }
567
568    /// Create a new runtime, optionally with the given [`ParentRuntime`] and [`SendableTaskSource`]
569    /// for networking.
570    ///
571    /// # Safety
572    ///
573    /// If panicking does not abort the program, any threads with child runtimes will continue
574    /// executing after the thread with the parent runtime panics, but they will be in an
575    /// invalid and undefined state.
576    ///
577    /// The `parent` pointer in the [`ParentRuntime`] argument must point to a valid object in memory.
578    ///
579    /// This, like many calls to the SpiderMoney API, is unsafe.
580    #[expect(unsafe_code)]
581    pub(crate) unsafe fn new_with_parent(
582        parent: Option<ParentRuntime>,
583        script_event_loop_sender: Option<ScriptEventLoopSender>,
584    ) -> Runtime {
585        let mut runtime = if let Some(parent) = parent {
586            unsafe { RustRuntime::create_with_parent(parent) }
587        } else {
588            RustRuntime::new(current_js_engine_handle())
589        };
590        let cx = runtime.cx();
591
592        let have_event_loop_sender = script_event_loop_sender.is_some();
593        let runtime_callback_data = Box::new(RuntimeCallbackData {
594            script_event_loop_sender,
595            script_thread: None,
596        });
597        let runtime_callback_data = Box::into_raw(runtime_callback_data);
598
599        unsafe {
600            JS_AddExtraGCRootsTracer(
601                cx,
602                Some(trace_rust_roots),
603                runtime_callback_data as *mut c_void,
604            );
605
606            JS_SetSecurityCallbacks(cx, &SECURITY_CALLBACKS);
607
608            JS_InitDestroyPrincipalsCallback(cx, Some(principals::destroy_servo_jsprincipal));
609            JS_InitReadPrincipalsCallback(cx, Some(principals::read_jsprincipal));
610
611            // Needed for debug assertions about whether GC is running.
612            if cfg!(debug_assertions) {
613                JS_SetGCCallback(cx, Some(debug_gc_callback), ptr::null_mut());
614            }
615
616            if opts::get()
617                .debug
618                .is_enabled(DiagnosticsLoggingOption::GcProfile)
619            {
620                SetGCSliceCallback(cx, Some(gc_slice_callback));
621            }
622        }
623
624        unsafe extern "C" fn empty_wrapper_callback(_: *mut RawJSContext, _: HandleObject) -> bool {
625            true
626        }
627        unsafe extern "C" fn empty_has_released_callback(_: HandleObject) -> bool {
628            // fixme: return true when the Drop impl for a DOM object has been invoked
629            false
630        }
631
632        unsafe {
633            SetDOMCallbacks(cx, &DOM_CALLBACKS);
634            SetPreserveWrapperCallbacks(
635                cx,
636                Some(empty_wrapper_callback),
637                Some(empty_has_released_callback),
638            );
639        }
640
641        unsafe extern "C" fn dispatch_to_event_loop(
642            data: *mut c_void,
643            dispatchable: *mut DispatchablePointer,
644        ) -> bool {
645            let runtime_callback_data: &RuntimeCallbackData =
646                unsafe { &*(data as *mut RuntimeCallbackData) };
647            let Some(script_event_loop_sender) =
648                runtime_callback_data.script_event_loop_sender.as_ref()
649            else {
650                return false;
651            };
652
653            let runnable = Runnable(dispatchable);
654            let task = task!(dispatch_to_event_loop_message: move |cx| {
655                runnable.run(cx, Dispatchable_MaybeShuttingDown::NotShuttingDown);
656            });
657
658            script_event_loop_sender
659                .send(CommonScriptMsg::Task(
660                    ScriptThreadEventCategory::NetworkEvent,
661                    Box::new(task),
662                    None, /* pipeline_id */
663                    TaskSourceName::Networking,
664                ))
665                .is_ok()
666        }
667
668        if have_event_loop_sender {
669            unsafe {
670                SetUpEventLoopDispatch(
671                    cx,
672                    Some(dispatch_to_event_loop),
673                    None,
674                    None,
675                    runtime_callback_data as *mut c_void,
676                );
677            }
678        }
679
680        unsafe {
681            InitConsumeStreamCallback(cx, Some(consume_stream), Some(report_stream_error));
682        }
683
684        let cx_opts;
685        let job_queue;
686        unsafe {
687            let cx = runtime.cx();
688            job_queue = JobQueue::new();
689            job_queue.set_on_context(cx);
690            SetPromiseRejectionTrackerCallback(
691                cx,
692                Some(promise_rejection_tracker),
693                ptr::null_mut(),
694            );
695
696            RegisterScriptEnvironmentPreparer(
697                cx.raw_cx(),
698                Some(invoke_script_environment_preparer),
699            );
700
701            EnsureModuleHooksInitialized(runtime.rt());
702
703            let cx = runtime.cx();
704
705            set_gc_zeal_options(cx.raw_cx());
706
707            // Enable or disable the JITs.
708            cx_opts = &mut *ContextOptionsRef(cx);
709            JS_SetGlobalJitCompilerOption(
710                cx,
711                JSJitCompilerOption::JSJITCOMPILER_BASELINE_INTERPRETER_ENABLE,
712                pref!(js_baseline_interpreter_enabled) as u32,
713            );
714            JS_SetGlobalJitCompilerOption(
715                cx,
716                JSJitCompilerOption::JSJITCOMPILER_BASELINE_ENABLE,
717                pref!(js_baseline_jit_enabled) as u32,
718            );
719            JS_SetGlobalJitCompilerOption(
720                cx,
721                JSJitCompilerOption::JSJITCOMPILER_ION_ENABLE,
722                pref!(js_ion_enabled) as u32,
723            );
724        }
725        cx_opts.compileOptions_.asmJSOption_ = if pref!(js_asmjs_enabled) {
726            AsmJSOption::Enabled
727        } else {
728            AsmJSOption::DisabledByAsmJSPref
729        };
730        let wasm_enabled = pref!(js_wasm_enabled);
731        cx_opts.set_wasm_(wasm_enabled);
732        if wasm_enabled {
733            // If WASM is enabled without setting the buildIdOp,
734            // initializing a module will report an out of memory error.
735            // https://dxr.mozilla.org/mozilla-central/source/js/src/wasm/WasmTypes.cpp#458
736            unsafe { SetProcessBuildIdOp(Some(servo_build_id)) };
737        }
738        cx_opts.set_wasmBaseline_(pref!(js_wasm_baseline_enabled));
739        cx_opts.set_wasmIon_(pref!(js_wasm_ion_enabled));
740
741        unsafe {
742            let cx = runtime.cx();
743            // TODO: handle js.throw_on_asmjs_validation_failure (needs new Spidermonkey)
744            JS_SetGlobalJitCompilerOption(
745                cx,
746                JSJitCompilerOption::JSJITCOMPILER_NATIVE_REGEXP_ENABLE,
747                pref!(js_native_regex_enabled) as u32,
748            );
749            JS_SetOffthreadIonCompilationEnabled(cx, pref!(js_offthread_compilation_enabled));
750            JS_SetGlobalJitCompilerOption(
751                cx,
752                JSJitCompilerOption::JSJITCOMPILER_BASELINE_WARMUP_TRIGGER,
753                if pref!(js_baseline_jit_unsafe_eager_compilation_enabled) {
754                    0
755                } else {
756                    u32::MAX
757                },
758            );
759            JS_SetGlobalJitCompilerOption(
760                cx,
761                JSJitCompilerOption::JSJITCOMPILER_ION_NORMAL_WARMUP_TRIGGER,
762                if pref!(js_ion_unsafe_eager_compilation_enabled) {
763                    0
764                } else {
765                    u32::MAX
766                },
767            );
768            // TODO: handle js.discard_system_source.enabled
769            // TODO: handle js.asyncstack.enabled (needs new Spidermonkey)
770            // TODO: handle js.throw_on_debugee_would_run (needs new Spidermonkey)
771            // TODO: handle js.dump_stack_on_debugee_would_run (needs new Spidermonkey)
772            // TODO: handle js.shared_memory.enabled
773            JS_SetGCParameter(
774                cx,
775                JSGCParamKey::JSGC_MAX_BYTES,
776                in_range(pref!(js_mem_max), 1, 0x100)
777                    .map(|val| (val * 1024 * 1024) as u32)
778                    .unwrap_or(u32::MAX),
779            );
780
781            // Pre-barriers aren't implemented correctly at the moment, so this preference
782            // defaults to false.
783            JS_SetGCParameter(
784                cx,
785                JSGCParamKey::JSGC_INCREMENTAL_GC_ENABLED,
786                pref!(js_mem_gc_incremental_enabled) as u32,
787            );
788
789            JS_SetGCParameter(
790                cx,
791                JSGCParamKey::JSGC_PER_ZONE_GC_ENABLED,
792                pref!(js_mem_gc_per_zone_enabled) as u32,
793            );
794            if let Some(val) = in_range(pref!(js_mem_gc_incremental_slice_ms), 0, 100_000) {
795                JS_SetGCParameter(cx, JSGCParamKey::JSGC_SLICE_TIME_BUDGET_MS, val as u32);
796            }
797            JS_SetGCParameter(
798                cx,
799                JSGCParamKey::JSGC_COMPACTING_ENABLED,
800                pref!(js_mem_gc_compacting_enabled) as u32,
801            );
802
803            if let Some(val) = in_range(pref!(js_mem_gc_high_frequency_time_limit_ms), 0, 10_000) {
804                JS_SetGCParameter(cx, JSGCParamKey::JSGC_HIGH_FREQUENCY_TIME_LIMIT, val as u32);
805            }
806            if let Some(val) = in_range(pref!(js_mem_gc_low_frequency_heap_growth), 0, 10_000) {
807                JS_SetGCParameter(cx, JSGCParamKey::JSGC_LOW_FREQUENCY_HEAP_GROWTH, val as u32);
808            }
809            if let Some(val) = in_range(pref!(js_mem_gc_high_frequency_heap_growth_min), 0, 10_000)
810            {
811                JS_SetGCParameter(
812                    cx,
813                    JSGCParamKey::JSGC_HIGH_FREQUENCY_LARGE_HEAP_GROWTH,
814                    val as u32,
815                );
816            }
817            if let Some(val) = in_range(pref!(js_mem_gc_high_frequency_heap_growth_max), 0, 10_000)
818            {
819                JS_SetGCParameter(
820                    cx,
821                    JSGCParamKey::JSGC_HIGH_FREQUENCY_SMALL_HEAP_GROWTH,
822                    val as u32,
823                );
824            }
825            if let Some(val) = in_range(pref!(js_mem_gc_high_frequency_low_limit_mb), 0, 10_000) {
826                JS_SetGCParameter(cx, JSGCParamKey::JSGC_SMALL_HEAP_SIZE_MAX, val as u32);
827            }
828            if let Some(val) = in_range(pref!(js_mem_gc_high_frequency_high_limit_mb), 0, 10_000) {
829                JS_SetGCParameter(cx, JSGCParamKey::JSGC_LARGE_HEAP_SIZE_MIN, val as u32);
830            }
831            if let Some(val) = in_range(pref!(js_mem_gc_empty_chunk_count_min), 0, 10_000) {
832                JS_SetGCParameter(cx, JSGCParamKey::JSGC_MIN_EMPTY_CHUNK_COUNT, val as u32);
833            }
834            if let Some(val) = in_range(pref!(js_mem_gc_malloc_threshold_base_mb), 0, 10_000) {
835                JS_SetGCParameter(cx, JSGCParamKey::JSGC_MALLOC_THRESHOLD_BASE, val as u32);
836            }
837            if let Some(val) = in_range(pref!(js_mem_gc_urgent_threshold_mb), 0, 10_000) {
838                JS_SetGCParameter(cx, JSGCParamKey::JSGC_URGENT_THRESHOLD_MB, val as u32);
839            }
840        }
841        Runtime {
842            rt: runtime,
843            job_queue,
844            runtime_callback_data: unsafe { Box::from_raw(runtime_callback_data) },
845        }
846    }
847
848    pub(crate) fn set_script_thread(&mut self, script_thread: Weak<ScriptThread>) {
849        self.runtime_callback_data
850            .script_thread
851            .replace(script_thread);
852    }
853
854    pub(crate) fn thread_safe_js_context(&self) -> ThreadSafeJSContext {
855        self.rt.thread_safe_js_context()
856    }
857}
858
859impl Drop for Runtime {
860    fn drop(&mut self) {
861        // Clear our main microtask_queue.
862        job_queue_clear(self.rt.cx_no_gc());
863
864        LivePromiseReferences::destruct();
865        script_bindings::refcounted::LiveDOMReferences::destruct();
866        mark_runtime_dead();
867    }
868}
869
870impl Deref for Runtime {
871    type Target = RustRuntime;
872    fn deref(&self) -> &RustRuntime {
873        &self.rt
874    }
875}
876
877impl DerefMut for Runtime {
878    fn deref_mut(&mut self) -> &mut RustRuntime {
879        &mut self.rt
880    }
881}
882
883fn in_range<T: PartialOrd + Copy>(val: T, min: T, max: T) -> Option<T> {
884    if val < min || val >= max {
885        None
886    } else {
887        Some(val)
888    }
889}
890
891thread_local!(static MALLOC_SIZE_OF_OPS: Cell<*mut MallocSizeOfOps> = const { Cell::new(ptr::null_mut()) });
892
893#[derive(Default)]
894struct InterfaceSizeData {
895    /// How many live instances of this interface exist.
896    count: usize,
897    /// The total number of bytes allocated for instances of this interface.
898    bytes: usize,
899}
900
901struct GlobalSizeData {
902    /// The URL associated with this global.
903    url: ServoUrl,
904    /// A map of WebIDL interface names to size information.
905    interface_sizes: HashMap<&'static str, InterfaceSizeData>,
906}
907
908#[derive(Default)]
909/// A map of globals to size information for those globals.
910/// The key is the global's JS reflector pointer as an integer.
911pub(crate) struct PerGlobalInterfaceSizes(HashMap<usize, GlobalSizeData>);
912
913thread_local!(
914    static DOM_OBJECT_SIZES: LazyCell<RefCell<PerGlobalInterfaceSizes>> = const {
915        LazyCell::new(Default::default)
916    }
917);
918
919#[expect(unsafe_code)]
920unsafe extern "C" fn get_size(obj: *mut JSObject) -> usize {
921    let ops = MALLOC_SIZE_OF_OPS.get();
922    ALREADY_COMPUTED_OBJECTS.with(|objects| {
923        let ignored = objects.borrow();
924        DOM_OBJECT_SIZES.with(|dom_sizes| {
925            let mut per_global_interface_sizes = dom_sizes.borrow_mut();
926            compute_size(
927                obj,
928                unsafe { &mut *ops },
929                &ignored,
930                Some(&mut per_global_interface_sizes),
931            )
932        })
933    })
934}
935
936thread_local!(static GC_CYCLE_START: Cell<Option<Instant>> = const { Cell::new(None) });
937thread_local!(static GC_SLICE_START: Cell<Option<Instant>> = const { Cell::new(None) });
938
939#[expect(unsafe_code)]
940unsafe extern "C" fn gc_slice_callback(
941    _cx: *mut RawJSContext,
942    progress: GCProgress,
943    desc: *const GCDescription,
944) {
945    match progress {
946        GCProgress::GC_CYCLE_BEGIN => GC_CYCLE_START.with(|start| {
947            start.set(Some(Instant::now()));
948            println!("GC cycle began");
949        }),
950        GCProgress::GC_SLICE_BEGIN => GC_SLICE_START.with(|start| {
951            start.set(Some(Instant::now()));
952            println!("GC slice began");
953        }),
954        GCProgress::GC_SLICE_END => GC_SLICE_START.with(|start| {
955            let duration = start.get().unwrap().elapsed();
956            start.set(None);
957            println!("GC slice ended: duration={:?}", duration);
958        }),
959        GCProgress::GC_CYCLE_END => GC_CYCLE_START.with(|start| {
960            let duration = start.get().unwrap().elapsed();
961            start.set(None);
962            println!("GC cycle ended: duration={:?}", duration);
963        }),
964    };
965    if !desc.is_null() {
966        let desc: &GCDescription = unsafe { &*desc };
967        let options = match desc.options_ {
968            GCOptions::Normal => "Normal",
969            GCOptions::Shrink => "Shrink",
970            GCOptions::Shutdown => "Shutdown",
971        };
972        println!("  isZone={}, options={}", desc.isZone_, options);
973    }
974    let _ = stdout().flush();
975}
976
977#[expect(unsafe_code)]
978unsafe extern "C" fn debug_gc_callback(
979    _cx: *mut RawJSContext,
980    status: JSGCStatus,
981    _reason: GCReason,
982    _data: *mut os::raw::c_void,
983) {
984    match status {
985        JSGCStatus::JSGC_BEGIN => thread_state::enter(ThreadState::IN_GC),
986        JSGCStatus::JSGC_END => thread_state::exit(ThreadState::IN_GC),
987    }
988}
989
990#[expect(unsafe_code)]
991unsafe extern "C" fn trace_rust_roots(tr: *mut JSTracer, data: *mut os::raw::c_void) {
992    if !runtime_is_alive() {
993        return;
994    }
995    trace!("starting custom root handler");
996
997    let runtime_callback_data = unsafe { &*(data as *const RuntimeCallbackData) };
998    if let Some(script_thread) = runtime_callback_data
999        .script_thread
1000        .as_ref()
1001        .and_then(Weak::upgrade)
1002    {
1003        trace!("tracing fields of ScriptThread");
1004        unsafe { script_thread.trace(tr) };
1005    };
1006
1007    unsafe {
1008        trace_roots(tr);
1009        trace_refcounted_objects(tr);
1010        settings_stack::trace(tr);
1011    }
1012    trace!("done custom root handler");
1013}
1014
1015#[expect(unsafe_code)]
1016unsafe extern "C" fn servo_build_id(build_id: *mut BuildIdCharVector) -> bool {
1017    let servo_id = b"Servo\0";
1018    unsafe { SetBuildId(build_id, servo_id[0] as *const c_char, servo_id.len()) }
1019}
1020
1021#[expect(unsafe_code)]
1022#[cfg(feature = "debugmozjs")]
1023unsafe fn set_gc_zeal_options(cx: *mut RawJSContext) {
1024    use js::jsapi::SetGCZeal;
1025
1026    let level = match pref!(js_mem_gc_zeal_level) {
1027        level @ 0..=14 => level as u8,
1028        _ => return,
1029    };
1030    let frequency = match pref!(js_mem_gc_zeal_frequency) {
1031        frequency if frequency >= 0 => frequency as u32,
1032        // https://searchfox.org/mozilla-esr128/source/js/public/GCAPI.h#1392
1033        _ => 5000,
1034    };
1035    unsafe {
1036        SetGCZeal(cx, level, frequency);
1037    }
1038}
1039
1040#[expect(unsafe_code)]
1041#[cfg(not(feature = "debugmozjs"))]
1042unsafe fn set_gc_zeal_options(_: *mut RawJSContext) {}
1043
1044thread_local!(static ALREADY_COMPUTED_OBJECTS: LazyCell<RefCell<HashSet<*const JSObject>>> = const {
1045    LazyCell::new(Default::default)
1046});
1047
1048#[expect(unsafe_code)]
1049pub(crate) fn compute_size(
1050    obj: *mut JSObject,
1051    ops: &mut MallocSizeOfOps,
1052    ignored: &HashSet<*const JSObject>,
1053    per_global_interface_sizes: Option<&mut PerGlobalInterfaceSizes>,
1054) -> usize {
1055    if ignored.contains(&(obj as *const JSObject)) {
1056        return 0;
1057    }
1058
1059    match unsafe { get_dom_class(obj) } {
1060        Ok(v) => {
1061            let dom_object = unsafe { private_from_object(obj) as *const c_void };
1062
1063            if dom_object.is_null() {
1064                return 0;
1065            }
1066            let size = unsafe { (v.malloc_size_of)(&mut *ops, dom_object) };
1067
1068            let Some(per_global_interface_sizes) = per_global_interface_sizes else {
1069                return size;
1070            };
1071
1072            let global = unsafe { js::jsapi::GetNonCCWObjectGlobal(obj) };
1073            let interface = v.interface_chain[v.depth as usize];
1074            let interface_size = per_global_interface_sizes
1075                .0
1076                .entry(global as usize)
1077                .or_insert_with(|| {
1078                    let global = unsafe { GlobalScope::from_object(obj) };
1079                    GlobalSizeData {
1080                        url: global.get_url(),
1081                        interface_sizes: HashMap::new(),
1082                    }
1083                })
1084                .interface_sizes
1085                .entry(interface.into())
1086                .or_default();
1087            interface_size.count += 1;
1088            interface_size.bytes += size;
1089            // Always report a size of zero, since we report this value
1090            // in a separate tree from the main JS heap.
1091            0
1092        },
1093        Err(_e) => 0,
1094    }
1095}
1096
1097#[expect(unsafe_code)]
1098pub(crate) fn get_reports(
1099    cx: &mut JSContext,
1100    path_seg: String,
1101    ops: &mut MallocSizeOfOps,
1102    already_computed_objects: HashSet<*const JSObject>,
1103) -> Vec<Report> {
1104    MALLOC_SIZE_OF_OPS.with(|ops_tls| ops_tls.set(ops));
1105    ALREADY_COMPUTED_OBJECTS.with(|objects| {
1106        *objects.borrow_mut() = already_computed_objects;
1107    });
1108
1109    let stats = unsafe {
1110        let mut stats = ::std::mem::zeroed();
1111        if !CollectServoSizes(cx, &mut stats, Some(get_size)) {
1112            return vec![];
1113        }
1114        stats
1115    };
1116    MALLOC_SIZE_OF_OPS.with(|ops| ops.set(ptr::null_mut()));
1117    ALREADY_COMPUTED_OBJECTS.with(|objects| {
1118        let mut objects = objects.borrow_mut();
1119        objects.clear();
1120        objects.shrink_to_fit();
1121    });
1122
1123    let mut reports = vec![];
1124    let mut report = |mut path_suffix, kind, size| {
1125        let mut path = path![path_seg, "js"];
1126        path.append(&mut path_suffix);
1127        reports.push(Report { path, kind, size })
1128    };
1129
1130    DOM_OBJECT_SIZES.with(|sizes| {
1131        let mut sizes = sizes.borrow_mut();
1132        for global_size_data in sizes.0.values() {
1133            let url = global_size_data.url.as_str();
1134            for (interface, interface_data) in &global_size_data.interface_sizes {
1135                report(
1136                    path![
1137                        "dom",
1138                        "out-of-tree",
1139                        format!("url({url})"),
1140                        format!("{interface} [{}]", interface_data.count)
1141                    ],
1142                    ReportKind::ExplicitJemallocHeapSize,
1143                    interface_data.bytes,
1144                );
1145            }
1146        }
1147        sizes.0.clear();
1148    });
1149
1150    // A note about possibly confusing terminology: the JS GC "heap" is allocated via
1151    // mmap/VirtualAlloc, which means it's not on the malloc "heap", so we use
1152    // `ExplicitNonHeapSize` as its kind.
1153    report(
1154        path!["gc-heap", "used"],
1155        ReportKind::ExplicitNonHeapSize,
1156        stats.gcHeapUsed,
1157    );
1158
1159    report(
1160        path!["gc-heap", "unused"],
1161        ReportKind::ExplicitNonHeapSize,
1162        stats.gcHeapUnused,
1163    );
1164
1165    report(
1166        path!["gc-heap", "admin"],
1167        ReportKind::ExplicitNonHeapSize,
1168        stats.gcHeapAdmin,
1169    );
1170
1171    report(
1172        path!["gc-heap", "decommitted"],
1173        ReportKind::ExplicitNonHeapSize,
1174        stats.gcHeapDecommitted,
1175    );
1176
1177    // SpiderMonkey uses the system heap, not jemalloc.
1178    report(
1179        path!["malloc-heap"],
1180        ReportKind::ExplicitSystemHeapSize,
1181        stats.mallocHeap,
1182    );
1183
1184    report(
1185        path!["non-heap"],
1186        ReportKind::ExplicitNonHeapSize,
1187        stats.nonHeap,
1188    );
1189    reports
1190}
1191
1192pub(crate) struct StreamConsumer(*mut JSStreamConsumer);
1193
1194#[expect(unsafe_code)]
1195impl StreamConsumer {
1196    pub(crate) fn consume_chunk(&self, stream: &[u8]) -> bool {
1197        unsafe {
1198            let stream_ptr = stream.as_ptr();
1199            StreamConsumerConsumeChunk(self.0, stream_ptr, stream.len())
1200        }
1201    }
1202
1203    pub(crate) fn stream_end(&self) {
1204        unsafe {
1205            StreamConsumerStreamEnd(self.0);
1206        }
1207    }
1208
1209    pub(crate) fn stream_error(&self, error_code: usize) {
1210        unsafe {
1211            StreamConsumerStreamError(self.0, error_code);
1212        }
1213    }
1214
1215    pub(crate) fn note_response_urls(
1216        &self,
1217        maybe_url: Option<String>,
1218        maybe_source_map_url: Option<String>,
1219    ) {
1220        unsafe {
1221            let maybe_url = maybe_url.map(|url| CString::new(url).unwrap());
1222            let maybe_source_map_url = maybe_source_map_url.map(|url| CString::new(url).unwrap());
1223
1224            let maybe_url_param = match maybe_url.as_ref() {
1225                Some(url) => url.as_ptr(),
1226                None => ptr::null(),
1227            };
1228            let maybe_source_map_url_param = match maybe_source_map_url.as_ref() {
1229                Some(url) => url.as_ptr(),
1230                None => ptr::null(),
1231            };
1232
1233            StreamConsumerNoteResponseURLs(self.0, maybe_url_param, maybe_source_map_url_param);
1234        }
1235    }
1236}
1237
1238/// Implements the steps to compile webassembly response mentioned here
1239/// <https://webassembly.github.io/spec/web-api/#compile-a-potential-webassembly-response>
1240#[expect(unsafe_code)]
1241unsafe extern "C" fn consume_stream(
1242    cx: *mut RawJSContext,
1243    obj: HandleObject,
1244    _mime_type: MimeType,
1245    _consumer: *mut JSStreamConsumer,
1246) -> bool {
1247    let mut cx = unsafe {
1248        // SAFETY: We are in SM hook
1249        JSContext::from_ptr(NonNull::new(cx).expect("JSContext should not be null in SM hook"))
1250    };
1251    let cx = &mut cx;
1252    let mut realm = CurrentRealm::assert(cx);
1253    let global = GlobalScope::from_current_realm(&mut realm);
1254
1255    // Step 2.1 Upon fulfillment of source, store the Response with value unwrappedSource.
1256    if let Ok(unwrapped_source) =
1257        unsafe { root_from_handleobject::<Response>(cx, RustHandleObject::from_raw(obj)) }
1258    {
1259        // Step 2.2 Let mimeType be the result of extracting a MIME type from response’s header list.
1260        let mimetype = unwrapped_source.Headers(cx).extract_mime_type();
1261
1262        // Step 2.3 If mimeType is not `application/wasm`, return with a TypeError and abort these substeps.
1263        if !&mimetype[..].eq_ignore_ascii_case(b"application/wasm") {
1264            throw_dom_exception(
1265                cx,
1266                &global,
1267                Error::Type(c"Response has unsupported MIME type".to_owned()),
1268            );
1269            return false;
1270        }
1271
1272        // Step 2.4 If response is not CORS-same-origin, return with a TypeError and abort these substeps.
1273        match unwrapped_source.Type() {
1274            DOMResponseType::Basic | DOMResponseType::Cors | DOMResponseType::Default => {},
1275            _ => {
1276                throw_dom_exception(
1277                    cx,
1278                    &global,
1279                    Error::Type(c"Response.type must be 'basic', 'cors' or 'default'".to_owned()),
1280                );
1281                return false;
1282            },
1283        }
1284
1285        // Step 2.5 If response’s status is not an ok status, return with a TypeError and abort these substeps.
1286        if !unwrapped_source.Ok() {
1287            throw_dom_exception(
1288                cx,
1289                &global,
1290                Error::Type(c"Response does not have ok status".to_owned()),
1291            );
1292            return false;
1293        }
1294
1295        // Step 2.6.1 If response body is locked, return with a TypeError and abort these substeps.
1296        if unwrapped_source.is_locked() {
1297            throw_dom_exception(
1298                cx,
1299                &global,
1300                Error::Type(c"There was an error consuming the Response".to_owned()),
1301            );
1302            return false;
1303        }
1304
1305        // Step 2.6.2 If response body is alreaady consumed, return with a TypeError and abort these substeps.
1306        if unwrapped_source.is_disturbed() {
1307            throw_dom_exception(
1308                cx,
1309                &global,
1310                Error::Type(c"Response already consumed".to_owned()),
1311            );
1312            return false;
1313        }
1314        unwrapped_source.set_stream_consumer(Some(StreamConsumer(_consumer)));
1315    } else {
1316        // Step 3 Upon rejection of source, return with reason.
1317        throw_dom_exception(
1318            cx,
1319            &global,
1320            Error::Type(c"expected Response or Promise resolving to Response".to_owned()),
1321        );
1322        return false;
1323    }
1324    true
1325}
1326
1327#[expect(unsafe_code)]
1328unsafe extern "C" fn report_stream_error(_cx: *mut RawJSContext, error_code: usize) {
1329    error!("Error initializing StreamConsumer: {:?}", unsafe {
1330        RUST_js_GetErrorMessage(ptr::null_mut(), error_code as u32)
1331    });
1332}
1333
1334#[expect(unsafe_code)]
1335unsafe extern "C" fn invoke_script_environment_preparer(
1336    global: HandleObject,
1337    closure: *mut ScriptEnvironmentPreparer_Closure,
1338) {
1339    // SAFETY: always safe from a JS engine hook.
1340    let mut cx = unsafe { temp_cx() };
1341    let global = unsafe { GlobalScope::from_object(global.get()) };
1342    let mut realm = enter_auto_realm(&mut cx, &*global);
1343    let cx = &mut realm.current_realm();
1344
1345    run_a_script::<DomTypeHolder, _, _>(cx, &global, |cx| {
1346        if unsafe { !RunScriptEnvironmentPreparerClosure(cx.raw_cx(), closure) } {
1347            report_pending_exception(cx);
1348        };
1349    });
1350}
1351
1352pub(crate) struct Runnable(*mut DispatchablePointer);
1353
1354#[expect(unsafe_code)]
1355unsafe impl Sync for Runnable {}
1356#[expect(unsafe_code)]
1357unsafe impl Send for Runnable {}
1358
1359#[expect(unsafe_code)]
1360impl Runnable {
1361    fn run(&self, cx: &mut JSContext, maybe_shutting_down: Dispatchable_MaybeShuttingDown) {
1362        unsafe {
1363            DispatchableRun(cx, self.0, maybe_shutting_down);
1364        }
1365    }
1366}
1367
1368/// `introductionType` values in SpiderMonkey TransitiveCompileOptions.
1369///
1370/// Value definitions are based on the SpiderMonkey Debugger API docs:
1371/// <https://firefox-source-docs.mozilla.org/js/Debugger/Debugger.Source.html#introductiontype>
1372// TODO: squish `scriptElement` <https://searchfox.org/mozilla-central/rev/202069c4c5113a1a9052d84fa4679d4c1b22113e/devtools/server/actors/source.js#199-201>
1373pub(crate) struct IntroductionType;
1374impl IntroductionType {
1375    /// `introductionType` for code passed to `eval`.
1376    pub const EVAL: &CStr = c"eval";
1377    pub const EVAL_STR: &str = "eval";
1378
1379    /// `introductionType` for code evaluated by debugger.
1380    /// This includes code run via the devtools repl, even if the thread is not paused.
1381    pub const DEBUGGER_EVAL: &CStr = c"debugger eval";
1382    pub const DEBUGGER_EVAL_STR: &str = "debugger eval";
1383
1384    /// `introductionType` for code passed to the `Function` constructor.
1385    pub const FUNCTION: &CStr = c"Function";
1386    pub const FUNCTION_STR: &str = "Function";
1387
1388    /// `introductionType` for code loaded by worklet.
1389    pub const WORKLET: &CStr = c"Worklet";
1390    pub const WORKLET_STR: &str = "Worklet";
1391
1392    /// `introductionType` for code assigned to DOM elements’ event handler IDL attributes as a string.
1393    pub const EVENT_HANDLER: &CStr = c"eventHandler";
1394    pub const EVENT_HANDLER_STR: &str = "eventHandler";
1395
1396    /// `introductionType` for code belonging to `<script src="file.js">` elements.
1397    /// This includes `<script type="module" src="...">`.
1398    pub const SRC_SCRIPT: &CStr = c"srcScript";
1399    pub const SRC_SCRIPT_STR: &str = "srcScript";
1400
1401    /// `introductionType` for code belonging to `<script>code;</script>` elements.
1402    /// This includes `<script type="module" src="...">`.
1403    pub const INLINE_SCRIPT: &CStr = c"inlineScript";
1404    pub const INLINE_SCRIPT_STR: &str = "inlineScript";
1405
1406    /// `introductionType` for code belonging to scripts that *would* be `"inlineScript"` except that they were not
1407    /// part of the initial file itself.
1408    /// For example, scripts created via:
1409    /// - `document.write("<script>code;</script>")`
1410    /// - `var s = document.createElement("script"); s.text = "code";`
1411    pub const INJECTED_SCRIPT: &CStr = c"injectedScript";
1412    pub const INJECTED_SCRIPT_STR: &str = "injectedScript";
1413
1414    /// `introductionType` for code that was loaded indirectly by being imported by another script
1415    /// using ESM static or dynamic imports.
1416    pub const IMPORTED_MODULE: &CStr = c"importedModule";
1417    pub const IMPORTED_MODULE_STR: &str = "importedModule";
1418
1419    /// `introductionType` for code presented in `javascript:` URLs.
1420    pub const JAVASCRIPT_URL: &CStr = c"javascriptURL";
1421    pub const JAVASCRIPT_URL_STR: &str = "javascriptURL";
1422
1423    /// `introductionType` for code passed to `setTimeout`/`setInterval` as a string.
1424    pub const DOM_TIMER: &CStr = c"domTimer";
1425    pub const DOM_TIMER_STR: &str = "domTimer";
1426
1427    /// `introductionType` for web workers.
1428    /// FIXME: only documented in older(?) devtools user docs
1429    /// <https://firefox-source-docs.mozilla.org/devtools-user/debugger-api/debugger.source/index.html>
1430    pub const WORKER: &CStr = c"Worker";
1431    pub const WORKER_STR: &str = "Worker";
1432}