1use anyhow::Result;
2use clap::{command, Parser};
3use monerochan_prover::{components::MONEROCHANProverComponents, utils::get_cycles, MONEROCHANProver};
4use monerochan::{MONEROCHANContext, MONEROCHANStdin};
5use monerochan_stark::MONEROCHANProverOpts;
6use reqwest::Client;
7use serde::Serialize;
8use serde_json::json;
9use slack_rust::{
10 chat::post_message::{post_message, PostMessageRequest},
11 http_client::default_client,
12};
13use std::time::{Duration, Instant};
14
15use program::load_program;
16
17use crate::program::{TesterProgram, PROGRAMS};
18
19mod program;
20
21#[derive(Parser, Clone)]
22#[command(about = "Evaluate the performance of MONEROCHAN on programs.")]
23struct EvalArgs {
24 #[arg(long, use_value_delimiter = true, value_delimiter = ',')]
27 pub programs: Vec<String>,
28
29 #[arg(long)]
31 pub shard_size: Option<usize>,
32
33 #[arg(long, default_missing_value="true", num_args=0..=1)]
35 pub post_to_slack: Option<bool>,
36
37 #[arg(long)]
39 pub slack_channel_id: Option<String>,
40
41 #[arg(long)]
43 pub slack_token: Option<String>,
44
45 #[arg(long, default_missing_value="true", num_args=0..=1)]
47 pub post_to_github: Option<bool>,
48
49 #[arg(long)]
51 pub github_token: Option<String>,
52
53 #[arg(long)]
55 pub repo_owner: Option<String>,
56
57 #[arg(long)]
59 pub repo_name: Option<String>,
60
61 #[arg(long)]
63 pub pr_number: Option<String>,
64
65 #[arg(long)]
67 pub branch_name: Option<String>,
68
69 #[arg(long)]
71 pub commit_hash: Option<String>,
72
73 #[arg(long)]
75 pub author: Option<String>,
76}
77
78pub async fn evaluate_performance<C: MONEROCHANProverComponents>(
79 opts: MONEROCHANProverOpts,
80) -> Result<(), Box<dyn std::error::Error>> {
81 println!("opts: {opts:?}");
82
83 let args = EvalArgs::parse();
84
85 if let Some(shard_size) = args.shard_size {
87 std::env::set_var("SHARD_SIZE", format!("{}", 1 << shard_size));
88 }
89
90 let programs: Vec<&TesterProgram> = if args.programs.is_empty() {
92 PROGRAMS.iter().collect()
93 } else {
94 PROGRAMS
95 .iter()
96 .filter(|p| args.programs.iter().any(|arg| arg.eq_ignore_ascii_case(p.name)))
97 .collect()
98 };
99
100 monerochan::utils::setup_logger();
101
102 let mut reports = Vec::new();
104 for program in &programs {
105 println!("Evaluating program: {}", program.name);
106 let (elf, stdin) = load_program(program.elf, program.input);
107 let report = run_evaluation::<C>(program.name, &elf, &stdin, opts);
108 reports.push(report);
109 println!("Finished Program: {}", program.name);
110 }
111
112 let reports_len = reports.len();
114 let success_count = reports.iter().filter(|r| r.success).count();
115 let results_text = format_results(&args, &reports);
116
117 println!("{}", results_text.join("\n"));
119
120 if args.post_to_slack.unwrap_or(false) {
122 match (&args.slack_token, &args.slack_channel_id) {
123 (Some(token), Some(channel)) => {
124 for message in &results_text {
125 post_to_slack(token, channel, message).await?;
126 }
127 }
128 _ => println!("Warning: post_to_slack is true, required Slack arguments are missing."),
129 }
130 }
131
132 if args.post_to_github.unwrap_or(false) {
134 match (&args.repo_owner, &args.repo_name, &args.pr_number, &args.github_token) {
135 (Some(owner), Some(repo), Some(pr_number), Some(token)) => {
136 let message = format_github_message(&results_text);
137 post_to_github_pr(owner, repo, pr_number, token, &message).await?;
138 }
139 _ => {
140 println!("Warning: post_to_github is true, required GitHub arguments are missing.")
141 }
142 }
143 }
144
145 let all_successful = success_count == reports_len;
147 if !all_successful {
148 println!("Some programs failed. Please check the results above.");
149 std::process::exit(1);
150 }
151
152 Ok(())
153}
154
155#[derive(Debug, Serialize)]
156pub struct PerformanceReport {
157 program: String,
158 cycles: u64,
159 exec_khz: f64,
160 core_khz: f64,
161 compressed_khz: f64,
162 time: f64,
163 success: bool,
164}
165
166fn run_evaluation<C: MONEROCHANProverComponents>(
167 program_name: &str,
168 elf: &[u8],
169 stdin: &MONEROCHANStdin,
170 opts: MONEROCHANProverOpts,
171) -> PerformanceReport {
172 let cycles = get_cycles(elf, stdin);
173
174 let prover = MONEROCHANProver::<C>::new();
175 let (_, pk_d, program, vk) = prover.setup(elf);
176
177 let context = MONEROCHANContext::default();
178
179 let (exec_result, exec_duration) =
180 time_operation(|| prover.execute(elf, stdin, context.clone()));
181 let exec_ok = exec_result.is_ok();
182
183 let (core_result, core_duration) =
184 time_operation(|| prover.prove_core(&pk_d, program, stdin, opts, context));
185 let (core_ok, core_proof_opt) = match core_result {
186 Ok(proof) => (true, Some(proof)),
187 Err(_) => (false, None),
188 };
189
190 let (compress_ok, compress_duration) = if let Some(core_proof) = core_proof_opt {
191 let (compress_result, dur) =
192 time_operation(|| prover.compress(&vk, core_proof, vec![], opts));
193 (compress_result.is_ok(), dur)
194 } else {
195 (false, Duration::from_secs(0))
196 };
197
198 let total_duration = exec_duration + core_duration + compress_duration;
199
200 PerformanceReport {
201 program: program_name.to_string(),
202 cycles,
203 exec_khz: if exec_ok { calculate_khz(cycles, exec_duration) } else { 0.0 },
204 core_khz: if core_ok { calculate_khz(cycles, core_duration) } else { 0.0 },
205 compressed_khz: if core_ok && compress_ok {
206 calculate_khz(cycles, compress_duration + core_duration)
207 } else {
208 0.0
209 },
210 time: total_duration.as_secs_f64(),
211 success: exec_ok && core_ok && compress_ok,
212 }
213}
214
215fn format_results(args: &EvalArgs, results: &[PerformanceReport]) -> Vec<String> {
216 let mut detail_text = String::new();
217 if let Some(branch_name) = &args.branch_name {
218 detail_text.push_str(&format!("*Branch*: {branch_name}\n"));
219 }
220 if let Some(commit_hash) = &args.commit_hash {
221 detail_text.push_str(&format!("*Commit*: {}\n", &commit_hash[..8]));
222 }
223 if let Some(author) = &args.author {
224 detail_text.push_str(&format!("*Author*: {author}\n"));
225 }
226
227 let mut table_text = String::new();
228 table_text.push_str("```\n");
229 table_text.push_str("| program | cycles | execute (mHz) | core (kHZ) | compress (KHz) | time | success |\n");
230 table_text.push_str("|-------------------|-------------|----------------|----------------|----------------|--------|----------|");
231
232 for result in results.iter() {
233 table_text.push_str(&format!(
234 "\n| {:<17} | {:>11} | {:>14.2} | {:>14.2} | {:>14.2} | {:>6} | {:<7} |",
235 result.program,
236 result.cycles,
237 result.exec_khz / 1000.0,
238 result.core_khz,
239 result.compressed_khz,
240 format_duration(result.time),
241 if result.success { "✅" } else { "❌" }
242 ));
243 }
244 table_text.push_str("\n```");
245
246 vec!["*MONEROCHAN Performance Test Results*\n".to_string(), detail_text, table_text]
247}
248
249pub fn time_operation<T, F: FnOnce() -> T>(operation: F) -> (T, Duration) {
250 let start = Instant::now();
251 let result = operation();
252 let duration = start.elapsed();
253 (result, duration)
254}
255
256fn calculate_khz(cycles: u64, duration: Duration) -> f64 {
257 let duration_secs = duration.as_secs_f64();
258 if duration_secs > 0.0 {
259 (cycles as f64 / duration_secs) / 1_000.0
260 } else {
261 0.0
262 }
263}
264
265fn format_duration(duration: f64) -> String {
266 let secs = duration.round() as u64;
267 let minutes = secs / 60;
268 let seconds = secs % 60;
269
270 if minutes > 0 {
271 format!("{minutes}m{seconds}s")
272 } else if seconds > 0 {
273 format!("{seconds}s")
274 } else {
275 format!("{}ms", (duration * 1000.0).round() as u64)
276 }
277}
278
279async fn post_to_slack(slack_token: &str, slack_channel_id: &str, message: &str) -> Result<()> {
280 let slack_api_client = default_client();
281 let request = PostMessageRequest {
282 channel: slack_channel_id.to_string(),
283 text: Some(message.to_string()),
284 ..Default::default()
285 };
286
287 post_message(&slack_api_client, &request, slack_token).await.expect("slack api call error");
288
289 Ok(())
290}
291
292fn format_github_message(results_text: &[String]) -> String {
293 let mut formatted_message = String::new();
294
295 if let Some(title) = results_text.first() {
296 formatted_message.push_str(&title.replace('*', "**"));
298 formatted_message.push('\n');
299 }
300
301 if let Some(details) = results_text.get(1) {
302 formatted_message.push_str(&details.replace('*', "**"));
304 formatted_message.push('\n');
305 }
306
307 if let Some(table) = results_text.get(2) {
308 let cleaned_table = table.trim_start_matches("```").trim_end_matches("```");
310 formatted_message.push_str(cleaned_table);
311 }
312
313 formatted_message
314}
315
316async fn post_to_github_pr(
317 owner: &str,
318 repo: &str,
319 pr_number: &str,
320 token: &str,
321 message: &str,
322) -> Result<(), Box<dyn std::error::Error>> {
323 let client = Client::new();
324 let base_url = format!("https://api.github.com/repos/{owner}/{repo}");
325
326 let comments_url = format!("{base_url}/issues/{pr_number}/comments");
328 let comments_response = client
329 .get(&comments_url)
330 .header("Authorization", format!("token {token}"))
331 .header("User-Agent", "monerochan-perf-bot")
332 .send()
333 .await?;
334
335 let comments: Vec<serde_json::Value> = comments_response.json().await?;
336
337 let bot_comment = comments.iter().find(|comment| {
339 comment["user"]["login"]
340 .as_str()
341 .map(|login| login == "github-actions[bot]")
342 .unwrap_or(false)
343 });
344
345 if let Some(existing_comment) = bot_comment {
346 let comment_url = existing_comment["url"].as_str().unwrap();
348 let response = client
349 .patch(comment_url)
350 .header("Authorization", format!("token {token}"))
351 .header("User-Agent", "monerochan-perf-bot")
352 .json(&json!({
353 "body": message
354 }))
355 .send()
356 .await?;
357
358 if !response.status().is_success() {
359 return Err(format!("Failed to update comment: {:?}", response.text().await?).into());
360 }
361 } else {
362 let response = client
364 .post(&comments_url)
365 .header("Authorization", format!("token {token}"))
366 .header("User-Agent", "monerochan-perf-bot")
367 .json(&json!({
368 "body": message
369 }))
370 .send()
371 .await?;
372
373 if !response.status().is_success() {
374 return Err(format!("Failed to post comment: {:?}", response.text().await?).into());
375 }
376 }
377
378 Ok(())
379}
380
381#[cfg(test)]
382mod tests {
383 use super::*;
384
385 #[test]
386 fn test_format_results() {
387 let dummy_reports = vec![
388 PerformanceReport {
389 program: "fibonacci".to_string(),
390 cycles: 11291,
391 exec_khz: 29290.0,
392 core_khz: 30.0,
393 compressed_khz: 0.1,
394 time: 622.385,
395 success: true,
396 },
397 PerformanceReport {
398 program: "super-program".to_string(),
399 cycles: 275735600,
400 exec_khz: 70190.0,
401 core_khz: 310.0,
402 compressed_khz: 120.0,
403 time: 812.285,
404 success: true,
405 },
406 ];
407
408 let args = EvalArgs {
409 programs: vec!["fibonacci".to_string(), "super-program".to_string()],
410 shard_size: None,
411 post_to_slack: Some(false),
412 slack_channel_id: None,
413 slack_token: None,
414 post_to_github: Some(true),
415 github_token: Some("abcdef1234567890".to_string()),
416 repo_owner: Some("monero-chan-foundation".to_string()),
417 repo_name: Some("monerochan".to_string()),
418 pr_number: Some("123456".to_string()),
419 branch_name: Some("feature-branch".to_string()),
420 commit_hash: Some("abcdef1234567890".to_string()),
421 author: Some("John Doe".to_string()),
422 };
423
424 let formatted_results = format_results(&args, &dummy_reports);
425
426 for line in &formatted_results {
427 println!("{line}");
428 }
429
430 assert_eq!(formatted_results.len(), 3);
431 assert!(formatted_results[0].contains("MONEROCHAN Performance Test Results"));
432 assert!(formatted_results[1].contains("*Branch*: feature-branch"));
433 assert!(formatted_results[1].contains("*Commit*: abcdef12"));
434 assert!(formatted_results[1].contains("*Author*: John Doe"));
435 assert!(formatted_results[2].contains("fibonacci"));
436 assert!(formatted_results[2].contains("super-program"));
437 }
438}