Skip to content

Conversation

FranciscoTGouveia
Copy link
Contributor

Continuation of #4436.

This change aims to speed up the overall performance of the rustup [toolchain] install command by allowing the installation of a component to start immediately after its download completes.
This approach is particularly beneficial for slower connections, as it enables installations to progress in parallel with ongoing downloads -- so by the time all downloads are finished, all installations should be completed as well.

NB: This PR is not yet ready for review. I believe this change should be backed by benchmarks and user feedback before moving forward. I’ll share those here in the next couple of days.

@FranciscoTGouveia FranciscoTGouveia force-pushed the install-after-download branch 4 times, most recently from 0ea9162 to a2a2c20 Compare September 15, 2025 22:14
@FranciscoTGouveia FranciscoTGouveia force-pushed the install-after-download branch 2 times, most recently from 947b8da to 9e5d0e0 Compare September 16, 2025 10:40
@FranciscoTGouveia
Copy link
Contributor Author

FranciscoTGouveia commented Sep 17, 2025

As noted in the opening message of the PR, I now present some benchmarks that will enable us to move forward with this change.
As stated before, this will bump the install and update commands' performance, as when the downloads finish, the installations have already executed.

The results are presented in the plot below, for both low-speed and high-speed internet connections.

image

These results were obtained by running the rustup toolchain install nightly command 10x times for each combination of variables (RUSTUP_CONCURRENT_DOWNLOADS, interleaved or not, and internet connection).
Please note that the y axis do not start on zero, and cannot be directly compared between themselves.


Below, I also leave a small animation of the previous and current behavior of the rustup toolchain install command to illustrate the UX changes that this PR also implies.
On the left you can see the previous version and on the right, the version with the interleaved installations.
Please note that this is just an animation and does not have the purpose of showing any kind of speedup.

animation4471-ezgif com-speed

Thanks for taking the time to review this PR!

Copy link
Member

@rami3l rami3l left a comment

Choose a reason for hiding this comment

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

Modulo the previously requested changes and the two extra nits, I think this PR is quite ready. Thanks a lot, @FranciscoTGouveia!

@FranciscoTGouveia FranciscoTGouveia force-pushed the install-after-download branch 2 times, most recently from d149360 to 1b88c36 Compare September 18, 2025 15:54
@FranciscoTGouveia FranciscoTGouveia marked this pull request as ready for review September 18, 2025 16:05
@rami3l rami3l requested a review from djc September 19, 2025 10:04
@rami3l rami3l removed their assignment Sep 19, 2025

let semaphore = Arc::new(Semaphore::new(concurrent_downloads));
let component_stream =
tokio_stream::iter(components.into_iter()).map(|(component, format, url, hash)| {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggest writing this closure as a named function.


let mut stream = component_stream.buffered(components_len);
let download_handle = async {
let mut hashes = Vec::new();
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggest writing this as a named function.

}
hashes
};
let install_handle = async {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggest writing this as a named function.

.keys()
.find(|comp| comp.contains(component))
.cloned();
if let Some(key) = key
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: invert these for early returns.

}

/// Notifies self that the component has been installed.
pub(crate) fn component_installed(&mut self, component: &str) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we share the duplicate code?

ProgressStyle::with_template(if pb.position() != 0 {
"{msg:>12.bold} downloaded {total_bytes} in {elapsed}"
} else {
"{msg:>12.bold} component already downloaded"
})
.unwrap(),
Copy link
Contributor

Choose a reason for hiding this comment

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

Again there's a lot of duplication here, suggest adding a little abstraction to make it more obvious what happens here.

tmp_cx: &'a temp::Context,
notify_handler: &'a dyn Fn(Notification<'_>),
changes: Vec<ChangedItem>,
tmp_cx: Arc<temp::Context>,
Copy link
Contributor

Choose a reason for hiding this comment

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

Seems to me that tmp_cx, notify_handler and process should be bundled up together.

@djc
Copy link
Contributor

djc commented Sep 19, 2025

Very excited about these performance improvements! Personally I don't really feel comfortable with the added complexity in the current changes -- I think there should be some more refactoring before this can be cleanly added.

@rbtcollins
Copy link
Contributor

These results were obtained by running the rustup toolchain install nightly command 10x times for each combination of variables (RUSTUP_CONCURRENT_DOWNLOADS, interleaved or not, and internet connection).
Please note that the y axis do not start on zero, and cannot be directly compared between themselves.

As I read these results:
low speed connections :
interleaved gives 65 seconds to 59 seconds or a 10% reduction, somewhat stable for 2 or more concurrent downloads.

high speed connections:
interleaved gives 18-12 for low concurrency and 15-13 for higher concurrency, or 33% reduction for 1 or 2 concurrent downloads, and a 13% reduction for higher concurrency, with best performance at 2 concurrent downloads, and performance decreasing for successively more downloads with interleaving, but increasing for more concurrent downloads w/out interleaving.

I suspect some interaction with memory limits showing up in the unpack phase there; I suggest raising the working memory much higher, if you're on a machine that has capacity, to see if thats the case.

But also it suggests the default should probably be quite conservative, as we still need to support unpacking on Rasberry PI class machines.

Even though downloads are done concurrently, the installations are done
sequentially. This means that, as downloads complete, they are in a
queue (an mpsc channel) waiting to be consumed by the future responsible
for the (sequential) installations.

There was a need to relax some test cases to allow for uninstall to
happen before the downloads.
Comment on lines +220 to +222
// Create a channel to communicate whenever a download is done and the component can be installed
// The `mpsc` channel was used as we need to send many messages from one producer (download's thread) to one consumer (install's thread)
// This is recommended in the official docs: https://docs.rs/tokio/latest/tokio/sync/index.html#mpsc-channel
Copy link
Contributor

Choose a reason for hiding this comment

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

I can't find it now, but I thought I wrote a comment on wondering why we're using a channel here, when we could instead compose the futures that download and install together.

Copy link
Member

@rami3l rami3l Sep 28, 2025

Choose a reason for hiding this comment

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

Hmmm would you mind clarifying a little more what you meant by composing the futures?

The current constraints as we previously agreed was that the downloads are concurrent but the installations happening alongside them aren't concurrent within themselves, so I think a channel would be ideal for that case in favor of, say, a mutex.

Copy link
Contributor

Choose a reason for hiding this comment

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

So you're saying, we only do one installation at a time? If that's the case, I would suggest something more like a FuturesUnordered containing all the downloads, and then pulling from that to run the installations.

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.

4 participants