-
Notifications
You must be signed in to change notification settings - Fork 2.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Hashed dependencies of metadata into the metadata of a lib #4469
Conversation
r? @matklad (rust_highfive has picked a reviewer for you, use r? to override) |
src/cargo/ops/cargo_rustc/context.rs
Outdated
dep_metadatas.sort(); | ||
for metadata in dep_metadatas { | ||
metadata.hash(&mut hasher); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is it cleaner to just do dep_metadatas.hash(&mut hasher)
and rely on Vec's impl of Hash?
As a follow up, would it be nicer to use a sorted datastructure (like BTreeSet) instead of calling .sort() on a Vec? I'm certain it doesn't matter from a perf perspective, so whatever the team likes stylistically is fine to me.
|
||
|
||
// Build caller1. should build the dep library. Because the features | ||
// are different than the full workspace, it rebuilds. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be beautiful if building the entire workspace simply covered this, but it doesn't as is. According to #3620, this is difficult to fix? I couldn't find an easy way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just left this comment which I think is related to this. I do believe it'll be a relatively invasive fix to fix this.
This got a bit nastier after getting the doctests to pass because the Here, I pull the central code inside dep_targets sans filtering up to the metadata calculation layer, and things worked out. There's probably a way to factor this cleaner, but running through CI / requesting feedback before embarking on that. |
Huh, oddly enough I've hit this issue myself a couple of days ago: #4463. However I feel that the problem here is deeper then just spurious rebuilds, because you actually get different artifacts depending on how you build your workspace(see #4463), and this seems pretty bad to me. @alexcrichton , if workspaces share the same dep graph, perhaps we should always activate the union of all features of the workspace? Currently for features we loop over requested summaries, but it shouldn't be that difficult to loop over all of them? @nipunn1313 I think that if you add 'cargo test --all --no-run' before the crate loop in your CI script, you won't get rebuilds. |
@matklad unfortunately running I also thought about having |
|
Huh, I am 100% wrong, sorry for the noise then :) |
I think in theory this should not be the case, because the workspace shares the dependency graph and the lockfile anyway, so even if you compile a single package, the whole workspace needs to be loaded. And that makes me think that the huge difference in start up time between single crate and whole workspace you are observing probably indicates some issue in Cargo. However I would say that the primary problem here is correctness (producing different artifacts for |
679639a
to
cbae325
Compare
Thanks for the PR @nipunn1313! I've long wanted to implement this! @matklad I think we'll want this solution no matter what for a number of reasons. Let's say you're working on just one crate and you do: cargo build
# edit files ...
cargo build --features a
# edit files ...
cargo build I'd personally expect the third build (second usage fo This has come up a good number of times in a whole slew of situations! Now the solution a lot of the time is to stop oscillating on features and instead just unify what's used everywhere, but that's not always possible. In any case though I think this boils down to "not a workspace problem" and instead a "how Cargo caches dependencies" problem. (in that workspaces aren't required to reproduce this issue, they just make it worse sometimes) |
src/cargo/ops/cargo_rustc/context.rs
Outdated
// Mix in the target-metadata of all the dependencies of this target | ||
if let Ok(deps) = self.used_deps(unit) { | ||
let mut deps_metadata = deps.into_iter().map(|dep_unit| { | ||
self.target_metadata(&dep_unit) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So runtime-wise we've had a lot of issues in the past with this sort of recursive calculation causing an exponential amount of code to run instead of linear (wrt the dependency graph). For example here I think that if we call target_metadata
for all targets this goes from linear (currently) to exponential (after this PR), right?
Perhaps we can introduce a cache for target_metadata
? (that's what we do for everything else!)
This tends to not show up much in small crate graphs but projects like Servo (and in theory Dropbox w/ 200+ crates) may end up showing this pretty badly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah. That sounds like a great solution. Envisioning a big hashmap from Unit -> Metadata in the ctx? We could probably even precalculate it if we walk the units in dependency order.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds great! I'm fine with doing the caching lazily or doing it all at once when we walk in dependency order. I know we have a few "prepopulate passes" at the beginning to walk the graph, but I sort of forget what they're doing. Basically whatever's easiest is fine by me
src/cargo/ops/cargo_rustc/context.rs
Outdated
return self.doc_deps(unit); | ||
} | ||
|
||
fn used_deps(&self, unit: &Unit<'a>) -> CargoResult<Vec<Unit<'a>>> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems like a somewhat unfortunate addition in the slew of already-too-many ways to calculate dependencies :(
I didn't quite follow your explanation earlier, mind detailing a bit more what was going on?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I agree. At least this one is private only. The issue here is the fork on
if unit.profile.run_custom_build
and if unit.profile.doc && !unit.profile.test
Specifically, OUT_DIR was set incorrectly in the build phase of doctests because doctests had a different dependency tree, but OUT_DIR was expected to be the same.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm I'm still not 100% following... In any case, I'll check this out locally and poke around.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah. Easier if you poke around. I'll give it another shot though.
Without this refactor:
- When you compile the build.rs script for a doctest vs compiling the build.rs script for a regular test, it ends up with different metadata. This causes OUT_DIR to get set to a different (nonexistent) directory during doctests. It shows up as a test failure.
- https://travis-ci.org/rust-lang/cargo/jobs/271875681
With this refactor
- Doctests and regular tests have the same
used_deps
despite having differentdep_targets
.
Overall, it definitely does feel like some elements are repeated here and architecturally there is some unnecessary complexity, but I don't understand it well enough right now to find a way out. I think we need one function that returns deps for the package vs deps for the unit of build?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah ok that sounds very surprising! The build script should be constant here and shouldn't change metadata, but let me poke around to see if I can't find where the difference is arising from
I don't think this'd be too hard to implement, but I'm not sure if this is what we'd want implemented per se. If one target of a workspace doesn't want a particular feature activated, wouldn't it be surprising if some other target present in a workspace far away activated the feature?
20 seconds in Cargo definitely sounds like a bug to me! I'd love to help investigate this and speed that up if it's a problem, but we can probably take that off this PR :) |
@alexcrichton yeah, totally agree that the fix here is needed in general! |
@alexcrichton We actually do already cache the target separately if the features change. The issue is that we cache the target the same if the features of a dep change (see discussion here #3620). It's a bit more subtle, but still in the same realm as the issue you're describing. Eg Copying my eg from #3620 where x=itertools, y=either
vs
FWIW, I think #3620 and #4463 may be duplicates. Lots of good discussion in both. |
@@ -483,6 +483,15 @@ impl<'a, 'cfg> Context<'a, 'cfg> { | |||
// when changing feature sets each lib is separately cached. | |||
self.resolve.features_sorted(unit.pkg.package_id()).hash(&mut hasher); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We've always mixed in features for the package itself. Just not for the deps. See this line
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll work on the cache to replace the recursive call.
src/cargo/ops/cargo_rustc/context.rs
Outdated
// Mix in the target-metadata of all the dependencies of this target | ||
if let Ok(deps) = self.used_deps(unit) { | ||
let mut deps_metadata = deps.into_iter().map(|dep_unit| { | ||
self.target_metadata(&dep_unit) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah. That sounds like a great solution. Envisioning a big hashmap from Unit -> Metadata in the ctx? We could probably even precalculate it if we walk the units in dependency order.
src/cargo/ops/cargo_rustc/context.rs
Outdated
return self.doc_deps(unit); | ||
} | ||
|
||
fn used_deps(&self, unit: &Unit<'a>) -> CargoResult<Vec<Unit<'a>>> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I agree. At least this one is private only. The issue here is the fork on
if unit.profile.run_custom_build
and if unit.profile.doc && !unit.profile.test
Specifically, OUT_DIR was set incorrectly in the build phase of doctests because doctests had a different dependency tree, but OUT_DIR was expected to be the same.
@nipunn1313 ah yeah I believe you and I are worried about the same case! I forgot long ago when you added "hash the feature selection into the metadata" to also account for the transitive case :( |
Cool. Just worked out the cache. Realized as I was writing it that I worked on one of the other caches (for target_filenames) last year. Had forgotten heh. |
Ok so one (existing) bug I've found is that the I've fixed that with this diff. There's one failure, however, remaining with the doctest issue like you were mentioning, looking into that now. |
Ok turns out the next bug is actually the same. After we've compiled everything a I fixed that test in specific by moving this line below this line, but that unfortunately breaks the I think the tl;dr; here is that the internal mutability causing a difference in |
Previously it depended on dynamic state that was calculated throughout a compilation which ended up causing different fingerprints showing up in a few locations, so this makes the invocation deterministic throughout `cargo_rustc`.
52e8a72
to
f90d21f
Compare
📌 Commit f90d21f has been approved by |
Hashed dependencies of metadata into the metadata of a lib This fixes one part of #3620. To my understanding, the more fundamental fix is more challenging
☀️ Test successful - status-appveyor, status-travis |
Nice find Alex! |
This fixes one part of #3620. To my understanding, the more fundamental fix is more challenging