Skip to content

Drop dependency on multiprocess - #140

Draft
bbannier wants to merge 21 commits into
masterfrom
topic/bbannier/asyncio
Draft

Drop dependency on multiprocess#140
bbannier wants to merge 21 commits into
masterfrom
topic/bbannier/asyncio

Conversation

@bbannier

Copy link
Copy Markdown
Member

Warning

All code changes in this branch were generated with Claude Sonnet 4.6. I reviewed all changes and am accountable.

@bbannier bbannier self-assigned this Jul 23, 2026
@bbannier
bbannier force-pushed the topic/bbannier/asyncio branch from ee2e167 to 909392a Compare July 23, 2026 21:33

@awelzel awelzel left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is clever. I like this.

I only looked at the final code though. Some minor nits and ideas while skimming the complete diff, but generally 👍

Drops the only dependency btest has, removes 168 lines of code, and running its own test-suite with -j4 gets a 1.6x 1.7x speed-up in elapsed time for me locally (15.42/8.97 = 1.72, not sure how I got to 1.6x before...).

$ taskset -c 0,1,2,3 /usr/bin/time btest -j 4 -d testing/
91 tests successful, 3 skipped
22.99user 12.30system 0:15.42elapsed 228%CPU (0avgtext+0avgdata 62100maxresident)k
$ taskset -c 0,1,2,3 /usr/bin/time btest -j 4 -d testing/
91 tests successful, 3 skipped
26.54user 7.01system 0:08.97elapsed 373%CPU (0avgtext+0avgdata 61900maxresident)k

Comment thread btest Outdated
Comment thread btest Outdated
Comment thread btest Outdated
Comment thread btest Outdated
Comment thread btest
Comment thread btest Outdated
Comment thread btest Outdated
Comment thread btest Outdated
@bbannier
bbannier force-pushed the topic/bbannier/asyncio branch 2 times, most recently from b4dec11 to 497a883 Compare July 31, 2026 10:56
…ss_mp` shim

Isolates the multiprocessing implementation behind a single callable so
later commits can swap it without touching callers.
The process-per-command pattern existed solely to contain `KeyboardInterrupt`.
Replace with a plain `Popen`+wait loop that kills the child on interrupt and
re-raises, removing the `mp.Queue` and `mp.Process` machinery from the hot path.
Moves all mutable state (counters, test queue, port pool, output handler,
lock) and the methods that operate on it out of `TestManager` into a plain
`RunState` class. `TestManager` delegates to it; the `SyncManager` proxy
machinery is otherwise unchanged.
Pulls the `test.run()`+`testReplayOutput()` body out of `threadRun` into a
top-level `run_test(test, state)` function, making the worker loop a thin
scheduler and preparing the seam for an async replacement.
…_progress_monitor`

Encapsulates the `threading.Thread`+`Event` lifecycle into two methods on
`Test`, hiding the threading details and preparing the seam for replacing
the thread with an asyncio task.
Adds `runSubprocess` and `runTestCommandLine` (with an async
`LinuxTimer.timeSubprocess` for the timing path; a matching stub is
added to `TimerBase`). `execute()` now calls
`asyncio.run(runTestCommandLine(...))`, a sync shim that a later
commit replaces with a plain `await` once the call stack goes async.

The new helpers take explicit `cwd`, `env`, `stdout`, `stderr` parameters
rather than `**kwargs`, which also drops the `shell=True` flag from the
`execute()` call site; `asyncio.create_subprocess_shell` preserves the
shell-expansion behaviour on non-Windows.
Makes `execute()`, `run()`, `run_cmdseq()`, and `run_test()` all async. The
`asyncio.run()` shim introduced in the previous commit becomes a plain
`await`; the progress monitor thread becomes an asyncio `Task`; and
`run_tests()` drives the worker pool via `asyncio.run(_async_run_all)`.

The `rc==200` (user-initiated abort) path no longer calls `sys.exit()` from
inside a coroutine; it raises `Abort` instead, which `run_tests()` catches
via a `user_abort` flag so the test summary can print before exiting. The
process exits with code 1 when a user-abort occurs. The
`--abort-on-failure` path still re-raises `Abort` to the top-level handler.

The threads test is simplified: the old baseline verified that
`@TEST-SERIALIZE` co-located tests 4 and 5 on the same thread; under
asyncio that thread-identity check no longer applies, so the test now
only verifies that all five tests complete successfully with `-j 5`.
Workers are now coroutines in a single process, so cross-process atomic
integers are no longer needed. Drop the `mp_sharedctypes` imports.
Workers are coroutines in the same process, so globals are already shared.
The proxy dict and the code that pushed it into child-process namespaces
is dead.
`TestManager` is now a plain class. Replace proxy list/lock objects with
plain list and `threading.Lock`, remove the `SyncManager` socket address
setup, remove `mp_managers` imports, and drop the `mgr.shutdown()` call.
`UPDATE_INTERACTIVE` now uses the same `asyncio.run` path as normal mode.
Remove `threadRun` and the `mp.set_start_method` calls that were only needed
for multiprocessing worker spawning. This also removes the Python >=3.8
version-detection block that forced the `"fork"` start method on macOS
(the workaround for the regression noted in issue #26); it is no longer
relevant once multiprocessing is gone. `import signal` is also removed as
it was only used inside `threadRun`.
…orts

`mp.cpu_count()` -> `os.cpu_count()`, `mp.current_process().pid` -> `os.getpid()`.
Remove the multiprocess/multiprocessing import block
and the `deepcopy` of tests in `main()`, no longer needed in a single-process model.
All test execution is now single-threaded (asyncio event loop), so
mutual exclusion is no longer needed. Drop `threading.Lock` from `RunState`,
the `progress_lock` from `Test`, and the sync progress monitor thread along
with `import threading`. Also remove the `asyncio.Semaphore(n)` from
`run_all_tests`: each worker already processes its assigned tests
sequentially, so the semaphore was redundant and inadvertently
serialized all workers.
`TestManager` was a pure delegation wrapper over `RunState` with no
independent state. Replace it with a top-level `run_tests()` function,
pass `RunState` directly as `mgr` to tests, and drop the `prepare(mgr)`
parameter since no handler uses it.
These comments referenced child worker processes and Windows multiprocess
spawn constraints that no longer apply after the asyncio migration.
The asyncio migration removed TestMgr and all locking; the docstring
claiming handlers are called under a lock is no longer accurate.
The last caller of `runSubprocess`, `LinuxTimer.available()`, can use
`subprocess.call()` directly now that the sync wrapper is gone. Remove
`runSubprocess` and the `shlex` import it needed.
All affected functions are already `async def`; the prefix adds no
information now that the sync counterparts are gone.
The concurrency model is now asyncio workers, not OS threads. Update
variable names, method names, and docstrings to match.

User-visible names (--threads flag, Options.threads) are unchanged.
`TestManager` is gone; the parameter and attribute now hold a `RunState`.
@bbannier
bbannier force-pushed the topic/bbannier/asyncio branch from 497a883 to 2bbcc36 Compare August 19, 2026 14:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants