From 399935aef8805e3649e3664976b12961c746b596 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ber=C3=A1nek?= Date: Sat, 9 Sep 2023 22:20:54 +0200 Subject: [PATCH 1/6] Model status page data in TypeScript --- site/frontend/src/pages/status/data.ts | 37 +++++++++++++++++++++----- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/site/frontend/src/pages/status/data.ts b/site/frontend/src/pages/status/data.ts index 90defe6e3..3c2f1bcb0 100644 --- a/site/frontend/src/pages/status/data.ts +++ b/site/frontend/src/pages/status/data.ts @@ -16,20 +16,43 @@ interface Step { current_progress: number; } -/** - * The `any` types in the interface below were chosen because the types are quite complex - * on the Rust side (they are modeled with enums encoded in a way that is not so simple to model in - * TS). - */ +type Artifact = + | { + Commit: Commit; + } + | { + Tag: string; + }; + +type MissingReason = + | { + Master: { + pr: number; + parent_sha: string; + is_try_parent: boolean; + }; + } + | { + Try: { + parent_sha: string; + include: string | null; + exclude: string | null; + runs: number | null; + }; + } + | { + InProgress: MissingReason; + }; + interface CurrentState { - artifact: any; + artifact: Artifact; progress: Step[]; } export interface StatusResponse { last_commit: Commit | null; benchmarks: BenchmarkStatus[]; - missing: Array<[Commit, any]>; + missing: Array<[Commit, MissingReason]>; current: CurrentState | null; most_recent_end: number | null; } From 98d4adfd72cdf1ce7cf3d2ebb5e5ab1b5c88a165 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ber=C3=A1nek?= Date: Sat, 9 Sep 2023 22:49:49 +0200 Subject: [PATCH 2/6] Show PR number and commit type for currently benchmarked commit --- site/frontend/src/pages/status/data.ts | 7 +-- site/frontend/src/pages/status/page.vue | 71 +++++++++++++++++++++---- 2 files changed, 64 insertions(+), 14 deletions(-) diff --git a/site/frontend/src/pages/status/data.ts b/site/frontend/src/pages/status/data.ts index 3c2f1bcb0..e76751810 100644 --- a/site/frontend/src/pages/status/data.ts +++ b/site/frontend/src/pages/status/data.ts @@ -1,4 +1,4 @@ -interface Commit { +export interface Commit { sha: string; date: string; type: "Try" | "Master"; @@ -16,7 +16,7 @@ interface Step { current_progress: number; } -type Artifact = +export type Artifact = | { Commit: Commit; } @@ -24,7 +24,7 @@ type Artifact = Tag: string; }; -type MissingReason = +export type MissingReason = | { Master: { pr: number; @@ -34,6 +34,7 @@ type MissingReason = } | { Try: { + pr: number; parent_sha: string; include: string | null; exclude: string | null; diff --git a/site/frontend/src/pages/status/page.vue b/site/frontend/src/pages/status/page.vue index 2e75302b8..9eb13d7ed 100644 --- a/site/frontend/src/pages/status/page.vue +++ b/site/frontend/src/pages/status/page.vue @@ -3,7 +3,7 @@ import {getJson} from "../../utils/requests"; import {STATUS_DATA_URL} from "../../urls"; import {withLoading} from "../../utils/loading"; import {computed, ref, Ref} from "vue"; -import {StatusResponse} from "./data"; +import {Artifact, Commit, MissingReason, StatusResponse} from "./data"; async function loadStatus(loading: Ref) { data.value = await withLoading(loading, () => @@ -28,20 +28,47 @@ function formatDuration(seconds: number): string { return s; } -function commitUrl(commit: {sha: string}): string { - return `${commit.sha.substring(0, 13)}`; +function getArtifactPr(reason: MissingReason): number { + if (reason.hasOwnProperty("InProgress")) { + return getArtifactPr(reason["InProgress"]); + } else if (reason.hasOwnProperty("Master")) { + return reason["Master"].pr; + } else if (reason.hasOwnProperty("Try")) { + return reason["Try"].pr; + } else { + throw new Error(`Unknown missing reason ${reason}`); + } } -function formatArtifact(artifact: any): string { - if (artifact.Commit) { - return commitUrl(artifact.Commit); +function getArtifactSha(artifact: Artifact): string { + if (artifact.hasOwnProperty("Commit")) { + return artifact["Commit"].sha; + } else if (artifact.hasOwnProperty("Tag")) { + return artifact["Tag"]; } else { - return artifact.Tag; + throw new Error(`Unknown artifact ${artifact}`); } } +function commitUrlAsHtml(sha: string): string { + return `${sha.substring( + 0, + 13 + )}`; +} + +function pullRequestUrlAsHtml(pr: number): string { + return `#${pr}`; +} + +function formatCommitAsHtml(commit: Commit, reason: MissingReason): string { + const url = commitUrlAsHtml(commit.sha); + const type = commit.type === "Try" ? "try" : "master"; + const pr = getArtifactPr(reason); + + return `${pullRequestUrlAsHtml(pr)} (${type}): ${url}`; +} + function formatReason(reason: any): string { if (typeof reason == "string") { return reason; @@ -81,6 +108,20 @@ const timeLeft = computed(() => { const recentEndDate = computed(() => { return new Date(data.value.most_recent_end * 1000); }); +const currentCommitAndReason: Ref<[Commit, MissingReason] | null> = computed( + () => { + const current = data.value?.current ?? null; + if (current === null) return null; + const sha = getArtifactSha(current.artifact); + + for (const [commit, reason] of data.value.missing) { + if (commit.sha === sha && reason.hasOwnProperty("InProgress")) { + return [commit, reason["InProgress"]]; + } + } + return null; + } +); const loading = ref(true); @@ -94,7 +135,15 @@ loadStatus(loading);

Currently benchmarking: - .
+ .
Time left: {{ formatDuration(timeLeft) }}

@@ -164,7 +213,7 @@ loadStatus(loading); - + From 9f147577beafe4a5a714251185d98c6fa9caf106 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ber=C3=A1nek?= Date: Sat, 9 Sep 2023 22:51:13 +0200 Subject: [PATCH 3/6] Refactor and simplify missing reason formatting --- site/frontend/src/pages/status/page.vue | 29 ++++++++----------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/site/frontend/src/pages/status/page.vue b/site/frontend/src/pages/status/page.vue index 9eb13d7ed..400e77f2b 100644 --- a/site/frontend/src/pages/status/page.vue +++ b/site/frontend/src/pages/status/page.vue @@ -69,26 +69,15 @@ function formatCommitAsHtml(commit: Commit, reason: MissingReason): string { return `${pullRequestUrlAsHtml(pr)} (${type}): ${url}`; } -function formatReason(reason: any): string { - if (typeof reason == "string") { - return reason; - } else if (reason.InProgress) { - return `${formatReason(reason.InProgress)} - in progress`; - } else if (reason["Master"] !== undefined && reason.Master.pr) { - return ` - #${reason["Master"].pr}${ - reason.Master.is_try_parent ? " - Try commit parent" : "" +function formatMissingReason(reason: MissingReason): string { + if (reason.hasOwnProperty("InProgress")) { + return `${formatMissingReason(reason["InProgress"])} - in progress`; + } else if (reason.hasOwnProperty("Master")) { + return `${pullRequestUrlAsHtml(reason["Master"].pr)}${ + reason["Master"].is_try_parent ? " - Try commit parent" : "" }`; - } else if (reason["Master"] !== undefined && reason.Master.pr == 0) { - return "Master"; - } else if (reason["Try"] !== undefined && reason.Try.pr) { - return ` - Try for - - #${reason["Try"].pr} - `; + } else if (reason.hasOwnProperty("Try")) { + return `Try for ${pullRequestUrlAsHtml(reason["Try"].pr)}`; } else { // Should never happen, but a reasonable fallback return JSON.stringify(reason); @@ -214,7 +203,7 @@ loadStatus(loading); - +
{{ new Date(commit.date).toLocaleString() }}
{{ new Date(commit.date).toLocaleString() }}
From 290481074e30fc14f91ebdfc8d18ad143ef799e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ber=C3=A1nek?= Date: Mon, 11 Sep 2023 23:26:06 +0200 Subject: [PATCH 4/6] Redesign status page --- database/src/lib.rs | 1 + database/src/pool.rs | 2 +- database/src/pool/postgres.rs | 65 +-- database/src/pool/sqlite.rs | 77 ++-- site/frontend/package-lock.json | 32 ++ site/frontend/package.json | 1 + site/frontend/src/pages/status/data.ts | 16 +- site/frontend/src/pages/status/expansion.ts | 19 + site/frontend/src/pages/status/page.vue | 435 +++++++++++++++----- site/src/api.rs | 21 +- site/src/github.rs | 4 +- site/src/request_handlers/status_page.rs | 58 ++- 12 files changed, 542 insertions(+), 189 deletions(-) create mode 100644 site/frontend/src/pages/status/expansion.ts diff --git a/database/src/lib.rs b/database/src/lib.rs index 48f7ad9fb..663d41d61 100644 --- a/database/src/lib.rs +++ b/database/src/lib.rs @@ -710,6 +710,7 @@ pub struct CompileBenchmark { #[derive(Debug)] pub struct ArtifactCollection { + pub artifact: ArtifactId, pub duration: Duration, pub end_time: DateTime, } diff --git a/database/src/pool.rs b/database/src/pool.rs index 1e40c2bb8..41e429828 100644 --- a/database/src/pool.rs +++ b/database/src/pool.rs @@ -158,7 +158,7 @@ pub trait Connection: Send + Sync { async fn in_progress_steps(&self, aid: &ArtifactId) -> Vec; - async fn last_artifact_collection(&self) -> Option; + async fn last_n_artifact_collections(&self, n: u32) -> Vec; /// Returns the sha of the parent commit, if available. /// diff --git a/database/src/pool/postgres.rs b/database/src/pool/postgres.rs index aabd5df3f..f8d744947 100644 --- a/database/src/pool/postgres.rs +++ b/database/src/pool/postgres.rs @@ -1216,21 +1216,31 @@ where }) .collect() } - async fn last_artifact_collection(&self) -> Option { + async fn last_n_artifact_collections(&self, n: u32) -> Vec { self.conn() - .query_opt( - "select date_recorded, duration \ - from artifact_collection_duration \ + .query( + "select art.name, art.date, art.type, acd.date_recorded, acd.duration \ + from artifact_collection_duration as acd \ + join artifact as art on art.id = acd.aid \ order by date_recorded desc \ - limit 1;", - &[], + limit $1;", + &[&n], ) .await .unwrap() - .map(|r| ArtifactCollection { - end_time: r.get(0), - duration: Duration::from_secs(r.get::<_, i32>(1) as u64), + .into_iter() + .map(|r| { + let sha = r.get::<_, String>(0); + let date = r.get::<_, Option>>(1); + let ty = r.get::<_, String>(2); + + ArtifactCollection { + artifact: parse_artifact_id(&ty, &sha, date), + end_time: r.get(3), + duration: Duration::from_secs(r.get::<_, i32>(4) as u64), + } }) + .collect() } async fn parent_of(&self, sha: &str) -> Option { self.conn() @@ -1374,23 +1384,7 @@ where .unwrap()?; let date = row.get::<_, Option>>(0); let ty = row.get::<_, String>(1); - - match ty.as_str() { - "master" => Some(ArtifactId::Commit(Commit { - sha: artifact.to_owned(), - date: Date(date.expect("date present for master commits")), - r#type: CommitType::Master, - })), - "try" => Some(ArtifactId::Commit(Commit { - sha: artifact.to_owned(), - date: date - .map(Date) - .unwrap_or_else(|| Date::ymd_hms(2000, 1, 1, 0, 0, 0)), - r#type: CommitType::Try, - })), - "release" => Some(ArtifactId::Tag(artifact.to_owned())), - _ => panic!("unknown artifact type: {:?}", ty), - } + Some(parse_artifact_id(&ty, artifact, date)) } async fn purge_artifact(&self, aid: &ArtifactId) { @@ -1403,3 +1397,22 @@ where .unwrap(); } } + +fn parse_artifact_id(ty: &str, sha: &str, date: Option>) -> ArtifactId { + match ty { + "master" => ArtifactId::Commit(Commit { + sha: sha.to_owned(), + date: Date(date.expect("date present for master commits")), + r#type: CommitType::Master, + }), + "try" => ArtifactId::Commit(Commit { + sha: sha.to_owned(), + date: date + .map(Date) + .unwrap_or_else(|| Date::ymd_hms(2000, 1, 1, 0, 0, 0)), + r#type: CommitType::Try, + }), + "release" => ArtifactId::Tag(sha.to_owned()), + _ => panic!("unknown artifact type: {:?}", ty), + } +} diff --git a/database/src/pool/sqlite.rs b/database/src/pool/sqlite.rs index a8cd21f84..c775ca439 100644 --- a/database/src/pool/sqlite.rs +++ b/database/src/pool/sqlite.rs @@ -577,25 +577,7 @@ impl Connection for SqliteConnection { .optional() .unwrap()?; - match ty.as_str() { - "master" => Some(ArtifactId::Commit(Commit { - sha: artifact.to_owned(), - date: Date( - Utc.timestamp_opt(date.expect("master has date"), 0) - .unwrap(), - ), - r#type: CommitType::Master, - })), - "try" => Some(ArtifactId::Commit(Commit { - sha: artifact.to_owned(), - date: date - .map(|d| Date(Utc.timestamp_opt(d, 0).unwrap())) - .unwrap_or_else(|| Date::ymd_hms(2000, 1, 1, 0, 0, 0)), - r#type: CommitType::Try, - })), - "release" => Some(ArtifactId::Tag(artifact.to_owned())), - _ => panic!("unknown artifact type: {:?}", ty), - } + Some(parse_artifact_id(ty.as_str(), artifact, date)) } async fn record_duration(&self, artifact: ArtifactIdNumber, duration: Duration) { @@ -1166,24 +1148,31 @@ impl Connection for SqliteConnection { .collect() } - async fn last_artifact_collection(&self) -> Option { + async fn last_n_artifact_collections(&self, n: u32) -> Vec { self.raw_ref() - .query_row( - "select date_recorded, duration \ - from artifact_collection_duration \ + .prepare_cached( + "select art.name, art.date, art.type, acd.date_recorded, acd.duration \ + from artifact_collection_duration as acd \ + join artifact as art on art.id = acd.aid \ order by date_recorded desc \ - limit 1;", - params![], - |r| { - Ok(( - Utc.timestamp_opt(r.get(0)?, 0).unwrap(), - Duration::from_secs(r.get(1)?), - )) - }, + limit ?;", ) - .optional() .unwrap() - .map(|(end_time, duration)| ArtifactCollection { end_time, duration }) + .query(params![&n]) + .unwrap() + .mapped(|r| { + let sha = r.get::<_, String>(0)?; + let date = r.get::<_, Option>(1)?; + let ty = r.get::<_, String>(2)?; + + Ok(ArtifactCollection { + artifact: parse_artifact_id(&ty, &sha, date), + end_time: Utc.timestamp_opt(r.get(3)?, 0).unwrap(), + duration: Duration::from_secs(r.get(4)?), + }) + }) + .collect::, _>>() + .unwrap() } async fn parent_of(&self, sha: &str) -> Option { @@ -1252,3 +1241,25 @@ impl Connection for SqliteConnection { .unwrap(); } } + +fn parse_artifact_id(ty: &str, sha: &str, date: Option) -> ArtifactId { + match ty { + "master" => ArtifactId::Commit(Commit { + sha: sha.to_owned(), + date: Date( + Utc.timestamp_opt(date.expect("master has date"), 0) + .unwrap(), + ), + r#type: CommitType::Master, + }), + "try" => ArtifactId::Commit(Commit { + sha: sha.to_owned(), + date: date + .map(|d| Date(Utc.timestamp_opt(d, 0).unwrap())) + .unwrap_or_else(|| Date::ymd_hms(2000, 1, 1, 0, 0, 0)), + r#type: CommitType::Try, + }), + "release" => ArtifactId::Tag(sha.to_owned()), + _ => panic!("unknown artifact type: {:?}", ty), + } +} diff --git a/site/frontend/package-lock.json b/site/frontend/package-lock.json index c52b065cd..a7a4bf45e 100644 --- a/site/frontend/package-lock.json +++ b/site/frontend/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "MIT", "dependencies": { + "date-fns": "^2.30.0", "highcharts": "6.0.7", "msgpack-lite": "^0.1.26", "parcel": "^2.8.3", @@ -135,6 +136,22 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/runtime": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.15.tgz", + "integrity": "sha512-T0O+aa+4w0u06iNmapipJXMV4HoUir03hpx3/YqXXhu9xim3w+dVphjFWl1OH8NbZHw5Lbm9k45drDkgq2VNNA==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/runtime/node_modules/regenerator-runtime": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", + "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" + }, "node_modules/@babel/types": { "version": "7.21.4", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.4.tgz", @@ -2291,6 +2308,21 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.21.tgz", "integrity": "sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==" }, + "node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, "node_modules/de-indent": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", diff --git a/site/frontend/package.json b/site/frontend/package.json index c802b28d4..565b05313 100644 --- a/site/frontend/package.json +++ b/site/frontend/package.json @@ -20,6 +20,7 @@ "vue-tsc": "^1.8.3" }, "dependencies": { + "date-fns": "^2.30.0", "highcharts": "6.0.7", "msgpack-lite": "^0.1.26", "parcel": "^2.8.3", diff --git a/site/frontend/src/pages/status/data.ts b/site/frontend/src/pages/status/data.ts index e76751810..6f86ffe4f 100644 --- a/site/frontend/src/pages/status/data.ts +++ b/site/frontend/src/pages/status/data.ts @@ -4,7 +4,7 @@ export interface Commit { type: "Try" | "Master"; } -interface BenchmarkStatus { +export interface BenchmarkError { name: string; error: string; } @@ -50,10 +50,16 @@ interface CurrentState { progress: Step[]; } +export interface FinishedRun { + artifact: Artifact; + pr: number | null; + errors: BenchmarkError[]; + duration: number; + finished_at: number; +} + export interface StatusResponse { - last_commit: Commit | null; - benchmarks: BenchmarkStatus[]; - missing: Array<[Commit, MissingReason]>; + finished_runs: FinishedRun[]; current: CurrentState | null; - most_recent_end: number | null; + missing: Array<[Commit, MissingReason]>; } diff --git a/site/frontend/src/pages/status/expansion.ts b/site/frontend/src/pages/status/expansion.ts new file mode 100644 index 000000000..c5347b40b --- /dev/null +++ b/site/frontend/src/pages/status/expansion.ts @@ -0,0 +1,19 @@ +import {ref} from "vue"; + +export function useExpandedStore() { + const expanded = ref(new Set()); + + function isExpanded(sha: string) { + return expanded.value.has(sha); + } + + function toggleExpanded(sha: string) { + if (isExpanded(sha)) { + expanded.value.delete(sha); + } else { + expanded.value.add(sha); + } + } + + return {toggleExpanded, isExpanded}; +} diff --git a/site/frontend/src/pages/status/page.vue b/site/frontend/src/pages/status/page.vue index 400e77f2b..99da36148 100644 --- a/site/frontend/src/pages/status/page.vue +++ b/site/frontend/src/pages/status/page.vue @@ -2,13 +2,31 @@ import {getJson} from "../../utils/requests"; import {STATUS_DATA_URL} from "../../urls"; import {withLoading} from "../../utils/loading"; -import {computed, ref, Ref} from "vue"; -import {Artifact, Commit, MissingReason, StatusResponse} from "./data"; +import {computed, ref, Ref, watch} from "vue"; +import { + Artifact, + BenchmarkError, + Commit, + FinishedRun, + MissingReason, + StatusResponse, +} from "./data"; +import { + addSeconds, + differenceInSeconds, + format, + fromUnixTime, + sub, + subSeconds, +} from "date-fns"; +import {CompileTestCase} from "../compare/compile/common"; +import {useExpandedStore} from "./expansion"; async function loadStatus(loading: Ref) { data.value = await withLoading(loading, () => getJson(STATUS_DATA_URL) ); + console.log(data.value); } function formatDuration(seconds: number): string { @@ -19,11 +37,11 @@ function formatDuration(seconds: number): string { let s = ""; if (hours > 0) { - s = `${hours}h${mins < 10 ? "0" + mins : mins}m${ + s = `${hours}h ${mins < 10 ? "0" + mins : mins}m ${ secs < 10 ? "0" + secs : secs }s`; } else { - s = `${mins < 10 ? " " + mins : mins}m${secs < 10 ? "0" + secs : secs}s`; + s = `${mins < 10 ? " " + mins : mins}m ${secs < 10 ? "0" + secs : secs}s`; } return s; } @@ -57,31 +75,41 @@ function commitUrlAsHtml(sha: string): string { )}`; } -function pullRequestUrlAsHtml(pr: number): string { +function pullRequestUrlAsHtml(pr: number | null): string { + if (pr === null) { + return ""; + } return `#${pr}`; } function formatCommitAsHtml(commit: Commit, reason: MissingReason): string { const url = commitUrlAsHtml(commit.sha); - const type = commit.type === "Try" ? "try" : "master"; + const type = commit.type === "Try" ? "Try" : "Master"; const pr = getArtifactPr(reason); - return `${pullRequestUrlAsHtml(pr)} (${type}): ${url}`; + return `${type} commit ${url} (${pullRequestUrlAsHtml(pr)})`; } -function formatMissingReason(reason: MissingReason): string { - if (reason.hasOwnProperty("InProgress")) { - return `${formatMissingReason(reason["InProgress"])} - in progress`; - } else if (reason.hasOwnProperty("Master")) { - return `${pullRequestUrlAsHtml(reason["Master"].pr)}${ - reason["Master"].is_try_parent ? " - Try commit parent" : "" - }`; - } else if (reason.hasOwnProperty("Try")) { - return `Try for ${pullRequestUrlAsHtml(reason["Try"].pr)}`; - } else { - // Should never happen, but a reasonable fallback - return JSON.stringify(reason); - } +interface CurrentRun { + commit: Commit; + missing_reason: MissingReason; + start: Date; + expected_end: Date; + expected_total: number; + progress: number; +} + +interface TimelineEntry { + pr: number | null; + kind: string; + sha: string; + status: string; + end_time: Date; + end_estimated: boolean; + duration: number | null; + errors: BenchmarkError[]; + warning: boolean; + current: boolean; } const timeLeft = computed(() => { @@ -89,28 +117,138 @@ const timeLeft = computed(() => { let left = 0; for (const step of steps) { if (!step.is_done) { - left += step.expected_duration - step.current_progress; + left += Math.max(0, step.expected_duration - step.current_progress); } } return left; }); -const recentEndDate = computed(() => { - return new Date(data.value.most_recent_end * 1000); + +const currentRun: Ref = computed(() => { + const current = data.value?.current ?? null; + if (current === null) return null; + const sha = getArtifactSha(current.artifact); + + const elapsed = current.progress.reduce( + (prev, current) => prev + current.current_progress, + 0 + ); + const start = subSeconds(new Date(), elapsed); + const expected_total = current.progress.reduce( + (prev, current) => prev + current.expected_duration, + 0 + ); + const progress = Math.min(1, elapsed / Math.max(1, expected_total)); + + for (const [commit, reason] of data.value.missing) { + if (commit.sha === sha && reason.hasOwnProperty("InProgress")) { + return { + commit, + missing_reason: reason["InProgress"], + start, + expected_end: addSeconds(new Date(), timeLeft.value), + expected_total, + progress, + }; + } + } + return null; +}); +const lastFinishedRun: Ref = computed(() => { + const finished = data?.value.finished_runs; + if (finished.length === 0) return null; + return finished[0]; }); -const currentCommitAndReason: Ref<[Commit, MissingReason] | null> = computed( - () => { - const current = data.value?.current ?? null; - if (current === null) return null; - const sha = getArtifactSha(current.artifact); - - for (const [commit, reason] of data.value.missing) { - if (commit.sha === sha && reason.hasOwnProperty("InProgress")) { - return [commit, reason["InProgress"]]; - } +const expectedDuration: Ref = computed(() => { + if (data.value === null) return null; + const current = currentRun.value; + if (current !== null) { + return current.expected_total; + } else { + return lastFinishedRun.value?.duration; + } +}); + +// Timeline of past, current and future runs. +// Ordered from future to past - at the beginning is the item with the least +// priority in the queue, and at the end is the oldest finished run. +const timeline: Ref = computed(() => { + if (data.value === null) return []; + const timeline: TimelineEntry[] = []; + + const current = currentRun.value; + const currentTimeLeft = timeLeft.value ?? 0; + const expectedRunDuration = expectedDuration.value; + + // The next commit to be benchmarked will be at last position of `queued` + const queued = data.value.missing + .filter(([_, reason]) => !reason.hasOwnProperty("InProgress")) + .reverse(); + let queued_before = queued.length - 1; + for (const [commit, reason] of queued) { + const expected_end = addSeconds( + current?.expected_end ?? new Date(), + currentTimeLeft + queued_before * expectedRunDuration + ); + + let kind = commit.type; + if (reason.hasOwnProperty("Master") && reason["Master"].is_try_parent) { + kind += " (try parent)"; } - return null; + + timeline.push({ + pr: getArtifactPr(reason), + kind, + sha: commit.sha, + status: `Queued (#${queued_before + 1})`, + end_time: expected_end, + end_estimated: true, + errors: [], + duration: null, + warning: false, + current: false, + }); + queued_before -= 1; } -); + + if (current !== null) { + timeline.push({ + pr: getArtifactPr(current.missing_reason), + kind: current.commit.type, + sha: current.commit.sha, + status: "In progress", + end_time: current.expected_end, + end_estimated: true, + errors: [], + duration: null, + warning: false, + current: true, + }); + } + + for (const run of data.value.finished_runs) { + const kind = run.artifact.hasOwnProperty("Tag") + ? run.artifact["Tag"] + : run.artifact["Commit"].type; + + timeline.push({ + pr: run.pr, + kind, + sha: getArtifactSha(run.artifact), + status: "Finished", + end_time: fromUnixTime(run.finished_at), + end_estimated: false, + errors: run.errors, + duration: run.duration, + warning: kind !== "Try" && run.errors.length > 0, + current: false, + }); + } + + return timeline; +}); + +const {toggleExpanded: toggleExpandedErrors, isExpanded: hasExpandedErrors} = + useExpandedStore(); const loading = ref(true); @@ -119,22 +257,42 @@ loadStatus(loading); diff --git a/site/src/api.rs b/site/src/api.rs index ffa9d3057..ad0ae1190 100644 --- a/site/src/api.rs +++ b/site/src/api.rs @@ -233,7 +233,7 @@ pub mod status { use serde::{Deserialize, Serialize}; #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] - pub struct BenchmarkStatus { + pub struct BenchmarkError { pub name: String, pub error: String, } @@ -254,14 +254,23 @@ pub mod status { pub progress: Vec, } + #[derive(Serialize, Debug)] + pub struct FinishedRun { + pub artifact: ArtifactId, + pub pr: Option, + pub errors: Vec, + // In seconds + pub duration: u64, + // Unix timestamp + pub finished_at: u64, + } + #[derive(Serialize, Debug)] pub struct Response { - pub last_commit: Option, - pub benchmarks: Vec, - pub missing: Vec<(Commit, MissingReason)>, + // Ordered from newest to oldest + pub finished_runs: Vec, pub current: Option, - // None if no recent end, otherwise seconds since epoch - pub most_recent_end: Option, + pub missing: Vec<(Commit, MissingReason)>, } } diff --git a/site/src/github.rs b/site/src/github.rs index 23a0c5c9b..85f74c92f 100644 --- a/site/src/github.rs +++ b/site/src/github.rs @@ -332,8 +332,10 @@ async fn estimate_queue_info( // Guess the expected full run duration of a waiting commit let last_duration = conn - .last_artifact_collection() + .last_n_artifact_collections(1) .await + .into_iter() + .next() .map(|collection| collection.duration) .unwrap_or(Duration::ZERO); diff --git a/site/src/request_handlers/status_page.rs b/site/src/request_handlers/status_page.rs index a5d0b2b7f..e1e2563e5 100644 --- a/site/src/request_handlers/status_page.rs +++ b/site/src/request_handlers/status_page.rs @@ -2,14 +2,16 @@ use std::str; use std::sync::Arc; use crate::api::status; +use crate::api::status::FinishedRun; use crate::db::{ArtifactId, Lookup}; use crate::load::SiteCtxt; -pub async fn handle_status_page(ctxt: Arc) -> status::Response { - let idx = ctxt.index.load(); - let last_commit = idx.commits().last().cloned(); +// How many historical (finished) runs should be returned from the status API. +const FINISHED_RUN_COUNT: u64 = 5; +pub async fn handle_status_page(ctxt: Arc) -> status::Response { let missing = ctxt.missing_commits().await; + // FIXME: no current builds let conn = ctxt.conn().await; let current = if let Some(artifact) = conn.in_progress_artifacts().await.pop() { @@ -33,29 +35,41 @@ pub async fn handle_status_page(ctxt: Arc) -> status::Response { None }; - let errors = if let Some(last) = &last_commit { - conn.get_error(ArtifactId::from(last.clone()).lookup(&idx).unwrap()) - .await - } else { - Default::default() - }; - let benchmark_state = errors - .into_iter() - .map(|(name, error)| { - let error = prettify_log(&error).unwrap_or(error); - status::BenchmarkStatus { name, error } - }) - .collect::>(); + // FIXME: load at least one master commit with errors, if no master commit is in last N commits? + // FIXME: cache this whole thing, or write a specific SQL query for it + let mut finished_runs = Vec::new(); + let idx = ctxt.index.load(); + + let recent_collections = conn + .last_n_artifact_collections(FINISHED_RUN_COUNT as u32) + .await; + for collection in recent_collections { + let errors = conn + .get_error(collection.artifact.lookup(&idx).unwrap()) + .await; + let mut pr = None; + if let ArtifactId::Commit(ref commit) = collection.artifact { + pr = conn.pr_of(&commit.sha).await; + } + finished_runs.push(FinishedRun { + artifact: collection.artifact, + pr, + errors: errors + .into_iter() + .map(|(name, error)| { + let error = prettify_log(&error).unwrap_or(error); + status::BenchmarkError { name, error } + }) + .collect::>(), + duration: collection.duration.as_secs(), + finished_at: collection.end_time.timestamp() as u64, + }); + } status::Response { - last_commit, - benchmarks: benchmark_state, + finished_runs, missing, current, - most_recent_end: conn - .last_artifact_collection() - .await - .map(|d| d.end_time.timestamp()), } } From 25872348e879b5552dc07f22914b7c01bcf5220e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ber=C3=A1nek?= Date: Mon, 2 Oct 2023 17:49:24 +0200 Subject: [PATCH 5/6] Small frontend changes --- site/frontend/src/pages/status/page.vue | 32 +++++++++++++++++-------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/site/frontend/src/pages/status/page.vue b/site/frontend/src/pages/status/page.vue index 99da36148..bd39e03e8 100644 --- a/site/frontend/src/pages/status/page.vue +++ b/site/frontend/src/pages/status/page.vue @@ -26,7 +26,6 @@ async function loadStatus(loading: Ref) { data.value = await withLoading(loading, () => getJson(STATUS_DATA_URL) ); - console.log(data.value); } function formatDuration(seconds: number): string { @@ -108,7 +107,7 @@ interface TimelineEntry { end_estimated: boolean; duration: number | null; errors: BenchmarkError[]; - warning: boolean; + warning: string | null; current: boolean; } @@ -154,7 +153,7 @@ const currentRun: Ref = computed(() => { return null; }); const lastFinishedRun: Ref = computed(() => { - const finished = data?.value.finished_runs; + const finished = data.value?.finished_runs; if (finished.length === 0) return null; return finished[0]; }); @@ -204,7 +203,7 @@ const timeline: Ref = computed(() => { end_estimated: true, errors: [], duration: null, - warning: false, + warning: null, current: false, }); queued_before -= 1; @@ -220,7 +219,7 @@ const timeline: Ref = computed(() => { end_estimated: true, errors: [], duration: null, - warning: false, + warning: null, current: true, }); } @@ -230,6 +229,10 @@ const timeline: Ref = computed(() => { ? run.artifact["Tag"] : run.artifact["Commit"].type; + let warning = null; + if (kind !== "Try" && run.errors.length > 0) { + warning = "Master commit with errors"; + } timeline.push({ pr: run.pr, kind, @@ -239,7 +242,7 @@ const timeline: Ref = computed(() => { end_estimated: false, errors: run.errors, duration: run.duration, - warning: kind !== "Try" && run.errors.length > 0, + warning, current: false, }); } @@ -369,11 +372,17 @@ loadStatus(loading);
- + - {{ item.kind }} + {{ item.kind + }} - + {{ item.status }} {{ item.end_time.toLocaleString() @@ -382,7 +391,7 @@ loadStatus(loading); {{ formatDuration(item.duration) }} - ? + -