diff --git a/database/src/lib.rs b/database/src/lib.rs index a8f39c28f..a78a73e1b 100644 --- a/database/src/lib.rs +++ b/database/src/lib.rs @@ -1147,6 +1147,10 @@ impl BenchmarkJob { | BenchmarkJobStatus::Completed { collector_name, .. } => Some(collector_name), } } + + pub fn status(&self) -> &BenchmarkJobStatus { + &self.status + } } /// Describes the final state of a job @@ -1201,3 +1205,11 @@ impl CollectorConfig { self.date_added } } + +/// The data that can be retrived from the database directly to populate the +/// status page +#[derive(Debug, PartialEq)] +pub struct PartialStatusPageData { + pub completed_requests: Vec<(BenchmarkRequest, String, Vec)>, + pub in_progress: Vec<(BenchmarkRequest, Vec)>, +} diff --git a/database/src/pool.rs b/database/src/pool.rs index dd0571045..fe2fea3ef 100644 --- a/database/src/pool.rs +++ b/database/src/pool.rs @@ -2,7 +2,7 @@ use crate::selector::CompileTestCase; use crate::{ ArtifactCollection, ArtifactId, ArtifactIdNumber, BenchmarkJob, BenchmarkJobConclusion, BenchmarkRequest, BenchmarkRequestIndex, BenchmarkRequestStatus, BenchmarkSet, CodegenBackend, - CollectorConfig, CompileBenchmark, Target, + CollectorConfig, CompileBenchmark, PartialStatusPageData, Target, }; use crate::{CollectionId, Index, Profile, QueuedCommit, Scenario, Step}; use chrono::{DateTime, Utc}; @@ -265,6 +265,8 @@ pub trait Connection: Send + Sync { id: u32, benchmark_job_conculsion: &BenchmarkJobConclusion, ) -> anyhow::Result<()>; + + async fn get_status_page_data(&self) -> anyhow::Result; } #[async_trait::async_trait] @@ -387,6 +389,7 @@ mod tests { use super::*; use crate::metric::Metric; use crate::tests::run_postgres_test; + use crate::BenchmarkJobStatus; use crate::{tests::run_db_test, BenchmarkRequestType, Commit, CommitType, Date}; use chrono::Utc; use std::str::FromStr; @@ -969,6 +972,142 @@ mod tests { let completed = db.load_benchmark_request_index().await.unwrap(); assert!(completed.contains_tag("sha-1")); + Ok(ctx) + }) + .await; + } + + #[tokio::test] + async fn get_status_page_data() { + run_postgres_test(|ctx| async { + let db = ctx.db_client().connection().await; + let benchmark_set = BenchmarkSet(0u32); + let time = chrono::DateTime::from_str("2021-09-01T00:00:00.000Z").unwrap(); + let tag = "sha-1"; + let tag_two = "sha-2"; + let collector_name = "collector-1"; + let target = Target::X86_64UnknownLinuxGnu; + + db.add_collector_config(collector_name, &target, benchmark_set.0, true) + .await + .unwrap(); + + let benchmark_request = BenchmarkRequest::create_release(tag, time); + db.insert_benchmark_request(&benchmark_request) + .await + .unwrap(); + + complete_request(&*db, tag, collector_name, benchmark_set.0, &target).await; + // record a couple of errors against the tag + let artifact_id = db.artifact_id(&ArtifactId::Tag(tag.to_string())).await; + + db.record_error(artifact_id, "example-1", "This is an error") + .await; + db.record_error(artifact_id, "example-2", "This is another error") + .await; + + let benchmark_request_two = BenchmarkRequest::create_release(tag_two, time); + db.insert_benchmark_request(&benchmark_request_two) + .await + .unwrap(); + + db.enqueue_benchmark_job( + benchmark_request_two.tag().unwrap(), + &target, + &CodegenBackend::Llvm, + &Profile::Opt, + benchmark_set.0, + ) + .await + .unwrap(); + db.enqueue_benchmark_job( + benchmark_request_two.tag().unwrap(), + &target, + &CodegenBackend::Llvm, + &Profile::Debug, + benchmark_set.0, + ) + .await + .unwrap(); + + db.update_benchmark_request_status( + benchmark_request_two.tag().unwrap(), + BenchmarkRequestStatus::InProgress, + ) + .await + .unwrap(); + + let status_page_data = db.get_status_page_data().await.unwrap(); + + assert!(status_page_data.completed_requests.len() == 1); + assert_eq!(status_page_data.completed_requests[0].0.tag().unwrap(), tag); + assert!(matches!( + status_page_data.completed_requests[0].0.status(), + BenchmarkRequestStatus::Completed { .. } + )); + // can't really test duration + // ensure errors are correct + assert_eq!( + status_page_data.completed_requests[0].2[0], + "This is an error".to_string() + ); + assert_eq!( + status_page_data.completed_requests[0].2[1], + "This is another error".to_string() + ); + + assert!(status_page_data.in_progress.len() == 1); + // we should have 2 jobs + assert!(status_page_data.in_progress[0].1.len() == 2); + // the request should be in progress + assert!(matches!( + status_page_data.in_progress[0].0.status(), + BenchmarkRequestStatus::InProgress + )); + + // Test the first job + assert!(matches!( + status_page_data.in_progress[0].1[0].target(), + Target::X86_64UnknownLinuxGnu + )); + assert!(matches!( + status_page_data.in_progress[0].1[0].status(), + BenchmarkJobStatus::Queued + )); + assert!(matches!( + status_page_data.in_progress[0].1[0].backend(), + CodegenBackend::Llvm + )); + assert!(matches!( + status_page_data.in_progress[0].1[0].profile(), + Profile::Opt + )); + assert_eq!( + status_page_data.in_progress[0].1[0].benchmark_set(), + &benchmark_set + ); + + // test the second job + assert!(matches!( + status_page_data.in_progress[0].1[1].target(), + Target::X86_64UnknownLinuxGnu + )); + assert!(matches!( + status_page_data.in_progress[0].1[1].status(), + BenchmarkJobStatus::Queued + )); + assert!(matches!( + status_page_data.in_progress[0].1[1].backend(), + CodegenBackend::Llvm + )); + assert!(matches!( + status_page_data.in_progress[0].1[1].profile(), + Profile::Debug + )); + assert_eq!( + status_page_data.in_progress[0].1[1].benchmark_set(), + &benchmark_set + ); Ok(ctx) }) diff --git a/database/src/pool/postgres.rs b/database/src/pool/postgres.rs index 9483a6239..40c2542f4 100644 --- a/database/src/pool/postgres.rs +++ b/database/src/pool/postgres.rs @@ -4,10 +4,10 @@ use crate::{ ArtifactCollection, ArtifactId, ArtifactIdNumber, Benchmark, BenchmarkJob, BenchmarkJobConclusion, BenchmarkJobStatus, BenchmarkRequest, BenchmarkRequestIndex, BenchmarkRequestStatus, BenchmarkRequestType, BenchmarkSet, CodegenBackend, CollectionId, - CollectorConfig, Commit, CommitType, CompileBenchmark, Date, Index, Profile, QueuedCommit, - Scenario, Target, BENCHMARK_JOB_STATUS_FAILURE_STR, BENCHMARK_JOB_STATUS_IN_PROGRESS_STR, - BENCHMARK_JOB_STATUS_QUEUED_STR, BENCHMARK_JOB_STATUS_SUCCESS_STR, - BENCHMARK_REQUEST_MASTER_STR, BENCHMARK_REQUEST_RELEASE_STR, + CollectorConfig, Commit, CommitType, CompileBenchmark, Date, Index, PartialStatusPageData, + Profile, QueuedCommit, Scenario, Target, BENCHMARK_JOB_STATUS_FAILURE_STR, + BENCHMARK_JOB_STATUS_IN_PROGRESS_STR, BENCHMARK_JOB_STATUS_QUEUED_STR, + BENCHMARK_JOB_STATUS_SUCCESS_STR, BENCHMARK_REQUEST_MASTER_STR, BENCHMARK_REQUEST_RELEASE_STR, BENCHMARK_REQUEST_STATUS_ARTIFACTS_READY_STR, BENCHMARK_REQUEST_STATUS_COMPLETED_STR, BENCHMARK_REQUEST_STATUS_IN_PROGRESS_STR, BENCHMARK_REQUEST_STATUS_WAITING_FOR_ARTIFACTS_STR, BENCHMARK_REQUEST_TRY_STR, @@ -21,8 +21,8 @@ use std::str::FromStr; use std::sync::Arc; use std::time::Duration; use tokio::sync::Mutex; -use tokio_postgres::GenericClient; use tokio_postgres::Statement; +use tokio_postgres::{GenericClient, Row}; pub struct Postgres(String, std::sync::Once); @@ -663,6 +663,9 @@ impl PostgresConnection { } } +const BENCHMARK_REQUEST_COLUMNS: &str = + "tag, parent_sha, pr, commit_type, status, created_at, completed_at, backends, profiles"; + #[async_trait::async_trait] impl

