Skip to content

perf(runtime): cap the default runtime at four worker threads - #200

Merged
greenhat616 merged 1 commit into
mainfrom
perf/cap-runtime-workers
Oct 17, 2026
Merged

greenhat616 merged 1 commit into
mainfrom
perf/cap-runtime-workers

Conversation

@greenhat616

Copy link
Copy Markdown
Contributor

Why

default_runtime() used Runtime::new(), which starts one worker per logical CPU. The app hands this runtime to Tauri, and nyanpasu-service runs on it through runtime::block_on, so on a 32-thread machine both start 32 workers.

The app was measured on Windows 11 with 32 logical CPUs: a profiling build of app main, six instances run concurrently under the same synthetic proxy load with TOKIO_WORKER_THREADS = 1/2/4/8/16/32. Each ran 37 min in the background with the window closed, then 16 min with the window open while scripted RPC operations looped.

workers threads bg/heavy private MiB bg/heavy timer-late p99 / max under heavy ops
1 16/23 17.1/26.8 505–606 ms / 4092 ms
2 17/22 17.6/27.2 12.2 ms / 103 ms
4 19/24 17.5/27.8 13.0 ms / 23 ms
8 23/29 18.1/28.0 12.4 ms / 28 ms
32 49/53 20.0/31.9 12.4 ms / 25 ms

CPU time showed no trend across worker counts. Above four workers the extra threads and memory bought nothing. With one worker, a single blocking operation stalled the runtime: collecting the environment took 3.8 s, profile activation 650–740 ms and a core restart 711 ms, and reading a 5.5 MB profile 213 ms. With two or more workers each of these operations stayed at or under 17 ms. Two workers still stalled for 103 ms when blocking operations overlapped, so four leaves headroom. Those blocking call sites belong in spawn_blocking, and the app will move them in a separate change.

Change

workers = available_parallelism().map_or(1, usize::from).min(MAX_WORKER_THREADS)   // MAX_WORKER_THREADS = 4
Builder::new_multi_thread().worker_threads(workers).enable_all().build()

When available_parallelism() fails, the runtime starts one worker. Apart from the worker count it is configured as Runtime::new() was: multi-thread, with every driver enabled.

TOKIO_WORKER_THREADS is no longer honoured by this runtime, because Builder::worker_threads takes precedence over it. Supporting it again would mean parsing the variable here as well. Nothing in the app or the runtime sets it; it was only used for the measurement above.

A unit test checks that the default runtime starts between one and MAX_WORKER_THREADS workers. On a machine with more than four CPUs it fails against the old Runtime::new().

Downstream

  • nyanpasu-runtime: the submodule bump comes next, as a draft that waits for this PR.
  • clash-nyanpasu: next after that, also a draft.

Not tested

  • cargo clippy -p nyanpasu-utils --all-targets --all-features and the new unit test were run inside the nyanpasu-runtime workspace on Windows. The standalone CI run of this repository was not reproduced locally.
  • macOS and Linux were not run.
  • The measurements cover the app only. nyanpasu-service under load was not measured with the cap.

Runtime::new() starts one worker per logical CPU, so the shared runtime
the app and the service run on starts 32 workers on a 32-thread machine.
Measured in the app, worker counts above four added threads and private
memory without lowering timer lateness under load or CPU time, while one
worker let a single blocking operation stall the whole runtime for
seconds.

Start one worker per available CPU but at most four, and one when the
parallelism cannot be queried. The explicit count overrides the
TOKIO_WORKER_THREADS environment variable.
@semanticdiff-com

semanticdiff-com Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

Review changes with  SemanticDiff

Changed Files
File Status
  src/runtime/mod.rs  6% smaller

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.

1 participant