Skip to content

Commit d54f3b9

Browse files
refactor(bench): improve utils.rs safety, error handling, and docs (#14057)
- Replace `unwrap` and `expect` with `anyhow::Result` for safer error handling - Add contextual error messages using `anyhow::Context` - Fix `home_path` on Windows by using `USERPROFILE` instead of `HOMEPATH` - Ensure process helpers (`run_collect`, `run`) return `Result` instead of panicking - Improve parsing logic in `parse_max_mem` and `parse_strace_output` - Add documentation comments for all public functions - Add best-effort cleanup and resilience against malformed input
1 parent 1e7aac3 commit d54f3b9

File tree

1 file changed

+133
-102
lines changed

1 file changed

+133
-102
lines changed

bench/src/utils.rs

Lines changed: 133 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,16 @@
22
// SPDX-License-Identifier: Apache-2.0
33
// SPDX-License-Identifier: MIT
44

5-
use anyhow::Result;
5+
//! Utility functions for benchmarking tasks in the Tauri project.
6+
//!
7+
//! This module provides helpers for:
8+
//! - Paths to project directories and targets
9+
//! - Running and collecting process outputs
10+
//! - Parsing memory profiler (`mprof`) and syscall profiler (`strace`) outputs
11+
//! - JSON read/write utilities
12+
//! - File download utilities (via `curl` or file copy)
13+
14+
use anyhow::{bail, Context, Result};
615
use serde::{Deserialize, Serialize};
716
use serde_json::Value;
817
use std::{
@@ -13,6 +22,7 @@ use std::{
1322
process::{Command, Output, Stdio},
1423
};
1524

25+
/// Holds the results of a benchmark run.
1626
#[derive(Default, Clone, Serialize, Deserialize, Debug)]
1727
pub struct BenchResult {
1828
pub created_at: String,
@@ -25,7 +35,7 @@ pub struct BenchResult {
2535
pub cargo_deps: HashMap<String, usize>,
2636
}
2737

28-
#[allow(dead_code)]
38+
/// Represents a single line of parsed `strace` output.
2939
#[derive(Debug, Clone, Serialize)]
3040
pub struct StraceOutput {
3141
pub percent_time: f64,
@@ -35,25 +45,30 @@ pub struct StraceOutput {
3545
pub errors: u64,
3646
}
3747

48+
/// Get the compilation target triple for the current platform.
3849
pub fn get_target() -> &'static str {
3950
#[cfg(target_os = "macos")]
4051
return if cfg!(target_arch = "aarch64") {
4152
"aarch64-apple-darwin"
4253
} else {
4354
"x86_64-apple-darwin"
4455
};
56+
4557
#[cfg(target_os = "ios")]
4658
return if cfg!(target_arch = "aarch64") {
4759
"aarch64-apple-ios"
4860
} else {
4961
"x86_64-apple-ios"
5062
};
63+
5164
#[cfg(target_os = "linux")]
5265
return "x86_64-unknown-linux-gnu";
66+
5367
#[cfg(target_os = "windows")]
54-
unimplemented!();
68+
unimplemented!("Windows target not implemented yet");
5569
}
5670

71+
/// Get the `target/release` directory path for benchmarks.
5772
pub fn target_dir() -> PathBuf {
5873
bench_root_path()
5974
.join("..")
@@ -62,83 +77,91 @@ pub fn target_dir() -> PathBuf {
6277
.join("release")
6378
}
6479

80+
/// Get the root path of the current benchmark crate.
6581
pub fn bench_root_path() -> PathBuf {
6682
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
6783
}
6884

69-
#[allow(dead_code)]
85+
/// Get the home directory of the current user.
7086
pub fn home_path() -> PathBuf {
7187
#[cfg(any(target_os = "macos", target_os = "ios", target_os = "linux"))]
72-
return PathBuf::from(env!("HOME"));
88+
{
89+
PathBuf::from(std::env::var("HOME").unwrap_or_default())
90+
}
91+
7392
#[cfg(target_os = "windows")]
74-
return PathBuf::from(env!("HOMEPATH"));
93+
{
94+
PathBuf::from(std::env::var("USERPROFILE").unwrap_or_default())
95+
}
7596
}
7697

77-
#[allow(dead_code)]
78-
pub fn tauri_root_path() -> PathBuf {
79-
bench_root_path().parent().unwrap().to_path_buf()
98+
/// Get the root path of the Tauri repository.
99+
/// Returns `None` if the parent path cannot be determined.
100+
pub fn tauri_root_path() -> Option<PathBuf> {
101+
bench_root_path().parent().map(|p| p.to_path_buf())
80102
}
81103

82-
#[allow(dead_code)]
83-
pub fn run_collect(cmd: &[&str]) -> (String, String) {
84-
let mut process_builder = Command::new(cmd[0]);
85-
process_builder
104+
/// Run a command and collect its stdout and stderr as strings.
105+
/// Returns an error if the command fails or exits with a non-zero status.
106+
pub fn run_collect(cmd: &[&str]) -> Result<(String, String)> {
107+
let output: Output = Command::new(cmd[0])
86108
.args(&cmd[1..])
87109
.stdin(Stdio::piped())
88110
.stdout(Stdio::piped())
89-
.stderr(Stdio::piped());
90-
let prog = process_builder.spawn().expect("failed to spawn script");
91-
let Output {
92-
stdout,
93-
stderr,
94-
status,
95-
} = prog.wait_with_output().expect("failed to wait on child");
96-
let stdout = String::from_utf8_lossy(&stdout).to_string();
97-
let stderr = String::from_utf8_lossy(&stderr).to_string();
98-
if !status.success() {
99-
eprintln!("stdout: <<<{stdout}>>>");
100-
eprintln!("stderr: <<<{stderr}>>>");
101-
panic!("Unexpected exit code: {:?}", status.code());
111+
.stderr(Stdio::piped())
112+
.output()
113+
.with_context(|| format!("failed to execute command: {:?}", cmd))?;
114+
115+
if !output.status.success() {
116+
bail!(
117+
"Command {:?} exited with {:?}\nstdout:\n{}\nstderr:\n{}",
118+
cmd,
119+
output.status.code(),
120+
String::from_utf8_lossy(&output.stdout),
121+
String::from_utf8_lossy(&output.stderr)
122+
);
102123
}
103-
(stdout, stderr)
124+
125+
Ok((
126+
String::from_utf8_lossy(&output.stdout).to_string(),
127+
String::from_utf8_lossy(&output.stderr).to_string(),
128+
))
104129
}
105130

106-
#[allow(dead_code)]
107-
pub fn parse_max_mem(file_path: &str) -> Option<u64> {
108-
let file = fs::File::open(file_path).unwrap();
131+
/// Parse a memory profiler (`mprof`) output file and return the maximum
132+
/// memory usage in bytes. Returns `None` if no values are found.
133+
pub fn parse_max_mem(file_path: &str) -> Result<Option<u64>> {
134+
let file = fs::File::open(file_path)
135+
.with_context(|| format!("failed to open mprof output file {file_path}"))?;
109136
let output = BufReader::new(file);
137+
110138
let mut highest: u64 = 0;
111-
// MEM 203.437500 1621617192.4123
139+
112140
for line in output.lines().map_while(Result::ok) {
113-
// split line by space
114-
let split = line.split(' ').collect::<Vec<_>>();
141+
let split: Vec<&str> = line.split(' ').collect();
115142
if split.len() == 3 {
116-
// mprof generate result in MB
117-
let current_bytes = str::parse::<f64>(split[1]).unwrap() as u64 * 1024 * 1024;
118-
if current_bytes > highest {
119-
highest = current_bytes;
143+
if let Ok(mb) = split[1].parse::<f64>() {
144+
let current_bytes = (mb * 1024.0 * 1024.0) as u64;
145+
highest = highest.max(current_bytes);
120146
}
121147
}
122148
}
123149

124-
fs::remove_file(file_path).unwrap();
150+
// Best-effort cleanup
151+
let _ = fs::remove_file(file_path);
125152

126-
if highest > 0 {
127-
return Some(highest);
128-
}
129-
130-
None
153+
Ok(if highest > 0 { Some(highest) } else { None })
131154
}
132155

133-
#[allow(dead_code)]
156+
/// Parse the output of `strace -c` and return a summary of syscalls.
134157
pub fn parse_strace_output(output: &str) -> HashMap<String, StraceOutput> {
135158
let mut summary = HashMap::new();
136159

137160
let mut lines = output
138161
.lines()
139162
.filter(|line| !line.is_empty() && !line.contains("detached ..."));
140-
let count = lines.clone().count();
141163

164+
let count = lines.clone().count();
142165
if count < 4 {
143166
return summary;
144167
}
@@ -148,88 +171,90 @@ pub fn parse_strace_output(output: &str) -> HashMap<String, StraceOutput> {
148171
let data_lines = lines.skip(2);
149172

150173
for line in data_lines {
151-
let syscall_fields = line.split_whitespace().collect::<Vec<_>>();
174+
let syscall_fields: Vec<&str> = line.split_whitespace().collect();
152175
let len = syscall_fields.len();
153-
let syscall_name = syscall_fields.last().unwrap();
154-
155-
if (5..=6).contains(&len) {
156-
summary.insert(
157-
syscall_name.to_string(),
158-
StraceOutput {
159-
percent_time: str::parse::<f64>(syscall_fields[0]).unwrap(),
160-
seconds: str::parse::<f64>(syscall_fields[1]).unwrap(),
161-
usecs_per_call: Some(str::parse::<u64>(syscall_fields[2]).unwrap()),
162-
calls: str::parse::<u64>(syscall_fields[3]).unwrap(),
163-
errors: if syscall_fields.len() < 6 {
176+
177+
if let Some(&syscall_name) = syscall_fields.last() {
178+
if (5..=6).contains(&len) {
179+
let output = StraceOutput {
180+
percent_time: syscall_fields[0].parse().unwrap_or(0.0),
181+
seconds: syscall_fields[1].parse().unwrap_or(0.0),
182+
usecs_per_call: syscall_fields[2].parse().ok(),
183+
calls: syscall_fields[3].parse().unwrap_or(0),
184+
errors: if len < 6 {
164185
0
165186
} else {
166-
str::parse::<u64>(syscall_fields[4]).unwrap()
187+
syscall_fields[4].parse().unwrap_or(0)
167188
},
168-
},
169-
);
189+
};
190+
summary.insert(syscall_name.to_string(), output);
191+
}
170192
}
171193
}
172194

173-
let total_fields = total_line.split_whitespace().collect::<Vec<_>>();
174-
175-
summary.insert(
176-
"total".to_string(),
177-
match total_fields.len() {
178-
// Old format, has no usecs/call
179-
5 => StraceOutput {
180-
percent_time: str::parse::<f64>(total_fields[0]).unwrap(),
181-
seconds: str::parse::<f64>(total_fields[1]).unwrap(),
182-
usecs_per_call: None,
183-
calls: str::parse::<u64>(total_fields[2]).unwrap(),
184-
errors: str::parse::<u64>(total_fields[3]).unwrap(),
185-
},
186-
6 => StraceOutput {
187-
percent_time: str::parse::<f64>(total_fields[0]).unwrap(),
188-
seconds: str::parse::<f64>(total_fields[1]).unwrap(),
189-
usecs_per_call: Some(str::parse::<u64>(total_fields[2]).unwrap()),
190-
calls: str::parse::<u64>(total_fields[3]).unwrap(),
191-
errors: str::parse::<u64>(total_fields[4]).unwrap(),
192-
},
193-
_ => panic!("Unexpected total field count: {}", total_fields.len()),
195+
let total_fields: Vec<&str> = total_line.split_whitespace().collect();
196+
let total = match total_fields.len() {
197+
5 => StraceOutput {
198+
percent_time: total_fields[0].parse().unwrap_or(0.0),
199+
seconds: total_fields[1].parse().unwrap_or(0.0),
200+
usecs_per_call: None,
201+
calls: total_fields[2].parse().unwrap_or(0),
202+
errors: total_fields[3].parse().unwrap_or(0),
194203
},
195-
);
204+
6 => StraceOutput {
205+
percent_time: total_fields[0].parse().unwrap_or(0.0),
206+
seconds: total_fields[1].parse().unwrap_or(0.0),
207+
usecs_per_call: total_fields[2].parse().ok(),
208+
calls: total_fields[3].parse().unwrap_or(0),
209+
errors: total_fields[4].parse().unwrap_or(0),
210+
},
211+
_ => {
212+
panic!("Unexpected total field count: {}", total_fields.len());
213+
}
214+
};
196215

216+
summary.insert("total".to_string(), total);
197217
summary
198218
}
199219

200-
#[allow(dead_code)]
201-
pub fn run(cmd: &[&str]) {
202-
let mut process_builder = Command::new(cmd[0]);
203-
process_builder.args(&cmd[1..]).stdin(Stdio::piped());
204-
let mut prog = process_builder.spawn().expect("failed to spawn script");
205-
let status = prog.wait().expect("failed to wait on child");
220+
/// Run a command and wait for completion.
221+
/// Returns an error if the command fails.
222+
pub fn run(cmd: &[&str]) -> Result<()> {
223+
let status = Command::new(cmd[0])
224+
.args(&cmd[1..])
225+
.stdin(Stdio::piped())
226+
.status()
227+
.with_context(|| format!("failed to execute command: {:?}", cmd))?;
228+
206229
if !status.success() {
207-
panic!("Unexpected exit code: {:?}", status.code());
230+
bail!("Command {:?} exited with {:?}", cmd, status.code());
208231
}
232+
Ok(())
209233
}
210234

211-
#[allow(dead_code)]
235+
/// Read a JSON file into a [`serde_json::Value`].
212236
pub fn read_json(filename: &str) -> Result<Value> {
213-
let f = fs::File::open(filename)?;
237+
let f =
238+
fs::File::open(filename).with_context(|| format!("failed to open JSON file {filename}"))?;
214239
Ok(serde_json::from_reader(f)?)
215240
}
216241

217-
#[allow(dead_code)]
242+
/// Write a [`serde_json::Value`] into a JSON file.
218243
pub fn write_json(filename: &str, value: &Value) -> Result<()> {
219-
let f = fs::File::create(filename)?;
244+
let f =
245+
fs::File::create(filename).with_context(|| format!("failed to create JSON file {filename}"))?;
220246
serde_json::to_writer(f, value)?;
221247
Ok(())
222248
}
223249

224-
#[allow(dead_code)]
225-
pub fn download_file(url: &str, filename: PathBuf) {
250+
/// Download a file from either a local path or an HTTP/HTTPS URL.
251+
/// Falls back to copying the file if the URL does not start with http/https.
252+
pub fn download_file(url: &str, filename: PathBuf) -> Result<()> {
226253
if !url.starts_with("http:") && !url.starts_with("https:") {
227-
fs::copy(url, filename).unwrap();
228-
return;
254+
fs::copy(url, &filename).with_context(|| format!("failed to copy from {url}"))?;
255+
return Ok(());
229256
}
230257

231-
// Downloading with curl this saves us from adding
232-
// a Rust HTTP client dependency.
233258
println!("Downloading {url}");
234259
let status = Command::new("curl")
235260
.arg("-L")
@@ -238,8 +263,14 @@ pub fn download_file(url: &str, filename: PathBuf) {
238263
.arg(&filename)
239264
.arg(url)
240265
.status()
241-
.unwrap();
266+
.with_context(|| format!("failed to execute curl for {url}"))?;
242267

243-
assert!(status.success());
244-
assert!(filename.exists());
268+
if !status.success() {
269+
bail!("curl failed with exit code {:?}", status.code());
270+
}
271+
if !filename.exists() {
272+
bail!("expected file {:?} to exist after download", filename);
273+
}
274+
275+
Ok(())
245276
}

0 commit comments

Comments
 (0)