From 7bfd369df7a63779a9a8306635aa161967fec0d5 Mon Sep 17 00:00:00 2001 From: Denis Cornehl Date: Tue, 10 Mar 2026 21:21:31 +0100 Subject: [PATCH] show build duration & estimated length in queu Co-authored-by: Guillaume Gomez --- ...3912b47078f48d724c29166af04b10a49e7d8.json | 38 ++++++++++ ...99a6e5a42e56aedf068a3acf39f923eb32ade.json | 26 ------- ...3912b47078f48d724c29166af04b10a49e7d8.json | 38 ++++++++++ ...99a6e5a42e56aedf068a3acf39f923eb32ade.json | 26 ------- ...3912b47078f48d724c29166af04b10a49e7d8.json | 38 ++++++++++ ...99a6e5a42e56aedf068a3acf39f923eb32ade.json | 26 ------- .../bin/docs_rs_web/src/handlers/releases.rs | 69 ++++++++++++------- .../templates/releases/build_queue.html | 68 +++++++++++------- .../docs_rs_web/templates/style/style.scss | 44 +++++++++++- 9 files changed, 245 insertions(+), 128 deletions(-) create mode 100644 .sqlx/query-cbe0780dcbb1724ab13a27b1d693912b47078f48d724c29166af04b10a49e7d8.json delete mode 100644 .sqlx/query-e1a95bfd43982d86a56cf542fde99a6e5a42e56aedf068a3acf39f923eb32ade.json create mode 100644 crates/bin/cratesfyi/.sqlx/query-cbe0780dcbb1724ab13a27b1d693912b47078f48d724c29166af04b10a49e7d8.json delete mode 100644 crates/bin/cratesfyi/.sqlx/query-e1a95bfd43982d86a56cf542fde99a6e5a42e56aedf068a3acf39f923eb32ade.json create mode 100644 crates/bin/docs_rs_web/.sqlx/query-cbe0780dcbb1724ab13a27b1d693912b47078f48d724c29166af04b10a49e7d8.json delete mode 100644 crates/bin/docs_rs_web/.sqlx/query-e1a95bfd43982d86a56cf542fde99a6e5a42e56aedf068a3acf39f923eb32ade.json diff --git a/.sqlx/query-cbe0780dcbb1724ab13a27b1d693912b47078f48d724c29166af04b10a49e7d8.json b/.sqlx/query-cbe0780dcbb1724ab13a27b1d693912b47078f48d724c29166af04b10a49e7d8.json new file mode 100644 index 000000000..b51a29259 --- /dev/null +++ b/.sqlx/query-cbe0780dcbb1724ab13a27b1d693912b47078f48d724c29166af04b10a49e7d8.json @@ -0,0 +1,38 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT\n crates.name as \"name: KrateName\",\n releases.version as \"version: Version\",\n CASE\n WHEN builds.build_started IS NULL THEN NULL\n ELSE (CURRENT_TIMESTAMP - builds.build_started)\n END AS \"elapsed: Duration\",\n (\n SELECT AVG(prev_builds.build_finished - prev_builds.build_started)\n FROM crates AS prev_crates\n INNER JOIN releases AS prev_releases ON prev_crates.id = prev_releases.crate_id\n INNER JOIN builds AS prev_builds ON prev_releases.id = prev_builds.rid\n WHERE\n prev_crates.id = releases.crate_id\n AND prev_builds.id <> builds.id\n AND prev_builds.build_status = 'success'\n AND prev_builds.build_started IS NOT NULL\n ) AS \"average_duration: Duration\"\n FROM builds\n INNER JOIN releases ON releases.id = builds.rid\n INNER JOIN crates ON releases.crate_id = crates.id\n WHERE\n builds.build_status = 'in_progress'\n ORDER BY builds.id ASC", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "name: KrateName", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "version: Version", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "elapsed: Duration", + "type_info": "Interval" + }, + { + "ordinal": 3, + "name": "average_duration: Duration", + "type_info": "Interval" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + null, + null + ] + }, + "hash": "cbe0780dcbb1724ab13a27b1d693912b47078f48d724c29166af04b10a49e7d8" +} diff --git a/.sqlx/query-e1a95bfd43982d86a56cf542fde99a6e5a42e56aedf068a3acf39f923eb32ade.json b/.sqlx/query-e1a95bfd43982d86a56cf542fde99a6e5a42e56aedf068a3acf39f923eb32ade.json deleted file mode 100644 index c9bf149a6..000000000 --- a/.sqlx/query-e1a95bfd43982d86a56cf542fde99a6e5a42e56aedf068a3acf39f923eb32ade.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT\n crates.name as \"name: KrateName\",\n releases.version as \"version: Version\"\n FROM builds\n INNER JOIN releases ON releases.id = builds.rid\n INNER JOIN crates ON releases.crate_id = crates.id\n WHERE\n builds.build_status = 'in_progress'\n ORDER BY builds.id ASC", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "name: KrateName", - "type_info": "Text" - }, - { - "ordinal": 1, - "name": "version: Version", - "type_info": "Text" - } - ], - "parameters": { - "Left": [] - }, - "nullable": [ - false, - false - ] - }, - "hash": "e1a95bfd43982d86a56cf542fde99a6e5a42e56aedf068a3acf39f923eb32ade" -} diff --git a/crates/bin/cratesfyi/.sqlx/query-cbe0780dcbb1724ab13a27b1d693912b47078f48d724c29166af04b10a49e7d8.json b/crates/bin/cratesfyi/.sqlx/query-cbe0780dcbb1724ab13a27b1d693912b47078f48d724c29166af04b10a49e7d8.json new file mode 100644 index 000000000..b51a29259 --- /dev/null +++ b/crates/bin/cratesfyi/.sqlx/query-cbe0780dcbb1724ab13a27b1d693912b47078f48d724c29166af04b10a49e7d8.json @@ -0,0 +1,38 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT\n crates.name as \"name: KrateName\",\n releases.version as \"version: Version\",\n CASE\n WHEN builds.build_started IS NULL THEN NULL\n ELSE (CURRENT_TIMESTAMP - builds.build_started)\n END AS \"elapsed: Duration\",\n (\n SELECT AVG(prev_builds.build_finished - prev_builds.build_started)\n FROM crates AS prev_crates\n INNER JOIN releases AS prev_releases ON prev_crates.id = prev_releases.crate_id\n INNER JOIN builds AS prev_builds ON prev_releases.id = prev_builds.rid\n WHERE\n prev_crates.id = releases.crate_id\n AND prev_builds.id <> builds.id\n AND prev_builds.build_status = 'success'\n AND prev_builds.build_started IS NOT NULL\n ) AS \"average_duration: Duration\"\n FROM builds\n INNER JOIN releases ON releases.id = builds.rid\n INNER JOIN crates ON releases.crate_id = crates.id\n WHERE\n builds.build_status = 'in_progress'\n ORDER BY builds.id ASC", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "name: KrateName", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "version: Version", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "elapsed: Duration", + "type_info": "Interval" + }, + { + "ordinal": 3, + "name": "average_duration: Duration", + "type_info": "Interval" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + null, + null + ] + }, + "hash": "cbe0780dcbb1724ab13a27b1d693912b47078f48d724c29166af04b10a49e7d8" +} diff --git a/crates/bin/cratesfyi/.sqlx/query-e1a95bfd43982d86a56cf542fde99a6e5a42e56aedf068a3acf39f923eb32ade.json b/crates/bin/cratesfyi/.sqlx/query-e1a95bfd43982d86a56cf542fde99a6e5a42e56aedf068a3acf39f923eb32ade.json deleted file mode 100644 index c9bf149a6..000000000 --- a/crates/bin/cratesfyi/.sqlx/query-e1a95bfd43982d86a56cf542fde99a6e5a42e56aedf068a3acf39f923eb32ade.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT\n crates.name as \"name: KrateName\",\n releases.version as \"version: Version\"\n FROM builds\n INNER JOIN releases ON releases.id = builds.rid\n INNER JOIN crates ON releases.crate_id = crates.id\n WHERE\n builds.build_status = 'in_progress'\n ORDER BY builds.id ASC", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "name: KrateName", - "type_info": "Text" - }, - { - "ordinal": 1, - "name": "version: Version", - "type_info": "Text" - } - ], - "parameters": { - "Left": [] - }, - "nullable": [ - false, - false - ] - }, - "hash": "e1a95bfd43982d86a56cf542fde99a6e5a42e56aedf068a3acf39f923eb32ade" -} diff --git a/crates/bin/docs_rs_web/.sqlx/query-cbe0780dcbb1724ab13a27b1d693912b47078f48d724c29166af04b10a49e7d8.json b/crates/bin/docs_rs_web/.sqlx/query-cbe0780dcbb1724ab13a27b1d693912b47078f48d724c29166af04b10a49e7d8.json new file mode 100644 index 000000000..b51a29259 --- /dev/null +++ b/crates/bin/docs_rs_web/.sqlx/query-cbe0780dcbb1724ab13a27b1d693912b47078f48d724c29166af04b10a49e7d8.json @@ -0,0 +1,38 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT\n crates.name as \"name: KrateName\",\n releases.version as \"version: Version\",\n CASE\n WHEN builds.build_started IS NULL THEN NULL\n ELSE (CURRENT_TIMESTAMP - builds.build_started)\n END AS \"elapsed: Duration\",\n (\n SELECT AVG(prev_builds.build_finished - prev_builds.build_started)\n FROM crates AS prev_crates\n INNER JOIN releases AS prev_releases ON prev_crates.id = prev_releases.crate_id\n INNER JOIN builds AS prev_builds ON prev_releases.id = prev_builds.rid\n WHERE\n prev_crates.id = releases.crate_id\n AND prev_builds.id <> builds.id\n AND prev_builds.build_status = 'success'\n AND prev_builds.build_started IS NOT NULL\n ) AS \"average_duration: Duration\"\n FROM builds\n INNER JOIN releases ON releases.id = builds.rid\n INNER JOIN crates ON releases.crate_id = crates.id\n WHERE\n builds.build_status = 'in_progress'\n ORDER BY builds.id ASC", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "name: KrateName", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "version: Version", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "elapsed: Duration", + "type_info": "Interval" + }, + { + "ordinal": 3, + "name": "average_duration: Duration", + "type_info": "Interval" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + null, + null + ] + }, + "hash": "cbe0780dcbb1724ab13a27b1d693912b47078f48d724c29166af04b10a49e7d8" +} diff --git a/crates/bin/docs_rs_web/.sqlx/query-e1a95bfd43982d86a56cf542fde99a6e5a42e56aedf068a3acf39f923eb32ade.json b/crates/bin/docs_rs_web/.sqlx/query-e1a95bfd43982d86a56cf542fde99a6e5a42e56aedf068a3acf39f923eb32ade.json deleted file mode 100644 index c9bf149a6..000000000 --- a/crates/bin/docs_rs_web/.sqlx/query-e1a95bfd43982d86a56cf542fde99a6e5a42e56aedf068a3acf39f923eb32ade.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT\n crates.name as \"name: KrateName\",\n releases.version as \"version: Version\"\n FROM builds\n INNER JOIN releases ON releases.id = builds.rid\n INNER JOIN crates ON releases.crate_id = crates.id\n WHERE\n builds.build_status = 'in_progress'\n ORDER BY builds.id ASC", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "name: KrateName", - "type_info": "Text" - }, - { - "ordinal": 1, - "name": "version: Version", - "type_info": "Text" - } - ], - "parameters": { - "Left": [] - }, - "nullable": [ - false, - false - ] - }, - "hash": "e1a95bfd43982d86a56cf542fde99a6e5a42e56aedf068a3acf39f923eb32ade" -} diff --git a/crates/bin/docs_rs_web/src/handlers/releases.rs b/crates/bin/docs_rs_web/src/handlers/releases.rs index 5bab5b34a..2ea92f704 100644 --- a/crates/bin/docs_rs_web/src/handlers/releases.rs +++ b/crates/bin/docs_rs_web/src/handlers/releases.rs @@ -21,7 +21,7 @@ use base64::{Engine, engine::general_purpose::STANDARD as b64}; use chrono::{DateTime, Utc}; use docs_rs_build_queue::{AsyncBuildQueue, PRIORITY_CONTINUOUS, QueuedCrate}; use docs_rs_registry_api::{self as registry_api, RegistryApi}; -use docs_rs_types::{KrateName, ReqVersion, Version}; +use docs_rs_types::{Duration, KrateName, ReqVersion, Version}; use docs_rs_uri::encode_url_path; use futures_util::stream::TryStreamExt; use http::StatusCode; @@ -731,12 +731,20 @@ struct BuildQueuePage { description: &'static str, queue: Vec, rebuild_queue: Vec, - in_progress_builds: Vec<(KrateName, Version)>, + in_progress_builds: Vec, expand_rebuild_queue: bool, } impl_axum_webpage! { BuildQueuePage } +#[derive(Debug, Clone, PartialEq, Eq)] +struct InProgressBuild { + name: KrateName, + version: Version, + elapsed: Option, + average_duration: Option, +} + #[derive(Deserialize)] pub(crate) struct BuildQueueParams { expand: Option, @@ -747,22 +755,35 @@ pub(crate) async fn build_queue_handler( mut conn: DbConnection, Query(params): Query, ) -> AxumResult { - let in_progress_builds: Vec<(KrateName, Version)> = sqlx::query!( + let in_progress_builds: Vec<_> = sqlx::query_as!( + InProgressBuild, r#"SELECT crates.name as "name: KrateName", - releases.version as "version: Version" + releases.version as "version: Version", + CASE + WHEN builds.build_started IS NULL THEN NULL + ELSE (CURRENT_TIMESTAMP - builds.build_started) + END AS "elapsed: Duration", + ( + SELECT AVG(prev_builds.build_finished - prev_builds.build_started) + FROM crates AS prev_crates + INNER JOIN releases AS prev_releases ON prev_crates.id = prev_releases.crate_id + INNER JOIN builds AS prev_builds ON prev_releases.id = prev_builds.rid + WHERE + prev_crates.id = releases.crate_id + AND prev_builds.id <> builds.id + AND prev_builds.build_status = 'success' + AND prev_builds.build_started IS NOT NULL + ) AS "average_duration: Duration" FROM builds INNER JOIN releases ON releases.id = builds.rid INNER JOIN crates ON releases.crate_id = crates.id WHERE builds.build_status = 'in_progress' - ORDER BY builds.id ASC"# + ORDER BY builds.id ASC"#, ) .fetch_all(&mut *conn) - .await? - .into_iter() - .map(|rec| (rec.name, rec.version)) - .collect(); + .await?; let mut rebuild_queue = Vec::new(); let mut queue = build_queue @@ -770,9 +791,9 @@ pub(crate) async fn build_queue_handler( .await? .into_iter() .filter(|krate| { - !in_progress_builds.iter().any(|(name, version)| { + !in_progress_builds.iter().any(|in_progress| { // use `.any` instead of `.contains` to avoid cloning name& version for the match - *name == krate.name && *version == krate.version + in_progress.name == krate.name && in_progress.version == krate.version }) }) .collect::>(); @@ -1858,28 +1879,28 @@ mod tests { let full = kuchikiki::parse_html().one(web.get("/releases/queue").await?.text().await?); - let lists = full - .select(".queue-list") - .expect("missing queues") - .collect::>(); - assert_eq!(lists.len(), 2); - - let in_progress_items: Vec<_> = lists[0] - .as_node() - .select("li > a") - .expect("missing in progress list items") + let in_progress_items: Vec<_> = full + .select("table tbody tr td:first-child > a") + .expect("missing in progress build rows") .map(|node| node.text_contents().trim().to_string()) .collect(); assert_eq!(in_progress_items, vec![format!("foo {V1}")]); - let queued_items: Vec<_> = lists[1] - .as_node() - .select("li > a") + let queued_items: Vec<_> = full + .select(".queue-list > li > a") .expect("missing queued list items") .map(|node| node.text_contents().trim().to_string()) .collect(); assert_eq!(queued_items, vec![format!("bar {V2}")]); + let runtime_columns: Vec<_> = full + .select("table tbody tr td:nth-child(2), table tbody tr td:nth-child(3)") + .expect("missing duration columns") + .map(|node| node.text_contents().trim().to_string()) + .collect(); + assert_eq!(runtime_columns.len(), 2); + assert!(runtime_columns.into_iter().all(|value| !value.is_empty())); + Ok(()) }); } diff --git a/crates/bin/docs_rs_web/templates/releases/build_queue.html b/crates/bin/docs_rs_web/templates/releases/build_queue.html index 3e110508e..beb11030e 100644 --- a/crates/bin/docs_rs_web/templates/releases/build_queue.html +++ b/crates/bin/docs_rs_web/templates/releases/build_queue.html @@ -19,32 +19,50 @@ {%- block body -%}
-
-
- currently being built -
-
- -
-
- {%- if !in_progress_builds.is_empty() %} -
    - {% for release in in_progress_builds -%} - {%- set release_params = RustdocParams::new(release.0).with_req_version(release.1) -%} -
  1. - - {{ release.0 }} {{ release.1 }} - -
  2. - {%- endfor %} -
- {%- else %} -
-

There is nothing currently being built

-
- {%- endif %} -
+
+ Currently being built
+ + + + + + + + + + {%- if !in_progress_builds.is_empty() %} + {% for release in in_progress_builds -%} + {%- set release_params = RustdocParams::new(release.name).with_req_version(release.version) -%} + + + + + + {%- endfor %} + {%- else %} + + + + {%- endif %} + +
releaserunning foraverage duration
+ + {{ release.name }} {{ release.version }} + + + {%- if let Some(elapsed) = release.elapsed -%} + {{ elapsed|format_duration }} + {%- else -%} + — + {%- endif -%} + + {%- if let Some(average_duration) = release.average_duration -%} + {{ average_duration|format_duration }} + {%- else -%} + — + {%- endif -%} +
There is nothing currently being built
Build Queue diff --git a/crates/bin/docs_rs_web/templates/style/style.scss b/crates/bin/docs_rs_web/templates/style/style.scss index ac8eb7faf..bd8e6fe46 100644 --- a/crates/bin/docs_rs_web/templates/style/style.scss +++ b/crates/bin/docs_rs_web/templates/style/style.scss @@ -276,7 +276,7 @@ div.recent-releases-container { padding: 0; } - ol.queue-list li, ol.rebuild-queue-list li { + .queue-list li, .rebuild-queue-list li { list-style-type: decimal; margin-left: 20px; @@ -414,6 +414,48 @@ div.recent-releases-container { padding: .2em .8em .2em .5em; border-radius: 1em; } + + table { + margin: 1.5em 1em; + --table-border: 1px solid var(--color-border); + border-radius: 4px; + + border-spacing: 0; + border-collapse: separate; + border: var(--table-border); + overflow: hidden; + + a { + color: var(--color-url); + } + } + + th, td { + padding: 4px 1em; + font-weight: 300; + text-align: center; + } + + th { + border-bottom: var(--table-border); + } + + /* All this code is to have rounded borders on the table */ + table th:not(:last-child), + table td:not(:last-child) { + border-right: var(--table-border); + } + + table thead>tr:not(:last-child)>th, + table thead>tr:not(:last-child)>td, + table tbody>tr:not(:last-child)>th, + table tbody>tr:not(:last-child)>td, + table tr:not(:last-child)>td, + table tr:not(:last-child)>th, + table thead:not(:last-child), + table tbody:not(:last-child) { + border-bottom: var(--table-border); + } } div.package-container {