Skip to content

Commit 6f55bc8

Browse files
authored
Merge pull request #1895 from Kobzol/binary-stats-ext
Add command to compare binary stats of two arbitrary binaries
2 parents 5f608ec + 909b573 commit 6f55bc8

File tree

8 files changed

+218
-133
lines changed

8 files changed

+218
-133
lines changed

collector/README.md

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -516,23 +516,28 @@ Codegen diff is currently only implemented for runtime benchmarks.
516516

517517
## Binary size statistics
518518
You can use the `binary_stats` command to display size statistics (section and symbol sizes) of
519-
binary artifacts (executables, libraries) of selected compile benchmarks.
520-
521-
```
522-
./target/release/collector binary_stats `<rustc>` --include <benchmark name> \
523-
[--profile <Debug|Opt>] \
524-
[--backend <Llvm|Cranelift>]
525-
```
526-
527-
You can also compare (diff) the size statistics between two compilers:
528-
```
529-
./target/release/collector binary_stats `<rustc>` --include <benchmark name> --rustc2 <rustc2>
530-
```
531-
or between two codegen backends:
532-
```
533-
./target/release/collector binary_stats `<rustc>` --include <benchmark name> --rustc2 <rustc>
534-
--backend <Llvm|Cranelift> --backend2 <Llvm|Cranelift>
535-
```
519+
binary artifacts (executables, libraries). You can compare the binary statistics of:
520+
521+
- Selected compile benchmarks:
522+
```bash
523+
./target/release/collector binary_stats compile `<rustc>` --include <benchmark name> \
524+
[--profile <Debug|Opt>] \
525+
[--backend <Llvm|Cranelift>]
526+
```
527+
528+
You can also compare (diff) the size statistics between two compilers:
529+
```bash
530+
./target/release/collector binary_stats compile `<rustc>` --include <benchmark name> --rustc2 <rustc2>
531+
```
532+
or between two codegen backends:
533+
```bash
534+
./target/release/collector binary_stats compile `<rustc>` --include <benchmark name> --rustc2 <rustc>
535+
--backend <Llvm|Cranelift> --backend2 <Llvm|Cranelift>
536+
```
537+
- Arbitrary binary artifacts on disk:
538+
```bash
539+
./target/release/collector binary_stats local `<artifact-path>` [<artifact-to-compare-to>]
540+
```
536541

537542
## How `rustc` wrapping works
538543
When a crate is benchmarked or profiled, the real `rustc` is replaced with the `rustc-fake` binary,

collector/src/artifact_stats.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
//! names and sizes.
33
44
use std::collections::{HashMap, VecDeque};
5+
use std::ffi::OsStr;
56
use std::path::{Path, PathBuf};
67
use std::process::Command;
78
use std::sync::OnceLock;
@@ -23,6 +24,17 @@ pub struct ArtifactStats {
2324
}
2425

