├── .gitignore ├── rustfmt.toml ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── src ├── main.rs ├── app.rs ├── opt.rs └── instruments.rs ├── Cargo.toml ├── .travis.yml ├── LICENSE ├── README.md └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | todo.md 4 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | use_small_heuristics = "Max" 2 | max_width = 100 3 | use_field_init_shorthand = true 4 | newline_style = "Unix" 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/en/code-security/dependabot/working-with-dependabot/dependabot-options-reference#package-ecosystem- 2 | version: 2 3 | updates: 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "monthly" 8 | - package-ecosystem: "cargo" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod app; 2 | mod instruments; 3 | mod opt; 4 | 5 | #[cfg(not(target_os = "macos"))] 6 | compile_error!("cargo-instruments requires macOS."); 7 | 8 | fn main() { 9 | env_logger::init(); 10 | use structopt::StructOpt; 11 | let opt::Cli::Instruments(args) = opt::Cli::from_args(); 12 | 13 | if let Err(e) = app::run(args) { 14 | eprintln!("{}", e); 15 | std::process::exit(1); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cargo-instruments" 3 | version = "0.4.13" 4 | authors = ["Colin Rofls "] 5 | license = "MIT" 6 | description = "Profile binary targets on macOS using Xcode Instruments." 7 | repository = "https://github.com/cmyr/cargo-instruments" 8 | documentation = "https://github.com/cmyr/cargo-instruments" 9 | categories = ["development-tools::cargo-plugins", "development-tools::profiling"] 10 | keywords = ["profiling", "xcode", "cargo-subcommand", "trace", "macos"] 11 | readme = "README.md" 12 | edition = "2021" 13 | 14 | [features] 15 | vendored-openssl = ["cargo/vendored-openssl"] 16 | 17 | [dependencies] 18 | anyhow = "1.0" 19 | cargo = "0.91" 20 | chrono = "0.4.6" 21 | structopt = { version = "^0.3", default-features = false } 22 | semver = "1.0" 23 | env_logger = "0.11.0" 24 | log = "0.4.20" 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | addons: 3 | apt: 4 | packages: 5 | - libssl-dev 6 | 7 | cache: cargo 8 | 9 | rust: 10 | - stable 11 | - nightly 12 | 13 | os: 14 | - osx 15 | 16 | matrix: 17 | allow_failures: 18 | - rust: nightly 19 | fast_finish: true 20 | 21 | script: 22 | - cargo test --verbose 23 | - if rustup component add rustfmt ; then cargo fmt --all -- --check ; fi 24 | - if rustup component add clippy ; then cargo clippy --all-targets -- -D warnings ; else echo "no clippy"; fi 25 | 26 | 27 | # TODO: set this up to auto-push to crates on a new tag 28 | # jobs: 29 | # include: 30 | # - stage: deploy 31 | # script: skip 32 | # rust: stable 33 | # deploy: 34 | # provider: cargo 35 | # on: 36 | # tags: true 37 | # token: 38 | # secure: $TOKEN 39 | # 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 Colin Rofls 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | pull_request: 6 | 7 | jobs: 8 | rustfmt: 9 | runs-on: macOS-latest 10 | name: rustfmt 11 | steps: 12 | - uses: actions/checkout@v5 13 | 14 | - name: install stable toolchain 15 | uses: actions-rs/toolchain@v1 16 | with: 17 | toolchain: stable 18 | profile: minimal 19 | components: rustfmt 20 | override: true 21 | 22 | - name: install rustfmt 23 | run: rustup component add rustfmt 24 | 25 | - name: cargo fmt 26 | uses: actions-rs/cargo@v1 27 | with: 28 | command: fmt 29 | args: --all -- --check 30 | 31 | test-stable: 32 | runs-on: macOS-latest 33 | name: cargo test stable 34 | steps: 35 | - uses: actions/checkout@v5 36 | 37 | - name: install cairo 38 | run: brew install cairo 39 | if: contains(matrix.os, 'mac') 40 | 41 | - name: install stable toolchain 42 | uses: actions-rs/toolchain@v1 43 | with: 44 | toolchain: stable 45 | components: clippy 46 | profile: minimal 47 | override: true 48 | 49 | - name: cargo clippy 50 | uses: actions-rs/cargo@v1 51 | with: 52 | command: clippy 53 | args: -- -D warnings 54 | 55 | - name: cargo test 56 | uses: actions-rs/cargo@v1 57 | with: 58 | command: test 59 | -------------------------------------------------------------------------------- /src/app.rs: -------------------------------------------------------------------------------- 1 | //! The main application logic. 2 | 3 | use std::path::{Path, PathBuf}; 4 | use std::process::Command; 5 | 6 | use anyhow::{anyhow, Result}; 7 | use cargo::GlobalContext; 8 | use cargo::{ 9 | core::Workspace, 10 | ops::CompileOptions, 11 | util::{important_paths, interning::InternedString}, 12 | }; 13 | 14 | use crate::instruments; 15 | use crate::opt::{AppConfig, CargoOpts, Target}; 16 | 17 | /// Main entrance point, after args have been parsed. 18 | pub(crate) fn run(app_config: AppConfig) -> Result<()> { 19 | // 1. Detect the type of Xcode Instruments installation 20 | let xctrace_tool = instruments::XcodeInstruments::detect()?; 21 | log::debug!("using {xctrace_tool}"); 22 | 23 | // 2. Render available templates if the user asked 24 | if app_config.list_templates { 25 | let catalog = xctrace_tool.available_templates()?; 26 | println!("{}", instruments::render_template_catalog(&catalog)); 27 | return Ok(()); 28 | } 29 | 30 | // 3. Build the specified target 31 | let cargo_config = GlobalContext::default()?; 32 | 33 | let manifest_path = match app_config.manifest_path.as_ref() { 34 | Some(path) if path.is_absolute() => Ok(path.to_owned()), 35 | Some(path) => Ok(cargo_config.cwd().join(path)), 36 | None => important_paths::find_root_manifest_for_wd(cargo_config.cwd()), 37 | }?; 38 | 39 | log::debug!("using cargo manifest at {}", manifest_path.display()); 40 | 41 | let workspace = Workspace::new(&manifest_path, &cargo_config)?; 42 | 43 | // 3.1: warn if --open passed. We do this here so we have access to cargo's 44 | // pretty-printer 45 | if app_config.open { 46 | workspace 47 | .gctx() 48 | .shell() 49 | .warn("--open is now the default behaviour, and will be ignored.")?; 50 | } 51 | 52 | let cargo_options = app_config.to_cargo_opts()?; 53 | 54 | log::debug!("building profile target {}", cargo_options.target); 55 | let target_filepath = match build_target(&cargo_options, &workspace) { 56 | Ok(path) => path, 57 | Err(e) => { 58 | workspace.gctx().shell().error(&e)?; 59 | return Err(e); 60 | } 61 | }; 62 | 63 | log::debug!("running against target {}", target_filepath.display()); 64 | 65 | #[cfg(target_arch = "aarch64")] 66 | codesign(&target_filepath, &workspace)?; 67 | 68 | // 4. Profile the built target, will display menu if no template was selected 69 | let trace_filepath = 70 | match instruments::profile_target(&target_filepath, &xctrace_tool, &app_config, &workspace) 71 | { 72 | Ok(path) => path, 73 | Err(e) => { 74 | workspace.gctx().shell().error(&e)?; 75 | return Ok(()); 76 | } 77 | }; 78 | 79 | // 5. Print the trace file's relative path 80 | { 81 | let trace_shortpath = trace_filepath 82 | .strip_prefix(workspace.root().as_os_str()) 83 | .unwrap_or(trace_filepath.as_path()) 84 | .to_string_lossy(); 85 | workspace.gctx().shell().status("Trace file", trace_shortpath)?; 86 | } 87 | 88 | // 6. Open Xcode Instruments if asked 89 | if !app_config.no_open { 90 | launch_instruments(&trace_filepath)?; 91 | } 92 | 93 | Ok(()) 94 | } 95 | 96 | /// On M1 we need to resign with the specified entitlement. 97 | /// 98 | /// See https://github.com/cmyr/cargo-instruments/issues/40#issuecomment-894287229 99 | /// for more information. 100 | #[cfg(target_arch = "aarch64")] 101 | fn codesign(path: &Path, workspace: &Workspace) -> Result<()> { 102 | use std::fmt::Write; 103 | 104 | static ENTITLEMENTS_FILENAME: &str = "entitlements.plist"; 105 | static ENTITLEMENTS_PLIST_DATA: &str = r#" 106 | 107 | 108 | 109 | com.apple.security.get-task-allow 110 | 111 | 112 | "#; 113 | 114 | let target_dir = path.parent().ok_or_else(|| anyhow!("failed to get target directory"))?; 115 | let entitlement_path = target_dir.join(ENTITLEMENTS_FILENAME); 116 | std::fs::write(&entitlement_path, ENTITLEMENTS_PLIST_DATA.as_bytes())?; 117 | 118 | let output = Command::new("codesign") 119 | .args(["-s", "-", "-f", "--entitlements"]) 120 | .args([&entitlement_path, path]) 121 | .output()?; 122 | if !output.status.success() { 123 | let mut msg = String::new(); 124 | if !output.stdout.is_empty() { 125 | msg = format!("stdout: \"{}\"", String::from_utf8_lossy(&output.stdout)); 126 | } 127 | if !output.stderr.is_empty() { 128 | if !msg.is_empty() { 129 | msg.push('\n'); 130 | } 131 | write!(&mut msg, "stderr: \"{}\"", String::from_utf8_lossy(&output.stderr))?; 132 | } 133 | 134 | workspace.gctx().shell().error("Code signing failed")?; 135 | } 136 | Ok(()) 137 | } 138 | 139 | /// Attempts to validate and build the specified target. On success, returns 140 | /// the path to the built executable. 141 | fn build_target(cargo_options: &CargoOpts, workspace: &Workspace) -> Result { 142 | use cargo::core::shell::Verbosity; 143 | workspace.gctx().shell().set_verbosity(Verbosity::Normal); 144 | 145 | let compile_options = make_compile_opts(cargo_options, workspace.gctx())?; 146 | let result = cargo::ops::compile(workspace, &compile_options)?; 147 | 148 | if let Target::Bench(ref bench) = cargo_options.target { 149 | result 150 | .tests 151 | .iter() 152 | .find(|unit_output| unit_output.unit.target.name() == bench) 153 | .map(|unit_output| unit_output.path.clone()) 154 | .ok_or_else(|| anyhow!("no benchmark '{}'", bench)) 155 | } else { 156 | match result.binaries.as_slice() { 157 | [unit_output] => Ok(unit_output.path.clone()), 158 | [] => Err(anyhow!("no targets found")), 159 | other => Err(anyhow!( 160 | "found multiple targets: {:?}", 161 | other 162 | .iter() 163 | .map(|unit_output| unit_output.unit.target.name()) 164 | .collect::>() 165 | )), 166 | } 167 | } 168 | } 169 | 170 | /// Generate `CompileOptions` for Cargo. 171 | /// 172 | /// This additionally filters options based on user args, so that Cargo 173 | /// builds as little as possible. 174 | fn make_compile_opts(cargo_options: &CargoOpts, cfg: &GlobalContext) -> Result { 175 | use cargo::core::compiler::UserIntent; 176 | use cargo::ops::CompileFilter; 177 | 178 | let mut compile_options = CompileOptions::new(cfg, UserIntent::Build)?; 179 | let profile = &cargo_options.profile; 180 | 181 | compile_options.build_config.requested_profile = InternedString::new(profile); 182 | compile_options.cli_features = cargo_options.features.clone(); 183 | compile_options.spec = cargo_options.package.clone().into(); 184 | 185 | if cargo_options.target != Target::Default { 186 | let (bins, examples, benches) = match &cargo_options.target { 187 | Target::Bin(bin) => (vec![bin.clone()], vec![], vec![]), 188 | Target::Example(bin) => (vec![], vec![bin.clone()], vec![]), 189 | Target::Bench(bin) => (vec![], vec![], vec![bin.clone()]), 190 | _ => unreachable!(), 191 | }; 192 | 193 | compile_options.filter = CompileFilter::from_raw_arguments( 194 | false, 195 | bins, 196 | false, 197 | Vec::new(), 198 | false, 199 | examples, 200 | false, 201 | benches, 202 | false, 203 | false, 204 | ); 205 | } 206 | Ok(compile_options) 207 | } 208 | 209 | /// Launch Xcode Instruments on the provided trace file. 210 | fn launch_instruments(trace_filepath: &Path) -> Result<()> { 211 | let status = Command::new("open").arg(trace_filepath).status()?; 212 | 213 | if !status.success() { 214 | return Err(anyhow!("open failed")); 215 | } 216 | Ok(()) 217 | } 218 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cargo-instruments 2 | 3 | Easily profile your rust crate with Xcode [Instruments]. 4 | 5 | `cargo-instruments` is the glue between Cargo and Xcode's bundled profiling 6 | suite. It allows you to easily profile any binary in your crate, generating 7 | files that can be viewed in the Instruments app. 8 | 9 | ![Instruments Time Profiler](https://raw.githubusercontent.com/cmyr/cargo-instruments/screenshots/instruments_time1.png) 10 | ![Instruments System Trace](https://raw.githubusercontent.com/cmyr/cargo-instruments/screenshots/instruments_sys1.png) 11 | 12 | ## Pre-requisites 13 | 14 | ### Xcode Instruments 15 | 16 | This crate only works on macOS because it uses [Instruments] for profiling 17 | and creating the trace file. The benefit is that Instruments provides great 18 | templates and UI to explore the Profiling Trace. 19 | 20 | To install Xcode Instruments, simply install the Command Line Tools: 21 | 22 | ```sh 23 | $ xcode-select --install 24 | ``` 25 | 26 | ### Compatibility 27 | 28 | This crate works on macOS 10.13+. In practice, it transparently detects and 29 | uses the appropriate Xcode Instruments version based on your macOS version: 30 | either `/usr/bin/instruments` on older macOS, or starting with macOS 10.15, the 31 | new `xcrun xctrace`. 32 | 33 | ## Installation 34 | 35 | ### brew 36 | 37 | The simplest way to install is via Homebrew: 38 | 39 | ```sh 40 | $ brew install cargo-instruments 41 | ``` 42 | 43 | Alternatively, you can install from source. 44 | 45 | ### Building from Source 46 | 47 | First, ensure that you are running macOS, with Cargo, Xcode, and the Xcode 48 | Command Line Tools installed. 49 | 50 | If OpenSSL is installed (e.g., via `brew`), then install with 51 | 52 | ```sh 53 | $ cargo install cargo-instruments 54 | ``` 55 | 56 | If OpenSSL is not installed or if `cargo install` fails with an error message starting with "Could not find directory of OpenSSL installation, and this `-sys` crate cannot proceed without this knowledge," then install with 57 | 58 | ```sh 59 | $ cargo install --features vendored-openssl cargo-instruments 60 | ``` 61 | 62 | #### Building from Source on nix 63 | 64 | If you're using [nix](https://nixos.org/guides/install-nix.html), this command should provide all dependencies and build `cargo-instruments` from source: 65 | 66 | ```sh 67 | $ nix-shell --command 'cargo install cargo-instruments' --pure -p \ 68 | darwin.apple_sdk.frameworks.SystemConfiguration \ 69 | darwin.apple_sdk.frameworks.CoreServices \ 70 | rustc cargo sccache libgit2 pkg-config libiconv \ 71 | llvmPackages_13.libclang openssl 72 | ``` 73 | 74 | ## Usage 75 | 76 | ### Basic 77 | 78 | `cargo-instruments` requires a binary target to run. By default, it will try to 79 | build the current crate's `main.rs`. You can specify an alternative binary by 80 | using the `--bin` or `--example` flags, or a benchmark target with the `--bench` 81 | flag. 82 | 83 | Assuming your crate has one binary target named `mybin`, and you want to profile 84 | using the `Allocations` Instruments template: 85 | 86 | _Generate a new trace file_ (by default saved in `target/instruments`) 87 | 88 | ```sh 89 | $ cargo instruments -t Allocations 90 | ``` 91 | 92 | _Open the trace file in Instruments.app manually_ 93 | 94 | By default the trace file will immediately be opened with `Instruments.app`. If you do not want this behavior use the `--no-open` flag. 95 | 96 | ```sh 97 | $ open target/instruments/mybin_Allocations_2021-05-09T12_34_56.trace 98 | ``` 99 | 100 | If there are multiple packages, you can specify the package to profile with 101 | the `--package` flag. 102 | 103 | For example, you use Cargo's workspace to manage multiple packages. To profile 104 | the bin `bar` of the package `foo`: 105 | 106 | ```sh 107 | $ cargo instruments --package foo --template alloc --bin bar 108 | ``` 109 | 110 | In many cases, a package only has one binary. In this case `--package` behaves the 111 | same as `--bin`. 112 | 113 | ### Profiling application in release mode 114 | 115 | When profiling the application in release mode the compiler doesn't provide 116 | debugging symbols in the default configuration. 117 | 118 | To let the compiler generate the debugging symbols even in release mode you 119 | can append the following section in your `Cargo.toml`. 120 | 121 | ```toml 122 | [profile.release] 123 | debug = true 124 | ``` 125 | 126 | ### All options 127 | 128 | As usual, thanks to Clap, running `cargo instruments -h` prints the compact help. 129 | 130 | ``` 131 | cargo-instruments 0.4.8 132 | Profile a binary with Xcode Instruments. 133 | 134 | By default, cargo-instruments will build your main binary. 135 | 136 | USAGE: 137 | cargo instruments [FLAGS] [OPTIONS] --template