mirror of
https://github.com/denoland/deno.git
synced 2024-12-22 23:34:47 -05:00
Simplify timer with macrotask callback (#4385)
This commit is contained in:
parent
8c1c929034
commit
2f3de4b425
6 changed files with 140 additions and 77 deletions
|
@ -91,6 +91,8 @@ declare global {
|
||||||
data?: ArrayBufferView
|
data?: ArrayBufferView
|
||||||
): null | Uint8Array;
|
): null | Uint8Array;
|
||||||
|
|
||||||
|
setMacrotaskCallback(cb: () => boolean): void;
|
||||||
|
|
||||||
shared: SharedArrayBuffer;
|
shared: SharedArrayBuffer;
|
||||||
|
|
||||||
evalContext(
|
evalContext(
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { setBuildInfo } from "./build.ts";
|
||||||
import { setVersions } from "./version.ts";
|
import { setVersions } from "./version.ts";
|
||||||
import { setPrepareStackTrace } from "./error_stack.ts";
|
import { setPrepareStackTrace } from "./error_stack.ts";
|
||||||
import { Start, start as startOp } from "./ops/runtime.ts";
|
import { Start, start as startOp } from "./ops/runtime.ts";
|
||||||
|
import { handleTimerMacrotask } from "./web/timers.ts";
|
||||||
|
|
||||||
export let OPS_CACHE: { [name: string]: number };
|
export let OPS_CACHE: { [name: string]: number };
|
||||||
|
|
||||||
|
@ -27,6 +28,7 @@ export function initOps(): void {
|
||||||
for (const [name, opId] of Object.entries(OPS_CACHE)) {
|
for (const [name, opId] of Object.entries(OPS_CACHE)) {
|
||||||
core.setAsyncHandler(opId, getAsyncHandler(name));
|
core.setAsyncHandler(opId, getAsyncHandler(name));
|
||||||
}
|
}
|
||||||
|
core.setMacrotaskCallback(handleTimerMacrotask);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function start(source?: string): Start {
|
export function start(source?: string): Start {
|
||||||
|
|
|
@ -127,6 +127,9 @@ unitTest(async function intervalSuccess(): Promise<void> {
|
||||||
clearInterval(id);
|
clearInterval(id);
|
||||||
// count should increment twice
|
// count should increment twice
|
||||||
assertEquals(count, 1);
|
assertEquals(count, 1);
|
||||||
|
// Similar false async leaking alarm.
|
||||||
|
// Force next round of polling.
|
||||||
|
await waitForMs(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
unitTest(async function intervalCancelSuccess(): Promise<void> {
|
unitTest(async function intervalCancelSuccess(): Promise<void> {
|
||||||
|
@ -330,24 +333,32 @@ unitTest(async function timerNestedMicrotaskOrdering(): Promise<void> {
|
||||||
s += "0";
|
s += "0";
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
s += "4";
|
s += "4";
|
||||||
setTimeout(() => (s += "8"));
|
setTimeout(() => (s += "A"));
|
||||||
Promise.resolve().then(() => {
|
Promise.resolve()
|
||||||
setTimeout(() => {
|
.then(() => {
|
||||||
s += "9";
|
setTimeout(() => {
|
||||||
resolve();
|
s += "B";
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
s += "5";
|
||||||
});
|
});
|
||||||
});
|
|
||||||
});
|
});
|
||||||
setTimeout(() => (s += "5"));
|
setTimeout(() => (s += "6"));
|
||||||
Promise.resolve().then(() => (s += "2"));
|
Promise.resolve().then(() => (s += "2"));
|
||||||
Promise.resolve().then(() =>
|
Promise.resolve().then(() =>
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
s += "6";
|
s += "7";
|
||||||
Promise.resolve().then(() => (s += "7"));
|
Promise.resolve()
|
||||||
|
.then(() => (s += "8"))
|
||||||
|
.then(() => {
|
||||||
|
s += "9";
|
||||||
|
});
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
Promise.resolve().then(() => Promise.resolve().then(() => (s += "3")));
|
Promise.resolve().then(() => Promise.resolve().then(() => (s += "3")));
|
||||||
s += "1";
|
s += "1";
|
||||||
await promise;
|
await promise;
|
||||||
assertEquals(s, "0123456789");
|
assertEquals(s, "0123456789AB");
|
||||||
});
|
});
|
||||||
|
|
|
@ -31,8 +31,21 @@ function clearGlobalTimeout(): void {
|
||||||
|
|
||||||
let pendingEvents = 0;
|
let pendingEvents = 0;
|
||||||
const pendingFireTimers: Timer[] = [];
|
const pendingFireTimers: Timer[] = [];
|
||||||
let hasPendingFireTimers = false;
|
|
||||||
let pendingScheduleTimers: Timer[] = [];
|
/** Process and run a single ready timer macrotask.
|
||||||
|
* This function should be registered through Deno.core.setMacrotaskCallback.
|
||||||
|
* Returns true when all ready macrotasks have been processed, false if more
|
||||||
|
* ready ones are available. The Isolate future would rely on the return value
|
||||||
|
* to repeatedly invoke this function until depletion. Multiple invocations
|
||||||
|
* of this function one at a time ensures newly ready microtasks are processed
|
||||||
|
* before next macrotask timer callback is invoked. */
|
||||||
|
export function handleTimerMacrotask(): boolean {
|
||||||
|
if (pendingFireTimers.length > 0) {
|
||||||
|
fire(pendingFireTimers.shift()!);
|
||||||
|
return pendingFireTimers.length === 0;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
async function setGlobalTimeout(due: number, now: number): Promise<void> {
|
async function setGlobalTimeout(due: number, now: number): Promise<void> {
|
||||||
// Since JS and Rust don't use the same clock, pass the time to rust as a
|
// Since JS and Rust don't use the same clock, pass the time to rust as a
|
||||||
|
@ -54,7 +67,29 @@ async function setGlobalTimeout(due: number, now: number): Promise<void> {
|
||||||
await startGlobalTimer(timeout);
|
await startGlobalTimer(timeout);
|
||||||
pendingEvents--;
|
pendingEvents--;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||||
fireTimers();
|
prepareReadyTimers();
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepareReadyTimers(): void {
|
||||||
|
const now = Date.now();
|
||||||
|
// Bail out if we're not expecting the global timer to fire.
|
||||||
|
if (globalTimeoutDue === null || pendingEvents > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// After firing the timers that are due now, this will hold the first timer
|
||||||
|
// list that hasn't fired yet.
|
||||||
|
let nextDueNode: DueNode | null;
|
||||||
|
while ((nextDueNode = dueTree.min()) !== null && nextDueNode.due <= now) {
|
||||||
|
dueTree.remove(nextDueNode);
|
||||||
|
// Fire all the timers in the list.
|
||||||
|
for (const timer of nextDueNode.timers) {
|
||||||
|
// With the list dropped, the timer is no longer scheduled.
|
||||||
|
timer.scheduled = false;
|
||||||
|
// Place the callback to pending timers to fire.
|
||||||
|
pendingFireTimers.push(timer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setOrClearGlobalTimeout(nextDueNode && nextDueNode.due, now);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setOrClearGlobalTimeout(due: number | null, now: number): void {
|
function setOrClearGlobalTimeout(due: number | null, now: number): void {
|
||||||
|
@ -68,15 +103,6 @@ function setOrClearGlobalTimeout(due: number | null, now: number): void {
|
||||||
function schedule(timer: Timer, now: number): void {
|
function schedule(timer: Timer, now: number): void {
|
||||||
assert(!timer.scheduled);
|
assert(!timer.scheduled);
|
||||||
assert(now <= timer.due);
|
assert(now <= timer.due);
|
||||||
// There are more timers pending firing.
|
|
||||||
// We must ensure new timer scheduled after them.
|
|
||||||
// Push them to a queue that would be depleted after last pending fire
|
|
||||||
// timer is fired.
|
|
||||||
// (This also implies behavior of setInterval)
|
|
||||||
if (hasPendingFireTimers) {
|
|
||||||
pendingScheduleTimers.push(timer);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Find or create the list of timers that will fire at point-in-time `due`.
|
// Find or create the list of timers that will fire at point-in-time `due`.
|
||||||
const maybeNewDueNode = { due: timer.due, timers: [] };
|
const maybeNewDueNode = { due: timer.due, timers: [] };
|
||||||
let dueNode = dueTree.find(maybeNewDueNode);
|
let dueNode = dueTree.find(maybeNewDueNode);
|
||||||
|
@ -99,10 +125,6 @@ function unschedule(timer: Timer): void {
|
||||||
// If either is true, they are not in tree, and their idMap entry
|
// If either is true, they are not in tree, and their idMap entry
|
||||||
// will be deleted soon. Remove it from queue.
|
// will be deleted soon. Remove it from queue.
|
||||||
let index = -1;
|
let index = -1;
|
||||||
if ((index = pendingScheduleTimers.indexOf(timer)) >= 0) {
|
|
||||||
pendingScheduleTimers.splice(index);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if ((index = pendingFireTimers.indexOf(timer)) >= 0) {
|
if ((index = pendingFireTimers.indexOf(timer)) >= 0) {
|
||||||
pendingFireTimers.splice(index);
|
pendingFireTimers.splice(index);
|
||||||
return;
|
return;
|
||||||
|
@ -157,57 +179,6 @@ function fire(timer: Timer): void {
|
||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
|
|
||||||
function fireTimers(): void {
|
|
||||||
const now = Date.now();
|
|
||||||
// Bail out if we're not expecting the global timer to fire.
|
|
||||||
if (globalTimeoutDue === null || pendingEvents > 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// After firing the timers that are due now, this will hold the first timer
|
|
||||||
// list that hasn't fired yet.
|
|
||||||
let nextDueNode: DueNode | null;
|
|
||||||
while ((nextDueNode = dueTree.min()) !== null && nextDueNode.due <= now) {
|
|
||||||
dueTree.remove(nextDueNode);
|
|
||||||
// Fire all the timers in the list.
|
|
||||||
for (const timer of nextDueNode.timers) {
|
|
||||||
// With the list dropped, the timer is no longer scheduled.
|
|
||||||
timer.scheduled = false;
|
|
||||||
// Place the callback to pending timers to fire.
|
|
||||||
pendingFireTimers.push(timer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (pendingFireTimers.length > 0) {
|
|
||||||
hasPendingFireTimers = true;
|
|
||||||
// Fire the list of pending timers as a chain of microtasks.
|
|
||||||
globalThis.queueMicrotask(firePendingTimers);
|
|
||||||
} else {
|
|
||||||
setOrClearGlobalTimeout(nextDueNode && nextDueNode.due, now);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function firePendingTimers(): void {
|
|
||||||
if (pendingFireTimers.length === 0) {
|
|
||||||
// All timer tasks are done.
|
|
||||||
hasPendingFireTimers = false;
|
|
||||||
// Schedule all new timers pushed during previous timer executions
|
|
||||||
const now = Date.now();
|
|
||||||
for (const newTimer of pendingScheduleTimers) {
|
|
||||||
newTimer.due = Math.max(newTimer.due, now);
|
|
||||||
schedule(newTimer, now);
|
|
||||||
}
|
|
||||||
pendingScheduleTimers = [];
|
|
||||||
// Reschedule for next round of timeout.
|
|
||||||
const nextDueNode = dueTree.min();
|
|
||||||
const due = nextDueNode && Math.max(nextDueNode.due, now);
|
|
||||||
setOrClearGlobalTimeout(due, now);
|
|
||||||
} else {
|
|
||||||
// Fire a single timer and allow its children microtasks scheduled first.
|
|
||||||
fire(pendingFireTimers.shift()!);
|
|
||||||
// ...and we schedule next timer after this.
|
|
||||||
globalThis.queueMicrotask(firePendingTimers);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Args = unknown[];
|
export type Args = unknown[];
|
||||||
|
|
||||||
function checkThis(thisArg: unknown): void {
|
function checkThis(thisArg: unknown): void {
|
||||||
|
|
|
@ -24,6 +24,9 @@ lazy_static! {
|
||||||
v8::ExternalReference {
|
v8::ExternalReference {
|
||||||
function: send.map_fn_to()
|
function: send.map_fn_to()
|
||||||
},
|
},
|
||||||
|
v8::ExternalReference {
|
||||||
|
function: set_macrotask_callback.map_fn_to()
|
||||||
|
},
|
||||||
v8::ExternalReference {
|
v8::ExternalReference {
|
||||||
function: eval_context.map_fn_to()
|
function: eval_context.map_fn_to()
|
||||||
},
|
},
|
||||||
|
@ -145,6 +148,19 @@ pub fn initialize_context<'s>(
|
||||||
send_val.into(),
|
send_val.into(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let mut set_macrotask_callback_tmpl =
|
||||||
|
v8::FunctionTemplate::new(scope, set_macrotask_callback);
|
||||||
|
let set_macrotask_callback_val = set_macrotask_callback_tmpl
|
||||||
|
.get_function(scope, context)
|
||||||
|
.unwrap();
|
||||||
|
core_val.set(
|
||||||
|
context,
|
||||||
|
v8::String::new(scope, "setMacrotaskCallback")
|
||||||
|
.unwrap()
|
||||||
|
.into(),
|
||||||
|
set_macrotask_callback_val.into(),
|
||||||
|
);
|
||||||
|
|
||||||
let mut eval_context_tmpl = v8::FunctionTemplate::new(scope, eval_context);
|
let mut eval_context_tmpl = v8::FunctionTemplate::new(scope, eval_context);
|
||||||
let eval_context_val =
|
let eval_context_val =
|
||||||
eval_context_tmpl.get_function(scope, context).unwrap();
|
eval_context_tmpl.get_function(scope, context).unwrap();
|
||||||
|
@ -429,6 +445,27 @@ fn send(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_macrotask_callback(
|
||||||
|
scope: v8::FunctionCallbackScope,
|
||||||
|
args: v8::FunctionCallbackArguments,
|
||||||
|
_rv: v8::ReturnValue,
|
||||||
|
) {
|
||||||
|
let deno_isolate: &mut Isolate =
|
||||||
|
unsafe { &mut *(scope.isolate().get_data(0) as *mut Isolate) };
|
||||||
|
|
||||||
|
if !deno_isolate.js_macrotask_cb.is_empty() {
|
||||||
|
let msg =
|
||||||
|
v8::String::new(scope, "Deno.core.setMacrotaskCallback already called.")
|
||||||
|
.unwrap();
|
||||||
|
scope.isolate().throw_exception(msg.into());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let macrotask_cb_fn =
|
||||||
|
v8::Local::<v8::Function>::try_from(args.get(0)).unwrap();
|
||||||
|
deno_isolate.js_macrotask_cb.set(scope, macrotask_cb_fn);
|
||||||
|
}
|
||||||
|
|
||||||
fn eval_context(
|
fn eval_context(
|
||||||
scope: v8::FunctionCallbackScope,
|
scope: v8::FunctionCallbackScope,
|
||||||
args: v8::FunctionCallbackArguments,
|
args: v8::FunctionCallbackArguments,
|
||||||
|
|
|
@ -166,6 +166,7 @@ pub struct Isolate {
|
||||||
pub(crate) global_context: v8::Global<v8::Context>,
|
pub(crate) global_context: v8::Global<v8::Context>,
|
||||||
pub(crate) shared_ab: v8::Global<v8::SharedArrayBuffer>,
|
pub(crate) shared_ab: v8::Global<v8::SharedArrayBuffer>,
|
||||||
pub(crate) js_recv_cb: v8::Global<v8::Function>,
|
pub(crate) js_recv_cb: v8::Global<v8::Function>,
|
||||||
|
pub(crate) js_macrotask_cb: v8::Global<v8::Function>,
|
||||||
pub(crate) pending_promise_exceptions: HashMap<i32, v8::Global<v8::Value>>,
|
pub(crate) pending_promise_exceptions: HashMap<i32, v8::Global<v8::Value>>,
|
||||||
shared_isolate_handle: Arc<Mutex<Option<*mut v8::Isolate>>>,
|
shared_isolate_handle: Arc<Mutex<Option<*mut v8::Isolate>>>,
|
||||||
pub(crate) js_error_create_fn: Box<JSErrorCreateFn>,
|
pub(crate) js_error_create_fn: Box<JSErrorCreateFn>,
|
||||||
|
@ -299,6 +300,7 @@ impl Isolate {
|
||||||
pending_promise_exceptions: HashMap::new(),
|
pending_promise_exceptions: HashMap::new(),
|
||||||
shared_ab: v8::Global::<v8::SharedArrayBuffer>::new(),
|
shared_ab: v8::Global::<v8::SharedArrayBuffer>::new(),
|
||||||
js_recv_cb: v8::Global::<v8::Function>::new(),
|
js_recv_cb: v8::Global::<v8::Function>::new(),
|
||||||
|
js_macrotask_cb: v8::Global::<v8::Function>::new(),
|
||||||
snapshot_creator: maybe_snapshot_creator,
|
snapshot_creator: maybe_snapshot_creator,
|
||||||
snapshot: load_snapshot,
|
snapshot: load_snapshot,
|
||||||
has_snapshotted: false,
|
has_snapshotted: false,
|
||||||
|
@ -495,6 +497,7 @@ impl Future for Isolate {
|
||||||
let v8_isolate = inner.v8_isolate.as_mut().unwrap();
|
let v8_isolate = inner.v8_isolate.as_mut().unwrap();
|
||||||
let js_error_create_fn = &*inner.js_error_create_fn;
|
let js_error_create_fn = &*inner.js_error_create_fn;
|
||||||
let js_recv_cb = &inner.js_recv_cb;
|
let js_recv_cb = &inner.js_recv_cb;
|
||||||
|
let js_macrotask_cb = &inner.js_macrotask_cb;
|
||||||
let pending_promise_exceptions = &mut inner.pending_promise_exceptions;
|
let pending_promise_exceptions = &mut inner.pending_promise_exceptions;
|
||||||
|
|
||||||
let mut hs = v8::HandleScope::new(v8_isolate);
|
let mut hs = v8::HandleScope::new(v8_isolate);
|
||||||
|
@ -550,6 +553,8 @@ impl Future for Isolate {
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
drain_macrotasks(scope, js_macrotask_cb, js_error_create_fn)?;
|
||||||
|
|
||||||
check_promise_exceptions(
|
check_promise_exceptions(
|
||||||
scope,
|
scope,
|
||||||
pending_promise_exceptions,
|
pending_promise_exceptions,
|
||||||
|
@ -603,6 +608,41 @@ fn async_op_response<'s>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn drain_macrotasks<'s>(
|
||||||
|
scope: &mut impl v8::ToLocal<'s>,
|
||||||
|
js_macrotask_cb: &v8::Global<v8::Function>,
|
||||||
|
js_error_create_fn: &JSErrorCreateFn,
|
||||||
|
) -> Result<(), ErrBox> {
|
||||||
|
let context = scope.get_current_context().unwrap();
|
||||||
|
let global: v8::Local<v8::Value> = context.global(scope).into();
|
||||||
|
let js_macrotask_cb = js_macrotask_cb.get(scope);
|
||||||
|
if js_macrotask_cb.is_none() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let js_macrotask_cb = js_macrotask_cb.unwrap();
|
||||||
|
|
||||||
|
// Repeatedly invoke macrotask callback until it returns true (done),
|
||||||
|
// such that ready microtasks would be automatically run before
|
||||||
|
// next macrotask is processed.
|
||||||
|
loop {
|
||||||
|
let mut try_catch = v8::TryCatch::new(scope);
|
||||||
|
let tc = try_catch.enter();
|
||||||
|
|
||||||
|
let is_done = js_macrotask_cb.call(scope, context, global, &[]);
|
||||||
|
|
||||||
|
if let Some(exception) = tc.exception() {
|
||||||
|
return exception_to_err_result(scope, exception, js_error_create_fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
let is_done = is_done.unwrap();
|
||||||
|
if is_done.is_true() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn attach_handle_to_error(
|
pub(crate) fn attach_handle_to_error(
|
||||||
scope: &mut impl v8::InIsolate,
|
scope: &mut impl v8::InIsolate,
|
||||||
err: ErrBox,
|
err: ErrBox,
|
||||||
|
|
Loading…
Reference in a new issue