Back to Codeberg

Consider using a single-threaded async runtime #457

forgejo-contrib-forgejo-cli-issues-457.md

latest17.7 KB
Original Source

forgejo-contrib/forgejo-cli

Watch20

Star402

Fork

You've already forked forgejo-cli

51

CodeIssues 44Pull requests 8Releases 11Packages 2WikiActivity

Consider using a single-threaded async runtime #457

New issue

Open

opened 2026-05-09 20:02:51 +02:00 by LordMZTE · 2 comments

LordMZTE commented 2026-05-09 20:02:51 +02:00

Collaborator

Copy link

Motivation

Currently, we decorate our main function with #[tokio::main]. This causes tokio to spawn a number of workers equal to the number of CPU cores on the host (this can be easily observed using strace, for example). Needless to say, there is little benefit to this given that almost everything that forgejo-cli does is purely sequential in nature, so we're paying for thread spawning overhead without getting much of any performance gain from parallelism in return. In fact, I'd even be in favor of using blocking IO everywhere instead, as asynchronous IO offers little advantage in scenarios where all IO actions are performed sequentially anyways, but this is likely unrealistic due to Rust's fundamental distinction between async and synchronous IO functions and forgejo-api also being based on tokio.

Notable functions

  • Here, we use spawn_blocking. While this is idiomatic, it would mean forcing tokio to spawn a worker if there isn't one already. If we don't want to do anything else on the tokio runtime anyways, there's also no reason not to block it.
  • Here, we spawn an HTTP server for an authentication callback. This is the only place I could find where we actually benefit from using an async runtime, as this allows us to handle multiple connections with one OS thread.

Tiny Benchmark

For what it's worth, we can also see a minor performance improvement for fj whoami simply by changing the attribute on main to #[tokio::main(flavor = "current_thread")]:

text
$ poop -- "target/release/fj-old whoami" "target/release/fj whoami"
Benchmark 1 (263 runs): target/release/fj-old whoami
  measurement mean ± σ min … max outliers delta
  wall_time 19.0ms ± 558us 18.2ms … 22.6ms 12 ( 5%) 0%
  peak_rss 18.4MB ± 45.0KB 18.3MB … 18.5MB 30 (11%) 0%
  cpu_cycles 77.0M ± 2.19M 74.7M … 92.5M 18 ( 7%) 0%
  instructions 153M ± 11.3K 153M … 153M 53 (20%) 0%
  cache_references 1.97M ± 53.2K 1.81M … 2.20M 3 ( 1%) 0%
  cache_misses 150K ± 12.4K 133K … 203K 10 ( 4%) 0%
  branch_misses 315K ± 37.6K 280K … 582K 14 ( 5%) 0%
Benchmark 2 (271 runs): target/release/fj whoami
  measurement mean ± σ min … max outliers delta
  wall_time 18.5ms ± 394us 17.9ms … 21.3ms 13 ( 5%) ⚡- 2.7% ± 0.4%
  peak_rss 18.4MB ± 11.1KB 18.3MB … 18.4MB 2 ( 1%) - 0.0% ± 0.0%
  cpu_cycles 75.4M ± 1.81M 73.6M … 89.2M 19 ( 7%) ⚡- 2.1% ± 0.4%
  instructions 153M ± 58.3 153M … 153M 0 ( 0%) - 0.1% ± 0.0%
  cache_references 1.86M ± 65.6K 1.64M … 2.10M 5 ( 2%) ⚡- 5.8% ± 0.5%
  cache_misses 143K ± 15.2K 121K … 223K 15 ( 6%) ⚡- 4.6% ± 1.6%
  branch_misses 294K ± 28.2K 273K … 450K 23 ( 8%) ⚡- 6.8% ± 1.8%

It would be interesting how this performs with blocking IO, but I cannot think of a good way to get a benchmark for that, sadly.

The binary size is currently identical for both builds, but we may also be able to reduce the feature flags we build tokio with, potentially improving binary size.

Code of Conduct

  • I agree to act in accordance with the CoC & AI Agreement.
  • This issue was not generated by an LLM, even in part.

Motivation Currently, we decorate our main function with #[tokio::main]. This causes tokio to spawn a number of workers equal to the number of CPU cores on the host (this can be easily observed using strace, for example). Needless to say, there is little benefit to this given that almost everything that forgejo-cli does is purely sequential in nature, so we're paying for thread spawning overhead without getting much of any performance gain from parallelism in return. In fact, I'd even be in favor of using blocking IO everywhere instead, as asynchronous IO offers little advantage in scenarios where all IO actions are performed sequentially anyways, but this is likely unrealistic due to Rust's fundamental distinction between async and synchronous IO functions and forgejo-api also being based on tokio. ## Notable functions - Here, we use spawn_blocking. While this is idiomatic, it would mean forcing tokio to spawn a worker if there isn't one already. If we don't want to do anything else on the tokio runtime anyways, there's also no reason not to block it. - Here, we spawn an HTTP server for an authentication callback. This is the only place I could find where we actually benefit from using an async runtime, as this allows us to handle multiple connections with one OS thread. ## Tiny Benchmark For what it's worth, we can also see a minor performance improvement for fj whoami simply by changing the attribute on main to #[tokio::main(flavor = "current_thread")]: $ poop -- "target/release/fj-old whoami" "target/release/fj whoami" Benchmark 1 (263 runs): target/release/fj-old whoami measurement mean ± σ min … max outliers delta wall_time 19.0ms ± 558us 18.2ms … 22.6ms 12 ( 5%) 0% peak_rss 18.4MB ± 45.0KB 18.3MB … 18.5MB 30 (11%) 0% cpu_cycles 77.0M ± 2.19M 74.7M … 92.5M 18 ( 7%) 0% instructions 153M ± 11.3K 153M … 153M 53 (20%) 0% cache_references 1.97M ± 53.2K 1.81M … 2.20M 3 ( 1%) 0% cache_misses 150K ± 12.4K 133K … 203K 10 ( 4%) 0% branch_misses 315K ± 37.6K 280K … 582K 14 ( 5%) 0% Benchmark 2 (271 runs): target/release/fj whoami measurement mean ± σ min … max outliers delta wall_time 18.5ms ± 394us 17.9ms … 21.3ms 13 ( 5%) ⚡- 2.7% ± 0.4% peak_rss 18.4MB ± 11.1KB 18.3MB … 18.4MB 2 ( 1%) - 0.0% ± 0.0% cpu_cycles 75.4M ± 1.81M 73.6M … 89.2M 19 ( 7%) ⚡- 2.1% ± 0.4% instructions 153M ± 58.3 153M … 153M 0 ( 0%) - 0.1% ± 0.0% cache_references 1.86M ± 65.6K 1.64M … 2.10M 5 ( 2%) ⚡- 5.8% ± 0.5% cache_misses 143K ± 15.2K 121K … 223K 15 ( 6%) ⚡- 4.6% ± 1.6% branch_misses 294K ± 28.2K 273K … 450K 23 ( 8%) ⚡- 6.8% ± 1.8% It would be interesting how this performs with blocking IO, but I cannot think of a good way to get a benchmark for that, sadly. The binary size is currently identical for both builds, but we may also be able to reduce the feature flags we build tokio with, potentially improving binary size. ### Code of Conduct - [x] I agree to act in accordance with the CoC & AI Agreement. - [x] This issue was not generated by an LLM, even in part.

Cyborus commented 2026-05-13 22:59:34 +02:00

Member

Copy link

I definitely agree with using a single-threaded executor, and very open to even using blocking IO. I can't think of a single place in fj that actually uses concurrency, and only one that could in theory benefit from it (here)

No reason we can't do both! Blocking IO is a much larger endeavor, so we could set flavor = "current_thread" right away while deciding on it.

[...] forgejo-api also being based on tokio.

forgejo-api does have a sync feature flag that gives a blocking version of the interface.

  • [...] This is the only place I could find where we actually benefit from using an async runtime, as this allows us to handle multiple connections with one OS thread.

Ideally this server only ever needs to handle one connection (from the browser after being authenticated). If any other app sends a request, it'll cause it to fail anyway. Not sure if hyper supports blocking though? If not, we could create a smol executor contained to just this function.

[...] we can also see a minor performance improvement for fj whoami simply by changing the attribute on main to #[tokio::main(flavor = "current_thread")]

I'd be extra interested in seeing what it does for something that makes a network call, since I imagine that's where most of the time is spent, and currently fj whoami only access the file system.

The binary size is currently identical for both builds, but we may also be able to reduce the feature flags we build tokio with, potentially improving binary size.

IIRC Rust trims unused things, so this isn't a concern.

I definitely agree with using a single-threaded executor, and very open to even using blocking IO. I can't think of a single place in fj that actually uses concurrency, and only one that could in theory benefit from it (here) No reason we can't do both! Blocking IO is a much larger endeavor, so we could set flavor = "current_thread" right away while deciding on it. > [...] forgejo-api also being based on tokio. forgejo-api does have a sync feature flag that gives a blocking version of the interface. > * [...] This is the only place I could find where we actually benefit from using an async runtime, as this allows us to handle multiple connections with one OS thread. Ideally this server only ever needs to handle one connection (from the browser after being authenticated). If any other app sends a request, it'll cause it to fail anyway. Not sure if hyper supports blocking though? If not, we could create a smol executor contained to just this function. > [...] we can also see a minor performance improvement for fj whoami simply by changing the attribute on main to #[tokio::main(flavor = "current_thread")] I'd be extra interested in seeing what it does for something that makes a network call, since I imagine that's where most of the time is spent, and currently fj whoami only access the file system. > The binary size is currently identical for both builds, but we may also be able to reduce the feature flags we build tokio with, potentially improving binary size. IIRC Rust trims unused things, so this isn't a concern.

LordMZTE commented 2026-05-13 23:41:04 +02:00

Author

Collaborator

Copy link

If [hyper does not support blocking], we could create a smol executor contained to just this function.

Good point! I doubt we'll find a blocking HTTP server, so that might be our only option here.

I'd be extra interested in seeing what it does for something that makes a network call, since I imagine that's where most of the time is spent

I'm not sure how we'd measure this. Since, when doing IO, the time we spend on the CPU is essentially always going to be negligible, hence why I chose to test using a command that doesn't do any networking - we'd just get noise measurements otherwise. This is another important point to bring up. fj is a very IO-bound application, which makes CPU-bound overhead like this rather irrelevant in the large picture. I, however, just don't really like things being unecessarily complicated like this whole runtime situation is.

> If [hyper does not support blocking], we could create a smol executor contained to just this function. Good point! I doubt we'll find a blocking HTTP server, so that might be our only option here. > I'd be extra interested in seeing what it does for something that makes a network call, since I imagine that's where most of the time is spent I'm not sure how we'd measure this. Since, when doing IO, the time we spend on the CPU is essentially always going to be negligible, hence why I chose to test using a command that doesn't do any networking - we'd just get noise measurements otherwise. This is another important point to bring up. fj is a very IO-bound application, which makes CPU-bound overhead like this rather irrelevant in the large picture. I, however, just don't really like things being unecessarily complicated like this whole runtime situation is.

Sign in to join this conversation.

No Branch/Tag specified

BranchesTags

main

renovate/lock-file-maintenance

renovate/serde-saphyr-0.x

docs/contributing

no-port-in-refspec

login/v16.next.forgejo.org

crates-io-publish

0.5.x

compile-time-fluent

0.4.x

localization-alias-demo

api_url_field

v0.5.0

v0.4.1

v0.4.0

v0.3.0

v0.2.0

v0.1.1

v0.1.0

v0.0.4

v0.0.3

v0.0.2

v0.0.1

Labels

Clear labels[ Kind/Breaking Breaking change that won't be backward compatible

](#)[ Kind/Bug Something is not working

](#)[ Kind/Design Discussion about UI/UX design

](#)[ Kind/Documentation Documentation changes

](#)[ Kind/Enhancement Improve existing functionality

](#)[ Kind/Feature New functionality

](#)[ Kind/Security This is security issue

](#)[ Kind/Testing Issue or pull request related to testing

](#)[ Kind/Upstream This is an issue with upstream software (Forgejo) that is probably not our fault

](#)

[ Priority

Critical The priority is critical

](#)[ Priority

High The priority is high

](#)[ Priority

Low The priority is low

](#)[ Priority

Medium The priority is medium

](#)

[ Reviewed

Confirmed Issue has been confirmed

](#)[ Reviewed

Duplicate This issue or pull request already exists

](#)[ Reviewed

Invalid Invalid issue

](#)[ Reviewed

Won't Fix This issue won't be fixed

](#)

[ Status

Abandoned Somebody has started to work on this but abandoned work

](#)[ Status

Blocked Something is blocking this issue or pull request

](#)[ Status

Need More Info Feedback is required to reproduce issue or to continue work

](#)

[ Suspicious

](#)

No labels Kind/Breaking Kind/Bug Kind/Design Kind/Documentation Kind/Enhancement Kind/Feature Kind/Security Kind/Testing Kind/Upstream [ Priority

Critical ](/forgejo-contrib/forgejo-cli/issues?labels=173012) [ Priority

High ](/forgejo-contrib/forgejo-cli/issues?labels=173013) [ Priority

Low ](/forgejo-contrib/forgejo-cli/issues?labels=173015) [ Priority

Medium ](/forgejo-contrib/forgejo-cli/issues?labels=173014) [ Reviewed

Confirmed ](/forgejo-contrib/forgejo-cli/issues?labels=173007) [ Reviewed

Duplicate ](/forgejo-contrib/forgejo-cli/issues?labels=173005) [ Reviewed

Invalid ](/forgejo-contrib/forgejo-cli/issues?labels=173006) [ Reviewed

Won't Fix ](/forgejo-contrib/forgejo-cli/issues?labels=173008) [ Status

Abandoned ](/forgejo-contrib/forgejo-cli/issues?labels=173011) [ Status

Blocked ](/forgejo-contrib/forgejo-cli/issues?labels=173010) [ Status

Need More Info ](/forgejo-contrib/forgejo-cli/issues?labels=173009) Suspicious

Milestone

Clear milestone

No items

No milestone

Projects

Clear projects

No items

No project

Assignees

Clear assignees

No assignees

2 participants

Notifications Subscribe

Due date

The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference

forgejo-contrib/forgejo-cli#457

WritePreview

Loading…

Add table

| Rows | | | Columns | |

CancelOK

Add a link

Url Description Hint: With a URL in your clipboard, you can paste directly into the editor to create a link.

CancelOK

CancelSave

Reference in a new issue

Repository

forgejo-contrib/forgejo-cli

Title

Body

Create issue

No description provided.

Delete branch "%!s()"

Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?

No Yes