mirror of https://github.com/boa-dev/boa.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
200 lines
6.9 KiB
200 lines
6.9 KiB
use std::{ |
|
cell::{Cell, RefCell}, |
|
collections::VecDeque, |
|
rc::Rc, |
|
time::{Duration, Instant}, |
|
}; |
|
|
|
use boa_engine::{ |
|
context::ContextBuilder, |
|
job::{FutureJob, JobQueue, NativeJob}, |
|
js_string, |
|
native_function::NativeFunction, |
|
property::Attribute, |
|
Context, JsArgs, JsResult, JsValue, Source, |
|
}; |
|
use boa_runtime::Console; |
|
use futures_util::{stream::FuturesUnordered, Future}; |
|
use smol::{future, stream::StreamExt, LocalExecutor}; |
|
|
|
/// An event queue that also drives futures to completion. |
|
struct Queue<'a> { |
|
executor: LocalExecutor<'a>, |
|
futures: RefCell<FuturesUnordered<FutureJob>>, |
|
jobs: RefCell<VecDeque<NativeJob>>, |
|
} |
|
|
|
impl<'a> Queue<'a> { |
|
fn new(executor: LocalExecutor<'a>) -> Self { |
|
Self { |
|
executor, |
|
futures: RefCell::default(), |
|
jobs: RefCell::default(), |
|
} |
|
} |
|
} |
|
|
|
impl JobQueue for Queue<'_> { |
|
fn enqueue_promise_job(&self, job: NativeJob, _context: &mut Context) { |
|
self.jobs.borrow_mut().push_back(job); |
|
} |
|
|
|
fn enqueue_future_job(&self, future: FutureJob, _context: &mut Context) { |
|
self.futures.borrow().push(future); |
|
} |
|
|
|
fn run_jobs(&self, context: &mut Context) { |
|
// Early return in case there were no jobs scheduled. |
|
if self.jobs.borrow().is_empty() && self.futures.borrow().is_empty() { |
|
return; |
|
} |
|
|
|
let context = RefCell::new(context); |
|
|
|
future::block_on(self.executor.run(async move { |
|
// Used to sync the finalization of both tasks |
|
let finished = Cell::new(0b00u8); |
|
|
|
let fut_queue = async { |
|
loop { |
|
if self.futures.borrow().is_empty() { |
|
finished.set(finished.get() | 0b01); |
|
if finished.get() >= 0b11 { |
|
// All possible futures and jobs were completed. Exit. |
|
return; |
|
} |
|
// All possible jobs were completed, but `jqueue` could have |
|
// pending jobs. Yield to the executor to try to progress on |
|
// `jqueue` until we have more pending futures. |
|
future::yield_now().await; |
|
continue; |
|
} |
|
finished.set(finished.get() & 0b10); |
|
|
|
// Blocks on all the enqueued futures, driving them all to completion. |
|
let futures = &mut std::mem::take(&mut *self.futures.borrow_mut()); |
|
while let Some(job) = futures.next().await { |
|
// Important to schedule the returned `job` into the job queue, since that's |
|
// what allows updating the `Promise` seen by ECMAScript for when the future |
|
// completes. |
|
self.enqueue_promise_job(job, &mut context.borrow_mut()); |
|
} |
|
} |
|
}; |
|
|
|
let job_queue = async { |
|
loop { |
|
if self.jobs.borrow().is_empty() { |
|
finished.set(finished.get() | 0b10); |
|
if finished.get() >= 0b11 { |
|
// All possible futures and jobs were completed. Exit. |
|
return; |
|
} |
|
// All possible jobs were completed, but `fqueue` could have |
|
// pending futures. Yield to the executor to try to progress on |
|
// `fqueue` until we have more pending jobs. |
|
future::yield_now().await; |
|
continue; |
|
}; |
|
finished.set(finished.get() & 0b01); |
|
|
|
let jobs = std::mem::take(&mut *self.jobs.borrow_mut()); |
|
for job in jobs { |
|
if let Err(e) = job.call(&mut context.borrow_mut()) { |
|
eprintln!("Uncaught {e}"); |
|
} |
|
future::yield_now().await; |
|
} |
|
} |
|
}; |
|
|
|
// Wait for both queues to complete |
|
future::zip(fut_queue, job_queue).await; |
|
})); |
|
} |
|
} |
|
|
|
// Example async code. Note that the returned future must be 'static. |
|
fn delay( |
|
_this: &JsValue, |
|
args: &[JsValue], |
|
context: &mut Context, |
|
) -> impl Future<Output = JsResult<JsValue>> { |
|
let millis = args.get_or_undefined(0).to_u32(context); |
|
|
|
async move { |
|
let millis = millis?; |
|
println!("Delaying for {millis} milliseconds ..."); |
|
let now = Instant::now(); |
|
smol::Timer::after(Duration::from_millis(u64::from(millis))).await; |
|
let elapsed = now.elapsed().as_secs_f64(); |
|
Ok(elapsed.into()) |
|
} |
|
} |
|
|
|
/// Adds the custom runtime to the context. |
|
fn add_runtime(context: &mut Context) { |
|
// First add the `console` object, to be able to call `console.log()`. |
|
let console = Console::init(context); |
|
context |
|
.register_global_property(js_string!(Console::NAME), console, Attribute::all()) |
|
.expect("the console builtin shouldn't exist"); |
|
|
|
// Then, bind the defined async function to the ECMAScript function "delay". |
|
context |
|
.register_global_builtin_callable( |
|
js_string!("delay"), |
|
1, |
|
NativeFunction::from_async_fn(delay), |
|
) |
|
.expect("the delay builtin shouldn't exist"); |
|
} |
|
|
|
fn main() { |
|
// Initialize the required executors and the context |
|
let executor = LocalExecutor::new(); |
|
let queue = Queue::new(executor); |
|
let context = &mut ContextBuilder::new() |
|
.job_queue(Rc::new(queue)) |
|
.build() |
|
.unwrap(); |
|
|
|
// Then, add a custom runtime. |
|
add_runtime(context); |
|
|
|
// Multiple calls to multiple async timers. |
|
let script = r" |
|
function print(elapsed) { |
|
console.log(`Finished. elapsed time: ${elapsed * 1000} ms`) |
|
} |
|
delay(1000).then(print); |
|
delay(500).then(print); |
|
delay(200).then(print); |
|
delay(600).then(print); |
|
delay(30).then(print); |
|
"; |
|
|
|
let now = Instant::now(); |
|
context.eval(Source::from_bytes(script)).unwrap(); |
|
|
|
// Important to run this after evaluating, since this is what triggers to run the enqueued jobs. |
|
context.run_jobs(); |
|
|
|
println!("Total elapsed time: {:?}", now.elapsed()); |
|
|
|
// Example output: |
|
|
|
// Delaying for 1000 milliseconds ... |
|
// Delaying for 500 milliseconds ... |
|
// Delaying for 200 milliseconds ... |
|
// Delaying for 600 milliseconds ... |
|
// Delaying for 30 milliseconds ... |
|
// Finished. elapsed time: 30.073821000000002 ms |
|
// Finished. elapsed time: 200.079116 ms |
|
// Finished. elapsed time: 500.10745099999997 ms |
|
// Finished. elapsed time: 600.098433 ms |
|
// Finished. elapsed time: 1000.118099 ms |
|
// Total elapsed time: 1.002628715s |
|
|
|
// The queue concurrently drove several timers to completion! |
|
}
|
|
|