Open Bug 1019721 Opened 10 years ago Updated 2 years ago

Native promises tracing

Categories

(DevTools :: General, defect, P3)

30 Branch
x86
macOS
defect

Tracking

(Not tracked)

People

(Reporter: canuckistani, Unassigned)

References

Details

Attachments

(1 file)

The ember inspector has a nice tool for tracing Promise chains leveraging some instrumentation of their 'rsvp' promises library. We should consider doing something similar for DOM Promises.
As far as UI goes, I'd prefer a waterfall view similar to the netmonitor, but with collapsed periods of inactivity. Then you can visualize concurrency based on Promise.all() and see what computations depend on what other computations.

Filtering on pending/fulfilled/rejected is a must.

Would be awesome to capture the JS stack on creation and fulfillment/rejection of a DOM promise and surface that information in the UI here.
Sounds good - I wasn't implying we copy their UI, just step back and say - now that Promises are in the web platform ( and even on Safari! ) developers will make increasing use of the native implementations. How can we help? 

I like the idea of resurrecting tag-stack here.
For sure.

What I proposed isn't really resurrecting tagged stacks, but I guess it is a very small subset. Once bug 961325 lands, tagged stacks will be a whole lot easier to implement.
This might be a cool intern project too...
Some complaints about promises and the solutions they're coming up with:

http://blog.soareschen.com/the-problem-with-es6-promises
https://github.com/soareschen/es6-promise-debugging

We should consider solving those problems in this tool.
:bz has very kindly volunteered to help us on the platform API side of things.

Jeff, can you figure out what our v1 requirements for this tool are?

Here is my shot at some requirements, feel free to run with it:

* I should be able to list all live promises in the page

* I should be able to inspect pending/resolved/rejected state

* I should be able to filter to show only the promises that are pending/resolved/rejected

* I should be able to inspect resolution/rejection values

* I should be able to see when a promise was created and when it was resolved/rejected

* I should be able to understand the relationship between promises (eg, if I have `p.then(x => returnSomeNewPromise(x))` I should be able to tell that the new promise and `p` are related)

* If the devtools are open at time of promise creation/fulfillment, I should be able to see the JS stack at the time of creation/fulfillment

* I should be able to find "dangling promises" that have been pending for a long time and will likely never be resolved/rejected

* I should be able to find cases where I could have better promise concurrency, eg I am doing:

    Task.spawn(function *() {
      var a = yield getA();
      var b = yield getB();
      var c = yield getC();
      doStuff(a, b, c);
    });

  instead of:

    Task.spawn(function *() {
      var [a, b, c] = yield Promise.all([getA(), getB(), getC()]);
      doStuff(a, b, c);
    });
Flags: needinfo?(jgriffiths)
If the promise only held a weak ref to the resolve/reject functions then we could force GC and then find out what promises were unresolvable.

I think double resolve and reject after resolve, etc are rare, but we could notify people about this with ease too.
> If the promise only held a weak ref to the resolve/reject functions

