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

"Release a wake lock": should we queue a task to fire the "release" event? #293

Closed
rakuco opened this issue Feb 1, 2021 · 13 comments · Fixed by #299
Closed

"Release a wake lock": should we queue a task to fire the "release" event? #293

rakuco opened this issue Feb 1, 2021 · 13 comments · Fixed by #299

Comments

@rakuco
Copy link
Member

rakuco commented Feb 1, 2021

(also related: #139)

I've been reviewing a Chromium CL that deals with bfcache, and ended up with a few questions about our "release a wake lock" algorithm and its relationship with the Page Lifecycle API and bfcache.

Since #242, the "release a wake lock" algorithm queues a task to fire the "release" event (and, since #279, to set the [[Released]] internal slot to true). The question is whether this should be done in a task or as a synchronous step in the algorithm.

In the Chromium CL in question, we've noticed that when navigating away from a page, we do reach https://w3c.github.io/screen-wake-lock/#handling-document-loss-of-visibility, the "release a wake lock" algorithm does run but the queued task is never run, as the page is frozen first (even without bfcache, in which case I guess it'd be unloaded first).

Queuing a task to fire an event seems idiomatic, but in this case I wonder if it's the best choice, especially since this happens when the document both loses visibility and loses full activity. Or, at the very least, I wonder if changing the [[Released]] slot should be done synchronously, or if doing that separately from firing the event makes even less sense.

@rakuco
Copy link
Member Author

rakuco commented Feb 1, 2021

I spoke to @kenchris about this earlier today, and he suggested CC'ing @domenic and @annevk for extra input.

@annevk
Copy link
Member

annevk commented Feb 1, 2021

I don't understand this algorithm. Is it intentional that the release event is always dispatched, even if the OS said no? Usually you queue a task if you want to announce the result of some other thread, but that's not what's happening here. (I.e., flipping the order of step 5 and 6 is a no-op...)

@rakuco
Copy link
Member Author

rakuco commented Feb 1, 2021

Is it intentional that the release event is always dispatched, even if the OS said no?

It might make sense to do something different later, but for now yeah, we always remove the lock from [[ActiveLocks]] and fire the event when this algorithm is called.

Usually you queue a task if you want to announce the result of some other thread, but that's not what's happening here. (I.e., flipping the order of step 5 and 6 is a no-op...)

OK, so does it make more sense to stop deferring the event and the change to [[Released]] here then? @marcoscaceres suggested queuing a task back in #242, but I didn't ask for a rationale back then :/

@annevk
Copy link
Member

annevk commented Feb 1, 2021

Yeah, though you want to audit all callers, e.g., release() will need to queue a task from its "in parallel" section (although is all it does there queue a task or would it also do some kind of permission check?). It would have to anyway as "release a wake lock" tries to access all kinds of state not typically available "in parallel".

@rakuco
Copy link
Member Author

rakuco commented Feb 1, 2021

Yeah, though you want to audit all callers, e.g., release() will need to queue a task from its "in parallel" section (although is all it does there queue a task or would it also do some kind of permission check?).

Correct, release() basically just calls the "release a wake lock" algorithm. Other callers include the handlers for losing document visibility or document full activity. It's not totally clear to me what to look for in this audit though -- if I need to queue a task in those handlers, doesn't it mean running into the same kind of issue described in this bug?

It would have to anyway as "release a wake lock" tries to access all kinds of state not typically available "in parallel".

Can you elaborate on why state isn't available in the "in parallel" blocks? I thought the algorithm would have access to WakeLockSentinel's entire state since it's passed as an argument, regardless of whether it is running "in parallel" or not.

@annevk
Copy link
Member

annevk commented Feb 1, 2021

It doesn't basically call it though, right? It goes in parallel first and calls it from there. If that wasn't meant to be in parallel, why does it say that? That's what I mean by audit. If you're going to change things around, you want to make sure it all still makes sense.

@rakuco
Copy link
Member Author

rakuco commented Feb 1, 2021

OK. My understanding of that particular method is that that part runs in parallel to avoid potentially blocking on calling the OS (and dispatching the event in case we do change the algorithm). If that part didn't run in parallel, I don't think there'd be much point in returning a promise in the first place.

@annevk
Copy link
Member

annevk commented Feb 1, 2021

