2
2
// SPDX-License-Identifier: Apache-2.0
3
3
// SPDX-License-Identifier: MIT
4
4
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 } ;
6
15
use serde:: { Deserialize , Serialize } ;
7
16
use serde_json:: Value ;
8
17
use std:: {
@@ -13,6 +22,7 @@ use std::{
13
22
process:: { Command , Output , Stdio } ,
14
23
} ;
15
24
25
+ /// Holds the results of a benchmark run.
16
26
#[ derive( Default , Clone , Serialize , Deserialize , Debug ) ]
17
27
pub struct BenchResult {
18
28
pub created_at : String ,
@@ -25,7 +35,7 @@ pub struct BenchResult {
25
35
pub cargo_deps : HashMap < String , usize > ,
26
36
}
27
37
28
- # [ allow ( dead_code ) ]
38
+ /// Represents a single line of parsed `strace` output.
29
39
#[ derive( Debug , Clone , Serialize ) ]
30
40
pub struct StraceOutput {
31
41
pub percent_time : f64 ,
@@ -35,25 +45,30 @@ pub struct StraceOutput {
35
45
pub errors : u64 ,
36
46
}
37
47
48
+ /// Get the compilation target triple for the current platform.
38
49
pub fn get_target ( ) -> & ' static str {
39
50
#[ cfg( target_os = "macos" ) ]
40
51
return if cfg ! ( target_arch = "aarch64" ) {
41
52
"aarch64-apple-darwin"
42
53
} else {
43
54
"x86_64-apple-darwin"
44
55
} ;
56
+
45
57
#[ cfg( target_os = "ios" ) ]
46
58
return if cfg ! ( target_arch = "aarch64" ) {
47
59
"aarch64-apple-ios"
48
60
} else {
49
61
"x86_64-apple-ios"
50
62
} ;
63
+
51
64
#[ cfg( target_os = "linux" ) ]
52
65
return "x86_64-unknown-linux-gnu" ;
66
+
53
67
#[ cfg( target_os = "windows" ) ]
54
- unimplemented ! ( ) ;
68
+ unimplemented ! ( "Windows target not implemented yet" ) ;
55
69
}
56
70
71
+ /// Get the `target/release` directory path for benchmarks.
57
72
pub fn target_dir ( ) -> PathBuf {
58
73
bench_root_path ( )
59
74
. join ( ".." )
@@ -62,83 +77,91 @@ pub fn target_dir() -> PathBuf {
62
77
. join ( "release" )
63
78
}
64
79
80
+ /// Get the root path of the current benchmark crate.
65
81
pub fn bench_root_path ( ) -> PathBuf {
66
82
PathBuf :: from ( env ! ( "CARGO_MANIFEST_DIR" ) )
67
83
}
68
84
69
- # [ allow ( dead_code ) ]
85
+ /// Get the home directory of the current user.
70
86
pub fn home_path ( ) -> PathBuf {
71
87
#[ 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
+
73
92
#[ cfg( target_os = "windows" ) ]
74
- return PathBuf :: from ( env ! ( "HOMEPATH" ) ) ;
93
+ {
94
+ PathBuf :: from ( std:: env:: var ( "USERPROFILE" ) . unwrap_or_default ( ) )
95
+ }
75
96
}
76
97
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 ( ) )
80
102
}
81
103
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 ] )
86
108
. args ( & cmd[ 1 ..] )
87
109
. stdin ( Stdio :: piped ( ) )
88
110
. 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 {:?}\n stdout:\n {}\n stderr:\n {}" ,
118
+ cmd,
119
+ output. status. code( ) ,
120
+ String :: from_utf8_lossy( & output. stdout) ,
121
+ String :: from_utf8_lossy( & output. stderr)
122
+ ) ;
102
123
}
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
+ ) )
104
129
}
105
130
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}" ) ) ?;
109
136
let output = BufReader :: new ( file) ;
137
+
110
138
let mut highest: u64 = 0 ;
111
- // MEM 203.437500 1621617192.4123
139
+
112
140
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 ( ) ;
115
142
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) ;
120
146
}
121
147
}
122
148
}
123
149
124
- fs:: remove_file ( file_path) . unwrap ( ) ;
150
+ // Best-effort cleanup
151
+ let _ = fs:: remove_file ( file_path) ;
125
152
126
- if highest > 0 {
127
- return Some ( highest) ;
128
- }
129
-
130
- None
153
+ Ok ( if highest > 0 { Some ( highest) } else { None } )
131
154
}
132
155
133
- # [ allow ( dead_code ) ]
156
+ /// Parse the output of `strace -c` and return a summary of syscalls.
134
157
pub fn parse_strace_output ( output : & str ) -> HashMap < String , StraceOutput > {
135
158
let mut summary = HashMap :: new ( ) ;
136
159
137
160
let mut lines = output
138
161
. lines ( )
139
162
. filter ( |line| !line. is_empty ( ) && !line. contains ( "detached ..." ) ) ;
140
- let count = lines. clone ( ) . count ( ) ;
141
163
164
+ let count = lines. clone ( ) . count ( ) ;
142
165
if count < 4 {
143
166
return summary;
144
167
}
@@ -148,88 +171,90 @@ pub fn parse_strace_output(output: &str) -> HashMap<String, StraceOutput> {
148
171
let data_lines = lines. skip ( 2 ) ;
149
172
150
173
for line in data_lines {
151
- let syscall_fields = line. split_whitespace ( ) . collect :: < Vec < _ > > ( ) ;
174
+ let syscall_fields: Vec < & str > = line. split_whitespace ( ) . collect ( ) ;
152
175
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 {
164
185
0
165
186
} else {
166
- str :: parse :: < u64 > ( syscall_fields[ 4 ] ) . unwrap ( )
187
+ syscall_fields[ 4 ] . parse ( ) . unwrap_or ( 0 )
167
188
} ,
168
- } ,
169
- ) ;
189
+ } ;
190
+ summary. insert ( syscall_name. to_string ( ) , output) ;
191
+ }
170
192
}
171
193
}
172
194
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 ) ,
194
203
} ,
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
+ } ;
196
215
216
+ summary. insert ( "total" . to_string ( ) , total) ;
197
217
summary
198
218
}
199
219
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
+
206
229
if !status. success ( ) {
207
- panic ! ( "Unexpected exit code: {:?}" , status. code( ) ) ;
230
+ bail ! ( "Command {:?} exited with {:?}" , cmd , status. code( ) ) ;
208
231
}
232
+ Ok ( ( ) )
209
233
}
210
234
211
- # [ allow ( dead_code ) ]
235
+ /// Read a JSON file into a [`serde_json::Value`].
212
236
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}" ) ) ?;
214
239
Ok ( serde_json:: from_reader ( f) ?)
215
240
}
216
241
217
- # [ allow ( dead_code ) ]
242
+ /// Write a [`serde_json::Value`] into a JSON file.
218
243
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}" ) ) ?;
220
246
serde_json:: to_writer ( f, value) ?;
221
247
Ok ( ( ) )
222
248
}
223
249
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 < ( ) > {
226
253
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 ( ( ) ) ;
229
256
}
230
257
231
- // Downloading with curl this saves us from adding
232
- // a Rust HTTP client dependency.
233
258
println ! ( "Downloading {url}" ) ;
234
259
let status = Command :: new ( "curl" )
235
260
. arg ( "-L" )
@@ -238,8 +263,14 @@ pub fn download_file(url: &str, filename: PathBuf) {
238
263
. arg ( & filename)
239
264
. arg ( url)
240
265
. status ( )
241
- . unwrap ( ) ;
266
+ . with_context ( || format ! ( "failed to execute curl for {url}" ) ) ? ;
242
267
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 ( ( ) )
245
276
}
0 commit comments