mirror of
https://github.com/denoland/deno.git
synced 2024-11-21 15:04:11 -05:00
perf: make deno test
10x faster (#20550)
This commit is contained in:
parent
b9b4ad31d9
commit
2772d302f5
4 changed files with 256 additions and 62 deletions
4
Cargo.lock
generated
4
Cargo.lock
generated
|
@ -1217,9 +1217,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "deno_core"
|
name = "deno_core"
|
||||||
version = "0.213.0"
|
version = "0.214.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d1799de49977a9b73cd8f931ff429b452f3adfb2d5e9648098905923381a374f"
|
checksum = "be0af76effe9a766f7c9a253171ab10b9adfaf4b10c6eb0b9f005f9dd0ba2948"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
|
|
@ -40,7 +40,7 @@ repository = "https://github.com/denoland/deno"
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
deno_ast = { version = "0.29.3", features = ["transpiling"] }
|
deno_ast = { version = "0.29.3", features = ["transpiling"] }
|
||||||
|
|
||||||
deno_core = { version = "0.213.0" }
|
deno_core = { version = "0.214.0" }
|
||||||
|
|
||||||
deno_runtime = { version = "0.126.0", path = "./runtime" }
|
deno_runtime = { version = "0.126.0", path = "./runtime" }
|
||||||
napi_sym = { version = "0.48.0", path = "./cli/napi/sym" }
|
napi_sym = { version = "0.48.0", path = "./cli/napi/sym" }
|
||||||
|
|
|
@ -22,7 +22,6 @@ const {
|
||||||
MapPrototypeSet,
|
MapPrototypeSet,
|
||||||
MathCeil,
|
MathCeil,
|
||||||
ObjectKeys,
|
ObjectKeys,
|
||||||
ObjectHasOwn,
|
|
||||||
ObjectPrototypeIsPrototypeOf,
|
ObjectPrototypeIsPrototypeOf,
|
||||||
Promise,
|
Promise,
|
||||||
SafeArrayIterator,
|
SafeArrayIterator,
|
||||||
|
@ -151,26 +150,14 @@ const OP_DETAILS = {
|
||||||
"op_ws_send_pong": ["send a message on a WebSocket", "closing a `WebSocket` or `WebSocketStream`"],
|
"op_ws_send_pong": ["send a message on a WebSocket", "closing a `WebSocket` or `WebSocketStream`"],
|
||||||
};
|
};
|
||||||
|
|
||||||
function collectReliableOpMetrics() {
|
let opIdHostRecvMessage = -1;
|
||||||
let metrics = core.metrics();
|
let opIdHostRecvCtrl = -1;
|
||||||
if (metrics.opsDispatched > metrics.opsCompleted) {
|
let opNames = null;
|
||||||
// If there are still async ops pending, we drain the event loop to the
|
|
||||||
// point where all ops that can return `Poll::Ready` have done so, to ensure
|
function populateOpNames() {
|
||||||
// that any ops are ready because of user cleanup code are completed.
|
opNames = core.ops.op_op_names();
|
||||||
const hasPendingWorkerOps = metrics.ops.op_host_recv_message && (
|
opIdHostRecvMessage = opNames.indexOf("op_host_recv_message");
|
||||||
metrics.ops.op_host_recv_message.opsDispatched >
|
opIdHostRecvCtrl = opNames.indexOf("op_host_recv_ctrl");
|
||||||
metrics.ops.op_host_recv_message.opsCompleted ||
|
|
||||||
metrics.ops.op_host_recv_ctrl.opsDispatched >
|
|
||||||
metrics.ops.op_host_recv_ctrl.opsCompleted
|
|
||||||
);
|
|
||||||
return opSanitizerDelay(hasPendingWorkerOps).then(() => {
|
|
||||||
metrics = core.metrics();
|
|
||||||
const traces = new Map(core.opCallTraces);
|
|
||||||
return { metrics, traces };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const traces = new Map(core.opCallTraces);
|
|
||||||
return { metrics, traces };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wrap test function in additional assertion that makes sure
|
// Wrap test function in additional assertion that makes sure
|
||||||
|
@ -181,50 +168,61 @@ function collectReliableOpMetrics() {
|
||||||
function assertOps(fn) {
|
function assertOps(fn) {
|
||||||
/** @param desc {TestDescription | TestStepDescription} */
|
/** @param desc {TestDescription | TestStepDescription} */
|
||||||
return async function asyncOpSanitizer(desc) {
|
return async function asyncOpSanitizer(desc) {
|
||||||
let metrics = collectReliableOpMetrics();
|
if (opNames === null) populateOpNames();
|
||||||
if (metrics.then) {
|
const res = core.ops.op_test_op_sanitizer_collect(
|
||||||
// We're delaying so await to get the result asynchronously.
|
desc.id,
|
||||||
metrics = await metrics;
|
false,
|
||||||
|
opIdHostRecvMessage,
|
||||||
|
opIdHostRecvCtrl,
|
||||||
|
);
|
||||||
|
if (res !== 0) {
|
||||||
|
await opSanitizerDelay(res === 2);
|
||||||
|
core.ops.op_test_op_sanitizer_collect(
|
||||||
|
desc.id,
|
||||||
|
true,
|
||||||
|
opIdHostRecvMessage,
|
||||||
|
opIdHostRecvCtrl,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const { metrics: pre, traces: preTraces } = metrics;
|
const preTraces = new Map(core.opCallTraces);
|
||||||
let post;
|
|
||||||
let postTraces;
|
let postTraces;
|
||||||
|
let report = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const innerResult = await fn(desc);
|
const innerResult = await fn(desc);
|
||||||
if (innerResult) return innerResult;
|
if (innerResult) return innerResult;
|
||||||
} finally {
|
} finally {
|
||||||
let metrics = collectReliableOpMetrics();
|
let res = core.ops.op_test_op_sanitizer_finish(
|
||||||
if (metrics.then) {
|
desc.id,
|
||||||
// We're delaying so await to get the result asynchronously.
|
false,
|
||||||
metrics = await metrics;
|
opIdHostRecvMessage,
|
||||||
|
opIdHostRecvCtrl,
|
||||||
|
);
|
||||||
|
if (res === 1 || res === 2) {
|
||||||
|
await opSanitizerDelay(res === 2);
|
||||||
|
res = core.ops.op_test_op_sanitizer_finish(
|
||||||
|
desc.id,
|
||||||
|
true,
|
||||||
|
opIdHostRecvMessage,
|
||||||
|
opIdHostRecvCtrl,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
postTraces = new Map(core.opCallTraces);
|
||||||
|
if (res === 3) {
|
||||||
|
report = core.ops.op_test_op_sanitizer_report(desc.id);
|
||||||
}
|
}
|
||||||
({ metrics: post, traces: postTraces } = metrics);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// We're checking diff because one might spawn HTTP server in the background
|
if (report === null) return null;
|
||||||
// that will be a pending async op before test starts.
|
|
||||||
const dispatchedDiff = post.opsDispatchedAsync - pre.opsDispatchedAsync;
|
|
||||||
const completedDiff = post.opsCompletedAsync - pre.opsCompletedAsync;
|
|
||||||
|
|
||||||
if (dispatchedDiff === completedDiff) return null;
|
|
||||||
|
|
||||||
const details = [];
|
const details = [];
|
||||||
for (const key in post.ops) {
|
for (const opReport of report) {
|
||||||
if (!ObjectHasOwn(post.ops, key)) {
|
const opName = opNames[opReport.id];
|
||||||
continue;
|
const diff = opReport.diff;
|
||||||
}
|
|
||||||
const preOp = pre.ops[key] ??
|
|
||||||
{ opsDispatchedAsync: 0, opsCompletedAsync: 0 };
|
|
||||||
const postOp = post.ops[key];
|
|
||||||
const dispatchedDiff = postOp.opsDispatchedAsync -
|
|
||||||
preOp.opsDispatchedAsync;
|
|
||||||
const completedDiff = postOp.opsCompletedAsync -
|
|
||||||
preOp.opsCompletedAsync;
|
|
||||||
|
|
||||||
if (dispatchedDiff > completedDiff) {
|
if (diff > 0) {
|
||||||
const [name, hint] = OP_DETAILS[key] || [key, null];
|
const [name, hint] = OP_DETAILS[opName] || [opName, null];
|
||||||
const count = dispatchedDiff - completedDiff;
|
const count = diff;
|
||||||
let message = `${count} async operation${
|
let message = `${count} async operation${
|
||||||
count === 1 ? "" : "s"
|
count === 1 ? "" : "s"
|
||||||
} to ${name} ${
|
} to ${name} ${
|
||||||
|
@ -234,8 +232,8 @@ function assertOps(fn) {
|
||||||
message += ` This is often caused by not ${hint}.`;
|
message += ` This is often caused by not ${hint}.`;
|
||||||
}
|
}
|
||||||
const traces = [];
|
const traces = [];
|
||||||
for (const [id, { opName, stack }] of postTraces) {
|
for (const [id, { opName: traceOpName, stack }] of postTraces) {
|
||||||
if (opName !== key) continue;
|
if (traceOpName !== opName) continue;
|
||||||
if (MapPrototypeHas(preTraces, id)) continue;
|
if (MapPrototypeHas(preTraces, id)) continue;
|
||||||
ArrayPrototypePush(traces, stack);
|
ArrayPrototypePush(traces, stack);
|
||||||
}
|
}
|
||||||
|
@ -247,9 +245,9 @@ function assertOps(fn) {
|
||||||
message += ArrayPrototypeJoin(traces, "\n\n");
|
message += ArrayPrototypeJoin(traces, "\n\n");
|
||||||
}
|
}
|
||||||
ArrayPrototypePush(details, message);
|
ArrayPrototypePush(details, message);
|
||||||
} else if (dispatchedDiff < completedDiff) {
|
} else if (diff < 0) {
|
||||||
const [name, hint] = OP_DETAILS[key] || [key, null];
|
const [name, hint] = OP_DETAILS[opName] || [opName, null];
|
||||||
const count = completedDiff - dispatchedDiff;
|
const count = -diff;
|
||||||
let message = `${count} async operation${
|
let message = `${count} async operation${
|
||||||
count === 1 ? "" : "s"
|
count === 1 ? "" : "s"
|
||||||
} to ${name} ${
|
} to ${name} ${
|
||||||
|
@ -261,8 +259,8 @@ function assertOps(fn) {
|
||||||
message += ` This is often caused by not ${hint}.`;
|
message += ` This is often caused by not ${hint}.`;
|
||||||
}
|
}
|
||||||
const traces = [];
|
const traces = [];
|
||||||
for (const [id, { opName, stack }] of preTraces) {
|
for (const [id, { opName: traceOpName, stack }] of preTraces) {
|
||||||
if (opName !== key) continue;
|
if (opName !== traceOpName) continue;
|
||||||
if (MapPrototypeHas(postTraces, id)) continue;
|
if (MapPrototypeHas(postTraces, id)) continue;
|
||||||
ArrayPrototypePush(traces, stack);
|
ArrayPrototypePush(traces, stack);
|
||||||
}
|
}
|
||||||
|
@ -274,6 +272,8 @@ function assertOps(fn) {
|
||||||
message += ArrayPrototypeJoin(traces, "\n\n");
|
message += ArrayPrototypeJoin(traces, "\n\n");
|
||||||
}
|
}
|
||||||
ArrayPrototypePush(details, message);
|
ArrayPrototypePush(details, message);
|
||||||
|
} else {
|
||||||
|
throw new Error("unreachable");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ use deno_core::op2;
|
||||||
use deno_core::serde_v8;
|
use deno_core::serde_v8;
|
||||||
use deno_core::v8;
|
use deno_core::v8;
|
||||||
use deno_core::ModuleSpecifier;
|
use deno_core::ModuleSpecifier;
|
||||||
|
use deno_core::OpMetrics;
|
||||||
use deno_core::OpState;
|
use deno_core::OpState;
|
||||||
use deno_runtime::permissions::create_child_permissions;
|
use deno_runtime::permissions::create_child_permissions;
|
||||||
use deno_runtime::permissions::ChildPermissionsArg;
|
use deno_runtime::permissions::ChildPermissionsArg;
|
||||||
|
@ -19,6 +20,9 @@ use deno_runtime::permissions::PermissionsContainer;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Deserializer;
|
use serde::Deserializer;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use std::cell::Ref;
|
||||||
|
use std::collections::hash_map::Entry;
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::sync::atomic::AtomicUsize;
|
use std::sync::atomic::AtomicUsize;
|
||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
@ -35,6 +39,9 @@ deno_core::extension!(deno_test,
|
||||||
op_register_test,
|
op_register_test,
|
||||||
op_register_test_step,
|
op_register_test_step,
|
||||||
op_dispatch_test_event,
|
op_dispatch_test_event,
|
||||||
|
op_test_op_sanitizer_collect,
|
||||||
|
op_test_op_sanitizer_finish,
|
||||||
|
op_test_op_sanitizer_report,
|
||||||
],
|
],
|
||||||
options = {
|
options = {
|
||||||
sender: TestEventSender,
|
sender: TestEventSender,
|
||||||
|
@ -42,6 +49,7 @@ deno_core::extension!(deno_test,
|
||||||
state = |state, options| {
|
state = |state, options| {
|
||||||
state.put(options.sender);
|
state.put(options.sender);
|
||||||
state.put(TestContainer::default());
|
state.put(TestContainer::default());
|
||||||
|
state.put(TestOpSanitizers::default());
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -202,3 +210,189 @@ fn op_dispatch_test_event(
|
||||||
sender.send(event).ok();
|
sender.send(event).ok();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct TestOpSanitizers(HashMap<u32, TestOpSanitizerState>);
|
||||||
|
|
||||||
|
enum TestOpSanitizerState {
|
||||||
|
Collecting { metrics: Vec<OpMetrics> },
|
||||||
|
Finished { report: Vec<TestOpSanitizerReport> },
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_collect_metrics(
|
||||||
|
state: &OpState,
|
||||||
|
force: bool,
|
||||||
|
op_id_host_recv_msg: usize,
|
||||||
|
op_id_host_recv_ctrl: usize,
|
||||||
|
) -> Result<Ref<Vec<OpMetrics>>, bool> {
|
||||||
|
let metrics = state.tracker.per_op();
|
||||||
|
for op_metric in &*metrics {
|
||||||
|
let has_pending_ops = op_metric.ops_dispatched_async
|
||||||
|
+ op_metric.ops_dispatched_async_unref
|
||||||
|
> op_metric.ops_completed_async + op_metric.ops_completed_async_unref;
|
||||||
|
if has_pending_ops && !force {
|
||||||
|
let host_recv_msg = metrics
|
||||||
|
.get(op_id_host_recv_msg)
|
||||||
|
.map(|op_metric| {
|
||||||
|
op_metric.ops_dispatched_async + op_metric.ops_dispatched_async_unref
|
||||||
|
> op_metric.ops_completed_async
|
||||||
|
+ op_metric.ops_completed_async_unref
|
||||||
|
})
|
||||||
|
.unwrap_or(false);
|
||||||
|
let host_recv_ctrl = metrics
|
||||||
|
.get(op_id_host_recv_ctrl)
|
||||||
|
.map(|op_metric| {
|
||||||
|
op_metric.ops_dispatched_async + op_metric.ops_dispatched_async_unref
|
||||||
|
> op_metric.ops_completed_async
|
||||||
|
+ op_metric.ops_completed_async_unref
|
||||||
|
})
|
||||||
|
.unwrap_or(false);
|
||||||
|
return Err(host_recv_msg || host_recv_ctrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(metrics)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[op2(fast)]
|
||||||
|
#[smi]
|
||||||
|
// Returns:
|
||||||
|
// 0 - success
|
||||||
|
// 1 - for more accurate results, spin event loop and call again with force=true
|
||||||
|
// 2 - for more accurate results, delay(1ms) and call again with force=true
|
||||||
|
fn op_test_op_sanitizer_collect(
|
||||||
|
state: &mut OpState,
|
||||||
|
#[smi] id: u32,
|
||||||
|
force: bool,
|
||||||
|
#[smi] op_id_host_recv_msg: usize,
|
||||||
|
#[smi] op_id_host_recv_ctrl: usize,
|
||||||
|
) -> Result<u8, AnyError> {
|
||||||
|
let metrics = {
|
||||||
|
let metrics = match try_collect_metrics(
|
||||||
|
state,
|
||||||
|
force,
|
||||||
|
op_id_host_recv_msg,
|
||||||
|
op_id_host_recv_ctrl,
|
||||||
|
) {
|
||||||
|
Ok(metrics) => metrics,
|
||||||
|
Err(false) => {
|
||||||
|
return Ok(1);
|
||||||
|
}
|
||||||
|
Err(true) => {
|
||||||
|
return Ok(2);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
metrics.clone()
|
||||||
|
};
|
||||||
|
let op_sanitizers = state.borrow_mut::<TestOpSanitizers>();
|
||||||
|
match op_sanitizers.0.entry(id) {
|
||||||
|
Entry::Vacant(entry) => {
|
||||||
|
entry.insert(TestOpSanitizerState::Collecting { metrics });
|
||||||
|
}
|
||||||
|
Entry::Occupied(_) => {
|
||||||
|
return Err(generic_error(format!(
|
||||||
|
"Test metrics already being collected for test id {id}",
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct TestOpSanitizerReport {
|
||||||
|
id: usize,
|
||||||
|
diff: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[op2(fast)]
|
||||||
|
#[smi]
|
||||||
|
// Returns:
|
||||||
|
// 0 - sanitizer finished with no pending ops
|
||||||
|
// 1 - for more accurate results, spin event loop and call again with force=true
|
||||||
|
// 2 - for more accurate results, delay(1ms) and call again with force=true
|
||||||
|
// 3 - sanitizer finished with pending ops, collect the report with op_test_op_sanitizer_report
|
||||||
|
fn op_test_op_sanitizer_finish(
|
||||||
|
state: &mut OpState,
|
||||||
|
#[smi] id: u32,
|
||||||
|
force: bool,
|
||||||
|
#[smi] op_id_host_recv_msg: usize,
|
||||||
|
#[smi] op_id_host_recv_ctrl: usize,
|
||||||
|
) -> Result<u8, AnyError> {
|
||||||
|
let report = {
|
||||||
|
let after_metrics = match try_collect_metrics(
|
||||||
|
state,
|
||||||
|
force,
|
||||||
|
op_id_host_recv_msg,
|
||||||
|
op_id_host_recv_ctrl,
|
||||||
|
) {
|
||||||
|
Ok(metrics) => metrics,
|
||||||
|
Err(false) => {
|
||||||
|
return Ok(1);
|
||||||
|
}
|
||||||
|
Err(true) => {
|
||||||
|
return Ok(2);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let op_sanitizers = state.borrow::<TestOpSanitizers>();
|
||||||
|
let before_metrics = match op_sanitizers.0.get(&id) {
|
||||||
|
Some(TestOpSanitizerState::Collecting { metrics }) => metrics,
|
||||||
|
_ => {
|
||||||
|
return Err(generic_error(format!(
|
||||||
|
"Metrics not collected before for test id {id}",
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut report = vec![];
|
||||||
|
|
||||||
|
for (id, (before, after)) in
|
||||||
|
before_metrics.iter().zip(after_metrics.iter()).enumerate()
|
||||||
|
{
|
||||||
|
let async_pending_before = before.ops_dispatched_async
|
||||||
|
+ before.ops_dispatched_async_unref
|
||||||
|
- before.ops_completed_async
|
||||||
|
- before.ops_completed_async_unref;
|
||||||
|
let async_pending_after = after.ops_dispatched_async
|
||||||
|
+ after.ops_dispatched_async_unref
|
||||||
|
- after.ops_completed_async
|
||||||
|
- after.ops_completed_async_unref;
|
||||||
|
let diff = async_pending_after as i64 - async_pending_before as i64;
|
||||||
|
if diff != 0 {
|
||||||
|
report.push(TestOpSanitizerReport { id, diff });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
report
|
||||||
|
};
|
||||||
|
|
||||||
|
let op_sanitizers = state.borrow_mut::<TestOpSanitizers>();
|
||||||
|
|
||||||
|
if report.is_empty() {
|
||||||
|
op_sanitizers
|
||||||
|
.0
|
||||||
|
.remove(&id)
|
||||||
|
.expect("TestOpSanitizerState::Collecting");
|
||||||
|
Ok(0)
|
||||||
|
} else {
|
||||||
|
op_sanitizers
|
||||||
|
.0
|
||||||
|
.insert(id, TestOpSanitizerState::Finished { report })
|
||||||
|
.expect("TestOpSanitizerState::Collecting");
|
||||||
|
Ok(3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[op2]
|
||||||
|
#[serde]
|
||||||
|
fn op_test_op_sanitizer_report(
|
||||||
|
state: &mut OpState,
|
||||||
|
#[smi] id: u32,
|
||||||
|
) -> Result<Vec<TestOpSanitizerReport>, AnyError> {
|
||||||
|
let op_sanitizers = state.borrow_mut::<TestOpSanitizers>();
|
||||||
|
match op_sanitizers.0.remove(&id) {
|
||||||
|
Some(TestOpSanitizerState::Finished { report }) => Ok(report),
|
||||||
|
_ => Err(generic_error(format!(
|
||||||
|
"Metrics not finished collecting for test id {id}",
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue