forgejo-contrib-forgejo-cli-issues-457.md
Watch20
Star402
Fork
You've already forked forgejo-cli
CodeIssues 44Pull requests 8Releases 11Packages 2WikiActivity
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
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.
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.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.
#[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 whoamisimply by changing the attribute onmainto#[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 [
hyperdoes 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
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
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