-
Notifications
You must be signed in to change notification settings - Fork 5.3k
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
feat(core): Add ability to "ref" and "unref" pending ops #12889
Conversation
core/01_core.js
Outdated
function opAsync(opName, arg1 = null, arg2 = null, unref = false) { | ||
const promiseId = nextPromiseId++; | ||
const maybeError = opcallAsync(opsCache[opName], promiseId, arg1, arg2); | ||
const maybeError = opcallAsync( | ||
opsCache[opName], | ||
promiseId, | ||
arg1, | ||
arg2, | ||
unref, | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This change was done so that ops that are unref by default don't have to call into Rust immediately after dispatch to mark them as "unrefed"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I probably would keep the old signature. The only "unref-by-default" ops are signal listeners and they're not a performance concern.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reverted.
core/01_core.js
Outdated
return PromisePrototypeThen(setPromise(promiseId), unwrapOpResult); | ||
const p = PromisePrototypeThen(setPromise(promiseId), unwrapOpResult); | ||
// Save the id on the promise so it can be latered ref'ed or unref'ed | ||
p.id = promiseId; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have you considered using a symbol here or a reverse WeakMap?
(I am not sure what that would do for performance, we'd have to measure that)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did not, but using a symbol might be preferable, that way we can better communicate that users shouldn't use that value. I'll explore that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Used Symbol.for("Deno.core.internalPromiseId")
instead, let's see if performance is better or worse
core/lib.deno_core.d.ts
Outdated
/** Mark following promises as "ref", ie. event loop won't exit | ||
* until they are resolved. All async ops are "ref" by default. */ | ||
function refOps( | ||
...promiseIds: number[] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why does it take multiple promiseId
s?
I do understand that it is faster to coalesce multiple refOp
calls into a single call, but I wonder how common that is, and also note that passing a single int into the binding layer is faster than passing an array.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In some resources we might have multiple pending ops associated with it. So instead of crossing boundary multiple times I thought it's better to do it in one go. Keep in mind that this is spread operator so you're passing multiple integers and not an array (Deno.core.refOps(1, 2, 3, 4)
). Also is there a situation where this API would be used in a hot path?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Keep in mind that this is spread operator so you're passing multiple integers and not an array (Deno.core.refOps(1, 2, 3, 4))
I see, in the binding layer you're just iterating over FunctionCallbackArguments
, that's pretty efficient and I have no concerns about it.
In some resources we might have multiple pending ops associated with it.
You'll find that this almost never happens in practice but oh well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you prefer to have it accept single ID that is fine for me as well. Your call
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks pretty good, some questions though.
Do we want all op(calls) to be unrefable ? I feel like it should be opt-in and take its distinct path (e.g: |
I don't see a reason why we would be more restrictive about this than necessary. |
Well if in practice there's only a very small subset of ops/opcalls that may be unref'd then I think it's cleaner to have its own distinct implementation path so as not to overload the simple async opcall path |
I agree with Bert, I also believe it makes it more confusing to have distinct type of op for that purpose. Besides having different API there would be no upside IMO as we won't be able to validate if you are unrefing an op of that type. I'll be happy to revisit the topic after we add |
I'm not proposing a different kind of op rust-side just that the opCall JS side is explicit (killing
It all depends on our call expectations but IMO:
From my understanding If that's the case I think I also think it's more ergonomic, the previous |
Okay, I misunderstood then. @AaronO I removed |
Also I'd appreciate some feedback on how to tackle metrics - right now there are two uncalled APIs, this needs to be addressed before landing. |
core/ops_metrics.rs
Outdated
@@ -85,12 +85,14 @@ impl OpsTracker { | |||
metrics.ops_completed_async += 1; | |||
} | |||
|
|||
#[allow(unused)] | |||
pub fn track_unref(&self, id: OpId) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just delete this, I guess?
@AaronO any opinions?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess that's fine. Right now "unrefed" ops don't take part in opSanitizer
but actually it might be preferable to include them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, let's land this and see what it does to the benchmarks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice work!
@@ -43,6 +44,7 @@ | |||
const RING_SIZE = 4 * 1024; | |||
const NO_PROMISE = null; // Alias to null is faster than plain nulls | |||
const promiseRing = ArrayPrototypeFill(new Array(RING_SIZE), NO_PROMISE); | |||
const promiseIdSymbol = SymbolFor("Deno.core.internalPromiseId"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Non-blocking but as a possible future improvement: you can use a v8::Private
if you're worried about the symbol leaking out to user land code (it's visible to Object.getOwnPropertySymbols()
after all.)
Private symbols are like regular symbols except more... er, private. (Great explanation, Ben. +1, would read again.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is that construct available in JS land?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not yet, but I can add it. You may have seen node's getHiddenValue() and setHiddenValue() - it's the same mechanism.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not yet, but I can add it. You may have seen node's getHiddenValue() and setHiddenValue() - it's the same mechanism.
I haven't seen it. I guess I'll add a TODO and we can change it once the API is added.
} | ||
}; | ||
|
||
state.unrefed_ops.insert(promise_id); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we check that promise_id
is in state.pending_ops
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, we'd have to iterate over all pending_ops
killing the performance.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh wait, this is bindings.rs
, it's not a hot path. I guess I can add a check here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually not possible to do that, there's no way to inspect promise_id
when iterating pending_ops
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the meantime, what probably can be done is change the unrefOp
JS function to check whether the promise ID exists and hasn't resolved, right?
It would be pretty bad if some careless code added an already-resolved promise ID to unrefed_ops
, since all that matters is the length compared to pending_ops
. But I suppose CI would catch that, though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the meantime, what probably can be done is change the unrefOp JS function to check whether the promise ID exists and hasn't resolved, right?
That would require to adding a wrapper around Deno.core.unrefOp
in core/01_core.js
just to verify the promise map.
I'm not sure if it's worth doing that at this moment; I think we should try this API in the wild and see if that's a problem.
This commit adds an ability to "ref" or "unref" pending ops.
Up to this point Deno had a notion of "async ops" and "unref async ops";
the former keep event loop alive, while the latter do not block event loop
from finishing. It was not possible to change between op types after
dispatching, one had to decide which type to use before dispatch.
Instead of storing ops in two separate "FuturesUnordered" collections,
now ops are stored in a single collection, with supplemental "HashSet"
storing ids of promises that were "unrefed".
Two APIs were added to "Deno.core":
to be "refed" and keep event loop alive (the default behavior)
id as "unrefed" which won't block event loop from exiting
Closes #12745
TODO:
ops_dispatched_async
and incrementops_dispatched_async_unref
, though it seems overly complicated)AsyncOpIterator
?)id
field to promises returned fromDeno.core.opcallAsync
Deno.core.ref(...promiseIds)
andDeno.core.unref(...promiseIds)