2526
impl ArtifactStats {
27+
/// Try to auto-detect the artifact type from the given path.
28+
/// If auto-detection fails, tries to load the artifact as a dynamic object.
29+
pub fn from_path(path: &Path) -> anyhow::Result<Self> {
30+
if path.extension() == Some(OsStr::new("a")) || path.extension() == Some(OsStr::new("rlib"))
31+
{
32+
Self::from_rlib(path)
33+
} else {
34+
Self::from_dynamic_object(path)
35+
}
36+
}
37+
2638
/// Loads size statistics from an ELF file (either an executable or a shared library).
2739
pub fn from_dynamic_object(path: &Path) -> anyhow::Result<Self> {
2840
let data = std::fs::read(path)

collector/src/bin/collector.rs

Lines changed: 175 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ use tabled::settings::{Alignment, Border, Color, Modify};
2626
use tokio::runtime::Runtime;
2727

2828
use collector::api::next_artifact::NextArtifact;
29-
use collector::artifact_stats::{compile_and_get_stats, ArtifactWithStats, CargoProfile};
29+
use collector::artifact_stats::{
30+
compile_and_get_stats, ArtifactStats, ArtifactWithStats, CargoProfile,
31+
};
3032
use collector::codegen::{codegen_diff, CodegenType};
3133
use collector::compile::benchmark::category::Category;
3234
use collector::compile::benchmark::codegen_backend::CodegenBackend;
@@ -408,39 +410,65 @@ struct PurgeOption {
408410
purge: Option<PurgeMode>,
409411
}
410412

411-
// For each subcommand we list the mandatory arguments in the required
412-
// order, followed by the options in alphabetical order.
413-
#[derive(Debug, clap::Subcommand)]
413+
#[derive(Debug, clap::Args)]
414414
#[command(rename_all = "snake_case")]
415-
enum Commands {
416-
/// Show binary (executable or library) section (and optionally symbol) size statistics of the
417-
/// selected compile benchmark(s).
418-
/// Optionally compares sizes between two compiler toolchains, if `--rustc2` is provided.
419-
BinaryStats {
420-
#[command(flatten)]
421-
local: LocalOptions,
415+
struct BinaryStatsCompile {
416+
#[command(flatten)]
417+
local: LocalOptions,
422418

423-
/// Cargo profile to use.
424-
#[arg(long, default_value = "Debug")]
425-
profile: Profile,
419+
/// Cargo profile to use.
420+
#[arg(long, default_value = "Debug")]
421+
profile: Profile,
426422

427-
/// Codegen backend to use.
428-
#[arg(long = "backend", default_value = "Llvm")]
429-
codegen_backend: CodegenBackend,
423+
/// Codegen backend to use.
424+
#[arg(long = "backend", default_value = "Llvm")]
425+
codegen_backend: CodegenBackend,
430426

431-
/// An optional second toolchain to compare to.
432-
#[arg(long)]
433-
rustc2: Option<String>,
427+
/// An optional second toolchain to compare to.
428+
#[arg(long)]
429+
rustc2: Option<String>,
430+
431+
/// Codegen backend to use for the second toolchain.
432+
#[arg(long = "backend2")]
433+
codegen_backend2: Option<CodegenBackend>,
434+
}
435+
436+
#[derive(Debug, clap::Args)]
437+
#[command(rename_all = "snake_case")]
438+
struct BinaryStatsLocal {
439+
/// Binary artifact to examine.
440+
artifact: PathBuf,
434441

435-
/// Codegen backend to use for the second toolchain.
436-
#[arg(long = "backend2")]
437-
codegen_backend2: Option<CodegenBackend>,
442+
/// Optional second artifact to compare with the first one.
443+
artifact2: Option<PathBuf>,
444+
}
445+
446+
#[derive(Debug, clap::Subcommand)]
447+
#[command(rename_all = "snake_case")]
448+
enum BinaryStatsMode {
449+
/// Show size statistics for the selected compile benchmark(s).
450+
/// Optionally compares sizes between two compiler toolchains, if `--rustc2` is provided.
451+
Compile(BinaryStatsCompile),
452+
/// Show size statistics for the selected binary artifact on disk.
453+
/// Optionally compares sizes with a second provided artifact, if `--artifact2` is provided.
454+
Local(BinaryStatsLocal),
455+
}
438456

457+
// For each subcommand we list the mandatory arguments in the required
458+
// order, followed by the options in alphabetical order.
459+
#[derive(Debug, clap::Subcommand)]
460+
#[command(rename_all = "snake_case")]
461+
enum Commands {
462+
/// Show binary (executable or library) section (and optionally symbol) size statistics.
463+
BinaryStats {
439464
/// Also print symbol comparison in addition to section comparison.
440465
///
441466
/// Warning: may generate *A LOT* of data.
442-
#[arg(long, default_value_t = false)]
467+
#[arg(long, default_value_t = false, global = true)]
443468
symbols: bool,
469+
470+
#[clap(subcommand)]
471+
mode: BinaryStatsMode,
444472
},
445473

446474
/// Benchmarks the performance of programs generated by a local rustc
@@ -649,89 +677,13 @@ fn main_result() -> anyhow::Result<i32> {
649677
let target_triple = format!("{}-unknown-linux-gnu", std::env::consts::ARCH);
650678

651679
match args.command {
652-
Commands::BinaryStats {
653-
local,
654-
codegen_backend,
655-
profile,
656-
rustc2,
657-
codegen_backend2,
658-
symbols,
659-
} => {
660-
let codegen_backend2 = codegen_backend2.unwrap_or(codegen_backend);
661-
let toolchain = get_local_toolchain(
662-
&[Profile::Debug, Profile::Opt],
663-
&[codegen_backend],
664-
&local.rustc,
665-
*ToolchainConfig::default()
666-
.cargo(local.cargo.as_deref())
667-
.id(local.id.as_deref()),
668-
"",
669-
target_triple.clone(),
670-
)?;
671-
let toolchain2 = rustc2
672-
.map(|rustc| {
673-
get_local_toolchain(
674-
&[Profile::Debug, Profile::Opt],
675-
&[codegen_backend2],
676-
&rustc,
677-
*ToolchainConfig::default()
678-
.cargo(local.cargo.as_deref())
679-
.id(local.id.as_deref()),
680-
"",
681-
target_triple,
682-
)
683-
})
684-
.transpose()?;
685-
let profile = match profile {
686-
Profile::Debug => CargoProfile::Debug,
687-
Profile::Opt => CargoProfile::Release,
688-
_ => return Err(anyhow::anyhow!("Only Debug and Opt profiles are supported")),
689-
};
690-
let benchmarks = get_compile_benchmarks(
691-
&compile_benchmark_dir,
692-
&local.include,
693-
&local.exclude,
694-
&local.exclude_suffix,
695-
)?;
696-
for benchmark in benchmarks {
697-
println!("Stats for benchmark `{}`", benchmark.name);
698-
println!("{}", "-".repeat(20));
699-
let artifacts =
700-
compile_and_get_stats(&benchmark.path, &toolchain, profile, codegen_backend)?;
701-
let archives2: HashMap<String, ArtifactWithStats> = toolchain2
702-
.as_ref()
703-
.map(|toolchain| {
704-
compile_and_get_stats(&benchmark.path, toolchain, profile, codegen_backend2)
705-
})
706-
.transpose()?
707-
.unwrap_or_default()
708-
.into_iter()
709-
.map(|artifact| (artifact.target_name.clone(), artifact))
710-
.collect();
711-
712-
for artifact in artifacts {
713-
let archive2 = archives2.get(&artifact.target_name);
714-
715-
println!(
716-
"Target `{}` (artifact `{}`)",
717-
artifact.target_name,
718-
artifact
719-
.path
720-
.file_name()
721-
.and_then(|s| s.to_str())
722-
.unwrap_or(&artifact.target_name)
723-
);
724-
725-
let sections = artifact.stats.sections;
726-
let sections2 = archive2.as_ref().map(|a| a.stats.sections.clone());
727-
print_binary_stats("Section", sections, sections2);
728-
729-
if symbols {
730-
let symbols = artifact.stats.symbols;
731-
let symbols2 = archive2.as_ref().map(|a| a.stats.symbols.clone());
732-
print_binary_stats("Symbol", symbols, symbols2);
733-
}
734-
println!();
680+
Commands::BinaryStats { mode, symbols } => {
681+
match mode {
682+
BinaryStatsMode::Compile(args) => {
683+
binary_stats_compile(args, symbols, &target_triple)?;
684+
}
685+
BinaryStatsMode::Local(args) => {
686+
binary_stats_local(args, symbols)?;
735687
}
736688
}
737689

@@ -1234,6 +1186,122 @@ Make sure to modify `{dir}/perf-config.json` if the category/artifact don't matc
12341186
}
12351187
}
12361188

1189+
fn binary_stats_local(args: BinaryStatsLocal, symbols: bool) -> anyhow::Result<()> {
1190+
let stats = ArtifactStats::from_path(&args.artifact)
1191+
.with_context(|| format!("Cannot load artifact from {}", args.artifact.display()))?;
1192+
let stats2 = args
1193+
.artifact2
1194+
.as_ref()
1195+
.map(|path| {
1196+
ArtifactStats::from_path(path)
1197+
.with_context(|| format!("Cannot load artifact from {}", path.display()))
1198+
})
1199+
.transpose()?;
1200+
print_binary_stats(
1201+
"Sections",
1202+
stats.sections,
1203+
stats2.as_ref().map(|s| s.sections.clone()),
1204+
);
1205+
if symbols {
1206+
print_binary_stats("Symbols", stats.symbols, stats2.map(|s| s.symbols));
1207+
}
1208+
1209+
Ok(())
1210+
}
1211+
1212+
fn binary_stats_compile(
1213+
args: BinaryStatsCompile,
1214+
symbols: bool,
1215+
target_triple: &str,
1216+
) -> anyhow::Result<()> {
1217+
let BinaryStatsCompile {
1218+
local,
1219+
profile,
1220+
codegen_backend,
1221+
rustc2,
1222+
codegen_backend2,
1223+
} = args;
1224+
1225+
let codegen_backend2 = codegen_backend2.unwrap_or(codegen_backend);
1226+
let toolchain = get_local_toolchain(
1227+
&[Profile::Debug, Profile::Opt],
1228+
&[codegen_backend],
1229+
&local.rustc,
1230+
*ToolchainConfig::default()
1231+
.cargo(local.cargo.as_deref())
1232+
.id(local.id.as_deref()),
1233+
"",
1234+
target_triple.to_string(),
1235+
)?;
1236+
let toolchain2 = rustc2
1237+
.map(|rustc| {
1238+
get_local_toolchain(
1239+
&[Profile::Debug, Profile::Opt],
1240+
&[codegen_backend2],
1241+
&rustc,
1242+
*ToolchainConfig::default()
1243+
.cargo(local.cargo.as_deref())
1244+
.id(local.id.as_deref()),
1245+
"",
1246+
target_triple.to_string(),
1247+
)
1248+
})
1249+
.transpose()?;
1250+
let profile = match profile {
1251+
Profile::Debug => CargoProfile::Debug,
1252+
Profile::Opt => CargoProfile::Release,
1253+
_ => return Err(anyhow::anyhow!("Only Debug and Opt profiles are supported")),
1254+
};
1255+
let benchmarks = get_compile_benchmarks(
1256+
&compile_benchmark_dir(),
1257+
&local.include,
1258+
&local.exclude,
1259+
&local.exclude_suffix,
1260+
)?;
1261+
for benchmark in benchmarks {
1262+
println!("Stats for benchmark `{}`", benchmark.name);
1263+
println!("{}", "-".repeat(20));
1264+
let artifacts =
1265+
compile_and_get_stats(&benchmark.path, &toolchain, profile, codegen_backend)?;
1266+
let archives2: HashMap<String, ArtifactWithStats> = toolchain2
1267+
.as_ref()
1268+
.map(|toolchain| {
1269+
compile_and_get_stats(&benchmark.path, toolchain, profile, codegen_backend2)
1270+
})
1271+
.transpose()?
1272+
.unwrap_or_default()
1273+
.into_iter()
1274+
.map(|artifact| (artifact.target_name.clone(), artifact))
1275+
.collect();
1276+
1277+
for artifact in artifacts {
1278+
let archive2 = archives2.get(&artifact.target_name);
1279+
1280+
println!(
1281+
"Target `{}` (artifact `{}`)",
1282+
artifact.target_name,
1283+
artifact
1284+
.path
1285+
.file_name()
1286+
.and_then(|s| s.to_str())
1287+
.unwrap_or(&artifact.target_name)
1288+
);
1289+
1290+
let sections = artifact.stats.sections;
1291+
let sections2 = archive2.as_ref().map(|a| a.stats.sections.clone());
1292+
print_binary_stats("Section", sections, sections2);
1293+
1294+
if symbols {
1295+
let symbols = artifact.stats.symbols;
1296+
let symbols2 = archive2.as_ref().map(|a| a.stats.symbols.clone());
1297+
print_binary_stats("Symbol", symbols, symbols2);
1298+
}
1299+
println!();
1300+
}
1301+
}
1302+
Ok(())
1303+
}
1304+
12371305
fn build_async_runtime() -> Runtime {
12381306
let mut builder = tokio::runtime::Builder::new_multi_thread();
12391307
// We want to minimize noise from the runtime

0 commit comments

Comments
 (0)