diff --git a/database/src/lib.rs b/database/src/lib.rs index e6f918845..6d112e6c4 100644 --- a/database/src/lib.rs +++ b/database/src/lib.rs @@ -199,7 +199,7 @@ impl Ord for Commit { /// The compilation profile (i.e., how the crate was built) #[derive( - Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize, + Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Deserialize, serde::Serialize, )] pub enum Profile { /// A checked build (i.e., no codegen) @@ -356,9 +356,7 @@ impl PartialOrd for Scenario { /// https://doc.rust-lang.org/nightly/rustc/platform-support.html /// /// Presently we only support x86_64 -#[derive( - Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize, -)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub enum Target { /// `x86_64-unknown-linux-gnu` X86_64UnknownLinuxGnu, @@ -393,9 +391,7 @@ impl fmt::Display for Target { } /// The codegen backend used for compilation. -#[derive( - Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize, -)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub enum CodegenBackend { /// The default LLVM backend Llvm, @@ -824,7 +820,7 @@ const BENCHMARK_REQUEST_STATUS_IN_PROGRESS_STR: &str = "in_progress"; const BENCHMARK_REQUEST_STATUS_COMPLETED_STR: &str = "completed"; impl BenchmarkRequestStatus { - pub(crate) fn as_str(&self) -> &str { + pub fn as_str(&self) -> &str { match self { Self::WaitingForArtifacts => BENCHMARK_REQUEST_STATUS_WAITING_FOR_ARTIFACTS_STR, Self::ArtifactsReady => BENCHMARK_REQUEST_STATUS_ARTIFACTS_READY_STR, @@ -986,6 +982,10 @@ impl BenchmarkRequest { self.created_at } + pub fn commit_date(&self) -> Option> { + self.commit_date + } + pub fn is_master(&self) -> bool { matches!(self.commit_type, BenchmarkRequestType::Master { .. }) } @@ -1100,8 +1100,8 @@ impl fmt::Display for BenchmarkJobStatus { } } -#[derive(Debug, Copy, Clone, PartialEq)] -pub struct BenchmarkSet(u32); +#[derive(Debug, Copy, Clone, PartialEq, serde::Deserialize, serde::Serialize)] +pub struct BenchmarkSet(pub u32); impl BenchmarkSet { pub fn new(id: u32) -> Self { @@ -1169,6 +1169,10 @@ impl BenchmarkJob { pub fn status(&self) -> &BenchmarkJobStatus { &self.status } + + pub fn created_at(&self) -> DateTime { + self.created_at + } } /// Describes the final state of a job diff --git a/site/frontend/package.json b/site/frontend/package.json index 2e47e95f0..452fe6332 100644 --- a/site/frontend/package.json +++ b/site/frontend/package.json @@ -38,6 +38,10 @@ "source": "src/pages/status.ts", "distDir": "dist/scripts" }, + "status_new": { + "source": "src/pages/status_new.ts", + "distDir": "dist/scripts" + }, "bootstrap": { "source": "src/pages/bootstrap.ts", "distDir": "dist/scripts" diff --git a/site/frontend/src/pages/status_new.ts b/site/frontend/src/pages/status_new.ts new file mode 100644 index 000000000..1af392fa8 --- /dev/null +++ b/site/frontend/src/pages/status_new.ts @@ -0,0 +1,8 @@ +import Status from "./status_new/page.vue"; +import {createApp} from "vue"; +import WithSuspense from "../components/with-suspense.vue"; + +const app = createApp(WithSuspense, { + component: Status, +}); +app.mount("#app"); diff --git a/site/frontend/src/pages/status_new/data.ts b/site/frontend/src/pages/status_new/data.ts new file mode 100644 index 000000000..6347e72c6 --- /dev/null +++ b/site/frontend/src/pages/status_new/data.ts @@ -0,0 +1,139 @@ +type BenchmarkRequestStatusComplete = { + state: "completed"; + completedAt: string; + duration: number; // time in milliseconds +}; + +type BenchmarkRequestStatusInProgress = { + state: "in_progress"; +}; + +type BenchmarkRequestStatusArtifactsReady = { + state: "artifacts_ready"; +}; + +export type BenchmarkRequestStatus = + | BenchmarkRequestStatusComplete + | BenchmarkRequestStatusInProgress + | BenchmarkRequestStatusArtifactsReady; + +type BenchmarkRequestTypeTry = { + type: "Try"; + tag: string | null; + parent_sha: string | null; + pr: number; +}; + +type BenchmarkRequestTypeMaster = { + type: "Master"; + tag: string; + parent_sha: string; + pr: number; +}; + +type BenchmarkRequestTypeRelease = { + type: "Try"; + tag: string; +}; + +type BenchmarkRequestType = + | BenchmarkRequestTypeTry + | BenchmarkRequestTypeMaster + | BenchmarkRequestTypeRelease; + +export type BenchmarkRequestComplete = { + status: BenchmarkRequestStatusComplete; + requestType: BenchmarkRequestType; + commitDate: string | null; + createdAt: string | null; + backends: string[]; + profiles: string; + errors: string[]; +}; + +export type BenchmarkRequestInProgress = { + status: BenchmarkRequestStatusInProgress; + requestType: BenchmarkRequestType; + commitDate: string | null; + createdAt: string | null; + backends: string[]; + profiles: string; + errors: string[]; +}; + +export type BenchmarkRequestArtifactsReady = { + status: BenchmarkRequestStatusArtifactsReady; + requestType: BenchmarkRequestType; + commitDate: string | null; + createdAt: string | null; + backends: string[]; + profiles: string; + errors: string[]; +}; + +export type BenchmarkRequest = + | BenchmarkRequestComplete + | BenchmarkRequestInProgress + | BenchmarkRequestArtifactsReady; + +export type BenchmarkJobStatusQueued = { + state: "queued"; +}; + +export type BenchmarkJobStatusInProgress = { + state: "in_progress"; + startedAt: string; + collectorName: string; +}; + +export type BenchmarkJobStatusFailed = { + state: "failed"; + startedAt: string; + completedAt: string; + collectorName: string; +}; + +export type BenchmarkJobStatusSuccess = { + state: "success"; + startedAt: string; + completedAt: string; + collectorName: string; +}; + +export type BenchmarkJobStatus = + | BenchmarkJobStatusSuccess + | BenchmarkJobStatusFailed + | BenchmarkJobStatusInProgress + | BenchmarkJobStatusQueued; + +export type BenchmarkJob = { + target: string; + backend: string; + profile: string; + requestTag: string; + benchmarkSet: number; + createdAt: string; + status: BenchmarkJobStatus; + dequeCounter: number; +}; + +export type CollectorConfig = { + name: string; + target: string; + benchmarkSet: number; + isActive: boolean; + lastHeartbeatAt: string; + dateAdded: string; +}; + +export type StatusResponseInProgress = { + request: BenchmarkRequestInProgress; + jobs: BenchmarkJob[]; +}; + +export type StatusResponse = { + completed: BenchmarkRequestComplete[]; + inProgress: StatusResponseInProgress[]; + collectorConfigs: CollectorConfig[]; + queue: BenchmarkRequest[]; +}; diff --git a/site/frontend/src/pages/status_new/page.vue b/site/frontend/src/pages/status_new/page.vue new file mode 100644 index 000000000..6c3a46aab --- /dev/null +++ b/site/frontend/src/pages/status_new/page.vue @@ -0,0 +1,189 @@ + + + + + diff --git a/site/frontend/src/urls.ts b/site/frontend/src/urls.ts index f2f629270..37c97cb54 100644 --- a/site/frontend/src/urls.ts +++ b/site/frontend/src/urls.ts @@ -4,6 +4,7 @@ export const INFO_URL = `${BASE_URL}/info`; export const DASHBOARD_DATA_URL = `${BASE_URL}/dashboard`; export const STATUS_DATA_URL = `${BASE_URL}/status_page`; +export const STATUS_DATA_NEW_URL = `${BASE_URL}/status_page_new`; export const BOOTSTRAP_DATA_URL = `${BASE_URL}/bootstrap`; export const GRAPH_DATA_URL = `${BASE_URL}/graphs`; export const COMPARE_DATA_URL = `${BASE_URL}/get`; diff --git a/site/frontend/src/utils/getType.ts b/site/frontend/src/utils/getType.ts new file mode 100644 index 000000000..9a77c9891 --- /dev/null +++ b/site/frontend/src/utils/getType.ts @@ -0,0 +1,16 @@ +function getTypeString(obj: any): string { + return Object.prototype.toString + .call(obj) + .toLowerCase() + .replace("[", "") + .replace("]", "") + .split(" ")[1]; +} + +export function isObject(maybeObject: any): maybeObject is object { + return getTypeString(maybeObject) === "object"; +} + +export function isString(maybeString: any): maybeString is string { + return getTypeString(maybeString) === "string"; +} diff --git a/site/frontend/templates/pages/status_new.html b/site/frontend/templates/pages/status_new.html new file mode 100644 index 000000000..fb95a81fb --- /dev/null +++ b/site/frontend/templates/pages/status_new.html @@ -0,0 +1,10 @@ +{% extends "layout.html" %} +{% block head %} + +{% endblock %} +{% block content %} +
+{% endblock %} +{% block script %} + +{% endblock %} diff --git a/site/src/api.rs b/site/src/api.rs index 3b78d729d..3002a2fa0 100644 --- a/site/src/api.rs +++ b/site/src/api.rs @@ -391,6 +391,94 @@ pub mod status { } } +pub mod status_new { + use chrono::{DateTime, Utc}; + use database::BenchmarkSet; + use serde::Serialize; + + #[derive(Serialize, Debug)] + #[serde(rename_all = "camelCase")] + pub struct BenchmarkRequestStatusUi { + pub state: String, + pub completed_at: Option>, + pub duration: Option, + } + + #[derive(Serialize, Debug)] + #[serde(rename_all = "camelCase")] + pub struct BenchmarkRequestTypeUi { + pub r#type: String, + pub tag: Option, + pub parent_sha: Option, + pub pr: Option, + } + + #[derive(Serialize, Debug)] + #[serde(rename_all = "camelCase")] + pub struct BenchmarkRequestUi { + pub status: BenchmarkRequestStatusUi, + pub request_type: BenchmarkRequestTypeUi, + pub commit_date: Option>, + pub created_at: DateTime, + pub backends: Vec, + pub profiles: String, + pub errors: Vec, + } + + #[derive(Serialize, Debug)] + #[serde(rename_all = "camelCase")] + pub struct BenchmarkJobStatusUi { + pub state: String, + pub started_at: Option>, + pub completed_at: Option>, + pub collector_name: Option, + } + + #[derive(Serialize, Debug)] + #[serde(rename_all = "camelCase")] + pub struct BenchmarkJobUi { + pub target: String, + pub backend: String, + pub profile: String, + pub request_tag: String, + pub benchmark_set: BenchmarkSet, + pub created_at: DateTime, + pub status: BenchmarkJobStatusUi, + pub deque_counter: u32, + } + + #[derive(Serialize, Debug)] + #[serde(rename_all = "camelCase")] + pub struct BenchmarkInProgressUi { + pub request: BenchmarkRequestUi, + pub jobs: Vec, + } + + #[derive(Serialize, Debug)] + #[serde(rename_all = "camelCase")] + pub struct CollectorConfigUi { + pub name: String, + pub target: String, + pub benchmark_set: BenchmarkSet, + pub is_active: bool, + pub last_heartbeat_at: DateTime, + pub date_added: DateTime, + } + + #[derive(Serialize, Debug)] + #[serde(rename_all = "camelCase")] + pub struct Response { + /// Completed requests alongside any errors + pub completed: Vec, + /// In progress requests alongside the jobs associated with the request + pub in_progress: Vec, + /// Configuration for all collectors including ones that are inactive + pub collector_configs: Vec, + /// The current queue + pub queue: Vec, + } +} + pub mod self_profile_raw { use serde::{Deserialize, Serialize}; diff --git a/site/src/request_handlers.rs b/site/src/request_handlers.rs index d73e3cbc1..3efea3894 100644 --- a/site/src/request_handlers.rs +++ b/site/src/request_handlers.rs @@ -5,6 +5,7 @@ mod graph; mod next_artifact; mod self_profile; mod status_page; +mod status_page_new; pub use bootstrap::handle_bootstrap; pub use dashboard::handle_dashboard; @@ -19,6 +20,7 @@ pub use self_profile::{ handle_self_profile_raw_download, }; pub use status_page::handle_status_page; +pub use status_page_new::handle_status_page_new; use crate::api::{info, ServerResult}; use crate::load::SiteCtxt; diff --git a/site/src/request_handlers/status_page_new.rs b/site/src/request_handlers/status_page_new.rs new file mode 100644 index 000000000..13cb92440 --- /dev/null +++ b/site/src/request_handlers/status_page_new.rs @@ -0,0 +1,160 @@ +use std::sync::Arc; + +use crate::api::status_new::{ + BenchmarkInProgressUi, BenchmarkJobStatusUi, BenchmarkJobUi, BenchmarkRequestStatusUi, + BenchmarkRequestTypeUi, BenchmarkRequestUi, CollectorConfigUi, +}; +use crate::api::{status_new, ServerResult}; +use crate::job_queue::build_queue; +use crate::load::SiteCtxt; +use database::{ + BenchmarkJob, BenchmarkJobStatus, BenchmarkRequest, BenchmarkRequestStatus, CollectorConfig, +}; + +fn benchmark_request_status_to_ui(status: BenchmarkRequestStatus) -> BenchmarkRequestStatusUi { + let (completed_at, duration) = match status { + BenchmarkRequestStatus::Completed { + duration, + completed_at, + } => (Some(completed_at), u32::try_from(duration.as_millis()).ok()), + _ => (None, None), + }; + + BenchmarkRequestStatusUi { + state: status.as_str().to_owned(), + completed_at, + duration, + } +} + +fn benchmark_request_type_to_ui(req: &BenchmarkRequest) -> BenchmarkRequestTypeUi { + BenchmarkRequestTypeUi { + r#type: match (req.is_release(), req.is_master()) { + (true, _) => "Release", + (_, true) => "Master", + _ => "Try", + } + .to_owned(), + tag: req.tag().map(|it| it.to_owned()), + parent_sha: req.parent_sha().map(|it| it.to_owned()), + pr: req.pr().copied(), + } +} + +fn benchmark_request_to_ui( + req: &BenchmarkRequest, + errors: Vec, +) -> anyhow::Result { + Ok(BenchmarkRequestUi { + status: benchmark_request_status_to_ui(req.status()), + request_type: benchmark_request_type_to_ui(req), + commit_date: req.commit_date(), + created_at: req.created_at(), + backends: req.backends()?.iter().map(|it| it.to_string()).collect(), + profiles: req.profiles()?.iter().map(|it| it.to_string()).collect(), + errors, + }) +} + +fn benchmark_job_status_to_ui(status: &BenchmarkJobStatus) -> BenchmarkJobStatusUi { + let (started_at, completed_at, collector_name_ref) = match status { + BenchmarkJobStatus::Queued => (None, None, None), + BenchmarkJobStatus::InProgress { + started_at, + collector_name, + } => (Some(*started_at), None, Some(collector_name)), + BenchmarkJobStatus::Completed { + started_at, + completed_at, + collector_name, + .. + } => (Some(*started_at), Some(*completed_at), Some(collector_name)), + }; + + BenchmarkJobStatusUi { + state: status.as_str().to_owned(), + started_at, + completed_at, + collector_name: collector_name_ref.cloned(), + } +} + +fn benchmark_job_to_ui(job: &BenchmarkJob) -> BenchmarkJobUi { + BenchmarkJobUi { + target: job.target().as_str().to_owned(), + backend: job.backend().as_str().to_owned(), + profile: job.profile().as_str().to_owned(), + request_tag: job.request_tag().to_owned(), + benchmark_set: job.benchmark_set(), + created_at: job.created_at(), + status: benchmark_job_status_to_ui(job.status()), + deque_counter: job.deque_count(), + } +} + +fn collector_config_to_ui(config: &CollectorConfig) -> CollectorConfigUi { + CollectorConfigUi { + name: config.name().to_owned(), + target: config.target().as_str().to_owned(), + benchmark_set: config.benchmark_set(), + is_active: config.is_active(), + last_heartbeat_at: config.last_heartbeat_at(), + date_added: config.date_added(), + } +} + +pub async fn handle_status_page_new(ctxt: Arc) -> ServerResult { + let conn = ctxt.conn().await; + + let error_to_string = |e: anyhow::Error| e.to_string(); + + let collector_configs = conn + .get_collector_configs() + .await + .map_err(error_to_string)? + .iter() + .map(collector_config_to_ui) + .collect(); + // The query gives us `max_completed_requests` number of completed requests + // and all inprogress requests without us needing to specify + // + // @TODO; for `in_progress` requests we could look at the the completed + // `requests`, then use the `duration_ms` to display an estimated job + // finish time. Could also do that on the frontend but probably makes + // sense to do in SQL. + let partial_data = conn.get_status_page_data().await.map_err(error_to_string)?; + + let index = conn + .load_benchmark_request_index() + .await + .map_err(error_to_string)?; + + // Create the queue + // @TODO; do we need both the queue and the inprogress jobs from the database? + let queue = build_queue(&*conn, &index).await.map_err(error_to_string)?; + + let mut completed: Vec = vec![]; + for it in partial_data.completed_requests { + completed.push(benchmark_request_to_ui(&it.0, it.1).map_err(error_to_string)?); + } + + let mut in_progress: Vec = vec![]; + for it in partial_data.in_progress { + in_progress.push(BenchmarkInProgressUi { + request: benchmark_request_to_ui(&it.0, vec![]).map_err(error_to_string)?, + jobs: it.1.iter().map(benchmark_job_to_ui).collect(), + }); + } + + let mut queue_ui: Vec = vec![]; + for it in queue { + queue_ui.push(benchmark_request_to_ui(&it, vec![]).map_err(error_to_string)?); + } + + Ok(status_new::Response { + completed, + in_progress, + collector_configs, + queue: queue_ui, + }) +} diff --git a/site/src/server.rs b/site/src/server.rs index 1452cafc6..cc507957f 100644 --- a/site/src/server.rs +++ b/site/src/server.rs @@ -382,6 +382,22 @@ async fn serve_req(server: Server, req: Request) -> Result { + let ctxt: Arc = server.ctxt.read().as_ref().unwrap().clone(); + let result = request_handlers::handle_status_page_new(ctxt).await; + return match result { + Ok(result) => Ok(http::Response::builder() + .header_typed(ContentType::json()) + .body(hyper::Body::from(serde_json::to_string(&result).unwrap())) + .unwrap()), + Err(err) => Ok(http::Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .header_typed(ContentType::text_utf8()) + .header_typed(CacheControl::new().with_no_cache().with_no_store()) + .body(hyper::Body::from(err)) + .unwrap()), + }; + } "/perf/next_artifact" => { return server .handle_get_async(&req, request_handlers::handle_next_artifact) @@ -640,6 +656,7 @@ async fn handle_fs_path( | "/dashboard.html" | "/detailed-query.html" | "/help.html" + | "/status_new.html" | "/status.html" => resolve_template(relative_path).await, _ => match TEMPLATES.get_static_asset(relative_path, use_compression)? { Payload::Compressed(data) => {