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.

}
}
ComponentInstalled(c, h, t) => {
if Some(h) == t.as_ref() || t.is_none() {
Copy link
Member

Choose a reason for hiding this comment

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

Nit: Now that we are at it, I would probably give an extra commit to avoid the t.unwrap() here by flipping the if and writing something like:

if let Some(t) = t && t != h { ... } else { ... }

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I did this based on how other Notifications were being handled.
Should I refactor all of them now, leave that for another PR, or just change this one?

Copy link
Member

Choose a reason for hiding this comment

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

@FranciscoTGouveia It should be worthwhile to refactor them all in a separate commit (before your addition) lest we forget about it :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If you agree, I would like to leave this for a follow-up PR, right after this one is merged.
So I suggest leaving this as unresolved for now.

Copy link
Member

Choose a reason for hiding this comment

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

Okay, let's do #4471 (comment) for the particular change in this PR, and then move the rest to that style in a subsequent PR.

@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
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.
@rami3l rami3l requested a review from djc September 19, 2025 10:04
@rami3l rami3l removed their assignment Sep 19, 2025
Comment on lines 547 to 553
component: Component,
format: CompressionKind,
installer_file: File,
tmp_cx: &temp::Context,
download_cfg: &DownloadCfg<'_>,
new_manifest: &Manifest,
tx: Transaction<'a>,
Copy link
Contributor

Choose a reason for hiding this comment

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

I think the number of arguments is here too large, and this should be rethought to have a named field struct that can hold all/most of the arguments.

I think the pre-existing code here could benefit from some refactoring as well, for example by packaging together the arguments passed into the compression wrappers. The uninitialized variables structure also sounds out as pretty unidiomatic.

let download_tx_cloned = download_tx.clone();
async move {
let _permit = sem.acquire().await.unwrap();
self.download_component(
Copy link
Contributor

Choose a reason for hiding this comment

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

Pre-existing, but this argument list is also just too long, and I think we should find a different way to express this before layering on more complexity.


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?

Comment on lines +128 to +134
if let Some(t) = t
&& t != h
{
write!(f, "installing component '{c}' for '{t}'")
} else {
write!(f, "installing component '{c}'")
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: I think this would look better as a match:

match t {
    Some(t) if != h => write!(f, ..),
    _ => write!(f, ..),
}

Comment on lines +136 to +141
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.

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.

3 participants