Skip to content

Commit efd3733

Browse files
committed
Auto merge of #11252 - lqd:deferred-debuginfo, r=ehuss
Turn off debuginfo for build dependencies v2 This PR is an alternative to #10493, fixing its most important issue: the unit graph optimization to reuse already built artifacts for dependencies shared between the build-time and runtime subgraphs is preserved (most of the time). By deferring the default debuginfo level in `dev.build-override` until unit graph sharing, we check whether re-use would happen. If so, we use the debuginfo level to ensure reuse does happen. Otherwise, we can avoid emitting debuginfo and improve compile times (on clean, debug and check builds -- although reuse only happens on debug builds). I've kept the message explaining how to bump the debuginfo level if an error occurs while running a build script (and backtraces were requested) that was in the previous PR. I've ran benchmarks on 800 crates, at different parallelism levels, and published the (surprisingly good) results with visualizations, summaries, and raw data [here](https://github.com/lqd/rustc-benchmarking-data/tree/main/experiments/cargo-build-defaults). Opening this PR as discussed in [Eric's message](https://rust-lang.zulipchat.com/#narrow/stream/246057-t-cargo/topic/Defaults.20for.20faster.20compilation.20of.20.22for.20host.22.20units/near/304236576l) as draft since 2 tests won't pass. That fixes the `cargo-crev` case we saw as a blocker last time, but doesn't fix all such cases of reuse, like the 2 failing tests: - [`optional_build_dep_and_required_normal_dep`](https://github.com/rust-lang/cargo/blob/642a0e625d10099a0ca289827de85499d073c572/tests/testsuite/build_script.rs#L4449) - and [`proc_macro_ws`](https://github.com/rust-lang/cargo/blob/bd5db301b0c45ae540afcb19e030dd7c29d2ea4f/tests/testsuite/features2.rs#L1051) These failures make sense, since the debuginfo optimization is done during traversal, it depends on the contents of the unit graph. These tests ensure that sharing happens even on finer-grained invocations: respectively, with an optional shared dependency that is later enabled, and building shared dependencies by themselves first and later as part of a complete workspace build. In both these situations, there is no unit that is shared in the graph during the first invocation, so we do the optimization and remove the debuginfo. When the graph changes in the following invocation, sharing is present and we have to build the shared units again with debuginfo. These cases feel rarer than `cargo-crev`'s case, but I do wonder if there's a way to fix them, or if it's acceptable to not support them.
2 parents cf410cb + 7ccda69 commit efd3733

File tree

12 files changed

+379
-55
lines changed

12 files changed

+379
-55
lines changed

src/cargo/core/compiler/custom_build.rs

