Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ either-rs = { package = "either", version = "1", optional = true }
rquickjs-core = { workspace = true }
rquickjs-macro = { workspace = true, optional = true }

[dev-dependencies]
futures = "0.3"
trybuild = "1"

[features]
default = ["std"]
Expand Down Expand Up @@ -139,8 +142,9 @@ allocator = ["rquickjs-core/allocator"]
properties = ["rquickjs-core/properties"]


[dev-dependencies]
trybuild = "1"
[[bench]]
name = "async_bench"
harness = false

[package.metadata.docs.rs]
features = ["full-async", "parallel", "doc-cfg"]
155 changes: 155 additions & 0 deletions benches/async_bench.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
//! Async polling benchmarks

use rquickjs::{AsyncContext, AsyncRuntime};
use std::time::Instant;

async fn bench_spawned_futures(n: usize) {
let rt = AsyncRuntime::new().unwrap();
let ctx = AsyncContext::full(&rt).await.unwrap();

let start = Instant::now();

ctx.with(|ctx| {
for _ in 0..n {
ctx.spawn(async {});
}
})
.await;

rt.idle().await;

let elapsed = start.elapsed();
println!(
"spawn {} futures: {:?} ({:.2} M ops/sec)",
n,
elapsed,
n as f64 / elapsed.as_secs_f64() / 1_000_000.0
);
}

async fn bench_js_promises(n: usize) {
let rt = AsyncRuntime::new().unwrap();
let ctx = AsyncContext::full(&rt).await.unwrap();

let start = Instant::now();

ctx.async_with(async |ctx| {
let code = format!(
r#"
(async function run() {{
let count = 0;
const promises = [];
for (let i = 0; i < {n}; i++) {{
promises.push(Promise.resolve(i).then(v => count++));
}}
await Promise.all(promises);
return count;
}})()
"#
);
ctx.eval::<rquickjs::Promise, _>(code)
.unwrap()
.into_future::<i32>()
.await
.unwrap();
})
.await;

let elapsed = start.elapsed();
println!(
"js promises {}: {:?} ({:.2} M ops/sec)",
n,
elapsed,
n as f64 / elapsed.as_secs_f64() / 1_000_000.0
);
}

async fn bench_chained_promises(depth: usize) {
let rt = AsyncRuntime::new().unwrap();
let ctx = AsyncContext::full(&rt).await.unwrap();

let start = Instant::now();

ctx.async_with(async |ctx| {
let code = format!(
r#"
(async function run() {{
let p = Promise.resolve(0);
for (let i = 0; i < {depth}; i++) {{
p = p.then(v => v + 1);
}}
return await p;
}})()
"#
);
ctx.eval::<rquickjs::Promise, _>(code)
.unwrap()
.into_future::<i32>()
.await
.unwrap();
})
.await;

let elapsed = start.elapsed();
println!(
"chained promises {}: {:?} ({:.2} M ops/sec)",
depth,
elapsed,
depth as f64 / elapsed.as_secs_f64() / 1_000_000.0
);
}

async fn bench_concurrent_spawns(n: usize) {
let rt = AsyncRuntime::new().unwrap();
let ctx = AsyncContext::full(&rt).await.unwrap();

let start = Instant::now();

ctx.with(|ctx| {
for _ in 0..100 {
let ctx2 = ctx.clone();
ctx.spawn(async move {
for _ in 0..n / 100 {
ctx2.spawn(async {});
}
});
}
})
.await;

rt.idle().await;

let elapsed = start.elapsed();
println!(
"concurrent spawns {}: {:?} ({:.2} M ops/sec)",
n,
elapsed,
n as f64 / elapsed.as_secs_f64() / 1_000_000.0
);
}

