use clap::{Args, Parser, Subcommand, ValueEnum};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use crate::{plots, profile};
#[derive(Parser, Debug)]
#[command(name = "mobench", author, version, about = "Mobile Rust benchmarking orchestrator", long_about = None)]
pub(crate) struct Cli {
#[arg(long, global = true)]
pub(crate) dry_run: bool,
#[arg(long, short = 'v', global = true)]
pub(crate) verbose: bool,
#[arg(long, global = true)]
pub(crate) yes: bool,
#[arg(long, global = true)]
pub(crate) non_interactive: bool,
#[command(subcommand)]
pub(crate) command: Command,
}
#[derive(Subcommand, Debug)]
pub(crate) enum Command {
Run {
#[arg(long, value_enum)]
target: Option<MobileTarget>,
#[arg(long, help = "Fully-qualified Rust function to benchmark")]
function: Option<String>,
#[arg(
long,
help = "Project root containing mobench.toml or the Cargo workspace"
)]
project_root: Option<PathBuf>,
#[arg(
long,
help = "Path to the benchmark crate directory containing Cargo.toml"
)]
crate_path: Option<PathBuf>,
#[arg(long)]
iterations: Option<u32>,
#[arg(long)]
warmup: Option<u32>,
#[arg(long, help = "Device identifiers or labels (BrowserStack devices)")]
devices: Vec<String>,
#[arg(long, help = "Device matrix YAML file to load device names from")]
device_matrix: Option<PathBuf>,
#[arg(
long,
value_delimiter = ',',
help = "Device tags to select from the device matrix (comma-separated or repeatable)"
)]
device_tags: Vec<String>,
#[arg(long, help = "Optional path to config file")]
config: Option<PathBuf>,
#[arg(long, help = "Optional output path for JSON report")]
output: Option<PathBuf>,
#[arg(long, help = "Write CSV summary alongside JSON")]
summary_csv: bool,
#[arg(
long,
help = "Enable CI mode (job summary, optional JUnit, regression exit codes)"
)]
ci: bool,
#[arg(long, help = "Baseline summary source (path|url|artifact:<path>)")]
baseline: Option<String>,
#[arg(
long,
default_value_t = 5.0,
help = "Regression threshold percentage when comparing to baseline"
)]
regression_threshold_pct: f64,
#[arg(long, help = "Write JUnit XML report to the given path")]
junit: Option<PathBuf>,
#[arg(long, help = "Skip mobile builds and only run the host harness")]
local_only: bool,
#[arg(
long,
help = "Build in release mode (recommended for BrowserStack to reduce APK size and upload time)"
)]
release: bool,
#[arg(
long,
help = "Path to iOS app bundle (.ipa or zipped .app) for BrowserStack XCUITest"
)]
ios_app: Option<PathBuf>,
#[arg(long, help = "Path to iOS XCUITest test suite package (.zip or .ipa)")]
ios_test_suite: Option<PathBuf>,
#[arg(
long,
hide = true,
help = "Deprecated compatibility flag for generated XCUITest harness timeout"
)]
ios_completion_timeout_secs: Option<u64>,
#[arg(long, help = "Fetch BrowserStack artifacts after the run completes")]
fetch: bool,
#[arg(long, default_value = "target/browserstack")]
fetch_output_dir: PathBuf,
#[arg(long, default_value_t = 5)]
fetch_poll_interval_secs: u64,
#[arg(long, default_value_t = 300)]
fetch_timeout_secs: u64,
#[arg(long, help = "Show simplified step-by-step progress output")]
progress: bool,
},
Init {
#[arg(long, default_value = "bench-config.toml")]
output: PathBuf,
#[arg(long, value_enum, default_value_t = MobileTarget::Android)]
target: MobileTarget,
},
Plan {
#[arg(long, default_value = "device-matrix.yaml")]
output: PathBuf,
},
Config {
#[command(subcommand)]
command: ConfigCommand,
},
Doctor {
#[arg(long, value_enum, default_value_t = SdkTarget::Both)]
target: SdkTarget,
#[arg(long, help = "Optional path to run config file to validate")]
config: Option<PathBuf>,
#[arg(long, help = "Optional path to device matrix YAML file to validate")]
device_matrix: Option<PathBuf>,
#[arg(
long,
value_delimiter = ',',
help = "Device tags to select from the device matrix (comma-separated or repeatable)"
)]
device_tags: Vec<String>,
#[arg(
long,
default_value_t = true,
action = clap::ArgAction::Set,
num_args = 0..=1,
default_missing_value = "true",
help = "Validate BrowserStack credentials"
)]
browserstack: bool,
#[arg(long, value_enum, default_value_t = CheckOutputFormat::Text)]
format: CheckOutputFormat,
},
Ci {
#[command(subcommand)]
command: CiCommand,
},
Fetch {
#[arg(long, value_enum)]
target: MobileTarget,
#[arg(long)]
build_id: String,
#[arg(long, default_value = "target/browserstack")]
output_dir: PathBuf,
#[arg(long, default_value_t = true)]
wait: bool,
#[arg(long, default_value_t = 10)]
poll_interval_secs: u64,
#[arg(long, default_value_t = 1800)]
timeout_secs: u64,
},
Compare {
#[arg(long, help = "Baseline JSON summary to compare against")]
baseline: PathBuf,
#[arg(long, help = "Candidate JSON summary to compare")]
candidate: PathBuf,
#[arg(long, help = "Optional output path for markdown report")]
output: Option<PathBuf>,
},
InitSdk {
#[arg(long, value_enum)]
target: SdkTarget,
#[arg(long, default_value = "bench-project")]
project_name: String,
#[arg(long, default_value = ".")]
output_dir: PathBuf,
#[arg(long, help = "Generate example benchmarks")]
examples: bool,
},
Build {
#[arg(long, value_enum)]
target: SdkTarget,
#[arg(long, help = "Build in release mode")]
release: bool,
#[arg(
long,
hide = true,
help = "Deprecated compatibility flag for generated XCUITest harness timeout"
)]
ios_completion_timeout_secs: Option<u64>,
#[arg(
long,
help = "Project root containing mobench.toml or the Cargo workspace"
)]
project_root: Option<PathBuf>,
#[arg(
long,
help = "Output directory for mobile artifacts (default: target/mobench)"
)]
output_dir: Option<PathBuf>,
#[arg(
long,
help = "Path to the benchmark crate (default: auto-detect bench-mobile/ or crates/{crate})"
)]
crate_path: Option<PathBuf>,
#[arg(long, help = "Show simplified step-by-step progress output")]
progress: bool,
},
PackageIpa {
#[arg(long, default_value = "BenchRunner", help = "Xcode scheme to build")]
scheme: String,
#[arg(long, value_enum, default_value = "adhoc", help = "Signing method")]
method: IosSigningMethodArg,
#[arg(
long,
help = "Project root containing mobench.toml or the Cargo workspace"
)]
project_root: Option<PathBuf>,
#[arg(
long,
help = "Path to the benchmark crate directory containing Cargo.toml"
)]
crate_path: Option<PathBuf>,
#[arg(
long,
help = "Output directory for mobile artifacts (default: target/mobench)"
)]
output_dir: Option<PathBuf>,
},
PackageXcuitest {
#[arg(long, default_value = "BenchRunner", help = "Xcode scheme to build")]
scheme: String,
#[arg(
long,
help = "Project root containing mobench.toml or the Cargo workspace"
)]
project_root: Option<PathBuf>,
#[arg(
long,
help = "Path to the benchmark crate directory containing Cargo.toml"
)]
crate_path: Option<PathBuf>,
#[arg(
long,
help = "Output directory for mobile artifacts (default: target/mobench)"
)]
output_dir: Option<PathBuf>,
},
List {
#[arg(
long,
help = "Project root containing mobench.toml or the Cargo workspace"
)]
project_root: Option<PathBuf>,
#[arg(
long,
help = "Path to the benchmark crate directory containing Cargo.toml"
)]
crate_path: Option<PathBuf>,
},
Verify {
#[arg(
long,
help = "Project root containing mobench.toml or the Cargo workspace"
)]
project_root: Option<PathBuf>,
#[arg(
long,
help = "Path to the benchmark crate directory containing Cargo.toml"
)]
crate_path: Option<PathBuf>,
#[arg(long, value_enum, help = "Target platform to verify artifacts for")]
target: Option<SdkTarget>,
#[arg(long, help = "Path to bench_spec.json to validate")]
spec_path: Option<PathBuf>,
#[arg(long, help = "Check that build artifacts exist")]
check_artifacts: bool,
#[arg(long, help = "Run a local smoke test with minimal iterations")]
smoke_test: bool,
#[arg(long, help = "Function name to verify/smoke test")]
function: Option<String>,
#[arg(
long,
help = "Output directory for mobile artifacts (default: target/mobench)"
)]
output_dir: Option<PathBuf>,
},
Summary {
#[arg(help = "Path to the benchmark report JSON file")]
report: PathBuf,
#[arg(long, help = "Output format: text (default), json, or csv")]
format: Option<SummaryFormat>,
},
Devices {
#[command(subcommand)]
command: Option<DevicesCommand>,
#[arg(long, value_enum, help = "Filter by platform (android or ios)")]
platform: Option<DevicePlatform>,
#[arg(long, help = "Output as JSON")]
json: bool,
#[arg(long, help = "Validate device specs against available devices")]
validate: Vec<String>,
},
Fixture {
#[command(subcommand)]
command: FixtureCommand,
},
Report {
#[command(subcommand)]
command: ReportCommand,
},
Profile {
#[command(subcommand)]
command: ProfileCommand,
},
Check {
#[arg(long, short, value_enum)]
target: SdkTarget,
#[arg(long, default_value = "text")]
format: CheckOutputFormat,
},
}
#[derive(Subcommand, Debug)]
#[allow(clippy::large_enum_variant)]
pub(crate) enum CiCommand {
Init {
#[arg(
long,
default_value = ".github/workflows/mobile-bench.yml",
help = "Path to write the workflow file"
)]
workflow: PathBuf,
#[arg(
long,
default_value = ".github/actions/mobench",
help = "Directory to write the local GitHub Action"
)]
action_dir: PathBuf,
},
Run(CiRunArgs),
Summarize(CiSummarizeArgs),
CheckRun(CiCheckRunArgs),
}
#[derive(Subcommand, Debug)]
pub(crate) enum DevicesCommand {
Resolve {
#[arg(long, value_enum)]
platform: DevicePlatform,
#[arg(long, help = "Device profile/tag to resolve (defaults to `default`)")]
profile: Option<String>,
#[arg(
long,
help = "Path to run config file (optional source for matrix/tags)"
)]
config: Option<PathBuf>,
#[arg(long, help = "Path to device matrix YAML file")]
device_matrix: Option<PathBuf>,
#[arg(long, value_enum, default_value_t = CheckOutputFormat::Text)]
format: CheckOutputFormat,
},
}
#[derive(Subcommand, Debug)]
pub(crate) enum ConfigCommand {
Validate {
#[arg(long, default_value = "bench-config.toml")]
config: PathBuf,
#[arg(long, value_enum, default_value_t = CheckOutputFormat::Text)]
format: CheckOutputFormat,
},
}
#[derive(Subcommand, Debug)]
pub(crate) enum FixtureCommand {
Init {
#[arg(long, default_value = "bench-config.toml")]
config: PathBuf,
#[arg(long, default_value = "device-matrix.yaml")]
device_matrix: PathBuf,
#[arg(long, help = "Overwrite existing fixture files")]
force: bool,
},
Build {
#[arg(long, value_enum, default_value_t = SdkTarget::Both)]
target: SdkTarget,
#[arg(long, help = "Build in release mode")]
release: bool,
#[arg(long, help = "Output directory for mobile artifacts")]
output_dir: Option<PathBuf>,
#[arg(long, help = "Path to benchmark crate")]
crate_path: Option<PathBuf>,
#[arg(long, help = "Show simplified step-by-step progress output")]
progress: bool,
},
Verify {
#[arg(long, default_value = "bench-config.toml")]
config: PathBuf,
#[arg(long)]
device_matrix: Option<PathBuf>,
#[arg(long, value_enum, default_value_t = SdkTarget::Both)]
target: SdkTarget,
#[arg(long, help = "Device profile/tag to verify")]
profile: Option<String>,
#[arg(long, value_enum, default_value_t = CheckOutputFormat::Text)]
format: CheckOutputFormat,
},
CacheKey {
#[arg(long, default_value = "bench-config.toml")]
config: PathBuf,
#[arg(long)]
device_matrix: Option<PathBuf>,
#[arg(long, value_enum, default_value_t = SdkTarget::Both)]
target: SdkTarget,
#[arg(long, help = "Device profile/tag for keying")]
profile: Option<String>,
#[arg(long, value_enum, default_value_t = CheckOutputFormat::Text)]
format: CheckOutputFormat,
},
}
#[derive(Subcommand, Debug)]
pub(crate) enum ReportCommand {
Summarize {
#[arg(long, default_value = "target/mobench/ci/summary.json")]
summary: PathBuf,
#[arg(long, help = "Write markdown output to file")]
output: Option<PathBuf>,
#[arg(long, value_enum, default_value_t = plots::PlotMode::Auto)]
plots: plots::PlotMode,
},
Github {
#[arg(
long,
help = "Pull request number (auto-detected from GITHUB_REF if omitted)"
)]
pr: Option<String>,
#[arg(long, default_value = "target/mobench/ci/summary.json")]
summary: PathBuf,
#[arg(long, default_value = "<!-- mobench-report -->")]
marker: String,
#[arg(long, help = "Publish via GitHub API using GITHUB_TOKEN")]
publish: bool,
#[arg(long, help = "Write generated comment body to file")]
output: Option<PathBuf>,
},
}
#[derive(Subcommand, Debug)]
pub(crate) enum ProfileCommand {
#[command(
about = "Plan or execute a native profiling session; local android-native and ios-instruments now attempt real native capture"
)]
Run(profile::ProfileRunArgs),
Diff(profile::ProfileDiffArgs),
Summarize(profile::ProfileSummarizeArgs),
}
#[derive(Args, Debug, Clone)]
pub(crate) struct CiRunArgs {
#[arg(long, value_enum)]
pub(crate) target: CiTarget,
#[arg(
long,
help = "Path to the benchmark crate directory containing Cargo.toml"
)]
pub(crate) crate_path: Option<PathBuf>,
#[arg(
long,
help = "Fully-qualified Rust function to benchmark (single function)"
)]
pub(crate) function: Option<String>,
#[arg(
long,
value_delimiter = ',',
help = "Multiple benchmark functions (comma-separated or JSON array). Runs each in sequence."
)]
pub(crate) functions: Vec<String>,
#[arg(long, default_value_t = 100)]
pub(crate) iterations: u32,
#[arg(long, default_value_t = 10)]
pub(crate) warmup: u32,
#[arg(long, help = "Device identifiers or labels (BrowserStack devices)")]
pub(crate) devices: Vec<String>,
#[arg(long, help = "Device matrix YAML file to load device names from")]
pub(crate) device_matrix: Option<PathBuf>,
#[arg(
long,
value_delimiter = ',',
help = "Device tags to select from the device matrix (comma-separated or repeatable)"
)]
pub(crate) device_tags: Vec<String>,
#[arg(long, help = "Optional path to config file")]
pub(crate) config: Option<PathBuf>,
#[arg(long, help = "Baseline summary source (path|url|artifact:<path>)")]
pub(crate) baseline: Option<String>,
#[arg(
long,
default_value_t = 5.0,
help = "Regression threshold percentage when comparing to baseline"
)]
pub(crate) regression_threshold_pct: f64,
#[arg(long, help = "Write JUnit XML report to the given path")]
pub(crate) junit: Option<PathBuf>,
#[arg(long, help = "Skip mobile builds and only run the host harness")]
pub(crate) local_only: bool,
#[arg(
long,
help = "Build in release mode (recommended for BrowserStack to reduce APK size and upload time)"
)]
pub(crate) release: bool,
#[arg(
long,
help = "Path to iOS app bundle (.ipa or zipped .app) for BrowserStack XCUITest"
)]
pub(crate) ios_app: Option<PathBuf>,
#[arg(long, help = "Path to iOS XCUITest test suite package (.zip or .ipa)")]
pub(crate) ios_test_suite: Option<PathBuf>,
#[arg(
long,
hide = true,
help = "Deprecated compatibility flag for generated XCUITest harness timeout"
)]
pub(crate) ios_completion_timeout_secs: Option<u64>,
#[arg(long, help = "Fetch BrowserStack artifacts after the run completes")]
pub(crate) fetch: bool,
#[arg(long, default_value = "target/browserstack")]
pub(crate) fetch_output_dir: PathBuf,
#[arg(long, default_value_t = 5)]
pub(crate) fetch_poll_interval_secs: u64,
#[arg(long, default_value_t = 300)]
pub(crate) fetch_timeout_secs: u64,
#[arg(long, help = "Show simplified step-by-step progress output")]
pub(crate) progress: bool,
#[arg(
long,
default_value = "target/mobench/ci",
help = "Output directory for CI contract files"
)]
pub(crate) output_dir: PathBuf,
#[arg(long, help = "Metadata: user or actor that requested the run")]
pub(crate) requested_by: Option<String>,
#[arg(long, help = "Metadata: pull request number")]
pub(crate) pr_number: Option<String>,
#[arg(long, help = "Metadata: original command requested by the caller")]
pub(crate) request_command: Option<String>,
#[arg(long, help = "Metadata: git ref/sha for this mobench invocation")]
pub(crate) mobench_ref: Option<String>,
#[arg(long, value_enum, default_value_t = plots::PlotMode::Auto)]
pub(crate) plots: plots::PlotMode,
}
#[derive(Args, Debug, Clone)]
pub(crate) struct CiSummarizeArgs {
#[arg(long)]
pub(crate) build_id: Option<String>,
#[arg(long)]
pub(crate) results_dir: Option<PathBuf>,
#[arg(long, value_enum, default_value_t = SummarizeFormat::Table)]
pub(crate) output_format: SummarizeFormat,
#[arg(long)]
pub(crate) output_file: Option<PathBuf>,
#[arg(long, value_enum)]
pub(crate) platform: Option<MobileTarget>,
}
#[derive(Args, Debug, Clone)]
pub(crate) struct CiCheckRunArgs {
#[arg(long, required_unless_present = "results_dir")]
pub(crate) results: Option<PathBuf>,
#[arg(long, required_unless_present = "results")]
pub(crate) results_dir: Option<PathBuf>,
#[arg(long)]
pub(crate) repo: String,
#[arg(long)]
pub(crate) sha: String,
#[arg(long, env = "GITHUB_TOKEN", hide = true)]
pub(crate) token: String,
#[arg(long, default_value = "Mobench")]
pub(crate) name: String,
#[arg(long)]
pub(crate) baseline: Option<PathBuf>,
#[arg(long, default_value_t = 5.0)]
pub(crate) regression_threshold_pct: f64,
#[arg(long, default_value = "src/lib.rs")]
pub(crate) annotation_path: String,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub(crate) enum SummarizeFormat {
Table,
Markdown,
Json,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub(crate) enum CiTarget {
Android,
Ios,
Both,
}
impl CiTarget {
pub(crate) fn targets(self) -> &'static [MobileTarget] {
match self {
CiTarget::Android => &[MobileTarget::Android],
CiTarget::Ios => &[MobileTarget::Ios],
CiTarget::Both => &[MobileTarget::Android, MobileTarget::Ios],
}
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)]
#[clap(rename_all = "lowercase")]
pub(crate) enum DevicePlatform {
Android,
Ios,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)]
#[clap(rename_all = "lowercase")]
pub(crate) enum SummaryFormat {
Text,
Json,
Csv,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)]
#[clap(rename_all = "lowercase")]
pub(crate) enum CheckOutputFormat {
Text,
Json,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum ContractErrorCategory {
Config,
Preflight,
Provider,
Build,
Benchmark,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum MobileTarget {
Android,
Ios,
}
impl MobileTarget {
pub(crate) fn as_str(self) -> &'static str {
match self {
Self::Android => "android",
Self::Ios => "ios",
}
}
pub(crate) fn display_name(self) -> &'static str {
match self {
Self::Android => "Android",
Self::Ios => "iOS",
}
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)]
#[clap(rename_all = "lowercase")]
pub(crate) enum SdkTarget {
Android,
Ios,
Both,
}
impl From<SdkTarget> for mobench_sdk::Target {
fn from(target: SdkTarget) -> Self {
match target {
SdkTarget::Android => mobench_sdk::Target::Android,
SdkTarget::Ios => mobench_sdk::Target::Ios,
SdkTarget::Both => mobench_sdk::Target::Both,
}
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)]
#[clap(rename_all = "lowercase")]
pub(crate) enum IosSigningMethodArg {
Adhoc,
Development,
}
impl From<IosSigningMethodArg> for mobench_sdk::builders::SigningMethod {
fn from(arg: IosSigningMethodArg) -> Self {
match arg {
IosSigningMethodArg::Adhoc => mobench_sdk::builders::SigningMethod::AdHoc,
IosSigningMethodArg::Development => mobench_sdk::builders::SigningMethod::Development,
}
}
}