1use std::convert::TryFrom;
6use std::ptr::{self, NonNull};
7use std::slice;
8
9use devtools_traits::{
10 ConsoleLogLevel, ConsoleMessage, ConsoleMessageFields, DebuggerValue, ObjectPreview,
11 PropertyDescriptor as DevtoolsPropertyDescriptor, ScriptToDevtoolsControlMsg, StackFrame,
12 get_time_stamp,
13};
14use embedder_traits::EmbedderMsg;
15use js::conversions::jsstr_to_string;
16use js::jsapi::{self, ESClass, PropertyDescriptor};
17use js::jsval::{Int32Value, UndefinedValue};
18use js::rust::wrappers::{
19 GetBuiltinClass, GetPropertyKeys, JS_GetOwnPropertyDescriptorById, JS_GetPropertyById,
20 JS_IdToValue, JS_Stringify, JS_ValueToSource,
21};
22use js::rust::{
23 CapturedJSStack, HandleObject, HandleValue, IdVector, ToNumber, ToString,
24 describe_scripted_caller,
25};
26use script_bindings::conversions::get_dom_class;
27
28use crate::dom::bindings::codegen::Bindings::ConsoleBinding::consoleMethods;
29use crate::dom::bindings::error::report_pending_exception;
30use crate::dom::bindings::inheritance::Castable;
31use crate::dom::bindings::str::DOMString;
32use crate::dom::globalscope::GlobalScope;
33use crate::dom::workerglobalscope::WorkerGlobalScope;
34use crate::realms::{AlreadyInRealm, InRealm};
35use crate::script_runtime::{CanGc, JSContext};
36
37const MAX_LOG_DEPTH: usize = 10;
39const MAX_LOG_CHILDREN: usize = 15;
41
42#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
44pub(crate) struct Console;
45
46impl Console {
47 #[expect(unsafe_code)]
48 fn build_message(
49 level: ConsoleLogLevel,
50 arguments: Vec<DebuggerValue>,
51 stacktrace: Option<Vec<StackFrame>>,
52 ) -> ConsoleMessage {
53 let cx = GlobalScope::get_cx();
54 let caller = unsafe { describe_scripted_caller(*cx) }.unwrap_or_default();
55
56 ConsoleMessage {
57 fields: ConsoleMessageFields {
58 level,
59 filename: caller.filename,
60 line_number: caller.line,
61 column_number: caller.col,
62 time_stamp: get_time_stamp(),
63 },
64 arguments,
65 stacktrace,
66 }
67 }
68
69 fn send_string_message(global: &GlobalScope, level: ConsoleLogLevel, message: String) {
71 let prefix = global.current_group_label().unwrap_or_default();
72 let formatted_message = format!("{prefix}{message}");
73
74 Self::send_to_embedder(global, level.clone(), formatted_message);
75
76 let console_message =
77 Self::build_message(level, vec![DebuggerValue::StringValue(message)], None);
78
79 Self::send_to_devtools(global, console_message);
80 }
81
82 fn method(
83 global: &GlobalScope,
84 level: ConsoleLogLevel,
85 messages: Vec<HandleValue>,
86 include_stacktrace: IncludeStackTrace,
87 ) {
88 let cx = GlobalScope::get_cx();
89
90 let (arguments, embedder_msg) = if !messages.is_empty() && messages[0].is_string() {
94 let (formatted, consumed) = apply_sprintf_substitutions(cx, &messages);
95 let remaining = &messages[consumed..];
96
97 let mut arguments: Vec<DebuggerValue> =
98 vec![DebuggerValue::StringValue(formatted.clone())];
99 for msg in remaining {
100 arguments.push(console_argument_from_handle_value(
101 cx,
102 *msg,
103 &mut Vec::new(),
104 ));
105 }
106
107 let embedder_msg = if remaining.is_empty() {
108 formatted
109 } else {
110 format!("{formatted} {}", stringify_handle_values(remaining))
111 };
112
113 (arguments, embedder_msg.into())
114 } else {
115 let arguments = messages
116 .iter()
117 .map(|msg| console_argument_from_handle_value(cx, *msg, &mut Vec::new()))
118 .collect();
119 (arguments, stringify_handle_values(&messages))
120 };
121
122 let stacktrace = (include_stacktrace == IncludeStackTrace::Yes)
123 .then_some(get_js_stack(*GlobalScope::get_cx()));
124 let console_message = Self::build_message(level.clone(), arguments, stacktrace);
125
126 Console::send_to_devtools(global, console_message);
127
128 let prefix = global.current_group_label().unwrap_or_default();
129 let formatted_message = format!("{prefix}{embedder_msg}");
130
131 Self::send_to_embedder(global, level, formatted_message);
132 }
133
134 fn send_to_devtools(global: &GlobalScope, message: ConsoleMessage) {
135 if let Some(chan) = global.devtools_chan() {
136 let worker_id = global
137 .downcast::<WorkerGlobalScope>()
138 .map(|worker| worker.worker_id());
139 let devtools_message =
140 ScriptToDevtoolsControlMsg::ConsoleAPI(global.pipeline_id(), message, worker_id);
141 chan.send(devtools_message).unwrap();
142 }
143 }
144
145 fn send_to_embedder(global: &GlobalScope, level: ConsoleLogLevel, message: String) {
146 global.send_to_embedder(EmbedderMsg::ShowConsoleApiMessage(
147 global.webview_id(),
148 level,
149 message,
150 ));
151 }
152
153 pub(crate) fn internal_warn(global: &GlobalScope, message: String) {
155 Console::send_string_message(global, ConsoleLogLevel::Warn, message);
156 }
157}
158
159#[expect(unsafe_code)]
160unsafe fn handle_value_to_string(cx: *mut jsapi::JSContext, value: HandleValue) -> DOMString {
161 rooted!(in(cx) let mut js_string = std::ptr::null_mut::<jsapi::JSString>());
162 match std::ptr::NonNull::new(unsafe { JS_ValueToSource(cx, value) }) {
163 Some(js_str) => {
164 js_string.set(js_str.as_ptr());
165 unsafe { jsstr_to_string(cx, js_str) }.into()
166 },
167 None => "<error converting value to string>".into(),
168 }
169}
170
171fn console_argument_from_handle_value(
172 cx: JSContext,
173 handle_value: HandleValue,
174 seen: &mut Vec<u64>,
175) -> DebuggerValue {
176 #[expect(unsafe_code)]
177 fn inner(
178 cx: JSContext,
179 handle_value: HandleValue,
180 seen: &mut Vec<u64>,
181 ) -> Result<DebuggerValue, ()> {
182 if handle_value.is_string() {
183 let js_string = ptr::NonNull::new(handle_value.to_string()).unwrap();
184 let dom_string = unsafe { jsstr_to_string(*cx, js_string) };
185 return Ok(DebuggerValue::StringValue(dom_string));
186 }
187
188 if handle_value.is_number() {
189 let number = handle_value.to_number();
190 return Ok(DebuggerValue::NumberValue(number));
191 }
192
193 if handle_value.is_boolean() {
194 let boolean = handle_value.to_boolean();
195 return Ok(DebuggerValue::BooleanValue(boolean));
196 }
197
198 if handle_value.is_object() {
199 if seen.contains(&handle_value.asBits_) {
201 return Ok(DebuggerValue::StringValue("[circular]".into()));
203 }
204
205 seen.push(handle_value.asBits_);
206 let maybe_argument_object = console_object_from_handle_value(cx, handle_value, seen);
207 let js_value = seen.pop();
208 debug_assert_eq!(js_value, Some(handle_value.asBits_));
209
210 if let Some(console_argument_object) = maybe_argument_object {
211 return Ok(DebuggerValue::ObjectValue {
212 uuid: "".to_string(),
213 class: "Object".to_owned(),
214 preview: Some(console_argument_object),
215 });
216 }
217
218 return Err(());
219 }
220
221 let stringified_value = stringify_handle_value(handle_value);
223
224 Ok(DebuggerValue::StringValue(stringified_value.into()))
225 }
226
227 match inner(cx, handle_value, seen) {
228 Ok(arg) => arg,
229 Err(()) => {
230 let in_realm_proof = AlreadyInRealm::assert_for_cx(cx);
231 report_pending_exception(
232 cx,
233 InRealm::Already(&in_realm_proof),
234 CanGc::deprecated_note(),
235 );
236 DebuggerValue::StringValue("<error>".into())
237 },
238 }
239}
240
241#[expect(unsafe_code)]
242fn console_object_from_handle_value(
243 cx: JSContext,
244 handle_value: HandleValue,
245 seen: &mut Vec<u64>,
246) -> Option<ObjectPreview> {
247 rooted!(in(*cx) let object = handle_value.to_object());
248 let mut object_class = ESClass::Other;
249 if !unsafe { GetBuiltinClass(*cx, object.handle(), &mut object_class as *mut _) } {
250 return None;
251 }
252 if object_class != ESClass::Object {
253 return None;
254 }
255
256 let mut own_properties = Vec::new();
257 let mut ids = unsafe { IdVector::new(*cx) };
258 if !unsafe {
259 GetPropertyKeys(
260 *cx,
261 object.handle(),
262 jsapi::JSITER_OWNONLY | jsapi::JSITER_SYMBOLS | jsapi::JSITER_HIDDEN,
263 ids.handle_mut(),
264 )
265 } {
266 return None;
267 }
268
269 for id in ids.iter() {
270 rooted!(in(*cx) let id = *id);
271 rooted!(in(*cx) let mut descriptor = PropertyDescriptor::default());
272
273 let mut is_none = false;
274 if !unsafe {
275 JS_GetOwnPropertyDescriptorById(
276 *cx,
277 object.handle(),
278 id.handle(),
279 descriptor.handle_mut(),
280 &mut is_none,
281 )
282 } {
283 return None;
284 }
285
286 rooted!(in(*cx) let mut property = UndefinedValue());
287 if !unsafe { JS_GetPropertyById(*cx, object.handle(), id.handle(), property.handle_mut()) }
288 {
289 return None;
290 }
291
292 let key = if id.is_string() {
293 rooted!(in(*cx) let mut key_value = UndefinedValue());
294 let raw_id: jsapi::HandleId = id.handle().into();
295 if !unsafe { JS_IdToValue(*cx, *raw_id.ptr, key_value.handle_mut()) } {
296 continue;
297 }
298 rooted!(in(*cx) let js_string = key_value.to_string());
299 let Some(js_string) = NonNull::new(js_string.get()) else {
300 continue;
301 };
302 unsafe { jsstr_to_string(*cx, js_string) }
303 } else {
304 continue;
305 };
306
307 own_properties.push(DevtoolsPropertyDescriptor {
308 name: key,
309 value: console_argument_from_handle_value(cx, property.handle(), seen),
310 configurable: descriptor.hasConfigurable_() && descriptor.configurable_(),
311 enumerable: descriptor.hasEnumerable_() && descriptor.enumerable_(),
312 writable: descriptor.hasWritable_() && descriptor.writable_(),
313 is_accessor: false,
314 });
315 }
316
317 Some(ObjectPreview {
318 kind: "Object".to_owned(),
319 own_properties_length: Some(own_properties.len() as u32),
320 own_properties: Some(own_properties),
321 function: None,
322 array_length: None,
323 })
324}
325
326#[expect(unsafe_code)]
327pub(crate) fn stringify_handle_value(message: HandleValue) -> DOMString {
328 let cx = GlobalScope::get_cx();
329 unsafe {
330 if message.is_string() {
331 let jsstr = std::ptr::NonNull::new(message.to_string()).unwrap();
332 return jsstr_to_string(*cx, jsstr).into();
333 }
334 unsafe fn stringify_object_from_handle_value(
335 cx: *mut jsapi::JSContext,
336 value: HandleValue,
337 parents: Vec<u64>,
338 ) -> DOMString {
339 rooted!(in(cx) let mut obj = value.to_object());
340 let mut object_class = ESClass::Other;
341 if !unsafe { GetBuiltinClass(cx, obj.handle(), &mut object_class as *mut _) } {
342 return DOMString::from("/* invalid */");
343 }
344 let mut ids = unsafe { IdVector::new(cx) };
345 if !unsafe {
346 GetPropertyKeys(
347 cx,
348 obj.handle(),
349 jsapi::JSITER_OWNONLY | jsapi::JSITER_SYMBOLS,
350 ids.handle_mut(),
351 )
352 } {
353 return DOMString::from("/* invalid */");
354 }
355 let truncate = ids.len() > MAX_LOG_CHILDREN;
356 if object_class != ESClass::Array && object_class != ESClass::Object {
357 if truncate {
358 return DOMString::from("…");
359 } else {
360 return unsafe { handle_value_to_string(cx, value) };
361 }
362 }
363
364 let mut explicit_keys = object_class == ESClass::Object;
365 let mut props = Vec::with_capacity(ids.len());
366 for id in ids.iter().take(MAX_LOG_CHILDREN) {
367 rooted!(in(cx) let id = *id);
368 rooted!(in(cx) let mut desc = PropertyDescriptor::default());
369
370 let mut is_none = false;
371 if !unsafe {
372 JS_GetOwnPropertyDescriptorById(
373 cx,
374 obj.handle(),
375 id.handle(),
376 desc.handle_mut(),
377 &mut is_none,
378 )
379 } {
380 return DOMString::from("/* invalid */");
381 }
382
383 rooted!(in(cx) let mut property = UndefinedValue());
384 if !unsafe {
385 JS_GetPropertyById(cx, obj.handle(), id.handle(), property.handle_mut())
386 } {
387 return DOMString::from("/* invalid */");
388 }
389
390 if !explicit_keys {
391 if id.is_int() {
392 if let Ok(id_int) = usize::try_from(id.to_int()) {
393 explicit_keys = props.len() != id_int;
394 } else {
395 explicit_keys = false;
396 }
397 } else {
398 explicit_keys = false;
399 }
400 }
401 let value_string = stringify_inner(
402 unsafe { JSContext::from_ptr(cx) },
403 property.handle(),
404 parents.clone(),
405 );
406 if explicit_keys {
407 let key = if id.is_string() || id.is_symbol() || id.is_int() {
408 rooted!(in(cx) let mut key_value = UndefinedValue());
409 let raw_id: jsapi::HandleId = id.handle().into();
410 if !unsafe { JS_IdToValue(cx, *raw_id.ptr, key_value.handle_mut()) } {
411 return DOMString::from("/* invalid */");
412 }
413 unsafe { handle_value_to_string(cx, key_value.handle()) }
414 } else {
415 return DOMString::from("/* invalid */");
416 };
417 props.push(format!("{}: {}", key, value_string,));
418 } else {
419 props.push(value_string.to_string());
420 }
421 }
422 if truncate {
423 props.push("…".to_string());
424 }
425 if object_class == ESClass::Array {
426 DOMString::from(format!("[{}]", itertools::join(props, ", ")))
427 } else {
428 DOMString::from(format!("{{{}}}", itertools::join(props, ", ")))
429 }
430 }
431 fn stringify_inner(cx: JSContext, value: HandleValue, mut parents: Vec<u64>) -> DOMString {
432 if parents.len() >= MAX_LOG_DEPTH {
433 return DOMString::from("...");
434 }
435 let value_bits = value.asBits_;
436 if parents.contains(&value_bits) {
437 return DOMString::from("[circular]");
438 }
439 if value.is_undefined() {
440 return DOMString::from("undefined");
442 } else if !value.is_object() {
443 return unsafe { handle_value_to_string(*cx, value) };
444 }
445 parents.push(value_bits);
446
447 if value.is_object() {
448 if let Some(repr) = maybe_stringify_dom_object(cx, value) {
449 return repr;
450 }
451 }
452 unsafe { stringify_object_from_handle_value(*cx, value, parents) }
453 }
454 stringify_inner(cx, message, Vec::new())
455 }
456}
457
458#[expect(unsafe_code)]
459fn maybe_stringify_dom_object(cx: JSContext, value: HandleValue) -> Option<DOMString> {
460 rooted!(in(*cx) let obj = value.to_object());
465 let is_dom_class = unsafe { get_dom_class(obj.get()).is_ok() };
466 if !is_dom_class {
467 return None;
468 }
469 rooted!(in(*cx) let class_name = unsafe { ToString(*cx, value) });
470 let Some(class_name) = NonNull::new(class_name.get()) else {
471 return Some("<error converting DOM object to string>".into());
472 };
473 let class_name = unsafe {
474 jsstr_to_string(*cx, class_name)
475 .replace("[object ", "")
476 .replace("]", "")
477 };
478 let mut repr = format!("{} ", class_name);
479 rooted!(in(*cx) let mut value = value.get());
480
481 #[expect(unsafe_code)]
482 unsafe extern "C" fn stringified(
483 string: *const u16,
484 len: u32,
485 data: *mut std::ffi::c_void,
486 ) -> bool {
487 let s = data as *mut String;
488 let string_chars = unsafe { slice::from_raw_parts(string, len as usize) };
489 unsafe { (*s).push_str(&String::from_utf16_lossy(string_chars)) };
490 true
491 }
492
493 rooted!(in(*cx) let space = Int32Value(2));
494 let stringify_result = unsafe {
495 JS_Stringify(
496 *cx,
497 value.handle_mut(),
498 HandleObject::null(),
499 space.handle(),
500 Some(stringified),
501 &mut repr as *mut String as *mut _,
502 )
503 };
504 if !stringify_result {
505 return Some("<error converting DOM object to string>".into());
506 }
507 Some(repr.into())
508}
509
510#[expect(unsafe_code)]
518fn apply_sprintf_substitutions(cx: JSContext, messages: &[HandleValue]) -> (String, usize) {
519 debug_assert!(!messages.is_empty() && messages[0].is_string());
520
521 let js_string = ptr::NonNull::new(messages[0].to_string()).unwrap();
522 let format_string = unsafe { jsstr_to_string(*cx, js_string) };
523
524 let mut result = String::new();
525 let mut arg_index = 1usize;
526 let mut chars = format_string.chars().peekable();
527
528 while let Some(c) = chars.next() {
529 if c != '%' {
530 result.push(c);
531 continue;
532 }
533
534 match chars.peek().copied() {
535 Some('s') => {
536 chars.next();
537 if arg_index < messages.len() {
538 result.push_str(&stringify_handle_value(messages[arg_index]).to_string());
539 arg_index += 1;
540 } else {
541 result.push_str("%s");
542 }
543 },
544 Some('d') | Some('i') => {
545 let spec = chars.next().unwrap();
546 if arg_index < messages.len() {
547 let num = unsafe { ToNumber(*cx, messages[arg_index]) };
548 if num.is_err() {
549 unsafe { jsapi::JS_ClearPendingException(*cx) };
550 }
551 arg_index += 1;
552 format_integer_substitution(&mut result, num);
553 } else {
554 result.push('%');
555 result.push(spec);
556 }
557 },
558 Some('f') => {
559 chars.next();
560 if arg_index < messages.len() {
561 let num = unsafe { ToNumber(*cx, messages[arg_index]) };
562 if num.is_err() {
563 unsafe { jsapi::JS_ClearPendingException(*cx) };
564 }
565 arg_index += 1;
566 format_float_substitution(&mut result, num);
567 } else {
568 result.push_str("%f");
569 }
570 },
571 Some('o') | Some('O') => {
572 let spec = chars.next().unwrap();
573 if arg_index < messages.len() {
574 result.push_str(&stringify_handle_value(messages[arg_index]).to_string());
575 arg_index += 1;
576 } else {
577 result.push('%');
578 result.push(spec);
579 }
580 },
581 Some('c') => {
582 chars.next();
583 if arg_index < messages.len() {
584 arg_index += 1; }
586 },
587 Some('%') => {
588 chars.next();
589 result.push('%');
590 },
591 _ => {
592 result.push('%');
593 },
594 }
595 }
596
597 (result, arg_index)
598}
599
600fn format_integer_substitution(result: &mut String, num: Result<f64, ()>) {
601 match num {
602 Ok(n) if n.is_nan() => result.push_str("NaN"),
603 Ok(n) if n == f64::INFINITY => result.push_str("Infinity"),
604 Ok(n) if n == f64::NEG_INFINITY => result.push_str("-Infinity"),
605 Ok(n) => result.push_str(&(n.trunc() as i64).to_string()),
606 Err(_) => result.push_str("NaN"),
607 }
608}
609
610fn format_float_substitution(result: &mut String, num: Result<f64, ()>) {
611 match num {
612 Ok(n) if n.is_nan() => result.push_str("NaN"),
613 Ok(n) if n == f64::INFINITY => result.push_str("Infinity"),
614 Ok(n) if n == f64::NEG_INFINITY => result.push_str("-Infinity"),
615 Ok(n) => result.push_str(&n.to_string()),
616 Err(_) => result.push_str("NaN"),
617 }
618}
619
620fn stringify_handle_values(messages: &[HandleValue]) -> DOMString {
621 DOMString::from(itertools::join(
622 messages.iter().copied().map(stringify_handle_value),
623 " ",
624 ))
625}
626
627#[derive(Debug, Eq, PartialEq)]
628enum IncludeStackTrace {
629 Yes,
630 No,
631}
632
633impl consoleMethods<crate::DomTypeHolder> for Console {
634 fn Log(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
636 Console::method(
637 global,
638 ConsoleLogLevel::Log,
639 messages,
640 IncludeStackTrace::No,
641 );
642 }
643
644 fn Clear(global: &GlobalScope) {
646 if let Some(chan) = global.devtools_chan() {
647 let worker_id = global
648 .downcast::<WorkerGlobalScope>()
649 .map(|worker| worker.worker_id());
650 let devtools_message =
651 ScriptToDevtoolsControlMsg::ClearConsole(global.pipeline_id(), worker_id);
652 if let Err(error) = chan.send(devtools_message) {
653 log::warn!("Error sending clear message to devtools: {error:?}");
654 }
655 }
656 }
657
658 fn Debug(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
660 Console::method(
661 global,
662 ConsoleLogLevel::Debug,
663 messages,
664 IncludeStackTrace::No,
665 );
666 }
667
668 fn Info(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
670 Console::method(
671 global,
672 ConsoleLogLevel::Info,
673 messages,
674 IncludeStackTrace::No,
675 );
676 }
677
678 fn Warn(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
680 Console::method(
681 global,
682 ConsoleLogLevel::Warn,
683 messages,
684 IncludeStackTrace::No,
685 );
686 }
687
688 fn Error(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
690 Console::method(
691 global,
692 ConsoleLogLevel::Error,
693 messages,
694 IncludeStackTrace::No,
695 );
696 }
697
698 fn Trace(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
700 Console::method(
701 global,
702 ConsoleLogLevel::Trace,
703 messages,
704 IncludeStackTrace::Yes,
705 );
706 }
707
708 fn Assert(_cx: JSContext, global: &GlobalScope, condition: bool, messages: Vec<HandleValue>) {
710 if !condition {
711 let message = format!("Assertion failed: {}", stringify_handle_values(&messages));
712
713 Console::send_string_message(global, ConsoleLogLevel::Log, message);
714 }
715 }
716
717 fn Time(global: &GlobalScope, label: DOMString) {
719 if let Ok(()) = global.time(label.clone()) {
720 let message = format!("{label}: timer started");
721 Console::send_string_message(global, ConsoleLogLevel::Log, message);
722 }
723 }
724
725 fn TimeLog(_cx: JSContext, global: &GlobalScope, label: DOMString, data: Vec<HandleValue>) {
727 if let Ok(delta) = global.time_log(&label) {
728 let message = format!("{label}: {delta}ms {}", stringify_handle_values(&data));
729
730 Console::send_string_message(global, ConsoleLogLevel::Log, message);
731 }
732 }
733
734 fn TimeEnd(global: &GlobalScope, label: DOMString) {
736 if let Ok(delta) = global.time_end(&label) {
737 let message = format!("{label}: {delta}ms");
738
739 Console::send_string_message(global, ConsoleLogLevel::Log, message);
740 }
741 }
742
743 fn Group(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
745 global.push_console_group(stringify_handle_values(&messages));
746 }
747
748 fn GroupCollapsed(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
750 global.push_console_group(stringify_handle_values(&messages));
751 }
752
753 fn GroupEnd(global: &GlobalScope) {
755 global.pop_console_group();
756 }
757
758 fn Count(global: &GlobalScope, label: DOMString) {
760 let count = global.increment_console_count(&label);
761 let message = format!("{label}: {count}");
762
763 Console::send_string_message(global, ConsoleLogLevel::Log, message);
764 }
765
766 fn CountReset(global: &GlobalScope, label: DOMString) {
768 if global.reset_console_count(&label).is_err() {
769 Self::internal_warn(global, format!("Counter “{label}” doesn’t exist."))
770 }
771 }
772}
773
774#[expect(unsafe_code)]
775fn get_js_stack(cx: *mut jsapi::JSContext) -> Vec<StackFrame> {
776 const MAX_FRAME_COUNT: u32 = 128;
777
778 let mut frames = vec![];
779 rooted!(in(cx) let mut handle = ptr::null_mut());
780 let captured_js_stack = unsafe { CapturedJSStack::new(cx, handle, Some(MAX_FRAME_COUNT)) };
781 let Some(captured_js_stack) = captured_js_stack else {
782 return frames;
783 };
784
785 captured_js_stack.for_each_stack_frame(|frame| {
786 rooted!(in(cx) let mut result: *mut jsapi::JSString = ptr::null_mut());
787
788 unsafe {
790 jsapi::GetSavedFrameFunctionDisplayName(
791 cx,
792 ptr::null_mut(),
793 frame.into(),
794 result.handle_mut().into(),
795 jsapi::SavedFrameSelfHosted::Include,
796 );
797 }
798 let function_name = if let Some(nonnull_result) = ptr::NonNull::new(*result) {
799 unsafe { jsstr_to_string(cx, nonnull_result) }
800 } else {
801 "<anonymous>".into()
802 };
803
804 result.set(ptr::null_mut());
806 unsafe {
807 jsapi::GetSavedFrameSource(
808 cx,
809 ptr::null_mut(),
810 frame.into(),
811 result.handle_mut().into(),
812 jsapi::SavedFrameSelfHosted::Include,
813 );
814 }
815 let filename = if let Some(nonnull_result) = ptr::NonNull::new(*result) {
816 unsafe { jsstr_to_string(cx, nonnull_result) }
817 } else {
818 "<anonymous>".into()
819 };
820
821 let mut line_number = 0;
823 unsafe {
824 jsapi::GetSavedFrameLine(
825 cx,
826 ptr::null_mut(),
827 frame.into(),
828 &mut line_number,
829 jsapi::SavedFrameSelfHosted::Include,
830 );
831 }
832
833 let mut column_number = jsapi::JS::TaggedColumnNumberOneOrigin { value_: 0 };
834 unsafe {
835 jsapi::GetSavedFrameColumn(
836 cx,
837 ptr::null_mut(),
838 frame.into(),
839 &mut column_number,
840 jsapi::SavedFrameSelfHosted::Include,
841 );
842 }
843 let frame = StackFrame {
844 filename,
845 function_name,
846 line_number,
847 column_number: column_number.value_,
848 };
849
850 frames.push(frame);
851 });
852
853 frames
854}