diff options
author | iximeow <me@iximeow.net> | 2023-07-04 14:21:58 -0700 |
---|---|---|
committer | iximeow <me@iximeow.net> | 2023-07-04 14:21:58 -0700 |
commit | d0e845d3ac4530cf281e90d8a3634355d153c8be (patch) | |
tree | 6d679b58170adf6b9c8bdc2e53e836413c6458ed /src | |
parent | 0f97747c2e3e6f3178e414adc5b6022ead275601 (diff) |
adjust metrics to show multi-host summaries?
Diffstat (limited to 'src')
-rw-r--r-- | src/dbctx.rs | 28 | ||||
-rw-r--r-- | src/main.rs | 120 | ||||
-rw-r--r-- | src/sql.rs | 20 |
3 files changed, 151 insertions, 17 deletions
diff --git a/src/dbctx.rs b/src/dbctx.rs index ae06d21..0c7d488 100644 --- a/src/dbctx.rs +++ b/src/dbctx.rs @@ -666,6 +666,34 @@ impl DbCtx { .map_err(|e| e.to_string()) } + pub fn host_model_info(&self, host_id: u64) -> Result<(String, String, String, String), String> { + let conn = self.conn.lock().unwrap(); + conn + .query_row("select hostname, cpu_vendor_id, cpu_family, cpu_model from hosts;", [host_id], |row| { + Ok(( + row.get(0).unwrap(), + row.get(1).unwrap(), + row.get(2).unwrap(), + row.get(3).unwrap(), + )) + }) + .map_err(|e| e.to_string()) + } + + pub fn runs_for_job_one_per_host(&self, job_id: u64) -> Result<Vec<Run>, String> { + let conn = self.conn.lock().unwrap(); + let mut runs_query = conn.prepare(crate::sql::RUNS_FOR_JOB).unwrap(); + let mut runs_results = runs_query.query([job_id]).unwrap(); + + let mut results = Vec::new(); + + while let Some(row) = runs_results.next().unwrap() { + results.push(crate::sql::row2run(row)); + } + + Ok(results) + } + pub fn last_run_for_job(&self, job_id: u64) -> Result<Option<Run>, String> { let conn = self.conn.lock().unwrap(); diff --git a/src/main.rs b/src/main.rs index b6e744b..2658e9d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,7 @@ use chrono::{Utc, TimeZone}; use lazy_static::lazy_static; use std::sync::RwLock; +use std::collections::HashMap; use serde_derive::{Deserialize, Serialize}; use tokio::spawn; use std::path::PathBuf; @@ -612,22 +613,7 @@ async fn handle_commit_status(Path(path): Path<(String, String, String)>, State( } } - let metrics = ctx.dbctx.metrics_for_run(run.id).unwrap(); - let metrics_section = if metrics.len() > 0 { - let mut section = String::new(); - section.push_str("<div>"); - section.push_str("<h3>metrics</h3>"); - section.push_str("<table style='font-family: monospace;'>"); - section.push_str("<tr><th>name</th><th>value</th></tr>"); - for metric in metrics { - section.push_str(&format!("<tr><td>{}</td><td>{}</td></tr>", &metric.name, &metric.value)); - } - section.push_str("</table>"); - section.push_str("</div>"); - Some(section) - } else { - None - }; + let metrics = summarize_job_metrics(&ctx.dbctx, run.id, run.job_id).unwrap(); let mut html = String::new(); html.push_str("<html>\n"); @@ -646,7 +632,7 @@ async fn handle_commit_status(Path(path): Path<(String, String, String)>, State( html.push_str(" <div>artifacts</div>\n"); html.push_str(&artifacts_fragment); } - if let Some(metrics) = metrics_section { + if let Some(metrics) = metrics { html.push_str(&metrics); } html.push_str(" </body>\n"); @@ -655,6 +641,86 @@ async fn handle_commit_status(Path(path): Path<(String, String, String)>, State( (StatusCode::OK, Html(html)) } +fn summarize_job_metrics(dbctx: &Arc<DbCtx>, run_id: u64, job_id: u64) -> Result<Option<String>, String> { + let runs = dbctx.runs_for_job_one_per_host(job_id)?; + + let mut section = String::new(); + section.push_str("<div>\n"); + section.push_str("<h3>metrics</h3>\n"); + section.push_str("<table style='font-family: monospace;'>\n"); + + if runs.len() == 1 { + let metrics = dbctx.metrics_for_run(run_id).unwrap(); + if metrics.is_empty() { + return Ok(None); + } + + section.push_str("<tr><th>name</th><th>value</th></tr>"); + for metric in metrics { + section.push_str(&format!("<tr><td>{}</td><td>{}</td></tr>", &metric.name, &metric.value)); + } + } else { + // very silly ordering issue: need an authoritative ordering of metrics to display metrics + // in a consistent order across runs (though they SHOULD all be ordered the same). + // + // the first run might not have all metrics (first run could be on the slowest build host + // f.ex), so for now just assume that metrics *will* be consistently ordered and build a + // list of metrics from the longest list of metrics we've seen. builders do not support + // concurrency so at least the per-run metrics-order-consistency assumption should hold.. + let mut all_names: Vec<String> = Vec::new(); + + let all_metrics: Vec<(HashMap<String, String>, HostDesc)> = runs.iter().map(|run| { + let metrics = dbctx.metrics_for_run(run.id).unwrap(); + + let mut metrics_map = HashMap::new(); + for metric in metrics.into_iter() { + if !all_names.contains(&metric.name) { + all_names.push(metric.name.clone()); + } + metrics_map.insert(metric.name, metric.value); + } + + let (hostname, cpu_vendor_id, cpu_family, cpu_model) = match run.host_id { + Some(host_id) => { + dbctx.host_model_info(host_id).unwrap() + } + None => { + ("unknown".to_string(), "unknown".to_string(), "0".to_string(), "0".to_string()) + } + }; + + (metrics_map, HostDesc::from_parts(hostname, cpu_vendor_id, cpu_family, cpu_model)) + }).collect(); + + if all_metrics.is_empty() { + return Ok(None); + } + + let mut header = "<tr><th>name</th>".to_string(); + for (_, host) in all_metrics.iter() { + header.push_str(&format!("<th>{} - {}</th>", &host.hostname, &host.cpu_desc)); + } + header.push_str("</tr>\n"); + section.push_str(&header); + + for name in all_names.iter() { + let mut row = format!("<tr><td>{}</td>", &name); + for (metrics, _) in all_metrics.iter() { + let value = metrics.get(name) + .map(|x| x.clone()) + .unwrap_or_else(String::new); + row.push_str(&format!("<td>{}</td>", value)); + } + row.push_str("</tr>\n"); + section.push_str(&row); + } + }; + section.push_str("</table>\n"); + section.push_str("</div>\n"); + + Ok(Some(section)) +} + async fn handle_get_artifact(Path(path): Path<(String, String)>, State(ctx): State<WebserverState>) -> impl IntoResponse { eprintln!("get artifact, run={}, artifact={}", path.0, path.1); let run: u64 = path.0.parse().unwrap(); @@ -1002,3 +1068,23 @@ async fn main() { tokio::time::sleep(std::time::Duration::from_millis(1000)).await; } } + +struct HostDesc { + hostname: String, + cpu_desc: String, +} +impl HostDesc { + fn from_parts(hostname: String, vendor_id: String, cpu_family: String, model: String) -> Self { + let cpu_desc = match (vendor_id.as_str(), cpu_family.as_str(), model.as_str()) { + ("Arm Limited", "8", "0xd03") => "aarch64 A53".to_string(), + ("GenuineIntel", "6", "85") => "x86_64 Skylake".to_string(), + ("AuthenticAMD", "23", "113") => "x86_64 Matisse".to_string(), + (vendor, family, model) => format!("unknown {}:{}:{}", vendor, family, model) + }; + + HostDesc { + hostname, + cpu_desc, + } + } +} @@ -176,6 +176,12 @@ pub const JOB_BY_ID: &'static str = "\ pub const METRICS_FOR_RUN: &'static str = "\ select * from metrics where run_id=?1 order by id asc;"; +pub const METRICS_FOR_JOB: &'static str = "\ + select metrics.id, metrics.run_id, metrics.name, metrics.value from metrics \ + join runs on runs.id=metrics.run_id \ + where runs.job_id=?1 \ + order by metrics.run_id desc, metrics.id desc;"; + pub const COMMIT_TO_ID: &'static str = "\ select id from commits where sha=?1;"; @@ -202,6 +208,20 @@ pub const LAST_RUN_FOR_JOB: &'static str = "\ build_result, final_status from runs where job_id=?1 order by started_time desc limit 1;"; +pub const RUNS_FOR_JOB: &'static str = "\ + select id, + job_id, + artifacts_path, + state, + host_id, + build_token, + created_time, + started_time, + complete_time, + run_timeout, + build_result, + final_status from runs where job_id=?1 group by host_id order by started_time desc, state asc;"; + pub const SELECT_ALL_RUNS_WITH_JOB_INFO: &'static str = "\ select jobs.id as job_id, runs.id as run_id, runs.state, runs.created_time, jobs.commit_id, jobs.run_preferences from jobs join runs on jobs.id=runs.job_id |