+30-2
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ fn build_work(cx: &mut Context<'_, '_>, unit: &Unit) -> CargoResult<Job> {
187187
// carried over.
188188
let to_exec = to_exec.into_os_string();
189189
let mut cmd = cx.compilation.host_process(to_exec, &unit.pkg)?;
190-
let debug = unit.profile.debuginfo.unwrap_or(0) != 0;
190+
let debug = unit.profile.debuginfo.is_turned_on();
191191
cmd.env("OUT_DIR", &script_out_dir)
192192
.env("CARGO_MANIFEST_DIR", unit.pkg.root())
193193
.env("NUM_JOBS", &bcx.jobs().to_string())
@@ -332,6 +332,15 @@ fn build_work(cx: &mut Context<'_, '_>, unit: &Unit) -> CargoResult<Job> {
332332
// Need a separate copy for the fresh closure.
333333
let targets_fresh = targets.clone();
334334

335+
let env_profile_name = unit.profile.name.to_uppercase();
336+
let built_with_debuginfo = cx
337+
.bcx
338+
.unit_graph
339+
.get(unit)
340+
.and_then(|deps| deps.iter().find(|dep| dep.unit.target == unit.target))
341+
.map(|dep| dep.unit.profile.debuginfo.is_turned_on())
342+
.unwrap_or(false);
343+
335344
// Prepare the unit of "dirty work" which will actually run the custom build
336345
// command.
337346
//
@@ -405,7 +414,26 @@ fn build_work(cx: &mut Context<'_, '_>, unit: &Unit) -> CargoResult<Job> {
405414
},
406415
true,
407416
)
408-
.with_context(|| format!("failed to run custom build command for `{}`", pkg_descr));
417+
.with_context(|| {
418+
let mut build_error_context =
419+
format!("failed to run custom build command for `{}`", pkg_descr);
420+
421+
// If we're opting into backtraces, mention that build dependencies' backtraces can
422+
// be improved by requesting debuginfo to be built, if we're not building with
423+
// debuginfo already.
424+
if let Ok(show_backtraces) = std::env::var("RUST_BACKTRACE") {
425+
if !built_with_debuginfo && show_backtraces != "0" {
426+
build_error_context.push_str(&format!(
427+
"\n\
428+
note: To improve backtraces for build dependencies, set the \
429+
CARGO_PROFILE_{env_profile_name}_BUILD_OVERRIDE_DEBUG=true environment \
430+
variable to enable debug information generation.",
431+
));
432+
}
433+
}
434+
435+
build_error_context
436+
});
409437

410438
if let Err(error) = output {
411439
insert_warnings_in_build_outputs(

src/cargo/core/compiler/job_queue.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -985,7 +985,7 @@ impl<'cfg> DrainState<'cfg> {
985985
} else {
986986
"optimized"
987987
});
988-
if profile.debuginfo.unwrap_or(0) != 0 {
988+
if profile.debuginfo.is_turned_on() {
989989
opt_type += " + debuginfo";
990990
}
991991

src/cargo/core/compiler/mod.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -550,7 +550,7 @@ fn link_targets(cx: &mut Context<'_, '_>, unit: &Unit, fresh: bool) -> CargoResu
550550
if json_messages {
551551
let art_profile = machine_message::ArtifactProfile {
552552
opt_level: profile.opt_level.as_str(),
553-
debuginfo: profile.debuginfo,
553+
debuginfo: profile.debuginfo.to_option(),
554554
debug_assertions: profile.debug_assertions,
555555
overflow_checks: profile.overflow_checks,
556556
test: unit_mode.is_any_test(),
@@ -1003,7 +1003,7 @@ fn build_base_args(
10031003
cmd.arg("-C").arg(&format!("codegen-units={}", n));
10041004
}
10051005

1006-
if let Some(debuginfo) = debuginfo {
1006+
if let Some(debuginfo) = debuginfo.to_option() {
10071007
cmd.arg("-C").arg(format!("debuginfo={}", debuginfo));
10081008
}
10091009

src/cargo/core/profiles.rs

+114-7
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ impl Profiles {
276276
// platform which has a stable `-Csplit-debuginfo` option for rustc,
277277
// and it's typically much faster than running `dsymutil` on all builds
278278
// in incremental cases.
279-
if let Some(debug) = profile.debuginfo {
279+
if let Some(debug) = profile.debuginfo.to_option() {
280280
if profile.split_debuginfo.is_none() && debug > 0 {
281281
let target = match &kind {
282282
CompileKind::Host => self.rustc_host.as_str(),
@@ -438,6 +438,20 @@ impl ProfileMaker {
438438
// well as enabling parallelism by not constraining codegen units.
439439
profile.opt_level = InternedString::new("0");
440440
profile.codegen_units = None;
441+
442+
// For build dependencies, we usually don't need debuginfo, and
443+
// removing it will compile faster. However, that can conflict with
444+
// a unit graph optimization, reusing units that are shared between
445+
// build dependencies and runtime dependencies: when the runtime
446+
// target is the same as the build host, we only need to build a
447+
// dependency once and reuse the results, instead of building twice.
448+
// We defer the choice of the debuginfo level until we can check if
449+
// a unit is shared. If that's the case, we'll use the deferred value
450+
// below so the unit can be reused, otherwise we can avoid emitting
451+
// the unit's debuginfo.
452+
if let Some(debuginfo) = profile.debuginfo.to_option() {
453+
profile.debuginfo = DebugInfo::Deferred(debuginfo);
454+
}
441455
}
442456
// ... and next comes any other sorts of overrides specified in
443457
// profiles, such as `[profile.release.build-override]` or
@@ -515,9 +529,9 @@ fn merge_profile(profile: &mut Profile, toml: &TomlProfile) {
515529
profile.codegen_units = toml.codegen_units;
516530
}
517531
match toml.debug {
518-
Some(U32OrBool::U32(debug)) => profile.debuginfo = Some(debug),
519-
Some(U32OrBool::Bool(true)) => profile.debuginfo = Some(2),
520-
Some(U32OrBool::Bool(false)) => profile.debuginfo = None,
532+
Some(U32OrBool::U32(debug)) => profile.debuginfo = DebugInfo::Explicit(debug),
533+
Some(U32OrBool::Bool(true)) => profile.debuginfo = DebugInfo::Explicit(2),
534+
Some(U32OrBool::Bool(false)) => profile.debuginfo = DebugInfo::None,
521535
None => {}
522536
}
523537
if let Some(debug_assertions) = toml.debug_assertions {
@@ -578,7 +592,7 @@ pub struct Profile {
578592
pub codegen_backend: Option<InternedString>,
579593
// `None` means use rustc default.
580594
pub codegen_units: Option<u32>,
581-
pub debuginfo: Option<u32>,
595+
pub debuginfo: DebugInfo,
582596
pub split_debuginfo: Option<InternedString>,
583597
pub debug_assertions: bool,
584598
pub overflow_checks: bool,
@@ -600,7 +614,7 @@ impl Default for Profile {
600614
lto: Lto::Bool(false),
601615
codegen_backend: None,
602616
codegen_units: None,
603-
debuginfo: None,
617+
debuginfo: DebugInfo::None,
604618
debug_assertions: false,
605619
split_debuginfo: None,
606620
overflow_checks: false,
@@ -669,7 +683,7 @@ impl Profile {
669683
Profile {
670684
name: InternedString::new("dev"),
671685
root: ProfileRoot::Debug,
672-
debuginfo: Some(2),
686+
debuginfo: DebugInfo::Explicit(2),
673687
debug_assertions: true,
674688
overflow_checks: true,
675689
incremental: true,
@@ -708,6 +722,99 @@ impl Profile {
708722
}
709723
}
710724

725+
/// The debuginfo level setting.
726+
///
727+
/// This is semantically an `Option<u32>`, and should be used as so via the
728+
/// [DebugInfo::to_option] method for all intents and purposes:
729+
/// - `DebugInfo::None` corresponds to `None`
730+
/// - `DebugInfo::Explicit(u32)` and `DebugInfo::Deferred` correspond to
731+
/// `Option<u32>::Some`
732+
///
733+
/// Internally, it's used to model a debuginfo level whose value can be deferred
734+
/// for optimization purposes: host dependencies usually don't need the same
735+
/// level as target dependencies. For dependencies that are shared between the
736+
/// two however, that value also affects reuse: different debuginfo levels would
737+
/// cause to build a unit twice. By deferring the choice until we know
738+
/// whether to choose the optimized value or the default value, we can make sure
739+
/// the unit is only built once and the unit graph is still optimized.
740+
#[derive(Debug, Copy, Clone, serde::Serialize)]
741+
#[serde(untagged)]
742+
pub enum DebugInfo {
743+
/// No debuginfo level was set.
744+
None,
745+
/// A debuginfo level that is explicitly set, by a profile or a user.
746+
Explicit(u32),
747+
/// For internal purposes: a deferred debuginfo level that can be optimized
748+
/// away, but has this value otherwise.
749+
///
750+
/// Behaves like `Explicit` in all situations except for the default build
751+
/// dependencies profile: whenever a build dependency is not shared with
752+
/// runtime dependencies, this level is weakened to a lower level that is
753+
/// faster to build (see [DebugInfo::weaken]).
754+
///
755+
/// In all other situations, this level value will be the one to use.
756+
Deferred(u32),
757+
}
758+
759+
impl DebugInfo {
760+
/// The main way to interact with this debuginfo level, turning it into an Option.
761+
pub fn to_option(&self) -> Option<u32> {
762+
match self {
763+
DebugInfo::None => None,
764+
DebugInfo::Explicit(v) | DebugInfo::Deferred(v) => Some(*v),
765+
}
766+
}
767+
768+
/// Returns true if the debuginfo level is high enough (at least 1). Helper
769+
/// for a common operation on the usual `Option` representation.
770+
pub(crate) fn is_turned_on(&self) -> bool {
771+
self.to_option().unwrap_or(0) != 0
772+
}
773+
774+
pub(crate) fn is_deferred(&self) -> bool {
775+
matches!(self, DebugInfo::Deferred(_))
776+
}
777+
778+
/// Force the deferred, preferred, debuginfo level to a finalized explicit value.
779+
pub(crate) fn finalize(self) -> Self {
780+
match self {
781+
DebugInfo::Deferred(v) => DebugInfo::Explicit(v),
782+
_ => self,
783+
}
784+
}
785+
786+
/// Reset to the lowest level: no debuginfo.
787+
pub(crate) fn weaken(self) -> Self {
788+
DebugInfo::None
789+
}
790+
}
791+
792+
impl PartialEq for DebugInfo {
793+
fn eq(&self, other: &DebugInfo) -> bool {
794+
self.to_option().eq(&other.to_option())
795+
}
796+
}
797+
798+
impl Eq for DebugInfo {}
799+
800+
impl Hash for DebugInfo {
801+
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
802+
self.to_option().hash(state);
803+
}
804+
}
805+
806+
impl PartialOrd for DebugInfo {
807+
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
808+
self.to_option().partial_cmp(&other.to_option())
809+
}
810+
}
811+
812+
impl Ord for DebugInfo {
813+
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
814+
self.to_option().cmp(&other.to_option())
815+
}
816+
}
817+
711818
/// The link-time-optimization setting.
712819
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)]
713820
pub enum Lto {

src/cargo/ops/cargo_compile/mod.rs

+74-11
Original file line numberDiff line numberDiff line change
@@ -428,17 +428,13 @@ pub fn create_bcx<'a, 'cfg>(
428428
// Rebuild the unit graph, replacing the explicit host targets with
429429
// CompileKind::Host, removing `artifact_target_for_features` and merging any dependencies
430430
// shared with build and artifact dependencies.
431-
let new_graph = rebuild_unit_graph_shared(
431+
(units, scrape_units, unit_graph) = rebuild_unit_graph_shared(
432432
interner,
433433
unit_graph,
434434
&units,
435435
&scrape_units,
436436
host_kind_requested.then_some(explicit_host_kind),
437437
);
438-
// This would be nicer with destructuring assignment.
439-
units = new_graph.0;
440-
scrape_units = new_graph.1;
441-
unit_graph = new_graph.2;
442438
}
443439

444440
let mut extra_compiler_args = HashMap::new();
@@ -578,7 +574,15 @@ fn rebuild_unit_graph_shared(
578574
let new_roots = roots
579575
.iter()
580576
.map(|root| {
581-
traverse_and_share(interner, &mut memo, &mut result, &unit_graph, root, to_host)
577+
traverse_and_share(
578+
interner,
579+
&mut memo,
580+
&mut result,
581+
&unit_graph,
582+
root,
583+
false,
584+
to_host,
585+
)
582586
})
583587
.collect();
584588
// If no unit in the unit graph ended up having scrape units attached as dependencies,
@@ -602,6 +606,7 @@ fn traverse_and_share(
602606
new_graph: &mut UnitGraph,
603607
unit_graph: &UnitGraph,
604608
unit: &Unit,
609+
unit_is_for_host: bool,
605610
to_host: Option<CompileKind>,
606611
) -> Unit {
607612
if let Some(new_unit) = memo.get(unit) {
@@ -612,25 +617,83 @@ fn traverse_and_share(
612617
let new_deps: Vec<_> = unit_graph[unit]
613618
.iter()
614619
.map(|dep| {
615-
let new_dep_unit =
616-
traverse_and_share(interner, memo, new_graph, unit_graph, &dep.unit, to_host);
620+
let new_dep_unit = traverse_and_share(
621+
interner,
622+
memo,
623+
new_graph,
624+
unit_graph,
625+
&dep.unit,
626+
dep.unit_for.is_for_host(),
627+
to_host,
628+
);
617629
new_dep_unit.hash(&mut dep_hash);
618630
UnitDep {
619631
unit: new_dep_unit,
620632
..dep.clone()
621633
}
622634
})
623635
.collect();
636+
// Here, we have recursively traversed this unit's dependencies, and hashed them: we can
637+
// finalize the dep hash.
624638
let new_dep_hash = dep_hash.finish();
625-
let new_kind = match to_host {
639+
640+
// This is the key part of the sharing process: if the unit is a runtime dependency, whose
641+
// target is the same as the host, we canonicalize the compile kind to `CompileKind::Host`.
642+
// A possible host dependency counterpart to this unit would have that kind, and if such a unit
643+
// exists in the current `unit_graph`, they will unify in the new unit graph map `new_graph`.
644+
// The resulting unit graph will be optimized with less units, thanks to sharing these host
645+
// dependencies.
646+
let canonical_kind = match to_host {
626647
Some(to_host) if to_host == unit.kind => CompileKind::Host,
627648
_ => unit.kind,
628649
};
650+
651+
let mut profile = unit.profile.clone();
652+
653+
// If this is a build dependency, and it's not shared with runtime dependencies, we can weaken
654+
// its debuginfo level to optimize build times. We do nothing if it's an artifact dependency,
655+
// as it and its debuginfo may end up embedded in the main program.
656+
if unit_is_for_host
657+
&& to_host.is_some()
658+
&& profile.debuginfo.is_deferred()
659+
&& !unit.artifact.is_true()
660+
{
661+
// We create a "probe" test to see if a unit with the same explicit debuginfo level exists
662+
// in the graph. This is the level we'd expect if it was set manually or the default value
663+
// set by a profile for a runtime dependency: its canonical value.
664+
let canonical_debuginfo = profile.debuginfo.finalize();
665+
let mut canonical_profile = profile.clone();
666+
canonical_profile.debuginfo = canonical_debuginfo;
667+
let unit_probe = interner.intern(
668+
&unit.pkg,
669+
&unit.target,
670+
canonical_profile,
671+
to_host.unwrap(),
672+
unit.mode,
673+
unit.features.clone(),
674+
unit.is_std,
675+
unit.dep_hash,
676+
unit.artifact,
677+
unit.artifact_target_for_features,
678+
);
679+
680+
// We can now turn the deferred value into its actual final value.
681+
profile.debuginfo = if unit_graph.contains_key(&unit_probe) {
682+
// The unit is present in both build time and runtime subgraphs: we canonicalize its
683+
// level to the other unit's, thus ensuring reuse between the two to optimize build times.
684+
canonical_debuginfo
685+
} else {
686+
// The unit is only present in the build time subgraph, we can weaken its debuginfo
687+
// level to optimize build times.
688+
canonical_debuginfo.weaken()
689+
}
690+
}
691+
629692
let new_unit = interner.intern(
630693
&unit.pkg,
631694
&unit.target,
632-
unit.profile.clone(),
633-
new_kind,
695+
profile,
696+
canonical_kind,
634697
unit.mode,
635698
unit.features.clone(),
636699
unit.is_std,

0 commit comments

Comments
 (0)