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

Handle FinalizationRegistry #69

Merged
merged 9 commits into from
Mar 6, 2024
Merged

Handle FinalizationRegistry #69

merged 9 commits into from
Mar 6, 2024

Conversation

jridgewell
Copy link
Member

This snapshots the ambient AC state when calling fr.register(), to be restored when invoking the FR's callback on that registered cell.


I also removed our changes of HostMakeJobCallback. First, because it felt awkward to refer to the JobCallback's [[AsyncContextSnapshot]] inside NewPromiseReactionJob and NewPromiseResolveThenableJob. And second, because it actually causes a memory leak with FinalizationRegistry.

The FR calls HostMakeJobCallback (causing it to snapshot) during its constructor to prepare the callback to be used for jobs later on. But that snapshot will never be used, we'll only use the registration time snapshot. So I had to move the snapshotting closer into PerformPromiseThen.

@andreubotella
Copy link
Member

andreubotella commented Feb 17, 2024

I thought this PR was meant to fix the regression introduced in #41. However, before #41, FinalizationRegistry callbacks were called with the FR construction context, rather than with the context at the time the register method was called. Have we actually discussed that the behavior in this PR is the right one?

This is actually similar to some events in the web:

const asyncVar = new AsyncContext.Variable();

const image = document.getElementById("image");  // <img id="image">

asyncVar.run("foo", () => {
  image.addEventListener("load", () => {
    console.log(asyncVar.get());
  });
});

asyncVar.run("bar", () => {
  image.src = "./some/image.jpg";
});

