monerochan_eval/
lib.rs

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    /// The programs to evaluate, specified by name. If not specified, all programs will be
25    /// evaluated.
26    #[arg(long, use_value_delimiter = true, value_delimiter = ',')]
27    pub programs: Vec<String>,
28
29    /// The shard size to use for the prover.
30    #[arg(long)]
31    pub shard_size: Option<usize>,
32
33    /// Whether to post results to Slack.
34    #[arg(long, default_missing_value="true", num_args=0..=1)]
35    pub post_to_slack: Option<bool>,
36
37    /// The Slack channel ID to post results to, only used if post_to_slack is true.
38    #[arg(long)]
39    pub slack_channel_id: Option<String>,
40
41    /// The Slack bot token to post results to, only used if post_to_slack is true.
42    #[arg(long)]
43    pub slack_token: Option<String>,
44
45    /// Whether to post results to GitHub PR.
46    #[arg(long, default_missing_value="true", num_args=0..=1)]
47    pub post_to_github: Option<bool>,
48
49    /// The GitHub token for authentication, only used if post_to_github is true.
50    #[arg(long)]
51    pub github_token: Option<String>,
52
53    /// The GitHub repository owner.
54    #[arg(long)]
55    pub repo_owner: Option<String>,
56
57    /// The GitHub repository name.
58    #[arg(long)]
59    pub repo_name: Option<String>,
60
61    /// The GitHub PR number.
62    #[arg(long)]
63    pub pr_number: Option<String>,
64
65    /// The name of the branch.
66    #[arg(long)]
67    pub branch_name: Option<String>,
68
69    /// The commit hash.
70    #[arg(long)]
71    pub commit_hash: Option<String>,
72
73    /// The author of the commit.
74    #[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    // Set environment variables to configure the prover.
86    if let Some(shard_size) = args.shard_size {
87        std::env::set_var("SHARD_SIZE", format!("{}", 1 << shard_size));
88    }
89
90    // Choose which programs to evaluate.
91    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    // Run the evaluations on each program.
103    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    // Prepare and format the results.
113    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    // Print results
118    println!("{}", results_text.join("\n"));
119
120    // Post to Slack if applicable
121    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    // Post to GitHub PR if applicable
133    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    // Exit with an error if any programs failed.
146    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        // Add an extra asterisk for GitHub bold formatting
297        formatted_message.push_str(&title.replace('*', "**"));
298        formatted_message.push('\n');
299    }
300
301    if let Some(details) = results_text.get(1) {
302        // Add an extra asterisk for GitHub bold formatting
303        formatted_message.push_str(&details.replace('*', "**"));
304        formatted_message.push('\n');
305    }
306
307    if let Some(table) = results_text.get(2) {
308        // Remove the triple backticks as GitHub doesn't require them for table formatting
309        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    // Get all comments on the PR
327    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    // Look for an existing comment from our bot
338    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        // Update the existing comment
347        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        // Create a new comment
363        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}