perf: replace batched device barriers with flat semaphore - #901
Draft
ChristopherJHart wants to merge 1 commit into
Draft
perf: replace batched device barriers with flat semaphore#901ChristopherJHart wants to merge 1 commit into
ChristopherJHart wants to merge 1 commit into
Conversation
The device execution loop previously chunked devices into batches of max_workers and awaited each batch with asyncio.gather before starting the next. This means if one device in a batch takes 3x longer than the others, all slots sit idle until it finishes. Replace with a single asyncio.Semaphore over all devices. A device finishing early immediately frees a slot for the next, maximizing utilization. At >16 devices this eliminates ~2x wall clock inflation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ChristopherJHart
force-pushed
the
fix/perf-flat-semaphore
branch
from
August 17, 2026 20:56
94bd6e5 to
5fdd05d
Compare
Collaborator
|
Hey @ChristopherJHart .. I would like to integrate this into the next nac-test release. I raised an issue so we can properly document this. could you please add test coverage, and possibly also an entry in changelog My agent suggests the following test strategy:
Implementation approach: Mock DeviceExecutor.run_device_job_with_semaphore with varying asyncio.sleep durations to simulate fast/slow devices. Track concurrent execution count and completion order to assert scheduling behavior. Tests would be fast (sub-second, no real subprocesses). Possibly put this into a new thanks! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Replaces the batched
asyncio.gatherdevice execution loop with a single flatasyncio.Semaphore, eliminating barrier-induced idle time.Problem
The device execution loop chunked devices into batches of
max_workersand awaited each batch withasyncio.gatherbefore starting the next. If one device in a batch takes 3x longer than others, all slots sit idle until it finishes — then the next batch starts fresh.Example with 20 devices and
max_workers=16:The previous semaphore was redundant
The old code created a per-batch semaphore:
But since
batch_size = max_workersandsemaphore_size = min(max_workers, len(device_batch)), the semaphore size always equaled the batch size. Every device in the batch could acquire it immediately — it never throttled anything. The actual concurrency control was the batch loop itself (onlymax_workersdevices launched at a time via chunking).So the old implementation was: batched barriers with a redundant semaphore inside each batch.
device_executor.run_device_job_with_semaphore()doesasync with semaphore:around the entire device job (job generation, testbed creation, subprocess execution). This is the correct concurrency-limiting mechanism — but it only works properly when the semaphore spans all devices, not just one batch.Solution
Single
asyncio.Semaphore(concurrency)over all devices. A device finishing early immediately frees a slot for the next, maximizing utilization. The semaphore insiderun_device_job_with_semaphorenow actually serves its intended purpose as the sole concurrency limiter.Measured Impact
Files Changed
nac_test/pyats_core/orchestrator.pyTest plan
--max-parallel-devicescap🤖 Generated with Claude Code