Repository navigation
concurrent tests are slow #47365
Description
Version
v19.8.1
Platform
Darwin Bumbles-MBP.home 21.6.0 Darwin Kernel Version 21.6.0: Mon Dec 19 20:44:01 PST 2022; root:xnu-8020.240.18~2/RELEASE_X86_64 x86_64
What steps will reproduce the bug?
Use this repo https://github.com/iambumblehead/demo-slow-node-test
from the package,
npm run test-avatakes about 10 seconds

npm run test-node-describetakes about 30 seconds

npm run test-node-testtakes about 30 seconds

How often does it reproduce? Is there a required condition?
It is reproduced any time the test repo is used on this machine
What is the expected behavior? Why is that the expected behavior?
node:test should use multiple cores and should not be slow
What do you see instead?
Hello, I recently replaced node:test with ava in a project using ~250 tests. The node:test runner would complete in ~18 minutes and the ava runner completes in ~9 minutes. The node:test version of the project used describe/it tests with the following pattern,
describe('app', { concurrency: true }, async () => {
beforeEach(() => {
nock.disableNetConnect();
});
it('test a', async () => {
assert.ok(true);
});
it('test b', async () => {
assert.ok(true);
});
// ...
});Additional information
Test concurrency might have some problems. Initially, I tried using { concurrency: true } with a single file that contained about a dozen tests and tests completed in a time that was observably faster. When { concurrency: true } was used with multiple files, the improvement seemed to go away and each group of tests would run slowly,
Test concurrency requires extra boilerplate. Eg, to run tests concurrently, one needs to use nested describe/it tests, nested tests wrapped in a promise or use a configuration file. related links here, and here. It would be nice if concurrent tests were easier to achieve with a special import import test from 'node:test/concurrent' or with a cli option --test-concurrency=true, link to a related comment here: do not expose a test-concurrency flag.
Thank you
Activity
cjihrig commentedon Apr 1, 2023
The fact that you're seeing almost exactly a 2x slowdown seems to me like something is being done in serial. It's impossible to say without knowing more about the test suite itself. I will say that I personally wouldn't recommend using
concurrencyblindly. The default behavior of running the test files in parallel and running the tests in each file serially seems to work well for most cases.This sounds to me like trying to run too many things in parallel lead to a slowdown, which is understandable and will also depend on your system and the nature of the tests.
I did a small comparison with ava. Given the following files and Node 19.8.1:
and
On my M1,
time node --test core.mjsran in about 0.11-0.13 seconds, whiletime node node_modules/.bin/ava ava.mjsran in about 0.32-0.34 seconds. Obviously, this example isn't usingconcurrencybut I doubt that alone would double the execution time without the nature of the tests playing a part too.iambumblehead commentedon Apr 2, 2023
@cjihrig screenshots of system monitor while running tests with ava (left) then node:test (right)


node:test uses one cpu it appears. No blip at "memory pressure" when running either test runner,