fn main() {
futures::executor::block_on(async {
println!("=== Async Benchmarks ===\n");

bench_spawned_futures(100).await;
bench_spawned_futures(1_000).await;
bench_spawned_futures(100_000).await;
bench_spawned_futures(1_000_000).await;

bench_js_promises(100).await;
bench_js_promises(1_000).await;
bench_js_promises(10_000).await;
bench_js_promises(100_000).await;

bench_chained_promises(100).await;
bench_chained_promises(1_000).await;
bench_chained_promises(10_000).await;
bench_chained_promises(100_000).await;

bench_concurrent_spawns(100).await;
bench_concurrent_spawns(1_000).await;
bench_concurrent_spawns(100_000).await;
bench_concurrent_spawns(1_000_000).await;
});
}
6 changes: 5 additions & 1 deletion core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ phf = { version = "0.13", optional = true }
indexmap = { version = "2", optional = true }
either = { version = "1", optional = true }
async-lock = { version = "3", optional = true, default-features = false }
parking_lot = { version = "0.12", optional = true }
chrono = { version = "0.4", optional = true }
dlopen2 = { version = "0.8", optional = true }
relative-path = { version = "2.0", optional = true, default-features = false, features = [
Expand All @@ -39,6 +40,9 @@ rquickjs.path = "../"
approx = "0.5"
trybuild = "1"

[target.'cfg(not(target_family = "wasm"))'.dev-dependencies]
tokio = { version = "1", default-features = false, features = ["rt-multi-thread"] }

[features]
default = ["std"]

Expand Down Expand Up @@ -77,7 +81,7 @@ rust-alloc = []
half = ["dep:half"]

# Enable interop between Rust futures and JS Promises
futures = ["dep:async-lock"]
futures = ["dep:async-lock", "dep:parking_lot"]

# Allows transferring objects between different contexts of the same runtime.
multi-ctx = []
Expand Down
18 changes: 7 additions & 11 deletions core/src/context/async.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,19 +79,14 @@ macro_rules! async_with{

impl DropContext for AsyncRuntime {
unsafe fn drop_context(&self, ctx: NonNull<qjs::JSContext>) {
//TODO
let guard = match self.inner.try_lock() {
let guard = match self.try_lock() {
Some(x) => x,
None => {
#[cfg(not(feature = "parallel"))]
{
let p =
unsafe { &mut *(ctx.as_ptr() as *mut crate::context::ctx::RefCountHeader) };
if p.ref_count <= 1 {
// Lock was poisoned, this should only happen on a panic.
// We should still free the context.
// TODO see if there is a way to recover from a panic which could cause the
// following assertion to trigger
#[cfg(feature = "std")]
assert!(std::thread::panicking());
}
Expand All @@ -109,7 +104,6 @@ impl DropContext for AsyncRuntime {
};
guard.runtime.update_stack_top();
unsafe { qjs::JS_FreeContext(ctx.as_ptr()) }
// Explicitly drop the guard to ensure it is valid during the entire use of runtime
mem::drop(guard);
}
}
Expand All @@ -133,17 +127,19 @@ impl AsyncContext {
}

/// Creates a base context with only the required functions registered.
///
/// If additional functions are required use [`AsyncContext::custom`],
/// [`AsyncContext::builder`] or [`AsyncContext::full`].
pub async fn base(runtime: &AsyncRuntime) -> Result<Self> {
Self::custom::<intrinsic::None>(runtime).await
}

/// Creates a context with only the required intrinsics registered.
///
/// If additional functions are required use [`AsyncContext::custom`],
/// [`AsyncContext::builder`] or [`AsyncContext::full`].
pub async fn custom<I: Intrinsic>(runtime: &AsyncRuntime) -> Result<Self> {
let guard = runtime.inner.lock().await;
let guard = runtime.lock().await;
let ctx = NonNull::new(unsafe { qjs::JS_NewContextRaw(guard.runtime.rt.as_ptr()) })
.ok_or(Error::Allocation)?;
unsafe { qjs::JS_AddIntrinsicBaseObjects(ctx.as_ptr()) };
Expand All @@ -156,14 +152,14 @@ impl AsyncContext {
}

/// Creates a context with all standard available intrinsics registered.
///
/// If precise control is required of which functions are available use
/// [`AsyncContext::custom`] or [`AsyncContext::builder`].
pub async fn full(runtime: &AsyncRuntime) -> Result<Self> {
let guard = runtime.inner.lock().await;
let guard = runtime.lock().await;
let ctx = NonNull::new(unsafe { qjs::JS_NewContext(guard.runtime.rt.as_ptr()) })
.ok_or(Error::Allocation)?;
let res = unsafe { ContextOwner::new(ctx, runtime.clone()) };
// Explicitly drop the guard to ensure it is valid during the entire use of runtime
guard.drop_pending();
mem::drop(guard);

Expand Down Expand Up @@ -232,7 +228,7 @@ impl AsyncContext {
F: for<'js> FnOnce(Ctx<'js>) -> R + ParallelSend,
R: ParallelSend,
{
let guard = self.0.rt().inner.lock().await;
let guard = self.0.rt().lock().await;
guard.runtime.update_stack_top();
let ctx = unsafe { Ctx::new_async(self) };
let res = f(ctx);
Expand Down
Loading
Loading