Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Record timing info for promise resolve/reject and callback #1400

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

noamr
Copy link

@noamr noamr commented Apr 18, 2024


Preview | Diff

@saschanaz
Copy link
Member

Maybe I lack the context, but having this in Web IDL covers a lot more than animation frames, right? Are the standards positions and implementation bugs actually cover the IDL layer change?

@noamr
Copy link
Author

noamr commented Apr 18, 2024

Maybe I lack the context, but having this in Web IDL covers a lot more than animation frames, right?

It does not, this is the shim between WebIDL and long animation frames. For long animation frames to report script timing it needs to know when particular scripts are enabled. This doesn't add anything web-observable to WebIDL directly.

Are the standards positions and implementation bugs actually cover the IDL layer change?

The explainers included the observable effect of this change as exposed by long animation frames from the start.

@noamr
Copy link
Author

noamr commented Jul 11, 2024

@annevk PTAL?

Copy link
Member

@annevk annevk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given https://w3c.github.io/long-animation-frames/#webidl-monkey-patches this seems incomplete? There's also numerous other ways that give you some kind of fulfilled promise this doesn't capture?

Should probably also be reviewed by @sefeng211.

@noamr
Copy link
Author

noamr commented Jul 12, 2024

Given https://w3c.github.io/long-animation-frames/#webidl-monkey-patches this seems incomplete?

Yes, there's one more monkey patch which I'm going to handle separately as it's a bit more involved.

There's also numerous other ways that give you some kind of fulfilled promise this doesn't capture?

You mean "create a resolved promise" etc? Those don't need to be recorded as they don't resolve a promise asynchronously in a task. Or do you mean something else?

@annevk
Copy link
Member

annevk commented Jul 12, 2024

How do you know "resolve" or "reject" is called from a (newly-created) task? That doesn't seem like a safe assumption.

@noamr
Copy link
Author

noamr commented Jul 12, 2024

How do you know "resolve" or "reject" is called from a task? That doesn't seem like a safe assumption.

To be more specific, this is not about it being called from a task but rather being a script entry point (which is usually a task).
We check for that in https://w3c.github.io/long-animation-frames/#create-script-entry-point step 6: only scripts that are at the bottom of the stack (which usually, but not always, means that they are a result of a task) are recorded.

I guess though that this is a valid point as a spec can create a resolved promise and then react to it which would have the same effect. Perhaps a better place for this is HostEnqueuePromiseJob but I would have to pass to it whether it's being resolved or rejected.

@noamr
Copy link
Author

noamr commented Jul 15, 2024

@annevk I looked at this again and this is not an issue.

  • Creating a resolved promise in itself does not trigger an author-provided script.
  • Calling react to a promise on an already resolved promise also, in itself, doesn't trigger a user script. It triggers custom steps, some of which would trigger a user script (e.g. calling resolve, or evaluating a script, or executing an existing registered callback etc.)

If I missed one of the paths that runs a user script I'm happy to correct this but I couldn't so far so for the extent of my research this PR is correct.

@annevk
Copy link
Member

annevk commented Jul 15, 2024

By that logic, how does resolve trigger an author-provided script?

@noamr
Copy link
Author

noamr commented Jul 15, 2024

By that logic, how does resolve trigger an author-provided script?

It's a promise that was created before and already had an author-provided then.
That can't happen for a promise you've just created before running any author script.

@annevk
Copy link
Member

annevk commented Jul 15, 2024

I think I need a higher-level explanation of the objectives here in order to be able to review. It's not entirely clear to me what is being measured.

@noamr
Copy link
Author

noamr commented Jul 15, 2024

I think I need a higher-level explanation of the objectives here in order to be able to review. It's not entirely clear to me what is being measured.

Sure! What we're trying to measure here is the script entry point: from the moment an author script (callback, event-listener, promise resolution, script evaluation) started running until it has a clear stack and all the related microtasks have been executed. This should give authors a hint regarding hot-spots in their code that cause long animation frames (and potential jank as a result).

The start and end of an "entry point" are the places are equivalent to these spots in the HTML spec:

The time spent between those two points are "time spent in JS execution".
The only reason not to spec it exactly like the above is that in those two functions we "lost" some of the metadata of how the platform invoked the script, e.g. information about the callback/event or whether it's a resolved/rejected promise.

So instead, the "start" of the entry point is recorded at the locations that can trigger script execution, and the "end" is recorded at the aforementioned "cleanup" location.

Does this make more sense?