iambumblehead commentedon Apr 2, 2023
It would be really nice if node:test supported a cli option
--test-concurrency=trueto run all tests with maximum concurrency and no boilerplate or nesting ceremony.ForbiddenEra commentedon Apr 2, 2023
+1 or otherwise
--test-concurrency=<number of threads>(edit: eg. same behavior as docs say forrun())iambumblehead commentedon Apr 3, 2023
I wrote this little test forumula to multiple files to try and reproduce the single-processor issue, but node handles these tests correctly using three cores and they finish relatively quickly. There must be something in my application tests that is causing tests to run sequentially, but I don't know what that could be...
iambumblehead commentedon Apr 3, 2023
Update: possible reproduction here when using
node --loader=esmock --test ./path/to/file.test.jsat each test (above), esmock creates a deeply mocked import tree using query params that include a unique id for the tree. Adding esmock to the crypto-ciphering describe/it test loop submitted here an hour or so ago did not reproduce the issue, so the issue is not trivially reproduced by using "--loader"
cc @cjihrig possibly this can be reproduced with tests that generate deeply mocked import trees. In the loop above, increasing the loop number from 50 to 1000 causes the test runner to completely hang.
attached: updated describe/it test, using esmock. (using --loader and esmock here does not reproduce the issue. Tests complete quickly and use maximum number of cores, 3)
test.target.js
node.describe.many.test.js
cc @koshic @Swivelgames
iambumblehead commentedon Apr 4, 2023
The esmock usage here triggers a situation that ava is better able to manage than node:test
koshic commentedon Apr 4, 2023
@iambumblehead I still can't catch the root cause - there are no equal scripts for node:test / ava to compare them.
iambumblehead commentedon Apr 4, 2023
I apologize for expressing opinions without having a clear reproduction to share here.
@koshic thanks, I'll try to create a reproduction before next week.
iambumblehead commentedon Apr 18, 2023
sorry to not create a reproduction yet, I plan to make a reproduction soon
tniessen commentedon Apr 19, 2023
Could we clarify both the expectations and the actual implementation regarding concurrency? The
node:testdocs are a bit vague in this regard.It seems that different files are executed in different processes:
node/doc/api/test.md
Lines 376 to 377 in 9a7b971
It seems that the
concurrencyoption ofrun()is the number of files that are executed in parallel:node/doc/api/test.md
Lines 733 to 734 in 9a7b971
But I am not sure what the
concurrencyoption oftest()does. All I can find in the docs is:node/doc/api/test.md
Lines 797 to 803 in 9a7b971
But what mechanism is used for concurrency? Is the
test()itself again split into multiple processes? That could mean quite a lot of overhead. Is thetest()split into multiple threads? That would be much more efficient, but it does not seem likely based on the documentation. Or is it just parallel asynchronous operations in a single process? That would be easy to implement, but then using a value ofos.availableParallelism()seems illogical.Aside from these questions, the kind of workload makes a big difference. For example, synchronous tasks are going to exhaust the application thread, whereas asynchronous thread pool tasks (such as
crypto.scrypt()in the example above) are going to exhaust the thread pool across all application threads within a single process. Spawning many processes comes with a ton of overhead, and each process spawns its own thread pool, etc.MoLow commentedon Apr 19, 2023
I agree, we should probably document this better.
both
run()andnode --test(which usesruninternally) spawn a process per test file, usingos.availableParallelism() - 1as the number of concurrent tests.it is indeed a queue running parallel asynchronous operations, with no isolation between tests in the same file.
the default concurrency for tests inside a file is 1, meaning they run sequentially be default
using
os.availableParallelism() - 1for files and1fortest()'sis a heuristic that tries to aim at the common use cases (or what we believe they are).there might be a better value but as you said, that would really depend on the specific tests and use-case, thus this is configurable
Additionally, these choices take into account not just speed but also the isolation of tests,
where in case they are isolated in separate processes, it is safer to run them in parallel (not impossible to face race conditions but usually that would be in race conditions in accession I/O)
but in tests in the same file facing such issues is more likely when running in parallel
iambumblehead commentedon Apr 20, 2023
I'm reproducing the issue with the package at this repo https://github.com/iambumblehead/demo-slow-node-test
from the package,
npm run test-avatakes about 10 secondsnpm run test-node-describetakes about 30 secondsnpm run test-node-testtakes about 30 secondsincreasing the number defined in the package.json can make the node test runner hang, but ava reasonably handles any number. cc @cjihrig
25 remaining items
doc: clarify concurrency model of test runner
test_runner: fix test counting
test_runner: fix test runner concurrency
doc,test: fix concurrency option of test()
doc,test: fix concurrency option of test()
doc: clarify concurrency model of test runner
test_runner: fix test counting
test_runner: fix test runner concurrency
doc,test: fix concurrency option of test()
doc: clarify concurrency model of test runner
test_runner: fix test counting
test_runner: fix test runner concurrency
doc,test: fix concurrency option of test()
test_runner: fix test counting