It doesn't hold a ref to them at all... and there is no such thing as a "weak ref" in JS, really.
(In reply to Boris Zbarsky [:bz] from comment #9)
> > If the promise only held a weak ref to the resolve/reject functions
> 
> It doesn't hold a ref to them at all... and there is no such thing as a
> "weak ref" in JS, really.

So would it be possible to have a promise-tracing mode where we did keep track in that way?
What keep track of what?  SpiderMonkey doesn't provide us a way right now to be notified when a given function object is garbage collected, if that's what you mean.
(In reply to Nick Fitzgerald [:fitzgen] from comment #7)
> :bz has very kindly volunteered to help us on the platform API side of
> things.
> 
> Jeff, can you figure out what our v1 requirements for this tool are?
> 
> Here is my shot at some requirements, feel free to run with it:
> 
> * I should be able to list all live promises in the page
> 
> * I should be able to inspect pending/resolved/rejected state
> 
> * I should be able to filter to show only the promises that are
> pending/resolved/rejected
> 
> * I should be able to inspect resolution/rejection values
> 
> * I should be able to see when a promise was created and when it was
> resolved/rejected
> 
> * I should be able to understand the relationship between promises (eg, if I
> have `p.then(x => returnSomeNewPromise(x))` I should be able to tell that
> the new promise and `p` are related)
> 
> * If the devtools are open at time of promise creation/fulfillment, I should
> be able to see the JS stack at the time of creation/fulfillment
> 
> * I should be able to find "dangling promises" that have been pending for a
> long time and will likely never be resolved/rejected
> 
> * I should be able to find cases where I could have better promise
> concurrency, eg I am doing:
> 
>     Task.spawn(function *() {
>       var a = yield getA();
>       var b = yield getB();
>       var c = yield getC();
>       doStuff(a, b, c);
>     });
> 
>   instead of:
> 
>     Task.spawn(function *() {
>       var [a, b, c] = yield Promise.all([getA(), getB(), getC()]);
>       doStuff(a, b, c);
>     });

That's an awesome set. 

A couple of additional ideas:

1. 'break on promise rejection' similar to break on uncaught exception with a stack that hopefully gives enough context to be meaningful.

2. I should be able to reject a promise manually, which then could trigger 'break on promise rejection' so I can inspect what that Promise was doing being stalled like that.

bz: really appreciate your help with this! :)
^^
Flags: needinfo?(jgriffiths)
We can definitely expose a reject-manually API.

We can send some sort of notification for break-on-promise-rejection to work, as long as there isn't too much overhead.
Yes! If we're going to be able to reject manually, we should also be able to resolve manually.

The whole break-on-promise-rejection feels like a second iteration thing to me. Would this belong in the debugger or the promise tool or both?

Would also be nice to inspect the .then()'d functions attached to a promise. Would need to be able to differentiate the error handlers from the normal handlers. This feels like maybe a second iteration thing, too.

~~~~~~~~~~~~~~~~~~~~~~~~

Here's the set of platform APIs we will need for a first iteration, as far as I can tell:

* Get a list of all live promises for a given window

* An on-new-promise event which includes the new promise and a time stamp in its event data

* An on-promise-fulfilled event which includes the promise, whether it was resolved or rejected, and the value it was resolved/rejected with, and a time stamp in its event data

* Functions to manually resolve or reject a promise with a given value.

* Functions or getters to inspect a specific promise's state:
  * pending/resolved/rejected state
  * resolution/rejection value or null if it isn't fulfilled
  * creation time stamp
  * resolution/rejection time stamp or null if it isn't fulfilled
  * creation stack or null if the devtools weren't open when it was created
  * resolution/rejection stack or null if it is either pending or the devtools weren't open when it was fulfilled

Does this look correct/do-able, :bz? Did I miss anything?
Flags: needinfo?(bzbarsky)
We want this stuff for both SpiderMonkey and DOM promises. If it is easier to implement for only DOM promises first then so be it, but I'm changing the title to match our intent.
Summary: DOM promises tracing → Native promises tracing
(In reply to Nick Fitzgerald [:fitzgen] from comment #15)
> Yes! If we're going to be able to reject manually, we should also be able to
> resolve manually.

:)

> The whole break-on-promise-rejection feels like a second iteration thing to
> me. Would this belong in the debugger or the promise tool or both?

I feel like this a debugger feature, right beside uncaught exceptions? You would handle it in the debugger. But if we are broken at a promise rejection we could have a switching workflow ( like between the inspector and console currently ) where it's easy from the debugger to get to that promise chain in the promise tool, and vice versa, with a visual indicator in the promise tool that we're paused on it over in the debugger.

Fine with 2nd iteration, let's ship MVP & get some feedback :) People will be really interested in this I think.

> Would also be nice to inspect the .then()'d functions attached to a promise.
> Would need to be able to differentiate the error handlers from the normal
> handlers. This feels like maybe a second iteration thing, too.

+1, nice idea.
This bug has lots of really ambitious ideas, which are awesome. But I wonder if there's interest in getting a v0 out the door just by copying the Ember inspector code and UI? It's already a Firefox add-on; you just need to make it a bit less Ember-specific, I imagine.

I'm just scared that grandiose plans will be dreamt up, then become too intimidating to implement, when we have a really nice solution in the Ember inspector already.
(In reply to Domenic Denicola from comment #18)
> This bug has lots of really ambitious ideas, which are awesome. But I wonder
> if there's interest in getting a v0 out the door just by copying the Ember
> inspector code and UI? It's already a Firefox add-on; you just need to make
> it a bit less Ember-specific, I imagine.
> 
> I'm just scared that grandiose plans will be dreamt up, then become too
> intimidating to implement, when we have a really nice solution in the Ember
> inspector already.

The only thing that is really beyond the ember addon is my suggestion of using a waterfall to visualize promise concurrency and dependency. Everything else is just pretty much equivalent.

But yeah I am generally in favor of the "ship it now" attitude.
> * Get a list of all live promises for a given window

Where "live" just means "didn't happen to have been GCed yet"?  Note that this can get into a cycle where the debugger is the only thing that's keeping the promises alive...  But I'm not sure there's really a better criterion we can use here.

>* An on-new-promise event which includes the new promise and a time stamp in its event data

Or at least some sort of notification, right?  Using a DOM event here is a bit weird.  Can we do this only when actively using the promise tracer tool?

> * An on-promise-fulfilled event which includes the promise, whether it was resolved or
> rejected, and the value it was resolved/rejected with, and a time stamp in its event
> data

Again, can we do this only when the tool is live?

> * Functions to manually resolve or reject a promise with a given value.

This is easy.  Presumably we'd wrap that value into the compartment of the promise itself, right?

>  * resolution/rejection value or null if it isn't fulfilled

undefined, not null, I'd think.

>  * creation time stamp

In the same units as performance.now() for the Promise's window, right?

For the stack, we'll need to figure out what form to provide it in.  But it's certainly doable.

> If it is easier to implement for only DOM promises first then so be it

Well, since SpiderMonkey promises don't even exist yet, it's hard to implement anything for them.  :(
Flags: needinfo?(bzbarsky)
>>  * resolution/rejection value or null if it isn't fulfilled
>
> undefined, not null, I'd think.

That's not great since then you can't determine when the promise is fulfilled or rejected with `undefined`.

In most promise libraries we handle this via a state snapshot object of the form:

{ state: "pending" }
{ state: "fulfilled", value: v }
{ state: "rejected", reason: r }

And be careful when crafting this UI not to confuse resolution and fulfillment: https://github.com/domenic/promises-unwrapping/blob/master/docs/states-and-fates.md
(In reply to Domenic Denicola from comment #18)
> This bug has lots of really ambitious ideas, which are awesome. But I wonder
> if there's interest in getting a v0 out the door just by copying the Ember
> inspector code and UI? It's already a Firefox add-on; you just need to make
> it a bit less Ember-specific, I imagine.

We haven't really discussed scoping in detail yet. I agree an MVP will be a sub-set of the ideas here.

> I'm just scared that grandiose plans will be dreamt up, then become too
> intimidating to implement, when we have a really nice solution in the Ember
> inspector already.

The Ember tool relies on an instrumented version of rsvp specifically. I would hope that once we provide a DOM Promise implementation libraries will start to use it and build on top of it. Users will need a tool that is based on this standard and that hooks the native implementation. What we're doing here is working with the Gecko platform team to give them an idea of what our requirements / crazy ideas are and getting feedback on what is possible. As a bonus, we will expose the 'back-end' so that we can work with Promise-heavy code on remote devices as well as the local browser. 

This platform work is the really critical part. Once it is in place people will be free to create tools ( either in-product or as extensions ) that take advantage of these apis.

I really appreciate your feedback here; it's great to have a Promise library implementer's feedback as we work on creating tools for Promises.
(In reply to Jeff Griffiths (:canuckistani) from comment #22)
> This platform work is the really critical part. Once it is in place people
> will be free to create tools ( either in-product or as extensions ) that
> take advantage of these apis.

I would say that one of the first steps for the back-end work is defined in bug 966471, or at least the portion of it related to exposing Promise state to developer tools.
Depends on: 966471
I've added this bug to the dependency tree of bug 856878 which provides an overview of open Promises work. There might be other ideas there that could be useful for Promises debugging:

https://bugzilla.mozilla.org/showdependencytree.cgi?id=856878&maxdepth=3&hide_resolved=1
Blocks: 856878
I like the waterfall view idea, but I also think that a MVP could be an additional debugger panel tab, next to the Events tab, with similar functionality (list live promises, break on fulfillment/rejection).
> I would say that one of the first steps for the back-end work is defined in bug 966471

That's basically blocked on devtools folks saying what the want the API to look like.  It's just a few minutes work to implement things under the hood here, for most reasonable-looking APIs, but we need to decide the API first.
(In reply to Boris Zbarsky [:bz] from comment #26)
> That's basically blocked on devtools folks saying what the want the API to
> look like.  It's just a few minutes work to implement things under the hood
> here, for most reasonable-looking APIs, but we need to decide the API first.

If you already have an implementation idea, maybe you can put forward a proposal for a reasonable API on the bug and see whether it would work for the devtools and inspection add-ons?
OK, I'll do that in bug 966471.
(In reply to Boris Zbarsky [:bz] from comment #20)
> > * Get a list of all live promises for a given window
> 
> Where "live" just means "didn't happen to have been GCed yet"?  Note that
> this can get into a cycle where the debugger is the only thing that's
> keeping the promises alive...  But I'm not sure there's really a better
> criterion we can use here.

Yes, where live means not collected.

At the risk of diving into implementation details for things I don't know well: it seems we could either iterate over GC cells like `Debugger.prototype.findScripts` does (but I don't know how much of that infrastructure is exposed via JSAPI and its slow), or we could make promises (or some front/shadow/whatever for a promise) a `mozilla::LinkedListElement` so that they automatically remove themselves from the list of live promises on finalization. </fish out of water>

Regarding the debugger being the only thing keeping a promise alive: this is the same as how the debugger can keep objects the user is still inspecting alive. Not really a bug. But yes, we do have to be somewhat careful to avoid holding onto things forever. Luckily the actor and actor pool hierarchy helps with lifetime stuff.

> >* An on-new-promise event which includes the new promise and a time stamp in its event data
> 
> Or at least some sort of notification, right?  Using a DOM event here is a
> bit weird.  Can we do this only when actively using the promise tracer tool?

Doesn't have to be a DOM event, sorry if I implied that. We just need to be able to add listeners and get some kind of notification with the necessary data.

Yes, we can restrict this event to only when the tool is open.

> > * An on-promise-fulfilled event which includes the promise, whether it was resolved or
> > rejected, and the value it was resolved/rejected with, and a time stamp in its event
> > data
> 
> Again, can we do this only when the tool is live?

Yes.

> > * Functions to manually resolve or reject a promise with a given value.
> 
> This is easy.  Presumably we'd wrap that value into the compartment of the
> promise itself, right?

Definitely.

> >  * resolution/rejection value or null if it isn't fulfilled
> 
> undefined, not null, I'd think.

Either or. People shouldn't be checking this value without first checking the promises' fulfillment/rejection state. This wouldn't be ab API for figuring out promise state, just for getting the value once you know the promise is already completed.

Perhaps this is an opportunity to footgun and we should combine the state and resolution value methods like what Domenic proposed:

> { state: "pending" }
> { state: "fulfilled", value: v }
> { state: "rejected", reason: r }

This way we remove (somewhat) the footgun and there is no API for getting a resolution value without also checking the state. Someone could still do `GetPromiseState(p).value` without checking the state property, but it seems to discourage it more.

> 
> >  * creation time stamp
> 
> In the same units as performance.now() for the Promise's window, right?

I was thinking ms like Date.now(), but I see no reason why we shouldn't be more precise if it isn't a performance hit.

> For the stack, we'll need to figure out what form to provide it in.  But
> it's certainly doable.

It would be awesome just to return a SavedFrame instance.

> 
> > If it is easier to implement for only DOM promises first then so be it
> 
> Well, since SpiderMonkey promises don't even exist yet, it's hard to
> implement anything for them.  :(

Oh. :-/

(In reply to Domenic Denicola from comment #21)
> >>  * resolution/rejection value or null if it isn't fulfilled
> >
> > undefined, not null, I'd think.
> 
> That's not great since then you can't determine when the promise is
> fulfilled or rejected with `undefined`.
> 
> In most promise libraries we handle this via a state snapshot object of the
> form:
> 
> { state: "pending" }
> { state: "fulfilled", value: v }
> { state: "rejected", reason: r }

That API was supposed to just be for getting the resolution value, not for getting the state. Presumably, the user would have already checked that. But, you're right: that is a footgun. I'm now in favor of combining the methods for getting state and resolution values into a single method.
How much of this is needed on workers and how soon. That would require making some things async and stuff like that. Would be a fun intern project.
> I was thinking ms like Date.now(), but I see no reason why we shouldn't be more precise
> if it isn't a performance hit.

It's not a performance hit.  But the important thing is not the precision; it's where the 0 value lies (and also whether a monotonic clock is used, etc).  I'd generally aim for making times be monotonic times in the document timeline, not whatever Date.now() happens to be returning after the last NTP response.  ;)

> It would be awesome just to return a SavedFrame instance.

As long as you give me APIs to get (and keep alive as needed) such a thing and produce a JS::Value from it, that seems eminently doable.
(In reply to Boris Zbarsky [:bz] from comment #31)
> > I was thinking ms like Date.now(), but I see no reason why we shouldn't be more precise
> > if it isn't a performance hit.
> 
> It's not a performance hit.  But the important thing is not the precision;
> it's where the 0 value lies (and also whether a monotonic clock is used,
> etc).  I'd generally aim for making times be monotonic times in the document
> timeline, not whatever Date.now() happens to be returning after the last NTP
> response.  ;)

Sounds reasonable :)

> > It would be awesome just to return a SavedFrame instance.
> 
> As long as you give me APIs to get (and keep alive as needed) such a thing
> and produce a JS::Value from it, that seems eminently doable.

Will do. A SavedFrame is just a subclass of JSObject, we can even just return it as a JSObject if that is simpler. I assume you already have APIs for keeping a JSObject alive.
Yes, absolutely.  That's pretty easy.  Which compartment is the SavedFrame in?  The compartment where the stack-saving call happened?
(In reply to Nikhil Marathe [:nsm] (needinfo? please) from comment #30)
> How much of this is needed on workers and how soon. That would require
> making some things async and stuff like that. Would be a fun intern project.

Well considering that :ejpbruel has only just got pausing on debugger statements in a worker working and we aren't shipping a worker debugger, I'd say it isn't the highest priority, but it would be nice to have (maybe we can revisit once :ejpbruel has the worker debugger shipping).

(In reply to Boris Zbarsky [:bz] from comment #33)
> Yes, absolutely.  That's pretty easy.  Which compartment is the SavedFrame
> in?  The compartment where the stack-saving call happened?

That is correct.
(In reply to Panos Astithas [:past] from comment #25)
> I like the waterfall view idea, but I also think that a MVP could be an
> additional debugger panel tab, next to the Events tab, with similar
> functionality (list live promises, break on fulfillment/rejection).

The more I think about it, the more I believe that the waterfall view belongs in the performance tool and the inspection and modification stuff belongs in the debugger sidebar.
Hello everyone,

I just finished my first version of Promises Debugger Extension.
It's avaible here: https://github.com/NekR/PromisesDebuggerExtension

I am very interested in building great tool for debugging Promises. Of course, it's better to implement it nativily on browser, but for now it's good start. As I think.

At first time, I made this extension to Chrome, but their DevTools API for extensions is very limited.
Also Chrome team already start their own implementation of Promises panel for DevTools, see: https://code.google.com/p/chromium/issues/detail?id=348919



Now I ported extension to Firefox. It's now run on both Chrome and Firefox. I wrote some little guide on github page how to run extension, if you are interested. Also, if you do not want to download extension there is video which shown some basics (and bugs). Video exists here: https://www.youtube.com/watch?v=Fl54hVPiCWg

Implementation details:

First of all, I tried to use Front object for Actors, especially CallWatcherActor. But it does not work from extension code.
So I decided to do same thing as I do in Chrome, just evaluate on inspected page JavaScript code which wraps Promise constructor and its methods. Yes, yes I knew it's not good. But it works for now. Wrapper code (as I named it |backend|) of course can be better. It possibly may use Proxy objects, trace all promises creations inside callback and so on. It's on my plan :)

Also, for now code |promise instanceof Promise| will result to false. But I think it's not very popular case in promises workflow.

Thanks.
Depends on: 1183863
Product: Firefox → DevTools
Severity: normal → S3
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: