Set worker breakpoints before the worker begins executing
Categories
(DevTools :: Debugger, enhancement)
Tracking
(firefox67 fixed)
Tracking | Status | |
---|---|---|
firefox67 | --- | fixed |
People
(Reporter: bhackett1024, Assigned: bhackett1024)
References
Details
Attachments
(5 files, 5 obsolete files)
15.30 KB,
patch
|
Details | Diff | Splinter Review | |
2.49 KB,
patch
|
ochameau
:
review+
jlast
:
review+
|
Details | Diff | Splinter Review |
3.38 KB,
patch
|
Details | Diff | Splinter Review | |
8.42 KB,
patch
|
asuth
:
review+
|
Details | Diff | Splinter Review |
6.14 KB,
patch
|
ochameau
:
review+
|
Details | Diff | Splinter Review |
Installed breakpoints should be installed before a worker starts executing code. This ensures that breakpoints will always be hit, regardless of timing issues when attaching to the worker's thread.
The attached patch takes the following steps to make this happen:
-
When the client connects to the worker thread it sends the installed breakpoints in the connection options, which the server uses to install the breakpoints on the newly created thread actor.
-
nsIWorkerDebugger gets disallowContent() and allowContent() methods that can be used to prevent the worker's main script from loading. Currently, the main script can start loading as soon as the registration callbacks in the nsIWorkerDebuggerManager have (synchronously) finished. These registration callbacks can invoke disallowContent() to delay that loading until allowContent() is later called asynchronously.
-
disallowContent() and allowContent() are used to delay the loading of the main script until after the client and server have finished connecting.
The patch works, but needs tidying up, some simplifying, and more testing.
Assignee | ||
Comment 2•6 years ago
|
||
Updated patch, with test. I'm going to see how this looks on try before asking for review.
Assignee | ||
Comment 3•6 years ago
|
||
Updated patch which fixes a couple issues that showed up while testing:
-
While the debugger is attaching to the worker, we need to prevent all debuggee runnables from running, not just the one which loads the main script. Otherwise messages posted to the worker can be received before it has installed message listeners.
-
We should only prevent debuggee execution if the debugger is guaranteed to attach to the worker thread. Otherwise, the worker will never do anything. This is only guaranteed if we are doing windowless worker debugging, so the functionality here is only used if that preference is set.
Assignee | ||
Comment 4•6 years ago
|
||
Add a nsIWorkerDebugger.setDebuggerReady() interface. This can be used while registering the worker with the debugger to prevent debuggee runnables from executing in the worker until a later time.
Assignee | ||
Comment 5•6 years ago
|
||
Use the new nsIWorkerDebugger.setDebuggerReady() interface to prevent workers from executing debuggee code until the debugger has finished attaching.
Assignee | ||
Comment 6•6 years ago
|
||
Send the set of installed breakpoints to threads when attaching, and set them immediately in the server.
Assignee | ||
Comment 7•6 years ago
|
||
Test for the new functionality.
Comment 8•6 years ago
|
||
Comment 9•6 years ago
|
||
Assignee | ||
Comment 10•6 years ago
|
||
(In reply to Alexandre Poirot [:ochameau] from comment #8)
You are doing this only for the window-less worker usecase,
but I was wondering if that could be useful for regular tab target?What do we do for tabs?
Do we have a code similar to the one you removed from commands.js?- for (const { location, options } of (Object.values(breakpoints): any)) { - client.setBreakpoint(location, options); - }
If yes, it would be really great to also use this new feature (modulo
backward compatibility...)That would be really handy if we could make it so that all the targets are
managed the same way. It would help supporting fission and a world of
different kinds of target.
I agree that we should be doing the same thing for all thread targets, though it seems better to do in followup to keep this bug more focused. In the tab target case we set breakpoints in the thread from the async store, syncBreakpoints() in devtools/client/debugger/new/src/client/index.js, so for consistent handling we would need to load breakpoints from the async store before attaching and then supply them to the tab target's thread via the attach options.
::: devtools/server/actors/thread.js
@@ +277,5 @@
- if (request.options.breakpoints) {
for (const { location, options } of Object.values(request.options.breakpoints)) {
this.setBreakpoint(location, options);
}
- }
At first it is a bit surprising to see a call to
setBreakpoint
before we
calldbg.addDebuggees()
.As later, we end up iterating over the list of script here:
https://searchfox.org/mozilla-central/source/devtools/server/actors/source.
js#438-441It means that we won't iterate there, as I don't expect Debugger API to have
any script to iterate over as I don't think any global has been registered
to it yet.
But at the same time, this code is typical designed for worker debugging.
The worker has been paused before any script can be executed. So... it
should be fine... here. But I imagine it won't be fine if we start using it
from some other purpose.I don't know this codebase very well, especially the breakpoints world. Is
there still someone knowledgeable on the backend side who could review this
more precisely than me??Otherwise, I imagine we should discuss a bit of the location of this piece
of code.
I'll ask for an additional r? for this piece of code. The handling of breakpoints was just refactored in bug 1524374. The breakpoint store used by a sever thread is now independent from sources, and only refers to URLs and, for URL-less sources, source IDs. Breakpoints and sources can be added in any order, and matching breakpoints on a Debugger.Script will be installed appropriately.
Assignee | ||
Comment 11•6 years ago
|
||
Assignee | ||
Comment 12•6 years ago
|
||
(In reply to Alexandre Poirot [:ochameau] from comment #9)
Comment on attachment 9043446 [details] [diff] [review]
Part 2 - Prevent worker debuggee execution until the thread actor has
attached.Review of attachment 9043446 [details] [diff] [review]:
::: devtools/server/actors/thread.js
@@ +255,5 @@}
- if (this._parent.onThreadAttached) {
this._parent.onThreadAttached();
- }
I have the same concern than the other patch of this queue: did you gave
some thoughts about the very precise location of this call?It will end up being before the call to
setBreakpoint
.
Shouldn't it be after? After the call to setBreakpoint, and after we enabled
the Debugger?
I imagine it works because everything is very asynchronous, but it would
help the comprehension of this code if the order of things actually made
sense.
It would be fine to put the onThreadAttached() call at the end of onAttach(), but it doesn't matter where in this function we call it. It is dispatching a message to the main thread which unblocks content execution on the worker thread, but since onAttach() runs synchronously on the worker thread there won't be any difference in the possible thread interleavings.
::: devtools/server/actors/worker/worker-target-actor-list.js
@@ +120,5 @@
// Prevent the debuggee from executing in this worker until the debugger
// has finished attaching to it. We only do this if the windowless workers
// preference is set. Otherwise the debugger is not guaranteed to connect
// to the worker, in which case it will never do anything.
if (Services.prefs.getBoolPref("devtools.debugger.features.windowless-workers")) {
You are using a frontend preference from the backend, this is not going to
work for remote devices.If you are only targeting Web Workers, you can add an option argument to the
target actors's listWorkers methods as that's the one instantiating the
WorkerTargetActorList object:
https://searchfox.org/mozilla-central/source/devtools/server/actors/targets/
browsing-context.js#630-636
https://searchfox.org/mozilla-central/source/devtools/server/actors/targets/
content-process.js#127-129
That's more complex for service worker and privileged workers as
WorkerTargetActorList is instantiated before any request is received.
But I think your usecase is limited to web page's workers for now.
devtools.debugger.features.windowless-workers has been turned on by default and pretty soon we should be removing support for the old windowed worker interface, so that the debugger attaches to all workers it knows about. I'm not sure if this patch will need modifying in order for these changes to coexist with how service and privileged workers are handled, but I can attach a followup in that case. Would it be OK to just remove this test and always call dbg.setDebuggerReady(false)? This could be a little quirky until the pref is removed entirely (when using the debugger with the pref turned off, new workers will not be active until their toolbox is opened) but would simplify things in the interim.
Comment 14•6 years ago
|
||
Assignee | ||
Comment 15•6 years ago
|
||
(In reply to Andrew Sutherland [:asuth] from comment #14)
Comment on attachment 9043445 [details] [diff] [review]
Part 1 - Add nsIWorkerDebugger.setDebuggerReady.Review of attachment 9043445 [details] [diff] [review]:
Did you also mean to make CompileScriptRunnable be a WorkerDebugeeRunnable
(or at least report itself as one)?
The patch overrides IsDebuggeeRunnable() in CompileScriptRunnable so that it reports itself as a debuggee runnable and will go into the delayed runnable queue until the debugger finishes initializing. I'll change CompileScriptRunnable to inherit from WorkerDebuggeeRunnable, which seems clearer. We definitely can't guarantee that the CompileScriptRunnable runs at the appropriate time if it doesn't report itself as a debuggee runnable.
Assignee | ||
Comment 16•6 years ago
|
||
Updated patch per comments. This also fixes a bug that showed up when I did additional testing. If a debuggee runnable is dispatched before WorkerPrivate->mThread has been set, the runnable would go into mPreStartRunnables and unconditionally run once the worker thread started.
I fixed this by reordering the tests in Dispatch(), though it's not obvious in the patch due the code moving into DispatchLockHeld and the indentation level changing.
Assignee | ||
Comment 17•6 years ago
|
||
Updated patch. This moves the parent.onThreadAttached() call to a more intuitive place, and removes the test for the windowless workers preference now that it is the default.
This also fixes a bug that showed up in additional testing. WorkerTargetActorList does not always listen to worker list changes, and if its listener is not called then the worker will start running content immediately. This changes WorkerTargetActorList to always listen to worker list changes.
Comment 18•6 years ago
|
||
Comment 19•6 years ago
|
||
Comment 20•6 years ago
|
||
(In reply to Brian Hackett (:bhackett) from comment #10)
I'll ask for an additional r? for this piece of code. The handling of breakpoints was just refactored in bug 1524374. The breakpoint store used by a sever thread is now independent from sources, and only refers to URLs and, for URL-less sources, source IDs. Breakpoints and sources can be added in any order, and matching breakpoints on a Debugger.Script will be installed appropriately.
I would like to understand how this actually work when we do this:
if (request.options.breakpoints) {
for (const { location, options } of Object.values(request.options.breakpoints)) {
this.setBreakpoint(location, options);
}
}
Just before this:
this.dbg.addDebuggees();
When we call setBreakpoint, we end up trying to apply the breakpoint on the Debugger API here:
https://searchfox.org/mozilla-central/source/devtools/server/actors/source.js#440
let scripts = this._findDebuggeeScripts({ line });
I'm expecting this to return an empty list as we haven't added any debuggee yet. Am I correct?
So I'm not expecting the breakpoint to be applied at this stage.
We do call applyBreakpoint from other callsite, ThreadActor._addSource, which is only called from ThreadActor.onNewScript.
But I'm not expecting onNewScript to be called for already existing scripts, or am I wrong?
Now, again, that's not an issue for worker, because you assume that there is no already existing scripts.
But that's from from being an obvious exception when you read this attach function.
Comment 21•6 years ago
|
||
Assignee | ||
Comment 22•6 years ago
|
||
(In reply to Alexandre Poirot [:ochameau] from comment #20)
(In reply to Brian Hackett (:bhackett) from comment #10)
I'll ask for an additional r? for this piece of code. The handling of breakpoints was just refactored in bug 1524374. The breakpoint store used by a sever thread is now independent from sources, and only refers to URLs and, for URL-less sources, source IDs. Breakpoints and sources can be added in any order, and matching breakpoints on a Debugger.Script will be installed appropriately.
I would like to understand how this actually work when we do this:
if (request.options.breakpoints) { for (const { location, options } of Object.values(request.options.breakpoints)) { this.setBreakpoint(location, options); } }
Just before this:
this.dbg.addDebuggees();
When we call setBreakpoint, we end up trying to apply the breakpoint on the Debugger API here:
https://searchfox.org/mozilla-central/source/devtools/server/actors/source.js#440let scripts = this._findDebuggeeScripts({ line });
I'm expecting this to return an empty list as we haven't added any debuggee yet. Am I correct?
So I'm not expecting the breakpoint to be applied at this stage.
We do call applyBreakpoint from other callsite, ThreadActor._addSource, which is only called from ThreadActor.onNewScript.
But I'm not expecting onNewScript to be called for already existing scripts, or am I wrong?Now, again, that's not an issue for worker, because you assume that there is no already existing scripts.
But that's from from being an obvious exception when you read this attach function.
Oops, yes, you're right, we won't apply breakpoints to any sources here. The ordering of the setBreakpoint() and addDebuggees() call won't affect things though, as we can't apply breakpoints until we have created a source actor for the source. We should be using _addSource when responding to a client query for the existing sources, so that existing breakpoints get installed and so that the other updates in _addSource happen. Since this doesn't affect the worker case, would it be ok to just add a comment here for now?
Assignee | ||
Comment 23•6 years ago
|
||
This patch has the client indicate to the server that it wants workers to pause on startup.
The earlier patch changed WorkerTargetActorList's listener because there is no listener installed on the WorkerDebuggerManager between the time when the worker list changes and the time when a request for the workers is received from the client, during which the page is unpaused and can create new workers that will be unpaused.
This patch avoids changing WorkerTargetActorList's listener behavior by using a second listener on the WorkerDebuggerManager that is installed when the main thread actor has been configured to pause workers on startup.
Comment 24•6 years ago
|
||
Assignee | ||
Comment 25•6 years ago
|
||
(In reply to Alexandre Poirot [:ochameau] from comment #24)
Comment on attachment 9046078 [details] [diff] [review]
Part 2 - Prevent worker debuggee execution until the thread actor has
attached.Review of attachment 9046078 [details] [diff] [review]:
Sorry for all the delay in intermediate reviews...
It looks good now, thanks a lot for the additional comments.
Could you please test your patch against the Browser toolbox to ensure it
doesn't throw?::: devtools/server/actors/thread.js
@@ +459,5 @@
- if ("pauseWorkersUntilAttach" in options) {
if (this._parent.pauseWorkersUntilAttach) {
this._parent.pauseWorkersUntilAttach(options.pauseWorkersUntilAttach);
}
Instead of going through the target actor and the WorkerTargetActorList;
wouldn't it be easier to instantiate aPauseMatchingWorkers
directly from
the thread actor?new PauseMatchingWorkers(this.conn, { type: Ci.nsIWorkerDebugger.TYPE_DEDICATED, window: this._parent.window, });
I'm trying to reduce the dependencies between thread and target as things
start to be complicated to understand between these two.
This would be simpler, but it seems fragile. In order for workers to pause at the correct times with this patch, the workers returned by the WorkerTargetActorList need to be exactly those workers which are paused by the PauseMatchingWorkers. Otherwise, we get workers that don't pause when they should, or pause forever because the debugger never attached to them. So, this patch makes the PauseMatchingWorkers object a part of the WorkerTargetActorList, so they can use the same options and tests for whether a worker should be paused/listed, and there is no chance they can be out of sync with each other.
Comment 26•6 years ago
|
||
Assignee | ||
Comment 27•6 years ago
|
||
(In reply to Alexandre Poirot [:ochameau] from comment #26)
Comment on attachment 9043447 [details] [diff] [review]
Part 3 - Send breakpoints while attaching to thread.Review of attachment 9043447 [details] [diff] [review]:
(In reply to Brian Hackett (:bhackett) from comment #22)
Oops, yes, you're right, we won't apply breakpoints to any sources here.
The ordering of the setBreakpoint() and addDebuggees() call won't affect things though, as we can't apply breakpoints until we have created a source actor for the source. We should be using _addSource when responding to a client query for the existing sources, so that existing breakpoints get installed and so that the other updates in _addSource happen.
I'm bit confused and I would like to better understand what is going on.
You told me in comment 10:The handling of breakpoints was just refactored in bug 1524374.
The breakpoint store used by a sever thread is now independent from sources,
and only refers to URLs and, for URL-less sources, source IDs.
Breakpoints and sources can be added in any order, and matching breakpoints on a Debugger.Script will be installed appropriately.This seems to say the opposite of what you just said.
Was "Breakpoints and sources can be added in any order":
- something still not fully refactored, but it will be eventually true at
some point?- doesn't conflict with "we can't apply breakpoints until we have created a
source actor for the source" because I don't fully understand something here?- 3rd response, I really miss something ? :)
The statement I originally made is correct in its intent, but is not literally true. We should be applying breakpoints in response to the sources() request, but we aren't, because ThreadActor.onSources() has a bug.
When you say:
"We should be using _addSource when responding to a client query for the
existing sources, so that existing breakpoints get installed and so that the
other updates in _addSource happen." Doesn't that mean that, if a script for
which we set a breakpoint execute between attach() and sources() requests
(and the related _addSource call),
the breakpoint won't break?
Yes, it does.
Also, outside of the worker issue, don't we miss something like this in
ThreadActor.attach:
for (const source of this.dbg.findSources())
this._addSource(source);
Now that breakpoints can be set without a source actor, we could pass only
URLs and line/column, but the breakppoints would not work until we call
sources().
I'm starting to wonder if there is a design issue between attach() and
sources() requests. (An issue that was always here, outside of the worker
work you are doing)
I don't think this makes a difference in the end. If we're attaching to a thread that already has sources then we don't really have control over the exact timing of when that attach happens wrt content execution on the page.
::: devtools/client/debugger/new/src/client/firefox/workers.js
@@ +27,4 @@const { workers } = await tabTarget.listWorkers();
for (const workerTargetFront of workers) {
await workerTargetFront.attach();
- const [, workerThread] = await workerTargetFront.attachThread(options);
Yes. All targets should be handled the same way. So if we do set breakpoints
via attach for worker, we should do that for the regular tab target as well.But that's totally fine to do that in a followup, it is only important for
fission.
Yeah, I agree that we should be doing this for all threads in the future. This will allow breakpoints to hit when we first open a page with the debugger open, instead of requiring a reload to hit early breakpoints. Doing this for the main thread is a different problem from the one in this bug, though.
Updated•6 years ago
|
Comment 28•6 years ago
|
||
(In reply to Brian Hackett (:bhackett) from comment #27)
The statement I originally made is correct in its intent, but is not literally true. We should be applying breakpoints in response to the sources() request, but we aren't, because ThreadActor.onSources() has a bug.
Is there a bug filed about this?
Is there still a refactoring happening around the debugger actors or is it fully completed?
Assignee | ||
Comment 29•6 years ago
|
||
(In reply to Alexandre Poirot [:ochameau] from comment #28)
(In reply to Brian Hackett (:bhackett) from comment #27)
The statement I originally made is correct in its intent, but is not literally true. We should be applying breakpoints in response to the sources() request, but we aren't, because ThreadActor.onSources() has a bug.
Is there a bug filed about this?
Is there still a refactoring happening around the debugger actors or is it fully completed?
I just filed bug 1530699 for this. The refactoring is done for now, but we're still dealing with fallout from it. There might be followup work to remove source actors but I'm not sure at this point when/if that will happen.
Comment 30•6 years ago
|
||
Comment 31•6 years ago
|
||
bugherder |
https://hg.mozilla.org/mozilla-central/rev/7ba97389b183
https://hg.mozilla.org/mozilla-central/rev/34d35ba26c07
https://hg.mozilla.org/mozilla-central/rev/e5f0800182ae
https://hg.mozilla.org/mozilla-central/rev/389de12c8724
https://hg.mozilla.org/mozilla-central/rev/f5580a62e4e6
Description
•