Here we have a registration-time context (the registration of the load event, or the FR construction with a callback), and a separate context (the call to register, or the setting of the image's src) which asynchronously triggers the call to the registered callback.

It's not clear to me which of the two is the right behavior, and it clearly ties into the behavior of events in the HTML integration.

@mhofman
Copy link
Member

mhofman commented Feb 17, 2024

I remember this being discussed in Matrix. I believe register time is the correct one. Furthermore it's easy to do FR construction time by simply wrapping the callback.

Edit: the other direction is more tricky, requiring to create a snapshot on registration, pass it in the held value, and run from the snapshot in the callback.

@andreubotella
Copy link
Member

andreubotella commented Feb 17, 2024

I remember this being discussed in Matrix. I believe register time is the correct one. Furthermore it's easy to do FR construction time by simply wrapping the callback.

I think I misunderstood you when this was discussed, so I didn't realize there was a difference wrt the previous behavior.

By the way, the terminology gets confusing here because for events, "registration-time" is the time at which you register the event by e.g. calling addEventListener, which would be equivalent to FR construction. Calling register would be an asynchronous kind of "call-time".

Edit: the other direction is more tricky, requiring to create a snapshot on registration, pass it in the held value, and run from the snapshot in the callback.

Spec-wise it's not more tricky, since the snapshot doesn't need to be held in the FR's [[Cells]]. In the current spec text, HostMakeJobCallback takes the snapshot and stores it alongside the callback, so it could be swapped in CleanupFinalizationRegistry() in the same way it is swapped in NewPromiseReactionJob.

@mhofman
Copy link
Member

mhofman commented Feb 17, 2024

By the way, the terminology gets confusing here because for events, "registration-time" is the time at which you register the event by e.g. calling addEventListener, which would be equivalent to FR construction. Calling register would be an asynchronous kind of "call-time".

Ah yes I meant FR register. It's not the same as addEventListener, but it's not equivalent an async dispatch either. IMO semantically it's more like if the program did addEventListener for target, but with the event handler shared and added earlier.

Spec-wise it's not more tricky,

I was strictly taking the POV of the API consumer. It's harder to get the register time semantics manually.

the snapshot doesn't need to be held in the FR's [[Cells]]

I'm confused, if we want FR register time, doesn't the spec need to store the Snapshot in each cell?
I probably should read the PR

@andreubotella
Copy link
Member

andreubotella commented Feb 17, 2024

the snapshot doesn't need to be held in the FR's [[Cells]]

I'm confused, if we want FR register time, doesn't the spec need to store the Snapshot in each cell? I probably should read the PR

That's right, it would need to be stored in each cell. But I thought by "the other direction" you meant construction time.

spec.html Outdated Show resolved Hide resolved
Co-authored-by: Andreu Botella <abotella@igalia.com>
spec.html Outdated Show resolved Hide resolved
Co-authored-by: Jordan Harband <ljharb@gmail.com>
@littledan
Copy link
Member

I'd rather use the construction-time AsyncContext snapshot here, just for simplicity. Are there any cases that we're aware of where it's important to have the semantics described in this PR?

@jridgewell
Copy link
Member Author

jridgewell commented Feb 23, 2024

I think using construction-time is the incorrect choice, because observers like this (and DOM observers like MutationObserver) should be held in the module scope. If it's in the module scope, the ambient context is just empty, and that's not useful.

I think choosing the context during the call to register is directly analogous to choosing the context when you attach an event listener. Andreu had brought up during last meeting that "registration-time" to him was when you passed the callback, but I think that's incorrect. Registration is when I invoke the method to be notified of a future event, eg fr.register() or el.addEventListener().

@littledan
Copy link
Member

Not passing the callback at the time you register is a big difference from addEventListener. I think the consistency arguments can be made in both directions. Is there any actual utility to the semantics of this patch? If not, I really prefer the option which is simpler from an implementation and specification perspective. Note that it is always possible to build a wrapper around FinalizationRegistry which applies the semantics in this patch.

If we can’t come to a conclusion on this thread, let’s discuss this at the next AsyncContext meeting.

@Jamesernator
Copy link

Jamesernator commented Mar 1, 2024

because observers like this (and DOM observers like MutationObserver) should be held in the module scope

Other observers batch things anyway (which FinalizationRegistry was originally proposed to do as well, but was dropped at some point) so having a "registration time" for DOM observers is just a non-possibility anyway.

@jridgewell
Copy link
Member Author

jridgewell commented Mar 2, 2024

Other observers batch things anyway (which FinalizationRegistry was originally proposed to do as well, but was dropped at some point) so having a "registration time" for DOM observers is just a non-possibility anyway.

That's interesting. I've always assumed it only batched mutations per-observe calls (so two appendChilds on a subtree observe would only fire once per tick). I didn't realize all observe calls are invoked at once (so subtree observe and a separate attributes observe would also only fire once).

So it really isn't possible to automatically maintain "registration" time per observe call, and users that need it would have to use a separate WeakMap or something to handle Snapshots. Given that, I'm ok with either approach.

@mhofman
Copy link
Member

mhofman commented Mar 5, 2024

and users that need it would have to use a separate WeakMap

You don't even need to go that far.

class FinalizationRegistryWithSnapshot extends FinalizationRegistry {
  constructor(callback) {
    super(({snapshot, value}) => {
      snapshot.run(callback, value);
    })
  }
  register(target, value, token) {
    return super.register(target, {value, snapshot: new AsyncContext.Snapshot()}, token);
  }
}

@littledan
Copy link
Member

In the AsyncContext call today, the group apparently decided on construction-time snapshot semantics. (Please correct me if I'm mistaken.)

@jridgewell
Copy link
Member Author

You don't even need to go that far.

I was referencing MuationObserver in that comment, which would require a WeakMap to hold a #[element, registration-type] -> Snapshot mapping.

@jridgewell
Copy link
Member Author

Updated to use creation-time context.

spec.html Outdated
<dl class="header">
</dl>
<emu-alg>
1. Assert: _finalizationRegistry_ has [[Cells]], [[CleanupCallback]], and [[FinalizationRegistryAsyncContextSnapshot]] internal slots.
Copy link
Member

Choose a reason for hiding this comment

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

This needs an <ins>.

@jridgewell jridgewell merged commit b243400 into master Mar 6, 2024
5 checks passed
@jridgewell jridgewell deleted the final-registry branch March 6, 2024 19:57
andreubotella added a commit to andreubotella/test262 that referenced this pull request Mar 6, 2024
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 this pull request may close these issues.

6 participants