1use crate::types::{BenchError, InitConfig, Target};
7use std::fs;
8use std::path::{Path, PathBuf};
9
10use include_dir::{Dir, DirEntry, include_dir};
11
12const ANDROID_TEMPLATES: Dir = include_dir!("$CARGO_MANIFEST_DIR/templates/android");
13const IOS_TEMPLATES: Dir = include_dir!("$CARGO_MANIFEST_DIR/templates/ios");
14
15#[derive(Debug, Clone)]
17pub struct TemplateVar {
18 pub name: &'static str,
19 pub value: String,
20}
21
22pub fn generate_project(config: &InitConfig) -> Result<PathBuf, BenchError> {
39 let output_dir = &config.output_dir;
40 let project_slug = sanitize_package_name(&config.project_name);
41 let project_pascal = to_pascal_case(&project_slug);
42 let bundle_prefix = format!("dev.world.{}", project_slug);
43
44 fs::create_dir_all(output_dir)?;
46
47 generate_bench_mobile_crate(output_dir, &project_slug)?;
49
50 match config.target {
52 Target::Android => {
53 generate_android_project(output_dir, &project_slug)?;
54 }
55 Target::Ios => {
56 generate_ios_project(output_dir, &project_slug, &project_pascal, &bundle_prefix)?;
57 }
58 Target::Both => {
59 generate_android_project(output_dir, &project_slug)?;
60 generate_ios_project(output_dir, &project_slug, &project_pascal, &bundle_prefix)?;
61 }
62 }
63
64 generate_config_file(output_dir, config)?;
66
67 if config.generate_examples {
69 generate_example_benchmarks(output_dir)?;
70 }
71
72 Ok(output_dir.clone())
73}
74
75fn generate_bench_mobile_crate(output_dir: &Path, project_name: &str) -> Result<(), BenchError> {
77 let crate_dir = output_dir.join("bench-mobile");
78 fs::create_dir_all(crate_dir.join("src"))?;
79
80 let crate_name = format!("{}-bench-mobile", project_name);
81
82 let cargo_toml = format!(
84 r#"[package]
85name = "{}"
86version = "0.1.0"
87edition = "2021"
88
89[lib]
90crate-type = ["cdylib", "staticlib", "rlib"]
91
92[dependencies]
93mobench-sdk = {{ path = ".." }}
94uniffi = "0.28"
95{} = {{ path = ".." }}
96
97[features]
98default = []
99
100[build-dependencies]
101uniffi = {{ version = "0.28", features = ["build"] }}
102"#,
103 crate_name, project_name
104 );
105
106 fs::write(crate_dir.join("Cargo.toml"), cargo_toml)?;
107
108 let lib_rs_template = r#"//! Mobile FFI bindings for benchmarks
110//!
111//! This crate provides the FFI boundary between Rust benchmarks and mobile
112//! platforms (Android/iOS). It uses UniFFI to generate type-safe bindings.
113
114use uniffi;
115
116// Ensure the user crate is linked so benchmark registrations are pulled in.
117extern crate {{USER_CRATE}} as _bench_user_crate;
118
119// Re-export mobench-sdk types with UniFFI annotations
120#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, uniffi::Record)]
121pub struct BenchSpec {
122 pub name: String,
123 pub iterations: u32,
124 pub warmup: u32,
125}
126
127#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, uniffi::Record)]
128pub struct BenchSample {
129 pub duration_ns: u64,
130}
131
132#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, uniffi::Record)]
133pub struct BenchReport {
134 pub spec: BenchSpec,
135 pub samples: Vec<BenchSample>,
136}
137
138#[derive(Debug, thiserror::Error, uniffi::Error)]
139#[uniffi(flat_error)]
140pub enum BenchError {
141 #[error("iterations must be greater than zero")]
142 InvalidIterations,
143
144 #[error("unknown benchmark function: {name}")]
145 UnknownFunction { name: String },
146
147 #[error("benchmark execution failed: {reason}")]
148 ExecutionFailed { reason: String },
149}
150
151// Convert from mobench-sdk types
152impl From<mobench_sdk::BenchSpec> for BenchSpec {
153 fn from(spec: mobench_sdk::BenchSpec) -> Self {
154 Self {
155 name: spec.name,
156 iterations: spec.iterations,
157 warmup: spec.warmup,
158 }
159 }
160}
161
162impl From<BenchSpec> for mobench_sdk::BenchSpec {
163 fn from(spec: BenchSpec) -> Self {
164 Self {
165 name: spec.name,
166 iterations: spec.iterations,
167 warmup: spec.warmup,
168 }
169 }
170}
171
172impl From<mobench_sdk::BenchSample> for BenchSample {
173 fn from(sample: mobench_sdk::BenchSample) -> Self {
174 Self {
175 duration_ns: sample.duration_ns,
176 }
177 }
178}
179
180impl From<mobench_sdk::RunnerReport> for BenchReport {
181 fn from(report: mobench_sdk::RunnerReport) -> Self {
182 Self {
183 spec: report.spec.into(),
184 samples: report.samples.into_iter().map(Into::into).collect(),
185 }
186 }
187}
188
189impl From<mobench_sdk::BenchError> for BenchError {
190 fn from(err: mobench_sdk::BenchError) -> Self {
191 match err {
192 mobench_sdk::BenchError::Runner(runner_err) => {
193 BenchError::ExecutionFailed {
194 reason: runner_err.to_string(),
195 }
196 }
197 mobench_sdk::BenchError::UnknownFunction(name) => {
198 BenchError::UnknownFunction { name }
199 }
200 _ => BenchError::ExecutionFailed {
201 reason: err.to_string(),
202 },
203 }
204 }
205}
206
207/// Runs a benchmark by name with the given specification
208///
209/// This is the main FFI entry point called from mobile platforms.
210#[uniffi::export]
211pub fn run_benchmark(spec: BenchSpec) -> Result<BenchReport, BenchError> {
212 let sdk_spec: mobench_sdk::BenchSpec = spec.into();
213 let report = mobench_sdk::run_benchmark(sdk_spec)?;
214 Ok(report.into())
215}
216
217// Generate UniFFI scaffolding
218uniffi::setup_scaffolding!();
219"#;
220
221 let lib_rs = render_template(
222 lib_rs_template,
223 &[TemplateVar {
224 name: "USER_CRATE",
225 value: project_name.replace('-', "_"),
226 }],
227 );
228 fs::write(crate_dir.join("src/lib.rs"), lib_rs)?;
229
230 let build_rs = r#"fn main() {
232 uniffi::generate_scaffolding("src/lib.rs").unwrap();
233}
234"#;
235
236 fs::write(crate_dir.join("build.rs"), build_rs)?;
237
238 Ok(())
239}
240
241fn generate_android_project(output_dir: &Path, project_slug: &str) -> Result<(), BenchError> {
243 let target_dir = output_dir.join("android");
244 let vars = vec![
245 TemplateVar {
246 name: "PACKAGE_NAME",
247 value: format!("dev.world.{}", project_slug),
248 },
249 TemplateVar {
250 name: "UNIFFI_NAMESPACE",
251 value: project_slug.replace('-', "_"),
252 },
253 TemplateVar {
254 name: "LIBRARY_NAME",
255 value: project_slug.replace('-', "_"),
256 },
257 TemplateVar {
258 name: "DEFAULT_FUNCTION",
259 value: "example_fibonacci".to_string(),
260 },
261 ];
262 render_dir(&ANDROID_TEMPLATES, Path::new(""), &target_dir, &vars)?;
263 Ok(())
264}
265
266fn generate_ios_project(
268 output_dir: &Path,
269 project_slug: &str,
270 project_pascal: &str,
271 bundle_prefix: &str,
272) -> Result<(), BenchError> {
273 let target_dir = output_dir.join("ios");
274 let vars = vec![
275 TemplateVar {
276 name: "DEFAULT_FUNCTION",
277 value: "example_fibonacci".to_string(),
278 },
279 TemplateVar {
280 name: "PROJECT_NAME_PASCAL",
281 value: project_pascal.to_string(),
282 },
283 TemplateVar {
284 name: "BUNDLE_ID_PREFIX",
285 value: bundle_prefix.to_string(),
286 },
287 TemplateVar {
288 name: "BUNDLE_ID",
289 value: format!("{}.{}", bundle_prefix, project_slug),
290 },
291 TemplateVar {
292 name: "LIBRARY_NAME",
293 value: project_slug.replace('-', "_"),
294 },
295 ];
296 render_dir(&IOS_TEMPLATES, Path::new(""), &target_dir, &vars)?;
297 Ok(())
298}
299
300fn generate_config_file(output_dir: &Path, config: &InitConfig) -> Result<(), BenchError> {
302 let config_target = match config.target {
303 Target::Ios => "ios",
304 Target::Android | Target::Both => "android",
305 };
306 let config_content = format!(
307 r#"# mobench configuration
308# This file controls how benchmarks are executed on devices.
309
310target = "{}"
311function = "example_fibonacci"
312iterations = 100
313warmup = 10
314device_matrix = "device-matrix.yaml"
315device_tags = ["default"]
316
317[browserstack]
318app_automate_username = "${{BROWSERSTACK_USERNAME}}"
319app_automate_access_key = "${{BROWSERSTACK_ACCESS_KEY}}"
320project = "{}-benchmarks"
321
322[ios_xcuitest]
323app = "target/ios/BenchRunner.ipa"
324test_suite = "target/ios/BenchRunnerUITests.zip"
325"#,
326 config_target, config.project_name
327 );
328
329 fs::write(output_dir.join("bench-config.toml"), config_content)?;
330
331 Ok(())
332}
333
334fn generate_example_benchmarks(output_dir: &Path) -> Result<(), BenchError> {
336 let examples_dir = output_dir.join("benches");
337 fs::create_dir_all(&examples_dir)?;
338
339 let example_content = r#"//! Example benchmarks
340//!
341//! This file demonstrates how to write benchmarks with mobench-sdk.
342
343use mobench_sdk::benchmark;
344
345/// Simple benchmark example
346#[benchmark]
347fn example_fibonacci() {
348 let result = fibonacci(30);
349 std::hint::black_box(result);
350}
351
352/// Another example with a loop
353#[benchmark]
354fn example_sum() {
355 let mut sum = 0u64;
356 for i in 0..10000 {
357 sum = sum.wrapping_add(i);
358 }
359 std::hint::black_box(sum);
360}
361
362// Helper function (not benchmarked)
363fn fibonacci(n: u32) -> u64 {
364 match n {
365 0 => 0,
366 1 => 1,
367 _ => {
368 let mut a = 0u64;
369 let mut b = 1u64;
370 for _ in 2..=n {
371 let next = a.wrapping_add(b);
372 a = b;
373 b = next;
374 }
375 b
376 }
377 }
378}
379"#;
380
381 fs::write(examples_dir.join("example.rs"), example_content)?;
382
383 Ok(())
384}
385
386fn render_dir(
387 dir: &Dir,
388 prefix: &Path,
389 out_root: &Path,
390 vars: &[TemplateVar],
391) -> Result<(), BenchError> {
392 for entry in dir.entries() {
393 match entry {
394 DirEntry::Dir(sub) => {
395 if sub.path().components().any(|c| c.as_os_str() == ".gradle") {
397 continue;
398 }
399 let next_prefix = prefix.join(sub.path());
400 render_dir(sub, &next_prefix, out_root, vars)?;
401 }
402 DirEntry::File(file) => {
403 if file.path().components().any(|c| c.as_os_str() == ".gradle") {
404 continue;
405 }
406 let mut relative = prefix.join(file.path());
407 let mut contents = file.contents().to_vec();
408 if let Some(ext) = relative.extension()
409 && ext == "template"
410 {
411 relative.set_extension("");
412 let rendered = render_template(
413 std::str::from_utf8(&contents).map_err(|e| {
414 BenchError::Build(format!(
415 "invalid UTF-8 in template {:?}: {}",
416 file.path(),
417 e
418 ))
419 })?,
420 vars,
421 );
422 contents = rendered.into_bytes();
423 }
424 let out_path = out_root.join(relative);
425 if let Some(parent) = out_path.parent() {
426 fs::create_dir_all(parent)?;
427 }
428 fs::write(&out_path, contents)?;
429 }
430 }
431 }
432 Ok(())
433}
434
435fn render_template(input: &str, vars: &[TemplateVar]) -> String {
436 let mut output = input.to_string();
437 for var in vars {
438 output = output.replace(&format!("{{{{{}}}}}", var.name), &var.value);
439 }
440 output
441}
442
443fn sanitize_package_name(name: &str) -> String {
444 name.chars()
445 .map(|c| {
446 if c.is_ascii_alphanumeric() {
447 c.to_ascii_lowercase()
448 } else {
449 '-'
450 }
451 })
452 .collect::<String>()
453 .trim_matches('-')
454 .replace("--", "-")
455}
456
457fn to_pascal_case(input: &str) -> String {
458 input
459 .split(|c: char| !c.is_ascii_alphanumeric())
460 .filter(|s| !s.is_empty())
461 .map(|s| {
462 let mut chars = s.chars();
463 let first = chars.next().unwrap().to_ascii_uppercase();
464 let rest: String = chars.map(|c| c.to_ascii_lowercase()).collect();
465 format!("{}{}", first, rest)
466 })
467 .collect::<String>()
468}
469
470#[cfg(test)]
471mod tests {
472 use super::*;
473 use std::env;
474
475 #[test]
476 fn test_generate_bench_mobile_crate() {
477 let temp_dir = env::temp_dir().join("mobench-sdk-test");
478 fs::create_dir_all(&temp_dir).unwrap();
479
480 let result = generate_bench_mobile_crate(&temp_dir, "test_project");
481 assert!(result.is_ok());
482
483 assert!(temp_dir.join("bench-mobile/Cargo.toml").exists());
485 assert!(temp_dir.join("bench-mobile/src/lib.rs").exists());
486 assert!(temp_dir.join("bench-mobile/build.rs").exists());
487
488 fs::remove_dir_all(&temp_dir).ok();
490 }
491}