From f77ed130e19803ed23b2302c48763bb5cc55881a Mon Sep 17 00:00:00 2001 From: Joscha Date: Sun, 22 Oct 2023 16:29:20 +0200 Subject: [PATCH] Implement placeholder server responses --- ...1ca15ab3a58a8553a5837351ec63265edb5c2.json | 20 -- ...8c89a86ba9bb71368a413eb90593f5da166e0.json | 26 --- ...a00870a19f9336c3566b1daeb639f81cb2154.json | 26 --- scripts/graph.ts | 10 +- scripts/graph/requests.ts | 76 +++----- scripts/graph/state.ts | 8 +- src/server/web.rs | 5 +- src/server/web/pages/graph.rs | 174 +++++------------- src/server/web/pages/graph/util.rs | 89 --------- src/server/web/paths.rs | 8 +- 10 files changed, 85 insertions(+), 357 deletions(-) delete mode 100644 .sqlx/query-0d2711c13a7835dd6a6708c2dc81ca15ab3a58a8553a5837351ec63265edb5c2.json delete mode 100644 .sqlx/query-0dc1d000038b42bbbcfe16a11cb8c89a86ba9bb71368a413eb90593f5da166e0.json delete mode 100644 .sqlx/query-4f3c635a7026015f95d20823a25a00870a19f9336c3566b1daeb639f81cb2154.json delete mode 100644 src/server/web/pages/graph/util.rs diff --git a/.sqlx/query-0d2711c13a7835dd6a6708c2dc81ca15ab3a58a8553a5837351ec63265edb5c2.json b/.sqlx/query-0d2711c13a7835dd6a6708c2dc81ca15ab3a58a8553a5837351ec63265edb5c2.json deleted file mode 100644 index 8ffe09c..0000000 --- a/.sqlx/query-0d2711c13a7835dd6a6708c2dc81ca15ab3a58a8553a5837351ec63265edb5c2.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "SQLite", - "query": "WITH measurements AS ( SELECT hash, value, MAX(start) FROM runs JOIN run_measurements USING (id) WHERE metric = ? GROUP BY hash ) SELECT value FROM commits LEFT JOIN measurements USING (hash) WHERE reachable = 2 ORDER BY hash ASC ", - "describe": { - "columns": [ - { - "name": "value", - "ordinal": 0, - "type_info": "Float" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - true - ] - }, - "hash": "0d2711c13a7835dd6a6708c2dc81ca15ab3a58a8553a5837351ec63265edb5c2" -} diff --git a/.sqlx/query-0dc1d000038b42bbbcfe16a11cb8c89a86ba9bb71368a413eb90593f5da166e0.json b/.sqlx/query-0dc1d000038b42bbbcfe16a11cb8c89a86ba9bb71368a413eb90593f5da166e0.json deleted file mode 100644 index 666ee6b..0000000 --- a/.sqlx/query-0dc1d000038b42bbbcfe16a11cb8c89a86ba9bb71368a413eb90593f5da166e0.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "db_name": "SQLite", - "query": "SELECT hash, committer_date AS \"time: OffsetDateTime\" FROM commits WHERE reachable = 2 ORDER BY hash ASC ", - "describe": { - "columns": [ - { - "name": "hash", - "ordinal": 0, - "type_info": "Text" - }, - { - "name": "time: OffsetDateTime", - "ordinal": 1, - "type_info": "Text" - } - ], - "parameters": { - "Right": 0 - }, - "nullable": [ - false, - false - ] - }, - "hash": "0dc1d000038b42bbbcfe16a11cb8c89a86ba9bb71368a413eb90593f5da166e0" -} diff --git a/.sqlx/query-4f3c635a7026015f95d20823a25a00870a19f9336c3566b1daeb639f81cb2154.json b/.sqlx/query-4f3c635a7026015f95d20823a25a00870a19f9336c3566b1daeb639f81cb2154.json deleted file mode 100644 index 04e23d3..0000000 --- a/.sqlx/query-4f3c635a7026015f95d20823a25a00870a19f9336c3566b1daeb639f81cb2154.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "db_name": "SQLite", - "query": "SELECT parent, child FROM commit_links JOIN commits ON hash = parent WHERE reachable = 2 ORDER BY parent ASC, child ASC ", - "describe": { - "columns": [ - { - "name": "parent", - "ordinal": 0, - "type_info": "Text" - }, - { - "name": "child", - "ordinal": 1, - "type_info": "Text" - } - ], - "parameters": { - "Right": 0 - }, - "nullable": [ - false, - false - ] - }, - "hash": "4f3c635a7026015f95d20823a25a00870a19f9336c3566b1daeb639f81cb2154" -} diff --git a/scripts/graph.ts b/scripts/graph.ts index 37d829f..4cb290c 100644 --- a/scripts/graph.ts +++ b/scripts/graph.ts @@ -1,6 +1,3 @@ -import { Requests } from "./graph/requests.js"; -import { State } from "./graph/state.js"; - import uPlot from "./uPlot.js"; /* @@ -121,11 +118,6 @@ const COLORS = [ // Initialization -const plot_div = document.getElementById("plot")!; +const plotDiv = document.getElementById("plot")!; const metricsDiv = document.getElementById("metrics")!; let plot: uPlot | null = null; - -let requests = new Requests(); -let state = new State(requests, metricsDiv); - -state.update(); diff --git a/scripts/graph/requests.ts b/scripts/graph/requests.ts index 4eca6d1..249d10a 100644 --- a/scripts/graph/requests.ts +++ b/scripts/graph/requests.ts @@ -2,8 +2,7 @@ * `/graph/metrics` response data. */ export type MetricsResponse = { - // data_id: number; // TODO Uncomment - + dataId: number; metrics: string[]; }; @@ -11,61 +10,38 @@ export type MetricsResponse = { * `/graph/commits` response data. */ export type CommitsResponse = { - // graph_id: number; // TODO Uncomment - - hash_by_hash: string[]; - author_by_hash: number[]; - committer_date_by_hash: string[]; - message_by_hash: string[]; - parents: [string, string][]; + graphId: number; + hashByHash: string[]; + authorByHash: string[]; + committerDateByHash: number[]; + messageByHash: string[]; + parentsByHash: string[][]; }; /** * `/graph/measurements` response data. */ export type MeasurementsResponse = { - // graph_id: number; // TODO Uncomment - // data_id: number; // TODO Uncomment - + graphId: number; + dataId: number; measurements: { [key: string]: (number | null)[]; }; }; -/** - * Request different kinds of data from the server. - * - * This class has two main purposes: - * - * 1. Providing a nice interface for requesting data from the server - * 2. Preventing sending the same request again while still waiting for the server - */ -export class Requests { - #requesting_metrics: Promise | null = null; - #requesting_commits: Promise | null = null; - #requesting_measurements: Map> = new Map(); - - async #request_data(url: string): Promise { - let response = await fetch(url); - let data: R = await response.json(); - return data; - } - - async get_metrics(): Promise { - if (this.#requesting_metrics !== null) { - try { - return await this.#requesting_metrics; - } catch (error) { - return null; - } - } - - this.#requesting_metrics = this.#request_data("metrics"); - try { - return await this.#requesting_metrics; - } catch (error) { - console.error("Could not get metrics:", error); - return null; - } finally { - this.#requesting_metrics = null; - } - } +async function getData(url: string): Promise { + const response = await fetch(url); + const data: R = await response.json(); + return data; +} + +export async function getMetrics(): Promise { + return getData("metrics"); +} + +export async function getCommits(): Promise { + return getData("commits"); +} + +export async function getMeasurements(metrics: string[]): Promise { + const params = new URLSearchParams(metrics.map(m => ["metric", m])); + return getData(`measurements?${params}`); } diff --git a/scripts/graph/state.ts b/scripts/graph/state.ts index aef8756..01a9363 100644 --- a/scripts/graph/state.ts +++ b/scripts/graph/state.ts @@ -1,15 +1,13 @@ import { updateMetricsDiv } from "./metrics.js"; -import { Requests, MetricsResponse } from "./requests.js"; +import { MetricsResponse, getMetrics } from "./requests.js"; export class State { - #requests: Requests; #metricsDiv: HTMLElement; #updating: boolean = false; #metrics: MetricsResponse | null = null; - constructor(requests: Requests, metricsDiv: HTMLElement) { - this.#requests = requests; + constructor(metricsDiv: HTMLElement) { this.#metricsDiv = metricsDiv; } @@ -35,7 +33,7 @@ export class State { } async #update_metrics() { - this.#metrics = await this.#requests.get_metrics(); + this.#metrics = await getMetrics(); if (this.#metrics === null) { return; } updateMetricsDiv(this.#metricsDiv, this.#metrics.metrics); } diff --git a/src/server/web.rs b/src/server/web.rs index 2721292..544a63b 100644 --- a/src/server/web.rs +++ b/src/server/web.rs @@ -26,7 +26,7 @@ use self::{ }, pages::{ commit::get_commit_by_hash, - graph::{get_graph, get_graph_data, get_graph_metrics}, + graph::{get_graph, get_graph_commits, get_graph_measurements, get_graph_metrics}, index::get_index, queue::{get_queue, get_queue_delete, get_queue_inner}, run::get_run_by_id, @@ -48,7 +48,8 @@ pub async fn run(server: Server) -> somehow::Result<()> { .typed_get(get_api_worker_repo_by_hash_tree_tar_gz) .typed_get(get_commit_by_hash) .typed_get(get_graph) - .typed_get(get_graph_data) + .typed_get(get_graph_commits) + .typed_get(get_graph_measurements) .typed_get(get_graph_metrics) .typed_get(get_index) .typed_get(get_queue) diff --git a/src/server/web/pages/graph.rs b/src/server/web/pages/graph.rs index c7e1a41..99dce60 100644 --- a/src/server/web/pages/graph.rs +++ b/src/server/web/pages/graph.rs @@ -1,20 +1,16 @@ -mod util; - use std::collections::HashMap; use askama::Template; use axum::{extract::State, response::IntoResponse, Json}; use axum_extra::extract::Query; -use futures::{StreamExt, TryStreamExt}; use serde::{Deserialize, Serialize}; -use sqlx::{Acquire, SqlitePool}; -use time::OffsetDateTime; +use sqlx::SqlitePool; use crate::{ config::ServerConfig, server::web::{ base::{Base, Link, Tab}, - paths::{PathGraph, PathGraphData, PathGraphMetrics}, + paths::{PathGraph, PathGraphCommits, PathGraphMeasurements, PathGraphMetrics}, r#static::{GRAPH_JS, UPLOT_CSS}, }, somehow, @@ -41,7 +37,9 @@ pub async fn get_graph( } #[derive(Serialize)] +#[serde(rename_all = "camelCase")] struct MetricsResponse { + data_id: i64, metrics: Vec, } @@ -54,139 +52,59 @@ pub async fn get_graph_metrics( .fetch_all(&db) .await?; - Ok(Json(MetricsResponse { metrics })) + Ok(Json(MetricsResponse { + data_id: 0, // TODO Implement + metrics, + })) +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct CommitsResponse { + graph_id: i64, + hash_by_hash: Vec, + author_by_hash: Vec, + committer_date_by_hash: Vec, + message_by_hash: Vec, + parents_by_hash: Vec>, +} + +pub async fn get_graph_commits( + _path: PathGraphCommits, + State(db): State, +) -> somehow::Result { + Ok(Json(CommitsResponse { + graph_id: 0, // TODO Implement + hash_by_hash: vec![], // TODO Implement + author_by_hash: vec![], // TODO Implement + committer_date_by_hash: vec![], // TODO Implement + message_by_hash: vec![], // TODO Implement + parents_by_hash: vec![], // TODO Implement + })) } #[derive(Deserialize)] -pub struct QueryGraphData { +pub struct QueryGraphMeasurements { #[serde(default)] metric: Vec, } #[derive(Serialize)] -struct GraphData { - hashes: Vec, - parents: HashMap>, - times: Vec, - - // TODO f32 for smaller transmission size? - measurements: HashMap>>, +#[serde(rename_all = "camelCase")] +struct MeasurementsResponse { + graph_id: i64, + data_id: i64, + measurements: HashMap>, } -pub async fn get_graph_data( - _path: PathGraphData, +pub async fn get_graph_measurements( + _path: PathGraphMeasurements, State(db): State, - Query(form): Query, + Query(form): Query, ) -> somehow::Result { - let mut tx = db.begin().await?; - let conn = tx.acquire().await?; - - // The SQL queries that return one result per commit *must* return the same - // amount of rows in the same order! - - // TODO Limit by date or amount - - let mut unsorted_hashes = Vec::::new(); - let mut times_by_hash = HashMap::::new(); - let mut rows = sqlx::query!( - "\ - SELECT \ - hash, \ - committer_date AS \"time: OffsetDateTime\" \ - FROM commits \ - WHERE reachable = 2 \ - ORDER BY hash ASC \ - " - ) - .fetch(&mut *conn); - while let Some(row) = rows.next().await { - let row = row?; - unsorted_hashes.push(row.hash.clone()); - times_by_hash.insert(row.hash, row.time.unix_timestamp()); - } - drop(rows); - - let parent_child_pairs = sqlx::query!( - "\ - SELECT parent, child \ - FROM commit_links \ - JOIN commits ON hash = parent \ - WHERE reachable = 2 \ - ORDER BY parent ASC, child ASC \ - " - ) - .fetch(&mut *conn) - .map_ok(|r| (r.parent, r.child)) - .try_collect::>() - .await?; - - let mut hashes = util::sort_topologically(&unsorted_hashes, &parent_child_pairs); - hashes.sort_by_key(|hash| times_by_hash[hash]); - - let sorted_hash_indices = hashes - .iter() - .cloned() - .enumerate() - .map(|(i, hash)| (hash, i)) - .collect::>(); - - let mut parents = HashMap::>::new(); - for (parent, child) in &parent_child_pairs { - if let Some(parent_idx) = sorted_hash_indices.get(parent) { - if let Some(child_idx) = sorted_hash_indices.get(child) { - parents.entry(*parent_idx).or_default().push(*child_idx); - } - } - } - - // Collect times - let times = hashes - .iter() - .map(|hash| times_by_hash[hash]) - .collect::>(); - - // permutation[unsorted_index] = sorted_index - let permutation = unsorted_hashes - .iter() - .map(|hash| sorted_hash_indices[hash]) - .collect::>(); - - // Collect and permutate measurements - let mut measurements = HashMap::new(); - for metric in form.metric { - let mut values = vec![None; hashes.len()]; - let mut rows = sqlx::query_scalar!( - "\ - WITH \ - measurements AS ( \ - SELECT hash, value, MAX(start) \ - FROM runs \ - JOIN run_measurements USING (id) \ - WHERE metric = ? \ - GROUP BY hash \ - ) \ - SELECT value \ - FROM commits \ - LEFT JOIN measurements USING (hash) \ - WHERE reachable = 2 \ - ORDER BY hash ASC \ - ", - metric, - ) - .fetch(&mut *conn) - .enumerate(); - while let Some((i, value)) = rows.next().await { - values[permutation[i]] = value?; - } - drop(rows); - - measurements.insert(metric, values); - } - - Ok(Json(GraphData { - hashes, - parents, - times, - measurements, + Ok(Json(MeasurementsResponse { + graph_id: 0, // TODO Implement + data_id: 0, // TODO Implement + measurements: HashMap::new(), // TODO Implement })) } diff --git a/src/server/web/pages/graph/util.rs b/src/server/web/pages/graph/util.rs deleted file mode 100644 index 87740c9..0000000 --- a/src/server/web/pages/graph/util.rs +++ /dev/null @@ -1,89 +0,0 @@ -use std::collections::{HashMap, HashSet}; - -/// Sort commits topologically such that parents come before their children. -/// -/// Assumes that `parent_child_pairs` contains no duplicates and is in the -/// desired order (see below for more info on the order). -/// -/// The algorithm used is a version of [Kahn's algorithm][0] that starts at the -/// nodes with no parents. It uses a stack for the set of parentless nodes, -/// meaning the resulting commit order is depth-first-y, not breadth-first-y. -/// For example, this commit graph (where children are ordered top to bottom) -/// results in the order `A, B, C, D, E, F` and not an interleaved order like -/// `A, B, D, C, E, F` (which a queue would produce): -/// -/// ```text -/// A - B - C -/// \ \ -/// D - E - F -/// ``` -/// -/// When a node is visited and added to the list of sorted nodes, it is removed -/// as parent from all its children. Those who had no other parents are added to -/// the stack in reverse order. In the final list, the children appear in the -/// order they appeared in the parent child pairs, if possible. This means that -/// the order of the commits and of the pairs matters and should probably be -/// deterministic. -/// -/// [0]: https://en.wikipedia.org/wiki/Topological_sorting#Kahn's_algorithm -pub fn sort_topologically( - commits: &[String], - parent_child_pairs: &[(String, String)], -) -> Vec { - // These maps have entries for each commit hash we might want to inspect, so - // we know `.get()`, `.get_mut()` and `.remove()` must always succeed. - let mut parent_child_map = commits - .iter() - .map(|hash| (hash.clone(), Vec::::new())) - .collect::>(); - let mut child_parent_map = commits - .iter() - .map(|hash| (hash.clone(), HashSet::::new())) - .collect::>(); - for (parent, child) in parent_child_pairs { - if parent_child_map.contains_key(parent) && parent_child_map.contains_key(child) { - parent_child_map - .get_mut(parent) - .unwrap() - .push(child.clone()); - child_parent_map - .get_mut(child) - .unwrap() - .insert(parent.clone()); - } - } - - // Initialize parentless stack using commit list, in reverse order so that - // the order is right when popping. - let mut parentless = Vec::::new(); - for commit in commits.iter().rev() { - if child_parent_map[commit].is_empty() { - // A (quadratic-time) linear scan here is OK since the number of - // parentless commits is usually fairly small. - if !parentless.contains(commit) { - parentless.push(commit.clone()); - } - } - } - - let mut sorted = Vec::::new(); - while let Some(hash) = parentless.pop() { - // Inspect children in reverse order so that the order is right when - // popping off the parentless stack. - for child in parent_child_map.remove(&hash).unwrap().into_iter().rev() { - let child_parents = child_parent_map.get_mut(&child).unwrap(); - child_parents.remove(&hash); - if child_parents.is_empty() { - parentless.push(child); - } - } - - sorted.push(hash); - } - - assert!(parent_child_map.is_empty()); - assert!(child_parent_map.values().all(|v| v.is_empty())); - assert!(parentless.is_empty()); - assert_eq!(commits.len(), sorted.len()); - sorted -} diff --git a/src/server/web/paths.rs b/src/server/web/paths.rs index 3f10e94..01fe735 100644 --- a/src/server/web/paths.rs +++ b/src/server/web/paths.rs @@ -18,8 +18,12 @@ pub struct PathGraph {} pub struct PathGraphMetrics {} #[derive(Deserialize, TypedPath)] -#[typed_path("/graph/data")] -pub struct PathGraphData {} +#[typed_path("/graph/commits")] +pub struct PathGraphCommits {} + +#[derive(Deserialize, TypedPath)] +#[typed_path("/graph/measurements")] +pub struct PathGraphMeasurements {} #[derive(Deserialize, TypedPath)] #[typed_path("/queue/")]