Okay, but we are on the same page that if it goes in parallel, you cannot call "release a wake lock" synchronously from those in parallel steps? And I now see that release a wake lock itself says at the top it goes in parallel too... And then again in step 5?

I think someone needs to have another look at all this and decide where in parallel is important and where it's noise. And keep in mind that when you are in parallel you cannot access things such as documents or global objects and need to queue a task to get back to the main thread.

@rakuco
Copy link
Member Author

rakuco commented Feb 2, 2021

That someone would probably be me anyway :-) Dealing with the event loop from other specifications was very helpful, but it also left me with some questions about object creation in parallel steps and how promises and queued tasks are supposed to work together. It also left me paranoid about having to think about realms, global objects, task sources and objects creation and manipulation everywhere :-)

These are all the parts of the spec that have "in parallel" in their text, along with some questions and comments:

  • The obtain permission algorithm:

    • Says "run these steps in parallel", probably because it can block on user-facing permission prompts
    • Can IDL dictionaries (e.g. PermissionDescriptor) be created when "in parallel"?
    • Maybe it could possibly be replaced by "request permission to use"? Not sure if that always needs to be invoked in parallel or not.
  • "Acquire a wake lock" algorithm:

    • Entire algorithm is currently supposed to run "in parallel"
    • ... but it references the "responsible document" and the "current settings object"
    • Maybe only run "Ask the underlying OS to acquire the wake lock of type type" in parallel?
  • "Release a wake lock" algorithm:

    • Entire algorithm is currently supposed to run "in parallel", and one of the steps is supposed to run "in parallel" again
    • Despite supposedly running "in parallel", references "responsible document" and "current settings object"
    • Possible fixes:
      1. Only run step 5 (asking the OS to release the lock) in parallel
      2. Stop queuing a task to [[Released]] to true and fire "release" event (we're missing a task source anyway)
      3. Call the OS last? More generally speaking, if there are two subsequent parallel steps, one to queue a task to fire an event and another to resolve a promise, will the promise always be resolved after the event is fired?
  • WakeLock.request():

    • The "in parallel" steps handle permission request and invoking the "acquire a wake lock" algorithm to, among other things, ask the OS for a lock.
    • Not sure how to handle promises, permissions and OS calls together: permission request can block, so it always seems to be done "in parallel".
    • Can IDL interfaces (e.g. WakeLockSentinel) "in parallel"?
    • Not clear how resolving a promise in a parallel step (with an object created in parallel) works.
    • Does that "in parallel" step need to be broken into multiple steps?
  • WakeLockSentinel.release:

    • Possibly no need to change anything if the "release a wake lock" algorithm is adjusted?
    • Dunno how to avoid running the entire "release a wake lock" algorithm in parallel because of the promise there

@domenic
Copy link
Contributor

domenic commented Feb 2, 2021

Can IDL dictionaries (e.g. PermissionDescriptor) be created when "in parallel"?

Can IDL interfaces (e.g. WakeLockSentinel) "in parallel"?

Realm-agnostic things can be created in parallel. So, interfaces are realm-specific, and cannot be created there. For dictionaries, it depends on what the dictionary contains. Dictionary keys are strings, which are realm-agnostic, but their values vary. If one of the values is an interface, then that can't be created in parallel. If it's all primitives or sequences or records or similar, then it can.

More generally speaking, if there are two subsequent parallel steps, one to queue a task to fire an event and another to resolve a promise, will the promise always be resolved after the event is fired?

Not clear how resolving a promise in a parallel step (with an object created in parallel) works.

You can't resolve a promise from in parallel. You need to queue a task to do so. (And, you can use the same task to resolve the promise and fire the event.)

@annevk
Copy link
Member

annevk commented Feb 2, 2021

I'm afraid you will have to think about all those things, but I'm happy to help.

More generally speaking, if there are two subsequent parallel steps, one to queue a task to fire an event and another to resolve a promise, will the promise always be resolved after the event is fired?

What you need to do for cases like this is queue a task to fire the event and resolve the promise (a task can itself be a series of steps). I'm not sure if we decided on ideal order there. I guess I'd resolve first...

@rakuco
Copy link
Member Author

rakuco commented Feb 2, 2021

You can't resolve a promise from in parallel. You need to queue a task to do so.

Hmm, I wonder if the examples in https://heycam.github.io/webidl/#es-promise-examples need to be adjusted then?

Anyway, thanks for the help so far, @annevk and @domenic. I'll see what I can come up with tomorrow.

@annevk
Copy link
Member

annevk commented Feb 2, 2021

Yeah, they should be (only 55 does it correctly). I'll see if I can put a fix out for that today.

rakuco added a commit to rakuco/wake-lock that referenced this issue Feb 5, 2021
Steps that run "in parallel" need to take several extra restrictions into
account: we do not have access to documents or global objects, cannot create
IDL interfaces or manipulate promises, for example, all of which we had been
doing, along with needlessly running nested "in parallel" steps.

This change attempts to rectify the situation by only running things in
parallel when absolutely necessary (i.e. when reaching out to the OS), and
queueing tasks from there when we need to manipulate promises or create
objects (changes to the "obtain permission" algorithm, which also runs in
parallel, are being tracked separately in w3c#298).

`WakeLock.request()`:
* Queue a task to reject `|promise|` when the permission request run in
parallel is denied.
* Manipulate the [[ActiveLocks]] internal slot, create a new
WakeLockSentinel and resolve the returned promise in a queued task.

`WakeLockSentinel.release()`:
* Do not run the "release a wake lock" algorithm in parallel (see changes to
the algorithm itself below).
* Just return a resolved promise once the rest of the steps run. The
returned promise does not have much use, but has been kept to avoid breaking
API compatibility.

"Acquire wake lock" algorithm:
* Stop running everything in parallel.
* Move all [[ActiveLocks]] manipulation to `WakeLock.request()` itself, and
make the algorithm just ask the platform for a wake lock.

"Release wake lock" algorithm:
* Stop running everything in parallel. The only steps that still need to run
in parallel are the ones asking the platform to release the wake lock.
* Consequently, stop queueing a task to change [[Released]] and fire the
"release" event and do it directly in the algorithm.

Fixes w3c#293.
rakuco added a commit to rakuco/wake-lock that referenced this issue Feb 11, 2021
Steps that run "in parallel" need to take several extra restrictions into
account: we do not have access to documents or global objects, cannot create
IDL interfaces or manipulate promises, for example, all of which we had been
doing, along with needlessly running nested "in parallel" steps.

This change attempts to rectify the situation by only running things in
parallel when absolutely necessary (i.e. when reaching out to the OS), and
queueing tasks from there when we need to manipulate promises or create
objects (changes to the "obtain permission" algorithm, which also runs in
parallel, are being tracked separately in w3c#298).

`WakeLock.request()`:
* Queue a task to reject `|promise|` when the permission request run in
parallel is denied.
* Manipulate the [[ActiveLocks]] internal slot, create a new
WakeLockSentinel and resolve the returned promise in a queued task.

`WakeLockSentinel.release()`:
* Do not run the "release a wake lock" algorithm in parallel (see changes to
the algorithm itself below).
* Just return a resolved promise once the rest of the steps run. The
returned promise does not have much use, but has been kept to avoid breaking
API compatibility.

"Acquire wake lock" algorithm:
* Stop running everything in parallel.
* Move all [[ActiveLocks]] manipulation to `WakeLock.request()` itself, and
make the algorithm just ask the platform for a wake lock.

"Release wake lock" algorithm:
* Stop running everything in parallel. The only steps that still need to run
in parallel are the ones asking the platform to release the wake lock.
* Consequently, stop queueing a task to change [[Released]] and fire the
"release" event and do it directly in the algorithm.

Fixes w3c#293.
rakuco added a commit to rakuco/wake-lock that referenced this issue Feb 15, 2021
Steps that run "in parallel" need to take several extra restrictions into
account: we do not have access to documents or global objects, cannot create
IDL interfaces or manipulate promises, for example, all of which we had been
doing, along with needlessly running nested "in parallel" steps.

This change attempts to rectify the situation by only running things in
parallel when absolutely necessary (i.e. when reaching out to the OS), and
queueing tasks from there when we need to manipulate promises or create
objects (changes to the "obtain permission" algorithm, which also runs in
parallel, are being tracked separately in w3c#298).

`WakeLock.request()`:
* Queue a task to reject `|promise|` when the permission request run in
parallel is denied.
* Manipulate the [[ActiveLocks]] internal slot, create a new
WakeLockSentinel and resolve the returned promise in a queued task.

`WakeLockSentinel.release()`:
* Do not run the "release a wake lock" algorithm in parallel (see changes to
the algorithm itself below).
* Just return a resolved promise once the rest of the steps run. The
returned promise does not have much use, but has been kept to avoid breaking
API compatibility.

"Acquire wake lock" algorithm:
* Stop running everything in parallel.
* Move all [[ActiveLocks]] manipulation to `WakeLock.request()` itself, and
make the algorithm just ask the platform for a wake lock.

"Release wake lock" algorithm:
* Stop running everything in parallel. The only steps that still need to run
in parallel are the ones asking the platform to release the wake lock.
* Consequently, stop queueing a task to change [[Released]] and fire the
"release" event and do it directly in the algorithm.

Fixes w3c#293.
rakuco added a commit to rakuco/wake-lock that referenced this issue Feb 25, 2021
Steps that run "in parallel" need to take several extra restrictions into
account: we do not have access to documents or global objects, cannot create
IDL interfaces or manipulate promises, for example, all of which we had been
doing, along with needlessly running nested "in parallel" steps.

This change attempts to rectify the situation by only running things in
parallel when absolutely necessary (i.e. when reaching out to the OS), and
queueing tasks from there when we need to manipulate promises or create
objects (changes to the "obtain permission" algorithm, which also runs in
parallel, are being tracked separately in w3c#298).

`WakeLock.request()`:
* Queue a task to reject `|promise|` when the permission request run in
parallel is denied.
* Manipulate the [[ActiveLocks]] internal slot, create a new
WakeLockSentinel and resolve the returned promise in a queued task.

`WakeLockSentinel.release()`:
* Do not run the "release a wake lock" algorithm in parallel (see changes to
the algorithm itself below).
* Just return a resolved promise once the rest of the steps run. The
returned promise does not have much use, but has been kept to avoid breaking
API compatibility.

"Acquire wake lock" algorithm:
* Stop running everything in parallel.
* Move all [[ActiveLocks]] manipulation to `WakeLock.request()` itself, and
make the algorithm just ask the platform for a wake lock.

"Release wake lock" algorithm:
* Stop running everything in parallel. The only steps that still need to run
in parallel are the ones asking the platform to release the wake lock.
* Consequently, stop queueing a task to change [[Released]] and fire the
"release" event and do it directly in the algorithm.

Fixes w3c#293.
rakuco added a commit to rakuco/wake-lock that referenced this issue Feb 28, 2021
Steps that run "in parallel" need to take several extra restrictions into
account: we do not have access to documents or global objects, cannot create
IDL interfaces or manipulate promises, for example, all of which we had been
doing, along with needlessly running nested "in parallel" steps.

This change attempts to rectify the situation by only running things in
parallel when absolutely necessary (i.e. when reaching out to the OS), and
queueing tasks from there when we need to manipulate promises or create
objects (changes to the "obtain permission" algorithm, which also runs in
parallel, are being tracked separately in w3c#298).

`WakeLock.request()`:
* Queue a task to reject `|promise|` when the permission request run in
parallel is denied.
* Manipulate the [[ActiveLocks]] internal slot, create a new
WakeLockSentinel and resolve the returned promise in a queued task.

`WakeLockSentinel.release()`:
* Do not run the "release a wake lock" algorithm in parallel (see changes to
the algorithm itself below).
* Just return a resolved promise once the rest of the steps run. The
returned promise does not have much use, but has been kept to avoid breaking
API compatibility.

"Acquire wake lock" algorithm:
* Stop running everything in parallel.
* Move all [[ActiveLocks]] manipulation to `WakeLock.request()` itself, and
make the algorithm just ask the platform for a wake lock.

"Release wake lock" algorithm:
* Stop running everything in parallel. The only steps that still need to run
in parallel are the ones asking the platform to release the wake lock.
* Consequently, stop queueing a task to change [[Released]] and fire the
"release" event and do it directly in the algorithm.

Fixes w3c#293.
rakuco added a commit to rakuco/wake-lock that referenced this issue Mar 22, 2021
Steps that run "in parallel" need to take several extra restrictions into
account: we do not have access to documents or global objects, cannot create
IDL interfaces or manipulate promises, for example, all of which we had been
doing, along with needlessly running nested "in parallel" steps.

This change attempts to rectify the situation by only running things in
parallel when absolutely necessary (i.e. when reaching out to the OS), and
queuing global tasks (emphasis on "global" given whatwg/html#4980 and
whatwg/html#5653) from there when we need to manipulate promises or create
objects.

"Obtain permission" algorithm:
* Stop running in parallel. Callers should be responsible for choosing
  whether it should be run in parallel or not.

`WakeLock.request()`:
* Separate returning a promise and running steps in parallel. This style is
  more usual.
* Refer to the "relevant settings object" rather than the "current settings
  object", as we are inside a method of an IDL interface and can rely on it
  being defined. I do not think this is a user-visible change, and it looks
  cleaner.
* Queue a global task to reject `|promise|` when the permission request run
  in parallel is denied.
* Manipulate the `[[ActiveLocks]]` internal slot, check page visibility,
  invoke "acquire a wake lock", create a new WakeLockSentinel and resolve
  the returned promise in a queued global task.

`WakeLockSentinel.release()`:
* Do not run the "release a wake lock" algorithm in parallel (see the
  changes to the algorithm itself below).
* Just return a resolved promise once the rest of the steps run. The
  returned promise does not have much use, but has been kept to avoid
  breaking API compatibility.
* One user-visible consequence is that the "release" event is fired
  synchronously and before the function returns.

"Acquire wake lock" algorithm:
* Stop running everything in parallel.
* Move all `[[ActiveLocks]]` manipulation to `WakeLock.request()` itself,
  and make the algorithm just ask the platform for a wake lock.

"Release wake lock" algorithm:
* Stop running everything in parallel. The only steps that still need to run
  in parallel are the ones asking the platform to release the wake lock.
* Consequently, stop queueing a task to change `[[Released]]` and fire the
  "release" event, and do it directly in the algorithm instead.

Big thanks to Anne van Kesteren, Domenic Denicola and Marcos Cáceres for
their input.

Fixes w3c#293.
rakuco added a commit that referenced this issue Mar 22, 2021
Steps that run "in parallel" need to take several extra restrictions into
account: we do not have access to documents or global objects, cannot create
IDL interfaces or manipulate promises, for example, all of which we had been
doing, along with needlessly running nested "in parallel" steps.

This change attempts to rectify the situation by only running things in
parallel when absolutely necessary (i.e. when reaching out to the OS), and
queuing global tasks (emphasis on "global" given whatwg/html#4980 and
whatwg/html#5653) from there when we need to manipulate promises or create
objects.

"Obtain permission" algorithm:
* Stop running in parallel. Callers should be responsible for choosing
  whether it should be run in parallel or not.

`WakeLock.request()`:
* Separate returning a promise and running steps in parallel. This style is
  more usual.
* Refer to the "relevant settings object" rather than the "current settings
  object", as we are inside a method of an IDL interface and can rely on it
  being defined. I do not think this is a user-visible change, and it looks
  cleaner.
* Queue a global task to reject `|promise|` when the permission request run
  in parallel is denied.
* Manipulate the `[[ActiveLocks]]` internal slot, check page visibility,
  invoke "acquire a wake lock", create a new WakeLockSentinel and resolve
  the returned promise in a queued global task.

`WakeLockSentinel.release()`:
* Do not run the "release a wake lock" algorithm in parallel (see the
  changes to the algorithm itself below).
* Just return a resolved promise once the rest of the steps run. The
  returned promise does not have much use, but has been kept to avoid
  breaking API compatibility.
* One user-visible consequence is that the "release" event is fired
  synchronously and before the function returns.

"Acquire wake lock" algorithm:
* Stop running everything in parallel.
* Move all `[[ActiveLocks]]` manipulation to `WakeLock.request()` itself,
  and make the algorithm just ask the platform for a wake lock.

"Release wake lock" algorithm:
* Stop running everything in parallel. The only steps that still need to run
  in parallel are the ones asking the platform to release the wake lock.
* Consequently, stop queueing a task to change `[[Released]]` and fire the
  "release" event, and do it directly in the algorithm instead.

Big thanks to Anne van Kesteren, Domenic Denicola and Marcos Cáceres for
their input.

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

Successfully merging a pull request may close this issue.

3 participants