Open obi1kenobi opened 1 year ago
What is the rationale for the synthetic project?
It's due to issues like this one: #167
TL;DR: The user's copy of their project is not guaranteed to be in a state amenable to valid rustdoc generation. To get it there we need a fresh lockfile -- but we don't want to overwrite the user's own lockfile, so we make a synthetic project and lock there instead.
It's a really unfortunate but really serious problem, because it can trivially lead to all the flavors of semver-major false-positives one could imagine.
TL;DR: The user's copy of their project is not guaranteed to be in a state amenable to valid rustdoc generation. To get it there we need a fresh lockfile -- but we don't want to overwrite the user's own lockfile, so we make a synthetic project and lock there instead.
What if we backup the lockfile, run cargo semver-checks
and then move it back after?
Like:
mv Cargo.lock Cargo.lock.bkp
cargo semver-checks ...
rm Cargo.lock; mv Cargo.lock.bkp Cargo.lock
The solution to https://github.com/obi1kenobi/cargo-semver-checks/issues/167 seems a bit excessive. In our workspace, this means recompiling the same dependencies for every crate because the individual synthetic crates don't share a target
directory!
We could also state as a requirement that the repository needs to be versioned with git
.
You could then check for uncommitted changes and fail in that case.
Assuming no committed changes, run cargo update
and later git reset --hard
.
I totally get the frustration. We definitely need to do better — this issue isn't the only one like that.
In our workspace, this means recompiling the same dependencies for every crate because the individual synthetic crates don't share a target directory!
This can be changed today: the build should still obey cargo configuration. If you specify CARGO_TARGET_DIR
(or the equivalent config setting) then you can make all the crates share a target directory.
We could also state as a requirement that the repository needs to be versioned with git.
I'm worried that might get in the way of merging cargo-semver-checks
into cargo
. cargo publish
does check for dirty repo state, but cargo check
doesn't and shouldn't — and I think cargo-semver-checks
should be closer to the latter: usable with uncommitted state and useful not just when publishing.
It's also a horrifically bad user experience if cargo-semver-checks
crashes and leaves the project without a lockfile (and with a Cargo.lock.bkp
).
Please let me know if you get a chance to try the shared target directory trick I suggested! I'm curious how much of a difference it might make.
Like I mentioned in the issue summary, I'm open to adding a "secondary" mechanism to generate the rustdoc that does a cargo update
and generates the rustdoc in-place. I just don't think it should be the default, because it's more fiddly and easier to accidentally misuse, and we don't have anywhere near the bandwidth in the project to make that approach safe for general use.
This can be changed today: the build should still obey cargo configuration. If you specify
CARGO_TARGET_DIR
(or the equivalent config setting) then you can make all the crates share a target directory.
Oh that is an interesting approach. I'll see if copying the target
dir makes a difference. It is all compiled already so we'd save that bit of compute by just reusing the existing cache.
This can be changed today: the build should still obey cargo configuration. If you specify
CARGO_TARGET_DIR
(or the equivalent config setting) then you can make all the crates share a target directory.Oh that is an interesting approach. I'll see if copying the
target
dir makes a difference. It is all compiled already so we'd save that bit of compute by just reusing the existing cache.
Uh, what if I point it to the existing target
dir? :exploding_head:
This can be changed today: the build should still obey cargo configuration. If you specify
CARGO_TARGET_DIR
(or the equivalent config setting) then you can make all the crates share a target directory.Oh that is an interesting approach. I'll see if copying the
target
dir makes a difference. It is all compiled already so we'd save that bit of compute by just reusing the existing cache.Uh, what if I point it to the existing
target
dir? exploding_head
I guess that kind of circumvents the fix for #167 :joy:
Actually, no, because it still uses a different lockfile.
In our workspace, this means recompiling the same dependencies for every crate because the individual synthetic crates don't share a target directory!
This can be changed today: the build should still obey cargo configuration. If you specify
CARGO_TARGET_DIR
(or the equivalent config setting) then you can make all the crates share a target directory.
In my local testing, this is not the case. Environment variables are not passed through to the inner cargo invocation.
I think it doesn't work because we are using the commandline option to specify the target directory: https://github.com/obi1kenobi/cargo-semver-checks/blob/97f8a9bdc671040542e150bfb602409492356f7b/src/rustdoc_cmd.rs#L78-L79
I think it doesn't work because we are using the commandline option to specify the target directory:
If I remove these two, I can set the target directory via the env variable but then it doesn't find the JSON file because it is probably expecting it somewhere else. My question would be however, whether we can't just always use the original target
directory? As long as we have a separate lockfile, #167 should not be a problem?
It is a bit of a hack but with this patch, I can successfully reuse most of the build artifacts from my usual build:
Index: src/rustdoc_cmd.rs
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/rustdoc_cmd.rs b/src/rustdoc_cmd.rs
--- a/src/rustdoc_cmd.rs (revision 97f8a9bdc671040542e150bfb602409492356f7b)
+++ b/src/rustdoc_cmd.rs (date 1677229148044)
@@ -48,14 +48,7 @@
.manifest_path(manifest_path)
.no_deps()
.exec()?;
- let manifest_target_directory = metadata
- .target_directory
- .as_path()
- .as_std_path()
- // HACK: Avoid potential errors when mixing toolchains
- .join(crate::util::SCOPE)
- .join("target");
- let target_dir = manifest_target_directory.as_path();
+ let target_dir = metadata.workspace_root.ancestors().nth(2).unwrap().as_std_path();
let stderr = if self.silence {
std::process::Stdio::piped()
With:
A full run of cargo semver-checks check-release --workspace
takes just over 3 minutes!
This is roughly equivalent to what I'd have in CI as I'd be restoring a cache that has most things built already, plus a cache for the registry JSON.
A full run of
cargo semver-checks check-release --workspace
takes just over 3 minutes!
This also works if I clean out all the local-
directories, i.e. the mtime of the generated project doesn't seem to matter, cargo
will happily reuse the existing build artifacts.
Thanks for doing all these experiments! We'll definitely incorporate the findings into the V2 action that @mgr0dzicki is in the process of building.
IIRC, there were two concerns with target dirs that led to making the separate target dir:
cargo-semver-checks
no longer needs nightly--all-features
-- IIRC we weren't sure if the two compilations might overwrite something in the regular target dir. We didn't want to risk "cargo-semver-checks
slows down my iteration loop" due to some unfortunate and unpredictable interaction between features or between the two builds (e.g. with proc macros or something), so we went for the safe route.Short of doing a deep dive into how cargo build artifacts are generated, I feel that:
How about adding a --target-dir
CLI option, same as in cargo
itself? Then local users get to keep the "careful" setup but CI can reuse the cached target dir.
In fact, CI perhaps wants to use save-if: false
on the target dir caching, just in case the baseline wasn't cached (e.g. in the first build after a new release) so that its build doesn't poison the cache. Can you see if save-if: false
still results in good cache hits on the current rustdoc generation?
A full run of
cargo semver-checks check-release --workspace
takes just over 3 minutes!
If you have it handy, I'd love to see the timing data for this. I'm particularly curious how much time is spent running the lints vs building rustdoc (even with cache hits), so I know roughly how much impact the upcoming Trustfall optimizations API (demoed in my blog post) might have here.
- compiling both baseline and current, both with
--all-features
-- IIRC we weren't sure if the two compilations might overwrite something in the regular target dir. We didn't want to risk "cargo-semver-checks
slows down my iteration loop" due to some unfortunate and unpredictable interaction between features or between the two builds (e.g. with proc macros or something), so we went for the safe route.
In my experience, cargo
has become pretty good at separating build output artifacts and then reusing what it needs, even if the same crate is re-compiled with different features.
Even if it were a problem, I am not sure how common it is to run cargo build
and cargo semver-checks
in an alternating fashion to actually negatively affect the iteration loop. If anything, not reusing the target directory massively slows down iteration because I need to wait for a rebuild of my entire workspace to run cargo semver-checks
.
I'd consider any unnecessary re-compilation a cargo bug that needs to be fixed upstream. We are just running cargo doc
. That really shouldn't mess with the other compilation outputs.
Proposal
How about adding a
--target-dir
CLI option, same as incargo
itself? Then local users get to keep the "careful" setup but CI can reuse the cached target dir.
Unless the default is to use the same target dir, I don't think there is much value in another config option. On the contrary, I always dislike when you have to dig into the details of a tool's config options to make it perform well. The default should perform well and if it that causes problems, I'd consider those as bugs.
In fact, CI perhaps wants to use
save-if: false
on the target dir caching, just in case the baseline wasn't cached (e.g. in the first build after a new release) so that its build doesn't poison the cache. Can you see ifsave-if: false
still results in good cache hits on the current rustdoc generation?
I've only done local testing so far because I'd have to build my fork in CI to use it there and the target
dir generated by the current approach is so big for our workspace (> 22GB) that I can't cache it in CI.
A full run of
cargo semver-checks check-release --workspace
takes just over 3 minutes!If you have it handy, I'd love to see the timing data for this. I'm particularly curious how much time is spent running the lints vs building rustdoc (even with cache hits), so I know roughly how much impact the upcoming Trustfall optimizations API (demoed in my blog post) might have here.
I'll see to look into that.
@epage I'd love your take on this
TL;DR: @thomaseizinger has found that not creating a separate target dir specific to semver-checking results in significant speedup for large workspaces (~hour -> few minutes), and is proposing making that the default behavior. A long time ago, we considered this and felt it was too risky at the time, but the underlying assumptions have changed in the meantime (see above) and perhaps it's time to re-evaluate.
A full run of
cargo semver-checks check-release --workspace
takes just over 3 minutes!If you have it handy, I'd love to see the timing data for this. I'm particularly curious how much time is spent running the lints vs building rustdoc (even with cache hits), so I know roughly how much impact the upcoming Trustfall optimizations API (demoed in my blog post) might have here.
I read the blogpost but I am not sure I understood from it how to gather the timing data.
From memory, it took about 20s to "update index" (sparse registry will hopefully fix that soon!). All checks were skipped across the entire workspace because every crate had received a major version bump already when I ran that. So I think the time is mostly spent in creating files, parsing rustdoc and the like:
5 seconds is not amazing but an okay time, considering we are launching a couple of processes in the background. If you wanted to analyze that in detail, I'd probably run it through flamegraph to see where it spends its time. tracing
might also be a good idea to capture spans and see where we maybe wait for IO to complete or other non-CPU intensive things that don't show up in flamegraphs.
@obi1kenobi can you be more specific for what you are wanting to point at in this thread as being resolved?
@obi1kenobi can you be more specific for what you are wanting to point at in this thread as being resolved?
This is the context + my proposal: https://github.com/obi1kenobi/cargo-semver-checks/issues/401#issuecomment-1444980671
@thomaseizinger in the subsequent reply makes the case that we should reuse the target directory by default, instead of merely supporting it as an option, and that we should trust cargo to not rebuild unnecessarily.
Friendly bump. Unless I am mistaken, with 0.20 shipped, having to rebuild the current crate on every run is now the dominating factor for the CI runtime of cargo semver-checks
.
What is necessary to push this forward? With unrelated improvements to our CI, we are now spending about 50% of our time in cargo semver-checks
of the current crate: https://github.com/libp2p/rust-libp2p/actions/runs/4851553760/jobs/8645517496?pr=3715
In wall-clock time that is only 1min30s but it would still be nice if we could reuse the target dir here and thus benefit from the caching that we already do.
https://github.com/rust-lang/cargo/issues/12103 suggests that there might be another reason why the cache isn't being hit even if we manually moved the target files there. Going to keep an eye on it, it should make a positive difference or at least make the workaround of just copying in the target files work better.
We've now migrated to the provided GitHub action and semver-checking takes about 15 minutes for us. See https://github.com/libp2p/rust-libp2p/actions/runs/6068173889/job/16460787944#step:4:210. We have some interoperability tests that also take similarly long but the rest of CI finishes in < 5 minutes. It would be great if cargo semver-checks
would be among that.
At the moment, we are completely unnecessarily recompiling the entire workspace, both locally and in CI. This results in high disk usage and slow feedback loops because everything is compiled twice.
Can something like https://github.com/obi1kenobi/cargo-semver-checks/pull/402 please be considered?
Yes, definitely. I also found git-restore-mtime
, a tool that can restore mtimes based on the last time a file was touched in a commit, which may produce additional speedup. I want to look into both and see how we can get the biggest wins at the lowest maintenance costs.
I'm going to be travelling for a few weeks for RustConf and a couple of other conferences, but I look forward to picking thhis back up when I get back.
Describe your use case
For workspaces that semver-check many crates, generating the "current" rustdoc JSON takes a significant portion of time: almost an hour across all of
rust-libp2p
, for example.To ensure rustdoc JSON stability and correctness, "current" (also "baseline") rustdoc is generated by:
This synthetic project is generated at
cargo-semver-checks
runtime. Because of https://github.com/rust-lang/cargo/issues/6529, cached build artifacts for that project can't be used: cargo will ignore them since their mtime is older than the project. The hack of applying a synthetic mtime in the future to the build artifacts didn't seem to work, for unknown reasons: #399It would appear that build artifact caching for the current rustdoc JSON is just not possible at the moment. It would be nice to make that a supported use case.
Describe the solution you'd like
cargo-semver-checks
GitHub ActionAlternatives, if applicable
It may be possible to do this as a step-by-step process:
cargo-semver-checks
via the--current-rustdoc <path>
CLI option.This is certainly not as elegant, but may be substantially faster for large repos.
Additional Context
No response