Connection for P where @@ -1594,16 +1597,7 @@ where async fn load_pending_benchmark_requests(&self) -> anyhow::Result> { let query = format!( r#" - SELECT - tag, - parent_sha, - pr, - commit_type, - status, - created_at, - completed_at, - backends, - profiles + SELECT {BENCHMARK_REQUEST_COLUMNS} FROM benchmark_request WHERE status IN('{BENCHMARK_REQUEST_STATUS_ARTIFACTS_READY_STR}', '{BENCHMARK_REQUEST_STATUS_IN_PROGRESS_STR}')"# ); @@ -1616,59 +1610,7 @@ where let requests = rows .into_iter() - .map(|row| { - let tag = row.get::<_, Option>(0); - let parent_sha = row.get::<_, Option>(1); - let pr = row.get::<_, Option>(2); - let commit_type = row.get::<_, &str>(3); - let status = row.get::<_, &str>(4); - let created_at = row.get::<_, DateTime>(5); - let completed_at = row.get::<_, Option>>(6); - let backends = row.get::<_, String>(7); - let profiles = row.get::<_, String>(8); - - let pr = pr.map(|v| v as u32); - - let status = - BenchmarkRequestStatus::from_str_and_completion_date(status, completed_at) - .expect("Invalid BenchmarkRequestStatus data in the database"); - - match commit_type { - BENCHMARK_REQUEST_TRY_STR => BenchmarkRequest { - commit_type: BenchmarkRequestType::Try { - sha: tag, - parent_sha, - pr: pr.expect("Try commit in the DB without a PR"), - }, - created_at, - status, - backends, - profiles, - }, - BENCHMARK_REQUEST_MASTER_STR => BenchmarkRequest { - commit_type: BenchmarkRequestType::Master { - sha: tag.expect("Master commit in the DB without a SHA"), - parent_sha: parent_sha - .expect("Master commit in the DB without a parent SHA"), - pr: pr.expect("Master commit in the DB without a PR"), - }, - created_at, - status, - backends, - profiles, - }, - BENCHMARK_REQUEST_RELEASE_STR => BenchmarkRequest { - commit_type: BenchmarkRequestType::Release { - tag: tag.expect("Release commit in the DB without a SHA"), - }, - created_at, - status, - backends, - profiles, - }, - _ => panic!("Invalid `commit_type` for `BenchmarkRequest` {commit_type}",), - } - }) + .map(|it| row_to_benchmark_request(&it)) .collect(); Ok(requests) } @@ -1970,6 +1912,297 @@ where .context("Failed to mark benchmark job as completed")?; Ok(()) } + + async fn get_status_page_data(&self) -> anyhow::Result { + let max_completed_requests = 7; + + // Returns in progress requests along with their associated jobs + let in_progress_query = format!( + " + WITH in_progress_requests AS ( + SELECT + {BENCHMARK_REQUEST_COLUMNS} + FROM + benchmark_request + WHERE + status = '{BENCHMARK_REQUEST_STATUS_IN_PROGRESS_STR}' + ORDER BY + completed_at + ), in_progress_jobs AS ( + SELECT + request_tag AS tag, + ARRAY_AGG( + ROW( + job_queue.id, + job_queue.request_tag, + job_queue.target, + job_queue.backend, + job_queue.profile, + job_queue.benchmark_set, + job_queue.status, + job_queue.created_at, + job_queue.started_at, + job_queue.completed_at, + job_queue.retry, + job_queue.collector_name + )::TEXT + ) AS jobs + FROM + job_queue + LEFT JOIN in_progress_requests ON job_queue.request_tag = in_progress_requests.tag + GROUP BY + job_queue.request_tag + ) + SELECT + in_progress_requests.*, + in_progress_jobs.jobs + FROM + in_progress_requests + LEFT JOIN + in_progress_jobs ON in_progress_requests.tag = in_progress_jobs.tag; + " + ); + + // Gets requests along with how long the request took (latest job finish + // - earliest job start) and associated errors with the request if they + // exist + let completed_requests_query = format!( + " + WITH completed AS ( + SELECT + {BENCHMARK_REQUEST_COLUMNS} + FROM + benchmark_request + WHERE + status = '{BENCHMARK_REQUEST_STATUS_COMPLETED_STR}' + ORDER BY + completed_at + DESC LIMIT {max_completed_requests} + ), jobs AS ( + SELECT + completed.tag, + job_queue.started_at, + job_queue.completed_at + FROM + job_queue + LEFT JOIN completed ON job_queue.request_tag = completed.tag + ), stats AS ( + SELECT + tag, + MAX(jobs.completed_at - jobs.started_at) AS duration + FROM + jobs + GROUP BY + tag + ), artifacts AS ( + SELECT + artifact.id, + \"name\" + FROM + artifact + LEFT JOIN completed ON artifact.name = completed.tag + ), errors AS ( + SELECT + artifacts.name AS tag, + ARRAY_AGG(error) AS errors + FROM + error + LEFT JOIN + artifacts ON error.aid = artifacts.id + GROUP BY + tag + ) + SELECT + completed.*, + stats.duration::TEXT, + errors.errors AS errors + FROM + completed + LEFT JOIN stats ON stats.tag = completed.tag + LEFT JOIN errors ON errors.tag = completed.tag; + " + ); + + let in_progress: Vec<(BenchmarkRequest, Vec)> = self + .conn() + .query(&in_progress_query, &[]) + .await? + .iter() + .map(|it| { + let benchmark_request = row_to_benchmark_request(it); + let jobs: Vec = it + .get::<_, Vec>("jobs") + .iter() + .map(|it| benchmark_job_str_to_type(it).unwrap()) + .collect(); + (benchmark_request, jobs) + }) + .collect(); + + let completed_requests: Vec<(BenchmarkRequest, String, Vec)> = self + .conn() + .query(&completed_requests_query, &[]) + .await? + .iter() + .map(|it| { + ( + row_to_benchmark_request(it), + // Duration being a string feels odd, but we don't need to + // perform any computations on it + it.get::<_, String>(9), + // The errors, if there are none this will be an empty vector + it.get::<_, Vec>(10), + ) + }) + .collect(); + + Ok(PartialStatusPageData { + completed_requests, + in_progress, + }) + } +} + +fn row_to_benchmark_request(row: &Row) -> BenchmarkRequest { + let tag = row.get::<_, Option>(0); + let parent_sha = row.get::<_, Option>(1); + let pr = row.get::<_, Option>(2); + let commit_type = row.get::<_, &str>(3); + let status = row.get::<_, &str>(4); + let created_at = row.get::<_, DateTime>(5); + let completed_at = row.get::<_, Option>>(6); + let backends = row.get::<_, String>(7); + let profiles = row.get::<_, String>(8); + + let pr = pr.map(|v| v as u32); + + let status = BenchmarkRequestStatus::from_str_and_completion_date(status, completed_at) + .expect("Invalid BenchmarkRequestStatus data in the database"); + + match commit_type { + BENCHMARK_REQUEST_TRY_STR => BenchmarkRequest { + commit_type: BenchmarkRequestType::Try { + sha: tag, + parent_sha, + pr: pr.expect("Try commit in the DB without a PR"), + }, + created_at, + status, + backends, + profiles, + }, + BENCHMARK_REQUEST_MASTER_STR => BenchmarkRequest { + commit_type: BenchmarkRequestType::Master { + sha: tag.expect("Master commit in the DB without a SHA"), + parent_sha: parent_sha.expect("Master commit in the DB without a parent SHA"), + pr: pr.expect("Master commit in the DB without a PR"), + }, + created_at, + status, + backends, + profiles, + }, + BENCHMARK_REQUEST_RELEASE_STR => BenchmarkRequest { + commit_type: BenchmarkRequestType::Release { + tag: tag.expect("Release commit in the DB without a SHA"), + }, + created_at, + status, + backends, + profiles, + }, + _ => panic!("Invalid `commit_type` for `BenchmarkRequest` {commit_type}",), + } +} + +fn parse_timestamp(cell: &str) -> anyhow::Result>> { + if cell.is_empty() { + Ok(None) + } else { + // Massage postgres date string into something we can parse in Rust + // to a date + let raw_date = cell.trim_matches('"').replace(' ', "T") + ":00"; + Ok(Some( + DateTime::parse_from_rfc3339(&raw_date)?.with_timezone(&Utc), + )) + } +} + +fn benchmark_job_str_to_type(src: &str) -> anyhow::Result { + let line = src.trim_start_matches('(').trim_end_matches(')'); + + let mut col = line.split(','); + + let id: u32 = col.next().ok_or_else(|| anyhow::anyhow!("id"))?.parse()?; + let request_tag = col + .next() + .ok_or_else(|| anyhow::anyhow!("request_tag"))? + .to_owned(); + let target = col + .next() + .ok_or_else(|| anyhow::anyhow!("target"))? + .parse::() + .map_err(|e| anyhow::anyhow!(e))?; + let backend = col + .next() + .ok_or_else(|| anyhow::anyhow!("backend"))? + .parse::() + .map_err(|e| anyhow::anyhow!(e))?; + let profile = col + .next() + .ok_or_else(|| anyhow::anyhow!("profile"))? + .parse::() + .map_err(|e| anyhow::anyhow!(e))?; + let benchmark_set = BenchmarkSet( + col.next() + .ok_or_else(|| anyhow::anyhow!("benchmark_set"))? + .parse()?, + ); + + let status_str = col.next().ok_or_else(|| anyhow::anyhow!("status"))?; + let created_at = parse_timestamp(col.next().ok_or_else(|| anyhow::anyhow!("created_at"))?)? + .ok_or_else(|| anyhow::anyhow!("created_at missing"))?; + + let started_at = parse_timestamp(col.next().unwrap_or(""))?; + let completed_at = parse_timestamp(col.next().unwrap_or(""))?; + let retry: u32 = col + .next() + .ok_or_else(|| anyhow::anyhow!("retry"))? + .parse()?; + let collector_name_raw = col.next().unwrap_or("").to_owned(); + + let status = match status_str { + BENCHMARK_JOB_STATUS_QUEUED_STR => BenchmarkJobStatus::Queued, + + BENCHMARK_JOB_STATUS_IN_PROGRESS_STR => BenchmarkJobStatus::InProgress { + started_at: started_at.ok_or_else(|| anyhow::anyhow!("started_at missing"))?, + collector_name: collector_name_raw, + }, + + BENCHMARK_JOB_STATUS_SUCCESS_STR | BENCHMARK_JOB_STATUS_FAILURE_STR => { + BenchmarkJobStatus::Completed { + started_at: started_at.ok_or_else(|| anyhow::anyhow!("started_at missing"))?, + completed_at: completed_at + .ok_or_else(|| anyhow::anyhow!("completed_at missing"))?, + collector_name: collector_name_raw, + success: status_str == BENCHMARK_JOB_STATUS_SUCCESS_STR, + } + } + + _ => anyhow::bail!("unknown status `{status_str}`"), + }; + + Ok(BenchmarkJob { + id, + target, + backend, + profile, + request_tag, + benchmark_set, + created_at, + status, + retry, + }) } fn parse_artifact_id(ty: &str, sha: &str, date: Option>) -> ArtifactId { diff --git a/database/src/pool/sqlite.rs b/database/src/pool/sqlite.rs index c4391ba07..9c973baea 100644 --- a/database/src/pool/sqlite.rs +++ b/database/src/pool/sqlite.rs @@ -3,7 +3,8 @@ use crate::selector::CompileTestCase; use crate::{ ArtifactCollection, ArtifactId, Benchmark, BenchmarkJob, BenchmarkJobConclusion, BenchmarkRequest, BenchmarkRequestIndex, BenchmarkRequestStatus, BenchmarkSet, CodegenBackend, - CollectionId, CollectorConfig, Commit, CommitType, CompileBenchmark, Date, Profile, Target, + CollectionId, CollectorConfig, Commit, CommitType, CompileBenchmark, Date, + PartialStatusPageData, Profile, Target, }; use crate::{ArtifactIdNumber, Index, QueuedCommit}; use chrono::{DateTime, TimeZone, Utc}; @@ -1365,6 +1366,10 @@ impl Connection for SqliteConnection { ) -> anyhow::Result<()> { no_queue_implementation_abort!() } + + async fn get_status_page_data(&self) -> anyhow::Result { + no_queue_implementation_abort!() + } } fn parse_artifact_id(ty: &str, sha: &str, date: Option) -> ArtifactId {