@annevk
Copy link
Member

annevk commented Jul 15, 2024

A thing I don't understand is that promise resolution isn't a guarantee that script will run.

And there's also no guarantee that when a specification calls resolve/reject the JS stack is empty.

Why isn't this done as part of HTML's script running algorithms? It seems that already has the appropriate start and end points.

@smaug---- have you looked at this?

@noamr
Copy link
Author

noamr commented Jul 15, 2024

Example 1:

fetch(url).then(() => { ... })

If fetch returns a promise that will be resolved later, the resolve would be the entry point.
If fetch returns a resolved promise, then the script entry point is whoever called fetch, and that script execution continues (with an additional microtask)

Example 2:

// This might return an already resolved promise
const text = response.text()

setTimeout(() => do_something_with(await text))

If text returns a resolved promise here it doesn't trigger a user script. We have to have an extra author script that reacts to the promise. The point is that there's always a script that reacts to a resolved promise.

A thing I don't understand is that promise resolution isn't a guarantee that script will run.

True, the worth that can happen though is that a promise resolution that takes as long time would be counted as if it was an author script.

And there's also no guarantee that when a specification calls resolve/reject the JS stack is empty.

Those checks are done inside the Long Animation Frame timing spec. If there is already a running entry point, we return early (see https://w3c.github.io/long-animation-frames/#create-script-entry-point #6). The end point is anyway called directly from HTML here when we clear the mictorask queue.

Why isn't this done as part of HTML's script running algorithms? It seems that already has the appropriate start and end points.

The "end" point called from HTML (see above).
We call the start point from the call sites because that's where we have the metadata (e.g. that this is a promise resolver). We could do something like register the metadata somewhere and record the timing in HTML but the observable result would be very similar.

@annevk
Copy link
Member

annevk commented Jul 15, 2024

Hmm, but what if a task is used to fire an event and resolve a promise? You'd get two separate measurements if both are "reacted" to? Wouldn't it be more accurate to annotate the tasks?

@noamr
Copy link
Author

noamr commented Jul 15, 2024

Hmm, but what if a task is used to fire an event and resolve a promise? You'd get two separate measurements if both are "reacted" to? Wouldn't it be more accurate to annotate the tasks?

Separating them to script entry points is much more actionable, especially since this is in the context of a single long animation frame (which is a task + the rendering update)... e.g. if the event handler takes 30ms and the function you put in then takes 5ms you probably want to know this.

@annevk
Copy link
Member

annevk commented Jul 15, 2024

They'd both get attributed to the event handler as the JS stack will be non-empty by the time you get to the promise though.

I think it also gets confusing if you resolve multiple promises in a single task.

I don't like using "resolve" and "reject" as entry points coupled with the JS stack being empty as in my mind that's very much against the way promises are designed. Tying this directly to tasks (that may execute script) seems much more natural.

@noamr
Copy link
Author

noamr commented Jul 15, 2024

They'd both get attributed to the event handler as the JS stack will be non-empty by the time you get to the promise though.

That's not true, as firing an event clears the microtask queue (in "cleanup after script"). Each of those calls would be totally separate.

I think it also gets confusing if you resolve multiple promises in a single task.

Sure, though that's a rare case.

I don't like using "resolve" and "reject" as entry points coupled with the JS stack being empty as in my mind that's very much against the way promises are designed. Tying this directly to tasks (that may execute script) seems much more natural.

See above, this would lose important information if you do other things in that task.

@annevk
Copy link
Member

annevk commented Jul 15, 2024

That's not true, as firing an event clears the microtask queue (in "cleanup after script").

You're assuming the event is dispatched before the promise is resolved? I think I'd always want to resolve the promise first, but that's a good point and something to be careful about.

@noamr
Copy link
Author

noamr commented Jul 15, 2024

That's not true, as firing an event clears the microtask queue (in "cleanup after script").

You're assuming the event is dispatched before the promise is resolved? I think I'd always want to resolve the promise first, but that's a good point and something to be careful about.

We actually had issues like this with view-transitions, and had to wrap the "resolve the promise" call with a prepare/cleanup to avoid this jumbling of execution order... (see https://html.spec.whatwg.org/multipage/browsing-the-web.html#reveal step 5).

In most cases today there is only one "resolve a promise" in a task, and in the other cases I'd really recommend to do the above... The cases where "resolve a promise" calls blend their microtasks with other things your task is doing create an order-of-operation confusion and those are the exact same places where this type of measurement would also be confusing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

3 participants