#[repr(C)]
pub struct AutoDebuggerJobQueueInterruption { pub cx: *mut JSContext, pub saved: u64, }
Expand description

[SMDOC] Protecting the debuggee’s job/microtask queue from debugger activity.

When the JavaScript debugger interrupts the execution of some debuggee code (for a breakpoint, for example), the debuggee’s execution must be paused while the developer takes time to look at it. During this interruption, other tabs should remain active and usable. If the debuggee shares a main thread with non-debuggee tabs, that means that the thread will have to process non-debuggee HTML tasks and microtasks as usual, even as the debuggee’s are on hold until the debugger lets it continue execution. (Letting debuggee microtasks run during the interruption would mean that, from the debuggee’s point of view, their side effects would take place wherever the breakpoint was set - in general, not a place other code should ever run, and a violation of the run-to-completion rule.)

This means that, even though the timing and ordering of microtasks is carefully specified by the standard - and important to preserve for compatibility and predictability - debugger use may, correctly, have the effect of reordering microtasks. During the interruption, microtasks enqueued by non-debuggee tabs must run immediately alongside their HTML tasks as usual, whereas any debuggee microtasks that were in the queue when the interruption began must wait for the debuggee to be continued - and thus run after microtasks enqueued after they were.

Fortunately, this reordering is visible only at the global level: when implemented correctly, it is not detectable by an individual debuggee. Note that a debuggee should generally be a complete unit of similar-origin related browsing contexts. Since non-debuggee activity falls outside that unit, it should never be visible to the debuggee (except via mechanisms that are already asynchronous, like events), so the debuggee should be unable to detect non-debuggee microtasks running when they normally would not. As long as behavior visible to the debuggee is unaffected by the interruption, we have respected the spirit of the rule.

Of course, even as we accept the general principle that interrupting the debuggee should have as little detectable effect as possible, we still permit the developer to do things like evaluate expressions at the console that have arbitrary effects on the debuggee’s state—effects that could never occur naturally at that point in the program. But since these are explicitly requested by the developer, who presumably knows what they’re doing, we support this as best we can. If the developer evaluates an expression in the console that resolves a promise, it seems most natural for the promise’s reaction microtasks to run immediately, within the interruption. This is an ‘unnatural’ time for the microtasks to run, but no more unnatural than the evaluation that triggered them.

So the overall behavior we need is as follows:

  • When the debugger interrupts a debuggee, the debuggee’s microtask queue must be saved.

  • When debuggee execution resumes, the debuggee’s microtask queue must be restored exactly as it was when the interruption occurred.

  • Non-debuggee task and microtask execution must take place normally during the interruption.

Since each HTML task begins with an empty microtask queue, and it should not be possible for a task to mix debuggee and non-debuggee code, interrupting a debuggee should always find a microtask queue containing exclusively debuggee microtasks, if any. So saving and restoring the microtask queue should affect only the debuggee, not any non-debuggee content.

AutoDebuggerJobQueueInterruption

AutoDebuggerJobQueueInterruption is an RAII class, meant for use by the Debugger API implementation, that takes care of saving and restoring the queue.

Constructing and initializing an instance of AutoDebuggerJobQueueInterruption sets aside the given JSContext’s job queue, leaving the JSContext’s queue empty. When the AutoDebuggerJobQueueInterruption instance is destroyed, it asserts that the JSContext’s current job queue (holding jobs enqueued while the AutoDebuggerJobQueueInterruption was alive) is empty, and restores the saved queue to the JSContext.

Since the Debugger API’s behavior is up to us, we can specify that Debugger hooks begin execution with an empty job queue, and that we drain the queue after each hook function has run. This drain will be visible to debugger hooks, and makes hook calls resemble HTML tasks, with their own automatic microtask checkpoint. But, the drain will be invisible to the debuggee, as its queue is preserved across the hook invocation.

To protect the debuggee’s job queue, Debugger takes care to invoke callback functions only within the scope of an AutoDebuggerJobQueueInterruption instance.

Why not let the hook functions themselves take care of this?

Certainly, we could leave responsibility for saving and restoring the job queue to the Debugger hook functions themselves.

In fact, early versions of this change tried making the devtools server save and restore the queue explicitly, but because hooks are set and changed in numerous places, it was hard to be confident that every case had been covered, and it seemed that future changes could easily introduce new holes.

Later versions of this change modified the accessor properties on the Debugger objects’ prototypes to automatically protect the job queue when calling hooks, but the effect was essentially a monkeypatch applied to an API we defined and control, which doesn’t make sense.

In the end, since promises have become such a pervasive part of JavaScript programming, almost any imaginable use of Debugger would need to provide some kind of protection for the debuggee’s job queue, so it makes sense to simply handle it once, carefully, in the implementation of Debugger itself.

Fields§

§cx: *mut JSContext§saved: u64

Trait Implementations§

source§

impl Debug for AutoDebuggerJobQueueInterruption

source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error>

Formats the value using the given formatter. Read more
source§

impl PartialEq<AutoDebuggerJobQueueInterruption> for AutoDebuggerJobQueueInterruption

source§

fn eq(&self, other: &AutoDebuggerJobQueueInterruption) -> bool

This method tests for self and other values to be equal, and is used by ==.
1.0.0 · source§

fn ne(&self, other: &Rhs) -> bool

This method tests for !=. The default implementation is almost always sufficient, and should not be overridden without very good reason.
source§

impl StructuralPartialEq for AutoDebuggerJobQueueInterruption

Auto Trait Implementations§

Blanket Implementations§

source§

impl<T> Any for Twhere T: 'static + ?Sized,

source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
source§

impl<T> Borrow<T> for Twhere T: ?Sized,

source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
source§

impl<T> BorrowMut<T> for Twhere T: ?Sized,

source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
source§

impl<T> From<T> for T

source§

fn from(t: T) -> T

Returns the argument unchanged.

source§

impl<T, U> Into<U> for Twhere U: From<T>,

source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

source§

impl<T, U> TryFrom<U> for Twhere U: Into<T>,

§

type Error = Infallible

The type returned in the event of a conversion error.
source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
source§

impl<T, U> TryInto<U> for Twhere U: TryFrom<T>,

§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.