devtools: Show clients where they can set breakpoints (#37667)

devtools clients query source actors to determine where the user can set
breakpoints in a source. there are two relevant requests here:
`getBreakableLines` controls which line numbers can be clicked in the
margin, and once a line number is clicked,
`getBreakpointPositionsCompressed` controls where to show breakpoint
buttons within that line.

this patch handles those requests by querying the [SpiderMonkey Debugger
API](https://firefox-source-docs.mozilla.org/js/Debugger/) for that
information:
- devtools sends its script thread a GetPossibleBreakpoints message for
the source’s
[`spidermonkey_id`](https://firefox-source-docs.mozilla.org/js/Debugger/Debugger.Source.html#id)
- the script thread fires a `getPossibleBreakpoints` event into its
debugger global
- the debugger script looks up the
[root](https://firefox-source-docs.mozilla.org/js/Debugger/Debugger.html#onnewscript-script-global)
[Debugger.Script](https://firefox-source-docs.mozilla.org/js/Debugger/Debugger.Script.html#getpossiblebreakpoints-query)
for that source, calls
[getPossibleBreakpoints()](https://firefox-source-docs.mozilla.org/js/Debugger/Debugger.Script.html#getpossiblebreakpoints-query),
and returns the result via
DebuggerGlobalScope#getPossibleBreakpointsResult()
- that method takes the pending result sender, and sends the result back
to devtools
- devtools massages the result into the format required by the request,
and replies to the client

as a result, users of the Firefox devtools client can now set
breakpoints, though they don’t have any effect.

Testing: this patch adds new devtools tests
Fixes: part of #36027

<img width="1433" height="1328" alt="image"
src="https://github.com/user-attachments/assets/f0cd31e0-742f-44d3-8c5d-ceedd9a2706d"
/>

---------

Signed-off-by: Delan Azabani <dazabani@igalia.com>
Co-authored-by: atbrakhi <atbrakhi@igalia.com>
This commit is contained in:
shuppy 2025-08-12 12:53:53 +08:00 committed by GitHub
parent 1995e22e19
commit f5b631e270
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 340 additions and 8 deletions

View file

@ -0,0 +1,62 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::fmt::Debug;
use dom_struct::dom_struct;
use crate::dom::bindings::codegen::Bindings::DebuggerGetPossibleBreakpointsEventBinding::DebuggerGetPossibleBreakpointsEventMethods;
use crate::dom::bindings::codegen::Bindings::EventBinding::Event_Binding::EventMethods;
use crate::dom::bindings::reflector::reflect_dom_object;
use crate::dom::bindings::root::DomRoot;
use crate::dom::event::Event;
use crate::dom::types::GlobalScope;
use crate::script_runtime::CanGc;
#[dom_struct]
/// Event for Rust → JS calls in [`crate::dom::DebuggerGlobalScope`].
pub(crate) struct DebuggerGetPossibleBreakpointsEvent {
event: Event,
spidermonkey_id: u32,
}
impl DebuggerGetPossibleBreakpointsEvent {
pub(crate) fn new(
debugger_global: &GlobalScope,
spidermonkey_id: u32,
can_gc: CanGc,
) -> DomRoot<Self> {
let result = Box::new(Self {
event: Event::new_inherited(),
spidermonkey_id,
});
let result = reflect_dom_object(result, debugger_global, can_gc);
result
.event
.init_event("getPossibleBreakpoints".into(), false, false);
result
}
}
impl DebuggerGetPossibleBreakpointsEventMethods<crate::DomTypeHolder>
for DebuggerGetPossibleBreakpointsEvent
{
// check-tidy: no specs after this line
fn SpidermonkeyId(&self) -> u32 {
self.spidermonkey_id
}
fn IsTrusted(&self) -> bool {
self.event.IsTrusted()
}
}
impl Debug for DebuggerGetPossibleBreakpointsEvent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DebuggerGetPossibleBreakpointsEvent")
.field("spidermonkey_id", &self.spidermonkey_id)
.finish()
}
}

View file

@ -2,9 +2,11 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::cell::RefCell;
use base::id::{Index, PipelineId, PipelineNamespaceId};
use constellation_traits::ScriptToConstellationChan;
use devtools_traits::{ScriptToDevtoolsControlMsg, SourceInfo, WorkerId};
use devtools_traits::{DevtoolScriptControlMsg, ScriptToDevtoolsControlMsg, SourceInfo, WorkerId};
use dom_struct::dom_struct;
use embedder_traits::JavaScriptEvaluationError;
use embedder_traits::resources::{self, Resource};
@ -14,6 +16,7 @@ use js::rust::Runtime;
use js::rust::wrappers::JS_DefineDebuggerObject;
use net_traits::ResourceThreads;
use profile_traits::{mem, time};
use script_bindings::codegen::GenericBindings::DebuggerGetPossibleBreakpointsEventBinding::RecommendedBreakpointLocation;
use script_bindings::codegen::GenericBindings::DebuggerGlobalScopeBinding::{
DebuggerGlobalScopeMethods, NotifyNewSource,
};
@ -27,7 +30,7 @@ use crate::dom::bindings::inheritance::Castable;
use crate::dom::bindings::root::DomRoot;
use crate::dom::bindings::utils::define_all_exposed_interfaces;
use crate::dom::globalscope::GlobalScope;
use crate::dom::types::{DebuggerAddDebuggeeEvent, Event};
use crate::dom::types::{DebuggerAddDebuggeeEvent, DebuggerGetPossibleBreakpointsEvent, Event};
#[cfg(feature = "testbinding")]
#[cfg(feature = "webgpu")]
use crate::dom::webgpu::identityhub::IdentityHub;
@ -41,6 +44,11 @@ use crate::script_runtime::{CanGc, IntroductionType, JSContext};
/// <https://firefox-source-docs.mozilla.org/js/Debugger/>
pub(crate) struct DebuggerGlobalScope {
global_scope: GlobalScope,
#[no_trace]
devtools_to_script_sender: IpcSender<DevtoolScriptControlMsg>,
#[no_trace]
get_possible_breakpoints_result_sender:
RefCell<Option<IpcSender<Vec<devtools_traits::RecommendedBreakpointLocation>>>>,
}
impl DebuggerGlobalScope {
@ -55,7 +63,8 @@ impl DebuggerGlobalScope {
pub(crate) fn new(
runtime: &Runtime,
debugger_pipeline_id: PipelineId,
devtools_chan: Option<IpcSender<ScriptToDevtoolsControlMsg>>,
script_to_devtools_sender: Option<IpcSender<ScriptToDevtoolsControlMsg>>,
devtools_to_script_sender: IpcSender<DevtoolScriptControlMsg>,
mem_profiler_chan: mem::ProfilerChan,
time_profiler_chan: time::ProfilerChan,
script_to_constellation_chan: ScriptToConstellationChan,
@ -66,7 +75,7 @@ impl DebuggerGlobalScope {
let global = Box::new(Self {
global_scope: GlobalScope::new_inherited(
debugger_pipeline_id,
devtools_chan,
script_to_devtools_sender,
mem_profiler_chan,
time_profiler_chan,
script_to_constellation_chan,
@ -81,6 +90,8 @@ impl DebuggerGlobalScope {
None,
false,
),
devtools_to_script_sender,
get_possible_breakpoints_result_sender: RefCell::new(None),
});
let global = unsafe {
DebuggerGlobalScopeBinding::Wrap::<crate::DomTypeHolder>(
@ -155,6 +166,28 @@ impl DebuggerGlobalScope {
"Guaranteed by DebuggerAddDebuggeeEvent::new"
);
}
pub(crate) fn fire_get_possible_breakpoints(
&self,
can_gc: CanGc,
spidermonkey_id: u32,
result_sender: IpcSender<Vec<devtools_traits::RecommendedBreakpointLocation>>,
) {
assert!(
self.get_possible_breakpoints_result_sender
.replace(Some(result_sender))
.is_none()
);
let event = DomRoot::upcast::<Event>(DebuggerGetPossibleBreakpointsEvent::new(
self.upcast(),
spidermonkey_id,
can_gc,
));
assert!(
DomRoot::upcast::<Event>(event).fire(self.upcast(), can_gc),
"Guaranteed by DebuggerGetPossibleBreakpointsEvent::new"
);
}
}
impl DebuggerGlobalScopeMethods<crate::DomTypeHolder> for DebuggerGlobalScope {
@ -242,6 +275,7 @@ impl DebuggerGlobalScopeMethods<crate::DomTypeHolder> for DebuggerGlobalScope {
spidermonkey_id: args.spidermonkeyId,
};
if let Err(error) = devtools_chan.send(ScriptToDevtoolsControlMsg::CreateSourceActor(
self.devtools_to_script_sender.clone(),
pipeline_id,
source_info,
)) {
@ -251,4 +285,27 @@ impl DebuggerGlobalScopeMethods<crate::DomTypeHolder> for DebuggerGlobalScope {
debug!("Not creating debuggee for script with no `introductionType`");
}
}
fn GetPossibleBreakpointsResult(
&self,
event: &DebuggerGetPossibleBreakpointsEvent,
result: Vec<RecommendedBreakpointLocation>,
) {
info!("GetPossibleBreakpointsResult: {event:?} {result:?}");
let sender = self
.get_possible_breakpoints_result_sender
.take()
.expect("Guaranteed by Self::fire_get_possible_breakpoints()");
let _ = sender.send(
result
.into_iter()
.map(|entry| devtools_traits::RecommendedBreakpointLocation {
offset: entry.offset,
line_number: entry.lineNumber,
column_number: entry.columnNumber,
is_step_start: entry.isStepStart,
})
.collect(),
);
}
}

View file

@ -430,6 +430,9 @@ impl DedicatedWorkerGlobalScope {
&runtime,
pipeline_id,
init.to_devtools_sender.clone(),
init.from_devtools_sender
.clone()
.expect("Guaranteed by Worker::Constructor"),
init.mem_profiler_chan.clone(),
init.time_profiler_chan.clone(),
init.script_to_constellation_chan.clone(),

View file

@ -290,6 +290,7 @@ pub(crate) mod datatransfer;
pub(crate) mod datatransferitem;
pub(crate) mod datatransferitemlist;
pub(crate) mod debuggeradddebuggeeevent;
pub(crate) mod debuggergetpossiblebreakpointsevent;
pub(crate) mod debuggerglobalscope;
pub(crate) mod dedicatedworkerglobalscope;
pub(crate) mod defaultteereadrequest;