1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-21 15:04:11 -05:00

perf(cli): reduce overhead in test registration (#22552)

- Removes the origin call, since all origins are the same for an isolate
(ie: the main module)
- Collects the `TestDescription`s and sends them all at the same time
inside of an Arc, allowing us to (later on) re-use these instead of
cloning.

Needs a follow-up pass to remove all the cloning, but that's a thread
that is pretty long to pull

---------

Signed-off-by: Matt Mastracci <matthew@mastracci.com>
This commit is contained in:
Matt Mastracci 2024-02-27 20:30:17 -07:00 committed by GitHub
parent 9b5d2f8c1b
commit 96cfe82664
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 89 additions and 30 deletions

View file

@ -11,6 +11,7 @@ const {
op_test_event_step_result_ignored, op_test_event_step_result_ignored,
op_test_event_step_result_ok, op_test_event_step_result_ok,
op_test_event_step_wait, op_test_event_step_wait,
op_test_get_origin,
} = core.ops; } = core.ops;
const { const {
ArrayPrototypeFilter, ArrayPrototypeFilter,
@ -188,6 +189,9 @@ function wrapInner(fn) {
const registerTestIdRetBuf = new Uint32Array(1); const registerTestIdRetBuf = new Uint32Array(1);
const registerTestIdRetBufU8 = new Uint8Array(registerTestIdRetBuf.buffer); const registerTestIdRetBufU8 = new Uint8Array(registerTestIdRetBuf.buffer);
// As long as we're using one isolate per test, we can cache the origin since it won't change
let cachedOrigin = undefined;
function testInner( function testInner(
nameOrFnOrOptions, nameOrFnOrOptions,
optionsOrFn, optionsOrFn,
@ -279,11 +283,15 @@ function testInner(
// Delete this prop in case the user passed it. It's used to detect steps. // Delete this prop in case the user passed it. It's used to detect steps.
delete testDesc.parent; delete testDesc.parent;
if (cachedOrigin == undefined) {
cachedOrigin = op_test_get_origin();
}
testDesc.location = core.currentUserCallSite(); testDesc.location = core.currentUserCallSite();
testDesc.fn = wrapTest(testDesc); testDesc.fn = wrapTest(testDesc);
testDesc.name = escapeName(testDesc.name); testDesc.name = escapeName(testDesc.name);
const origin = op_register_test( op_register_test(
testDesc.fn, testDesc.fn,
testDesc.name, testDesc.name,
testDesc.ignore, testDesc.ignore,
@ -296,7 +304,7 @@ function testInner(
registerTestIdRetBufU8, registerTestIdRetBufU8,
); );
testDesc.id = registerTestIdRetBuf[0]; testDesc.id = registerTestIdRetBuf[0];
testDesc.origin = origin; testDesc.origin = cachedOrigin;
MapPrototypeSet(testStates, testDesc.id, { MapPrototypeSet(testStates, testDesc.id, {
context: createTestContext(testDesc), context: createTestContext(testDesc),
children: [], children: [],

View file

@ -324,8 +324,11 @@ impl TestRun {
while let Some((_, event)) = receiver.recv().await { while let Some((_, event)) = receiver.recv().await {
match event { match event {
test::TestEvent::Register(description) => { test::TestEvent::Register(description) => {
reporter.report_register(&description); for (_, description) in description.into_iter() {
tests.write().insert(description.id, description); reporter.report_register(description);
// TODO(mmastrac): we shouldn't need to clone here - we can re-use the descriptions
tests.write().insert(description.id, description.clone());
}
} }
test::TestEvent::Plan(plan) => { test::TestEvent::Plan(plan) => {
summary.total += plan.total; summary.total += plan.total;

View file

@ -1,5 +1,6 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
use crate::tools::test::TestContainer;
use crate::tools::test::TestDescription; use crate::tools::test::TestDescription;
use crate::tools::test::TestEvent; use crate::tools::test::TestEvent;
use crate::tools::test::TestEventSender; use crate::tools::test::TestEventSender;
@ -23,17 +24,13 @@ use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
use uuid::Uuid; use uuid::Uuid;
#[derive(Default)]
pub(crate) struct TestContainer(
pub Vec<(TestDescription, v8::Global<v8::Function>)>,
);
deno_core::extension!(deno_test, deno_core::extension!(deno_test,
ops = [ ops = [
op_pledge_test_permissions, op_pledge_test_permissions,
op_restore_test_permissions, op_restore_test_permissions,
op_register_test, op_register_test,
op_register_test_step, op_register_test_step,
op_test_get_origin,
op_test_event_step_wait, op_test_event_step_wait,
op_test_event_step_result_ok, op_test_event_step_result_ok,
op_test_event_step_result_ignored, op_test_event_step_result_ignored,
@ -106,7 +103,6 @@ static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
#[op2] #[op2]
#[string]
fn op_register_test( fn op_register_test(
state: &mut OpState, state: &mut OpState,
#[global] function: v8::Global<v8::Function>, #[global] function: v8::Global<v8::Function>,
@ -119,7 +115,7 @@ fn op_register_test(
#[smi] line_number: u32, #[smi] line_number: u32,
#[smi] column_number: u32, #[smi] column_number: u32,
#[buffer] ret_buf: &mut [u8], #[buffer] ret_buf: &mut [u8],
) -> Result<String, AnyError> { ) -> Result<(), AnyError> {
if ret_buf.len() != 4 { if ret_buf.len() != 4 {
return Err(type_error(format!( return Err(type_error(format!(
"Invalid ret_buf length: {}", "Invalid ret_buf length: {}",
@ -142,14 +138,16 @@ fn op_register_test(
column_number, column_number,
}, },
}; };
state let container = state.borrow_mut::<TestContainer>();
.borrow_mut::<TestContainer>() container.register(description, function);
.0
.push((description.clone(), function));
let sender = state.borrow_mut::<TestEventSender>();
sender.send(TestEvent::Register(description)).ok();
ret_buf.copy_from_slice(&(id as u32).to_le_bytes()); ret_buf.copy_from_slice(&(id as u32).to_le_bytes());
Ok(origin) Ok(())
}
#[op2]
#[string]
fn op_test_get_origin(state: &mut OpState) -> String {
state.borrow::<ModuleSpecifier>().to_string()
} }
#[op2(fast)] #[op2(fast)]

View file

@ -175,6 +175,51 @@ pub struct TestLocation {
pub column_number: u32, pub column_number: u32,
} }
#[derive(Default)]
pub(crate) struct TestContainer(
TestDescriptions,
Vec<v8::Global<v8::Function>>,
);
impl TestContainer {
pub fn register(
&mut self,
description: TestDescription,
function: v8::Global<v8::Function>,
) {
self.0.tests.insert(description.id, description);
self.1.push(function)
}
pub fn is_empty(&self) -> bool {
self.1.is_empty()
}
}
#[derive(Default, Debug)]
pub struct TestDescriptions {
tests: IndexMap<usize, TestDescription>,
}
impl TestDescriptions {
pub fn len(&self) -> usize {
self.tests.len()
}
pub fn is_empty(&self) -> bool {
self.tests.is_empty()
}
}
impl<'a> IntoIterator for &'a TestDescriptions {
type Item = <&'a IndexMap<usize, TestDescription> as IntoIterator>::Item;
type IntoIter =
<&'a IndexMap<usize, TestDescription> as IntoIterator>::IntoIter;
fn into_iter(self) -> Self::IntoIter {
(&self.tests).into_iter()
}
}
#[derive(Debug, Clone, PartialEq, Deserialize, Eq, Hash)] #[derive(Debug, Clone, PartialEq, Deserialize, Eq, Hash)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct TestDescription { pub struct TestDescription {
@ -335,10 +380,9 @@ pub enum TestStdioStream {
Stderr, Stderr,
} }
#[derive(Debug, Clone, Deserialize)] #[derive(Debug)]
#[serde(rename_all = "camelCase")]
pub enum TestEvent { pub enum TestEvent {
Register(TestDescription), Register(Arc<TestDescriptions>),
Plan(TestPlan), Plan(TestPlan),
Wait(usize), Wait(usize),
Output(TestStdioStream, Vec<u8>), Output(TestStdioStream, Vec<u8>),
@ -559,7 +603,7 @@ async fn test_specifier_inner(
pub fn worker_has_tests(worker: &mut MainWorker) -> bool { pub fn worker_has_tests(worker: &mut MainWorker) -> bool {
let state_rc = worker.js_runtime.op_state(); let state_rc = worker.js_runtime.op_state();
let state = state_rc.borrow(); let state = state_rc.borrow();
!state.borrow::<ops::testing::TestContainer>().0.is_empty() !state.borrow::<TestContainer>().is_empty()
} }
/// Yields to tokio to allow async work to process, and then polls /// Yields to tokio to allow async work to process, and then polls
@ -587,21 +631,23 @@ pub async fn run_tests_for_worker(
options: &TestSpecifierOptions, options: &TestSpecifierOptions,
fail_fast_tracker: &FailFastTracker, fail_fast_tracker: &FailFastTracker,
) -> Result<(), AnyError> { ) -> Result<(), AnyError> {
let (tests, mut sender) = { let (TestContainer(tests, test_functions), mut sender) = {
let state_rc = worker.js_runtime.op_state(); let state_rc = worker.js_runtime.op_state();
let mut state = state_rc.borrow_mut(); let mut state = state_rc.borrow_mut();
( (
std::mem::take(&mut state.borrow_mut::<ops::testing::TestContainer>().0), std::mem::take(&mut *state.borrow_mut::<TestContainer>()),
state.borrow::<TestEventSender>().clone(), state.borrow::<TestEventSender>().clone(),
) )
}; };
let tests: Arc<TestDescriptions> = tests.into();
sender.send(TestEvent::Register(tests.clone()))?;
let unfiltered = tests.len(); let unfiltered = tests.len();
let tests = tests let tests = tests
.into_iter() .into_iter()
.filter(|(d, _)| options.filter.includes(&d.name)) .filter(|(_, d)| options.filter.includes(&d.name))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let (only, no_only): (Vec<_>, Vec<_>) = let (only, no_only): (Vec<_>, Vec<_>) =
tests.into_iter().partition(|(d, _)| d.only); tests.into_iter().partition(|(_, d)| d.only);
let used_only = !only.is_empty(); let used_only = !only.is_empty();
let mut tests = if used_only { only } else { no_only }; let mut tests = if used_only { only } else { no_only };
if let Some(seed) = options.shuffle { if let Some(seed) = options.shuffle {
@ -637,7 +683,7 @@ pub async fn run_tests_for_worker(
filter = filter.omit_op(op_id_host_recv_ctrl as _); filter = filter.omit_op(op_id_host_recv_ctrl as _);
filter = filter.omit_op(op_id_host_recv_message as _); filter = filter.omit_op(op_id_host_recv_message as _);
for (desc, function) in tests { for ((_, desc), function) in tests.into_iter().zip(test_functions) {
if fail_fast_tracker.should_stop() { if fail_fast_tracker.should_stop() {
break; break;
} }
@ -1146,8 +1192,11 @@ pub async fn report_tests(
while let Some((_, event)) = receiver.recv().await { while let Some((_, event)) = receiver.recv().await {
match event { match event {
TestEvent::Register(description) => { TestEvent::Register(description) => {
reporter.report_register(&description); for (_, description) in description.into_iter() {
tests.insert(description.id, description); reporter.report_register(description);
// TODO(mmastrac): We shouldn't need to clone here -- we can reuse the descriptions everywhere
tests.insert(description.id, description.clone());
}
} }
TestEvent::Plan(plan) => { TestEvent::Plan(plan) => {
if !had_plan { if !had_plan {

View file

@ -578,6 +578,7 @@ const NOT_IMPORTED_OPS = [
"op_restore_test_permissions", "op_restore_test_permissions",
"op_register_test_step", "op_register_test_step",
"op_register_test", "op_register_test",
"op_test_get_origin",
"op_pledge_test_permissions", "op_pledge_test_permissions",
// TODO(bartlomieju): used in various integration tests - figure out a way // TODO(bartlomieju): used in various integration tests - figure out a way

View file

@ -1,6 +1,6 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
const EXPECTED_OP_COUNT = 11; const EXPECTED_OP_COUNT = 12;
Deno.test(function checkExposedOps() { Deno.test(function checkExposedOps() {
// @ts-ignore TS doesn't allow to index with symbol // @ts-ignore TS doesn't allow to index with symbol