From 36ce75b43d404f97665297fbacad01d088b8b8df Mon Sep 17 00:00:00 2001 From: Joscha Date: Sat, 11 May 2024 18:32:36 +0200 Subject: [PATCH] Reformat everything --- .prettierrc.json | 2 +- DESIGN.md | 142 ++++++++++---------- README.md | 2 + scripts/graph.ts | 44 +++---- scripts/graph/commits.ts | 271 +++++++++++++++++++------------------- scripts/graph/metrics.ts | 186 ++++++++++++++------------ scripts/graph/requests.ts | 40 +++--- scripts/graph/state.ts | 140 ++++++++++---------- scripts/graph/util.ts | 18 ++- scripts/queue.ts | 17 ++- static/base.css | 217 +++++++++++++++--------------- tsconfig.json | 7 +- 12 files changed, 560 insertions(+), 526 deletions(-) diff --git a/.prettierrc.json b/.prettierrc.json index ffa1b71..bf357fb 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -1,3 +1,3 @@ { - "trailingComma": "all" + "trailingComma": "all" } diff --git a/DESIGN.md b/DESIGN.md index 7f1702e..64d9944 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -11,57 +11,57 @@ think them through. - Locally, tablejohn should just work™ without custom config. - However, some cli args might need to be specified for full functionality. - The db contains... - - Commits and their relationships - - Branches and whether they're tracked - - Runs and their measurements - - Queue of commits + - Commits and their relationships + - Branches and whether they're tracked + - Runs and their measurements + - Queue of commits - The in-memory state also contains... - - Connected workers and their state - - From this follows the list of in-progress runs + - Connected workers and their state + - From this follows the list of in-progress runs - Workers... - - Should be robust - - Noone wants to lose a run a few hours in, for any reason - - Explicitly design for loss of connection, server restarts - - Also design for bench script failures - - Can connect to more than one tablejohn instance - - Use task runtime-based approach to fairness - - Steal tasks based on time already spen on task - - Use plain old http requests (with BASIC auth) to communicate with server - - Store no data permanently + - Should be robust + - Noone wants to lose a run a few hours in, for any reason + - Explicitly design for loss of connection, server restarts + - Also design for bench script failures + - Can connect to more than one tablejohn instance + - Use task runtime-based approach to fairness + - Steal tasks based on time already spen on task + - Use plain old http requests (with BASIC auth) to communicate with server + - Store no data permanently - Nice-to-have but not critical - - Statically checked links - - Statically checked paths for static files + - Statically checked links + - Statically checked paths for static files ## Web pages - GET `/` - - Tracked and untracked refs - - Recent significant changes? - - "What's the state of the repo?" + - Tracked and untracked refs + - Recent significant changes? + - "What's the state of the repo?" - GET `/graph/` - - Interactive graph - - Change scope interactively - - Change metrics interactively + - Interactive graph + - Change scope interactively + - Change metrics interactively - GET `/queue/` - - List of workers and their state - - List of unfinished runs - - "What's the state of the infrastructure?" + - List of workers and their state + - List of unfinished runs + - "What's the state of the infrastructure?" - GET `/commit//` - - Show details of a commit - - Link to parents, chilren, runs in chronological order - - Resolve refs and branch names to commit hashes -> redirect + - Show details of a commit + - Link to parents, chilren, runs in chronological order + - Resolve refs and branch names to commit hashes -> redirect - GET `/run//` - - Show details of a run - - Link to commit, other runs in chronological order - - Links to compare against previous run, closest tracked ancestors? - - Resolve refs, branch names and commits to their latest runs -> redirect + - Show details of a run + - Link to commit, other runs in chronological order + - Links to compare against previous run, closest tracked ancestors? + - Resolve refs, branch names and commits to their latest runs -> redirect - GET `/compare//` - - Select/search run to compare against? - - Enter commit hash or run id - - Resolve refs, branch names and commits to their latest runs -> redirect + - Select/search run to compare against? + - Enter commit hash or run id + - Resolve refs, branch names and commits to their latest runs -> redirect - GET `/compare/rid1//` - - Show changes from rid2 to rid1 - - Resolve refs, branch names and commits to their latest runs -> redirect + - Show changes from rid2 to rid1 + - Resolve refs, branch names and commits to their latest runs -> redirect ## Worker interaction @@ -76,34 +76,34 @@ This allows more human-readable and permanent links to workers than something like session ids. - POST `/api/worker/status` - - Main endpoint for worker/server coordination - - Worker periodically sends current status to server - - Includes a secret randomly chosen by the worker - - Subsequent requests must include exactly the same secret - - Protects against the case where multiple workers share the same name - - Worker may include request for new work - - If so, server may respond with a commit hash and bench method - - Worker may include current work - - If so, server may respond with request to abort the work + - Main endpoint for worker/server coordination + - Worker periodically sends current status to server + - Includes a secret randomly chosen by the worker + - Subsequent requests must include exactly the same secret + - Protects against the case where multiple workers share the same name + - Worker may include request for new work + - If so, server may respond with a commit hash and bench method + - Worker may include current work + - If so, server may respond with request to abort the work - GET `/api/worker/repo//tree.tar.gz` - - Get tar-ed commit from the server's repo, if any exists + - Get tar-ed commit from the server's repo, if any exists - GET `/api/worker/bench-repo//tree.tar.gz` - - Get tar-ed commit from the server's bench repo, if any exist + - Get tar-ed commit from the server's bench repo, if any exist ## CLI Args tablejohn can be run in one of two modes: Server mode, and worker mode. - server - - Run a web server that serves the contents of a db - - Optionally, specify repo to update the db from - - Optionally, launch local worker (only if repo is specified) - - When local worker is enabled, it ignores the worker section of the config - - Instead, a worker section is generated from the server config - - This approach should make `--local-worker` more fool-proof + - Run a web server that serves the contents of a db + - Optionally, specify repo to update the db from + - Optionally, launch local worker (only if repo is specified) + - When local worker is enabled, it ignores the worker section of the config + - Instead, a worker section is generated from the server config + - This approach should make `--local-worker` more fool-proof - worker - - Run only as worker (when using external machine for workers) - - Same config file format as server, just uses different parts + - Run only as worker (when using external machine for workers) + - Same config file format as server, just uses different parts ## Config file and options @@ -111,19 +111,19 @@ Regardless of the mode, the config file is always loaded the same way and has the same format. It is split into these chunks: - web (ignored in worker mode) - - Everything to do with the web server - - What address and port to bind on - - What url the site is being served under + - Everything to do with the web server + - What address and port to bind on + - What url the site is being served under - repo (ignored in worker mode) - - Everything to do with the repo the server is inspecting - - Name (derived from repo path if not specified here) - - How frequently to update the db from the repo - - A remote URL to update the repo from - - Whether to clone the repo if it doesn't yet exist + - Everything to do with the repo the server is inspecting + - Name (derived from repo path if not specified here) + - How frequently to update the db from the repo + - A remote URL to update the repo from + - Whether to clone the repo if it doesn't yet exist - worker (ignored in server mode) - - Name (uses system name by default) - - Custom bench dir path (creates temporary dir by default) - - List of servers, each of which has... - - Token to authenticate with - - Base url to contact - - Weight to prioritize with (by total run time + overhead?) + - Name (uses system name by default) + - Custom bench dir path (creates temporary dir by default) + - List of servers, each of which has... + - Token to authenticate with + - Base url to contact + - Weight to prioritize with (by total run time + overhead?) diff --git a/README.md b/README.md index 0407067..93f4443 100644 --- a/README.md +++ b/README.md @@ -5,11 +5,13 @@ Run benchmarks against commits in a git repo and present their results. ## Building from source The following tools are required: + - `cargo` and `rustc` (best installed via [rustup](https://rustup.rs/)) - `tsc`, the [typescript](https://www.typescriptlang.org/) compiler Once you have installed these tools, run the following command to install or update tablejohn to `~/.cargo/bin/tablejohn`: + ``` cargo install --force --git https://github.com/Garmelon/tablejohn ``` diff --git a/scripts/graph.ts b/scripts/graph.ts index 85d0401..9e7b9c7 100644 --- a/scripts/graph.ts +++ b/scripts/graph.ts @@ -116,28 +116,28 @@ ordering for both display modes, so this is also relevant to the normal mode. // https://sashamaps.net/docs/resources/20-colors/ // Related: https://en.wikipedia.org/wiki/Help:Distinguishable_colors const COLORS = [ - "#e6194B", // Red - "#3cb44b", // Green - "#ffe119", // Yellow - "#4363d8", // Blue - "#f58231", // Orange - // "#911eb4", // Purple - "#42d4f4", // Cyan - "#f032e6", // Magenta - // "#bfef45", // Lime - // "#fabed4", // Pink - "#469990", // Teal - // "#dcbeff", // Lavender - "#9A6324", // Brown - // "#fffac8", // Beige - "#800000", // Maroon - // "#aaffc3", // Mint - // "#808000", // Olive - // "#ffd8b1", // Apricot - "#000075", // Navy - "#a9a9a9", // Grey - // "#ffffff", // White - "#000000", // Black + "#e6194B", // Red + "#3cb44b", // Green + "#ffe119", // Yellow + "#4363d8", // Blue + "#f58231", // Orange + // "#911eb4", // Purple + "#42d4f4", // Cyan + "#f032e6", // Magenta + // "#bfef45", // Lime + // "#fabed4", // Pink + "#469990", // Teal + // "#dcbeff", // Lavender + "#9A6324", // Brown + // "#fffac8", // Beige + "#800000", // Maroon + // "#aaffc3", // Mint + // "#808000", // Olive + // "#ffd8b1", // Apricot + "#000075", // Navy + "#a9a9a9", // Grey + // "#ffffff", // White + "#000000", // Black ]; // Initialization diff --git a/scripts/graph/commits.ts b/scripts/graph/commits.ts index 5285922..9dd8faf 100644 --- a/scripts/graph/commits.ts +++ b/scripts/graph/commits.ts @@ -2,156 +2,161 @@ import { CommitsResponse } from "./requests.js"; import { SECONDS_PER_DAY } from "./util.js"; type Commit = { - indexByHash: number; - indexByGraph: number; - hash: string; - parents: Commit[]; - children: Commit[]; - author: string; - committerDate: number; - summary: string; + indexByHash: number; + indexByGraph: number; + hash: string; + parents: Commit[]; + children: Commit[]; + author: string; + committerDate: number; + summary: string; }; export class Commits { - #graphId: number | null = null; - #commitsByGraph: Commit[] = []; - #committerDatesNormal: Date[] = []; - #committerDatesDayEquidistant: Date[] = []; + #graphId: number | null = null; + #commitsByGraph: Commit[] = []; + #committerDatesNormal: Date[] = []; + #committerDatesDayEquidistant: Date[] = []; - requiresUpdate(graphId: number): boolean { - return this.#graphId === null || this.#graphId < graphId; + requiresUpdate(graphId: number): boolean { + return this.#graphId === null || this.#graphId < graphId; + } + + update(response: CommitsResponse) { + console.assert(response.hashByHash.length == response.authorByHash.length); + console.assert( + response.hashByHash.length == response.committerDateByHash.length, + ); + console.assert(response.hashByHash.length == response.summaryByHash.length); + + let commits = this.#loadCommits(response); + commits = this.#sortCommitsTopologically(commits); + this.#sortCommitsByCommitterDate(commits); + + // Fill in indexes - "later" is now + for (const [idx, commit] of commits.entries()) { + commit.indexByGraph = idx; } - update(response: CommitsResponse) { - console.assert(response.hashByHash.length == response.authorByHash.length); - console.assert(response.hashByHash.length == response.committerDateByHash.length); - console.assert(response.hashByHash.length == response.summaryByHash.length); + const committerDatesNormal = commits.map((c) => c.committerDate); + const committerDatesDayEquidistant = + this.#makeDayEquidistant(committerDatesNormal); - let commits = this.#loadCommits(response); - commits = this.#sortCommitsTopologically(commits); - this.#sortCommitsByCommitterDate(commits); + // To prevent exceptions and other weirdness from messing up our state, + // we update everything in one go. + this.#graphId = response.graphId; + this.#commitsByGraph = commits; + this.#committerDatesNormal = this.#epochTimesToDates(committerDatesNormal); + this.#committerDatesDayEquidistant = this.#epochTimesToDates( + committerDatesDayEquidistant, + ); + } - // Fill in indexes - "later" is now - for (const [idx, commit] of commits.entries()) { - commit.indexByGraph = idx; + #loadCommits(response: CommitsResponse): Commit[] { + const commits = new Map(); + const commitsByHash = []; + + for (const [idx, hash] of response.hashByHash.entries()) { + const commit = { + indexByHash: idx, + indexByGraph: NaN, // Filled in later + hash, + parents: [], + children: [], + author: response.authorByHash[idx]!, + committerDate: response.committerDateByHash[idx]!, + summary: response.summaryByHash[idx]!, + }; + commits.set(hash, commit); + commitsByHash.push(commit); + } + + // Fill in parents and children + for (const [childIdx, parentIdx] of response.childParentIndexPairs) { + const childHash = response.hashByHash[childIdx]!; + const parentHash = response.hashByHash[parentIdx]!; + + const child = commits.get(childHash)!; + const parent = commits.get(parentHash)!; + + child.parents.push(parent); + parent.children.push(child); + } + + return commitsByHash; + } + + #sortCommitsByCommitterDate(commits: Commit[]) { + commits.sort((a, b) => a.committerDate - b.committerDate); + } + + /** + * Sort commits topologically such that parents come before their children. + * + * Assumes that there are no duplicated commits anywhere. + * + * A reverse post-order DFS is a topological sort, so that is what this + * function implements. + */ + #sortCommitsTopologically(commits: Commit[]): Commit[] { + const visited: Set = new Set(); + const visiting: Commit[] = commits.filter((c) => c.parents.length == 0); + + const sorted: Commit[] = []; + + while (visiting.length > 0) { + const commit = visiting.at(-1)!; + if (visited.has(commit.hash)) { + visiting.pop(); + sorted.push(commit); + continue; + } + + for (const child of commit.children) { + if (!visited.has(child.hash)) { + visiting.push(child); } + } - const committerDatesNormal = commits.map(c => c.committerDate); - const committerDatesDayEquidistant = this.#makeDayEquidistant(committerDatesNormal); - - // To prevent exceptions and other weirdness from messing up our state, - // we update everything in one go. - this.#graphId = response.graphId; - this.#commitsByGraph = commits; - this.#committerDatesNormal = this.#epochTimesToDates(committerDatesNormal); - this.#committerDatesDayEquidistant = this.#epochTimesToDates(committerDatesDayEquidistant); + visited.add(commit.hash); } - #loadCommits(response: CommitsResponse): Commit[] { - const commits = new Map(); - const commitsByHash = []; + sorted.reverse(); - for (const [idx, hash] of response.hashByHash.entries()) { - const commit = { - indexByHash: idx, - indexByGraph: NaN, // Filled in later - hash, - parents: [], - children: [], - author: response.authorByHash[idx]!, - committerDate: response.committerDateByHash[idx]!, - summary: response.summaryByHash[idx]!, - }; - commits.set(hash, commit); - commitsByHash.push(commit); - } + console.assert(visited.size === commits.length); + console.assert(visiting.length === 0); + console.assert(sorted.length === commits.length); + return sorted; + } - // Fill in parents and children - for (const [childIdx, parentIdx] of response.childParentIndexPairs) { - const childHash = response.hashByHash[childIdx]!; - const parentHash = response.hashByHash[parentIdx]!; - - const child = commits.get(childHash)!; - const parent = commits.get(parentHash)!; - - child.parents.push(parent); - parent.children.push(child); - } - - return commitsByHash; + /** + * Assumes the times are sorted. + */ + #makeDayEquidistant(times: number[]): number[] { + const days: { day: number; amount: number }[] = []; + for (const time of times) { + const day = time % SECONDS_PER_DAY; + const prev = days.at(-1); + if (prev === undefined || prev.day !== day) { + days.push({ day, amount: 1 }); + } else { + prev.amount++; + } } - #sortCommitsByCommitterDate(commits: Commit[]) { - commits.sort((a, b) => a.committerDate - b.committerDate); + const result: number[] = []; + for (const day of days) { + const secondsPerCommit = SECONDS_PER_DAY / day.amount; + for (let i = 0; i < day.amount; i++) { + const time = day.day * SECONDS_PER_DAY + secondsPerCommit * (i + 0.5); + result.push(time); + } } - /** - * Sort commits topologically such that parents come before their children. - * - * Assumes that there are no duplicated commits anywhere. - * - * A reverse post-order DFS is a topological sort, so that is what this - * function implements. - */ - #sortCommitsTopologically(commits: Commit[]): Commit[] { - const visited: Set = new Set(); - const visiting: Commit[] = commits.filter(c => c.parents.length == 0); + return result; + } - const sorted: Commit[] = []; - - while (visiting.length > 0) { - const commit = visiting.at(-1)!; - if (visited.has(commit.hash)) { - visiting.pop(); - sorted.push(commit); - continue; - } - - for (const child of commit.children) { - if (!visited.has(child.hash)) { - visiting.push(child); - } - } - - visited.add(commit.hash); - } - - sorted.reverse(); - - console.assert(visited.size === commits.length); - console.assert(visiting.length === 0); - console.assert(sorted.length === commits.length); - return sorted; - } - - /** - * Assumes the times are sorted. - */ - #makeDayEquidistant(times: number[]): number[] { - const days: { day: number, amount: number; }[] = []; - for (const time of times) { - const day = time % SECONDS_PER_DAY; - const prev = days.at(-1); - if (prev === undefined || prev.day !== day) { - days.push({ day, amount: 1 }); - } else { - prev.amount++; - } - } - - const result: number[] = []; - for (const day of days) { - const secondsPerCommit = SECONDS_PER_DAY / day.amount; - for (let i = 0; i < day.amount; i++) { - const time = day.day * SECONDS_PER_DAY + secondsPerCommit * (i + 0.5); - result.push(time); - } - } - - return result; - } - - #epochTimesToDates(times: number[]): Date[] { - return times.map(t => new Date(1000 * t)); - } + #epochTimesToDates(times: number[]): Date[] { + return times.map((t) => new Date(1000 * t)); + } } diff --git a/scripts/graph/metrics.ts b/scripts/graph/metrics.ts index 4434823..ac1f306 100644 --- a/scripts/graph/metrics.ts +++ b/scripts/graph/metrics.ts @@ -2,110 +2,126 @@ import { MetricsResponse } from "./requests.js"; import { el } from "./util.js"; class Folder { - metric: string | null = null; - children: Map = new Map(); + metric: string | null = null; + children: Map = new Map(); - getOrCreateChild(name: string): Folder { - let child = this.children.get(name); - if (child === undefined) { - child = new Folder(); - this.children.set(name, child); - } - return child; + getOrCreateChild(name: string): Folder { + let child = this.children.get(name); + if (child === undefined) { + child = new Folder(); + this.children.set(name, child); } + return child; + } - add(metric: string) { - let current: Folder = this; - for (let segment of metric.split("/")) { - current = current.getOrCreateChild(segment); - } - current.metric = metric; + add(metric: string) { + let current: Folder = this; + for (let segment of metric.split("/")) { + current = current.getOrCreateChild(segment); } + current.metric = metric; + } - toHtmlElement(name: string): HTMLElement { - if (this.children.size > 0) { // Folder - name = `${name}/`; - if (this.metric === null) { // Folder without metric - return el("details", { "class": "no-metric" }, - el("summary", {}, name), - this.childrenToHtmlElements(), - ); - } else { // Folder with metric - return el("details", {}, - el("summary", {}, - el("input", { "type": "checkbox", "name": this.metric }), - " ", name, - ), - this.childrenToHtmlElements(), - ); - } - } else if (this.metric !== null) { // Normal metric - return el("label", {}, - el("input", { "type": "checkbox", "name": this.metric }), - " ", name, - ); - } else { // Metric without metric, should never happen - return el("label", {}, name); - } + toHtmlElement(name: string): HTMLElement { + if (this.children.size > 0) { + // Folder + name = `${name}/`; + if (this.metric === null) { + // Folder without metric + return el( + "details", + { class: "no-metric" }, + el("summary", {}, name), + this.childrenToHtmlElements(), + ); + } else { + // Folder with metric + return el( + "details", + {}, + el( + "summary", + {}, + el("input", { type: "checkbox", name: this.metric }), + " ", + name, + ), + this.childrenToHtmlElements(), + ); + } + } else if (this.metric !== null) { + // Normal metric + return el( + "label", + {}, + el("input", { type: "checkbox", name: this.metric }), + " ", + name, + ); + } else { + // Metric without metric, should never happen + return el("label", {}, name); } + } - childrenToHtmlElements(): HTMLElement { - let result: HTMLElement = el("ul", {}); - for (let [name, folder] of this.children.entries()) { - result.append(el("li", {}, folder.toHtmlElement(name))); - } - return result; + childrenToHtmlElements(): HTMLElement { + let result: HTMLElement = el("ul", {}); + for (let [name, folder] of this.children.entries()) { + result.append(el("li", {}, folder.toHtmlElement(name))); } + return result; + } } export class Metrics { - #div: HTMLElement; - #dataId: number | null = null; + #div: HTMLElement; + #dataId: number | null = null; - constructor(div: HTMLElement) { - this.#div = div; + constructor(div: HTMLElement) { + this.#div = div; + } + + getSelected(): Set { + const selected = new Set(); + + const checkedInputs = + this.#div.querySelectorAll("input:checked"); + for (const input of checkedInputs) { + selected.add(input.name); } - getSelected(): Set { - const selected = new Set(); + return selected; + } - const checkedInputs = this.#div.querySelectorAll("input:checked"); - for (const input of checkedInputs) { - selected.add(input.name); - } + requiresUpdate(dataId: number): boolean { + // At the moment, updating the metrics results in all
tags + // closing again. To prevent this (as it can be frustrating if you've + // navigated deep into the metrics hierarchy), we never require updates + // after the initial update. + return this.#dataId === null; + // return this.#dataId === null || this.#dataId < dataId; + } - return selected; + update(response: MetricsResponse) { + const selected = this.getSelected(); + + const folder = new Folder(); + for (const metric of response.metrics) { + folder.add(metric); } - requiresUpdate(dataId: number): boolean { - // At the moment, updating the metrics results in all
tags - // closing again. To prevent this (as it can be frustrating if you've - // navigated deep into the metrics hierarchy), we never require updates - // after the initial update. - return this.#dataId === null; - // return this.#dataId === null || this.#dataId < dataId; + this.#div.textContent = ""; // Remove children + if (folder.children.size == 0) { + this.#div.append("There aren't yet any metrics"); + } else { + this.#div.append(folder.childrenToHtmlElements()); } - update(response: MetricsResponse) { - const selected = this.getSelected(); - - const folder = new Folder(); - for (const metric of response.metrics) { - folder.add(metric); - } - - this.#div.textContent = ""; // Remove children - if (folder.children.size == 0) { - this.#div.append("There aren't yet any metrics"); - } else { - this.#div.append(folder.childrenToHtmlElements()); - } - - const inputs = this.#div.querySelectorAll("input"); - for (const input of inputs) { - input.checked = selected.has(input.name); - } - - this.#dataId = response.dataId; + const inputs = this.#div.querySelectorAll("input"); + for (const input of inputs) { + input.checked = selected.has(input.name); } + + this.#dataId = response.dataId; + } } diff --git a/scripts/graph/requests.ts b/scripts/graph/requests.ts index 9dea802..3c15a3c 100644 --- a/scripts/graph/requests.ts +++ b/scripts/graph/requests.ts @@ -2,46 +2,48 @@ * `/graph/metrics` response data. */ export type MetricsResponse = { - dataId: number; - metrics: string[]; + dataId: number; + metrics: string[]; }; /** * `/graph/commits` response data. */ export type CommitsResponse = { - graphId: number; - hashByHash: string[]; - authorByHash: string[]; - committerDateByHash: number[]; - summaryByHash: string[]; - childParentIndexPairs: [number, number][]; + graphId: number; + hashByHash: string[]; + authorByHash: string[]; + committerDateByHash: number[]; + summaryByHash: string[]; + childParentIndexPairs: [number, number][]; }; /** * `/graph/measurements` response data. */ export type MeasurementsResponse = { - graphId: number; - dataId: number; - measurements: { [key: string]: (number | null)[]; }; + graphId: number; + dataId: number; + measurements: { [key: string]: (number | null)[] }; }; async function getData(url: string): Promise { - const response = await fetch(url); - const data: R = await response.json(); - return data; + const response = await fetch(url); + const data: R = await response.json(); + return data; } export async function getMetrics(): Promise { - return getData("metrics"); + return getData("metrics"); } export async function getCommits(): Promise { - return getData("commits"); + return getData("commits"); } -export async function getMeasurements(metrics: string[]): Promise { - const params = new URLSearchParams(metrics.map(m => ["metric", m])); - return getData(`measurements?${params}`); +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 ec7c5ce..329aa4d 100644 --- a/scripts/graph/state.ts +++ b/scripts/graph/state.ts @@ -3,85 +3,85 @@ import { Metrics } from "./metrics.js"; import { getCommits, getMetrics } from "./requests.js"; export class State { - #latestGraphId: number = -Infinity; - #latestDataId: number = -Infinity; + #latestGraphId: number = -Infinity; + #latestDataId: number = -Infinity; - #metrics: Metrics; - #commits: Commits = new Commits(); + #metrics: Metrics; + #commits: Commits = new Commits(); - #requestingMetrics: boolean = false; - #requestingCommits: boolean = false; + #requestingMetrics: boolean = false; + #requestingCommits: boolean = false; - // raw measurements (with graph id and data id) - // processed measurements (with graph id and data id) + // raw measurements (with graph id and data id) + // processed measurements (with graph id and data id) - constructor(metrics: Metrics) { - this.#metrics = metrics; + constructor(metrics: Metrics) { + this.#metrics = metrics; + } + + /** + * Update state and plot and request new data if necessary. Tries to match + * the user's wishes as closely as possible. + * + * This function is idempotent. + */ + update() { + // TODO Invalidate and update data + // TODO Update graph + this.#requestDataWhereNecessary(); + } + + ////////////////////////////////// + // Requesting and updating data // + ////////////////////////////////// + + #updateDataId(dataId: number) { + if (dataId > this.#latestDataId) { + this.#latestDataId = dataId; + } + } + + #updateGraphId(graphId: number) { + if (graphId > this.#latestGraphId) { + this.#latestGraphId = graphId; + } + } + + #requestDataWhereNecessary() { + if (this.#metrics.requiresUpdate(this.#latestDataId)) { + this.#requestMetrics(); } - /** - * Update state and plot and request new data if necessary. Tries to match - * the user's wishes as closely as possible. - * - * This function is idempotent. - */ - update() { - // TODO Invalidate and update data - // TODO Update graph - this.#requestDataWhereNecessary(); + if (this.#commits.requiresUpdate(this.#latestGraphId)) { + this.#requestCommits(); } + } - ////////////////////////////////// - // Requesting and updating data // - ////////////////////////////////// - - #updateDataId(dataId: number) { - if (dataId > this.#latestDataId) { - this.#latestDataId = dataId; - } + async #requestMetrics() { + if (this.#requestingMetrics) return; + console.log("Requesting metrics"); + try { + this.#requestingMetrics = true; + const response = await getMetrics(); + this.#updateDataId(response.dataId); + this.#metrics.update(response); + this.update(); + } finally { + this.#requestingMetrics = false; } + } - #updateGraphId(graphId: number) { - if (graphId > this.#latestGraphId) { - this.#latestGraphId = graphId; - } - } - - #requestDataWhereNecessary() { - if (this.#metrics.requiresUpdate(this.#latestDataId)) { - this.#requestMetrics(); - } - - if (this.#commits.requiresUpdate(this.#latestGraphId)) { - this.#requestCommits(); - } - } - - async #requestMetrics() { - if (this.#requestingMetrics) return; - console.log("Requesting metrics"); - try { - this.#requestingMetrics = true; - const response = await getMetrics(); - this.#updateDataId(response.dataId); - this.#metrics.update(response); - this.update(); - } finally { - this.#requestingMetrics = false; - } - } - - async #requestCommits() { - if (this.#requestingCommits) return; - console.log("Requesting commits"); - try { - this.#requestingCommits = true; - const response = await getCommits(); - this.#updateGraphId(response.graphId); - this.#commits.update(response); - this.update(); - } finally { - this.#requestingCommits = false; - } + async #requestCommits() { + if (this.#requestingCommits) return; + console.log("Requesting commits"); + try { + this.#requestingCommits = true; + const response = await getCommits(); + this.#updateGraphId(response.graphId); + this.#commits.update(response); + this.update(); + } finally { + this.#requestingCommits = false; } + } } diff --git a/scripts/graph/util.ts b/scripts/graph/util.ts index 4cbb0d3..54ed512 100644 --- a/scripts/graph/util.ts +++ b/scripts/graph/util.ts @@ -1,13 +1,17 @@ /** * Create an {@link HTMLElement}. */ -export function el(name: string, attributes: { [key: string]: string; }, ...children: (string | Node)[]) { - let element = document.createElement(name); - for (let [name, value] of Object.entries(attributes)) { - element.setAttribute(name, value); - } - element.append(...children); - return element; +export function el( + name: string, + attributes: { [key: string]: string }, + ...children: (string | Node)[] +) { + let element = document.createElement(name); + for (let [name, value] of Object.entries(attributes)) { + element.setAttribute(name, value); + } + element.append(...children); + return element; } export const SECONDS_PER_DAY = 24 * 60 * 60; diff --git a/scripts/queue.ts b/scripts/queue.ts index 98f0c41..bcbc414 100644 --- a/scripts/queue.ts +++ b/scripts/queue.ts @@ -2,13 +2,16 @@ const INNER = document.getElementById("inner")!; const REFRESH_SECONDS = 10; function update() { - fetch("inner") - .then(response => response.text()) - .then(text => { - INNER.innerHTML = text; - let count = document.getElementById("queue")?.dataset["count"]!; - document.title = document.title.replace(/^queue \(\S+\)/, `queue (${count})`); - }); + fetch("inner") + .then((response) => response.text()) + .then((text) => { + INNER.innerHTML = text; + let count = document.getElementById("queue")?.dataset["count"]!; + document.title = document.title.replace( + /^queue \(\S+\)/, + `queue (${count})`, + ); + }); } setInterval(update, REFRESH_SECONDS * 1000); diff --git a/static/base.css b/static/base.css index 94bee76..662eae3 100644 --- a/static/base.css +++ b/static/base.css @@ -1,13 +1,13 @@ * { - font-family: monospace; - font-size: inherit; - margin: 0; - padding: 0; - box-sizing: border-box; + font-family: monospace; + font-size: inherit; + margin: 0; + padding: 0; + box-sizing: border-box; } body { - margin: .7em; + margin: 0.7em; } details, @@ -15,251 +15,252 @@ ol, p, table, ul { - margin: 1em 0; + margin: 1em 0; } h2 { - font-size: 1.7em; - margin: 1em 0 .5em; + font-size: 1.7em; + margin: 1em 0 0.5em; } a { - text-decoration: underline; - color: black; + text-decoration: underline; + color: black; } a:hover { - font-weight: bold; + font-weight: bold; } button { - padding: 0 .5ch; + padding: 0 0.5ch; } button.linkish { - background-color: unset; - border: unset; - text-decoration: underline; - padding: 0; - cursor: pointer; + background-color: unset; + border: unset; + text-decoration: underline; + padding: 0; + cursor: pointer; } details { - padding: .3em 1ch; - background-color: #ddd; + padding: 0.3em 1ch; + background-color: #ddd; } -details>summary { - font-weight: bold; - cursor: pointer; - list-style: "> " inside; +details > summary { + font-weight: bold; + cursor: pointer; + list-style: "> " inside; } -details[open]>summary { - list-style: "v " inside; +details[open] > summary { + list-style: "v " inside; } ul, ol { - list-style: "- " outside; - margin-left: 2ch; + list-style: "- " outside; + margin-left: 2ch; } dl { - margin: 1em 0; + margin: 1em 0; } dt { - margin-top: 1em; + margin-top: 1em; } dd { - margin-left: 4ch; + margin-left: 4ch; } table { - border-collapse: collapse; + border-collapse: collapse; } thead tr, tbody tr:hover { - background-color: #ddd; + background-color: #ddd; } th, td { - padding: .1em 0; - + padding: 0.1em 0; } -th+th, -td+td { - padding-left: 2ch; +th + th, +td + td { + padding-left: 2ch; } /* Nav bar */ nav { - font-size: 1.5em; - line-height: 1.2em; - padding: 0.3em; - background-color: #bdf; - border-radius: 0.3em; - display: flex; - flex-wrap: wrap; - align-items: baseline; + font-size: 1.5em; + line-height: 1.2em; + padding: 0.3em; + background-color: #bdf; + border-radius: 0.3em; + display: flex; + flex-wrap: wrap; + align-items: baseline; } -nav>* { - padding: 0 1ch; - border-right: 0.1em solid black; +nav > * { + padding: 0 1ch; + border-right: 0.1em solid black; } -nav>:first-child img { - width: 1.2em; - height: 1.2em; - vertical-align: bottom; - padding-right: 0.3em; +nav > :first-child img { + width: 1.2em; + height: 1.2em; + vertical-align: bottom; + padding-right: 0.3em; } -nav>:first-child { - padding-left: 0; +nav > :first-child { + padding-left: 0; } -nav>:last-child { - padding-right: 0; - border: none; +nav > :last-child { + padding-right: 0; + border: none; } -nav>.current { - font-weight: bold; +nav > .current { + font-weight: bold; } nav a { - text-decoration: none; + text-decoration: none; } nav a:hover { - text-decoration: underline; + text-decoration: underline; } /* Commit status */ .commit-reachable { - color: #777; + color: #777; } .commit-orphaned { - color: #a33; + color: #a33; } /* Index */ .refs-list dl { - margin-bottom: 0; + margin-bottom: 0; } /* Graph */ .graph-container { - display: flex; - flex-flow: row wrap; - align-items: flex-start; + display: flex; + flex-flow: row wrap; + align-items: flex-start; } .graph-container #plot { - margin-right: 1em; - margin-bottom: 1em; - box-shadow: 0 0 .5em black; + margin-right: 1em; + margin-bottom: 1em; + box-shadow: 0 0 0.5em black; } .graph-container #metrics { - flex: 0 50ch; - box-shadow: 0 0 .5em black; + flex: 0 50ch; + box-shadow: 0 0 0.5em black; } .metrics-list { - background-color: #ddd; - padding: .3em 1ch; + background-color: #ddd; + padding: 0.3em 1ch; } .metrics-list * { - margin: 0; - padding: 0; - background-color: unset; - list-style: none; + margin: 0; + padding: 0; + background-color: unset; + list-style: none; } -.metrics-list details[open]>summary { - list-style: none; +.metrics-list details[open] > summary { + list-style: none; } .metrics-list input[type="checkbox"] { - width: 2ch; + width: 2ch; } -.metrics-list details.no-metric>summary { - list-style: "-> " outside; - margin-left: 3ch; +.metrics-list details.no-metric > summary { + list-style: "-> " outside; + margin-left: 3ch; } -.metrics-list summary~* { - border-left: .1ch solid black; - margin-left: .9ch; - padding-left: 1ch; +.metrics-list summary ~ * { + border-left: 0.1ch solid black; + margin-left: 0.9ch; + padding-left: 1ch; } /* Queue */ .queue-commits form { - display: inline; + display: inline; } .queue-commits button:hover { - font-weight: bold; + font-weight: bold; } .queue-commits td:nth-child(2), .queue-commits td:nth-child(3) { - text-align: right; + text-align: right; } .queue-commits .odd { - background-color: #eee; + background-color: #eee; } .queue-commits .odd:hover { - background-color: #ddd; + background-color: #ddd; } /* Commit-like entities */ .commit-like dl { - display: grid; - grid: auto-flow / max-content 1fr; - column-gap: 1ch; + display: grid; + grid: auto-flow / max-content 1fr; + column-gap: 1ch; } -.commit-like dl, .commit-like dt, .commit-like dd { - margin: 0; +.commit-like dl, +.commit-like dt, +.commit-like dd { + margin: 0; } .commit-like pre { - margin: 1em 0ch 1em 4ch; - white-space: pre-wrap; + margin: 1em 0ch 1em 4ch; + white-space: pre-wrap; } .commit-like.commit .title { - color: #b70; - font-weight: bold; + color: #b70; + font-weight: bold; } .commit-like.run .title { - color: #07e; - font-weight: bold; + color: #07e; + font-weight: bold; } .commit-like.worker .title { - color: #380; - font-weight: bold; + color: #380; + font-weight: bold; } diff --git a/tsconfig.json b/tsconfig.json index 8cc5bb7..e109503 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,6 @@ -{ // See also https://aka.ms/tsconfig - "include": [ "scripts/**/*" ], +{ + // See also https://aka.ms/tsconfig + "include": ["scripts/**/*"], "compilerOptions": { "target": "ES2022", // Should be fine according to caniuse.com @@ -19,6 +20,6 @@ "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noUncheckedIndexedAccess": true, - "strict": true, + "strict": true } }