diff --git a/torchci/clickhouse_queries/oss_ci_list_util_metadata_info/params.json b/torchci/clickhouse_queries/oss_ci_list_util_metadata_info/params.json new file mode 100644 index 0000000000..c9708ed68a --- /dev/null +++ b/torchci/clickhouse_queries/oss_ci_list_util_metadata_info/params.json @@ -0,0 +1,7 @@ +{ + "params": { + "workflowId": "UInt64", + "repo": "String" + }, + "tests":[] +} diff --git a/torchci/clickhouse_queries/oss_ci_list_util_metadata_info/query.sql b/torchci/clickhouse_queries/oss_ci_list_util_metadata_info/query.sql new file mode 100644 index 0000000000..2b873c528c --- /dev/null +++ b/torchci/clickhouse_queries/oss_ci_list_util_metadata_info/query.sql @@ -0,0 +1,12 @@ +SELECT + workflow_id, + job_id, + workflow_name, + job_name, + run_attempt, + repo +FROM + misc.oss_ci_utilization_metadata +WHERE + workflow_id = { workflowId: UInt64} + AND repo = {repo: String } diff --git a/torchci/components/WorkflowBox.tsx b/torchci/components/WorkflowBox.tsx index 972b736d50..1c14369354 100644 --- a/torchci/components/WorkflowBox.tsx +++ b/torchci/components/WorkflowBox.tsx @@ -1,8 +1,13 @@ +import { Button, styled } from "@mui/material"; import styles from "components/commit.module.css"; import { fetcher } from "lib/GeneralUtils"; import { isFailedJob } from "lib/jobUtils"; import { getSearchRes, LogSearchResult } from "lib/searchLogs"; import { Artifact, IssueData, JobData } from "lib/types"; +import { + ListUtilizationMetadataInfoAPIResponse, + UtilizationMetadataInfo, +} from "lib/utilization/types"; import React, { useEffect, useState } from "react"; import useSWR from "swr"; import { getConclusionSeverityForSorting } from "../lib/JobClassifierUtil"; @@ -25,14 +30,22 @@ function sortJobsByConclusion(jobA: JobData, jobB: JobData): number { return ("" + jobA.jobName).localeCompare("" + jobB.jobName); // the '' forces the type to be a string } +const JobButton = styled(Button)({ + fontSize: "8px", + padding: "0 1px 0 1px", + color: "green", + margin: "2px", +}); function WorkflowJobSummary({ job, + utilMetadata, artifacts, artifactsToShow, setArtifactsToShow, unstableIssues, }: { job: JobData; + utilMetadata?: UtilizationMetadataInfo[]; artifacts?: Artifact[]; artifactsToShow: Set; setArtifactsToShow: any; @@ -73,15 +86,34 @@ function WorkflowJobSummary({ if (hasArtifacts) { subInfo.push( - setArtifactsToShowHelper()}>Show artifacts + setArtifactsToShowHelper()}> + artifacts + ); } - if (job.logUrl) { subInfo.push( - + Raw logs - + + ); + } + if (utilMetadata && utilMetadata.length > 0) { + if (utilMetadata.length > 1) { + console.log( + `Multiple util metadata found for job ${job.id}, currently only showing the first one` + ); + } + const m = utilMetadata[0]; + subInfo.push( + <> + + Utilization Report{" "} + + ); } @@ -95,7 +127,7 @@ function WorkflowJobSummary({ return ( {info} - {ind < subInfo.length - 1 && ", "} + {ind < subInfo.length - 1 && " "} ); })} @@ -124,18 +156,20 @@ export default function WorkflowBox({ setWide: any; repoFullName: string; }) { + const workflowId = jobs[0].workflowId; const isFailed = jobs.some(isFailedJob) !== false; const workflowClass = isFailed ? styles.workflowBoxFail : styles.workflowBoxSuccess; - const workflowId = jobs[0].workflowId; const anchorName = encodeURIComponent(workflowName.toLowerCase()); + const { utilMetadataList } = useUtilMetadata(workflowId); + const groupUtilMetadataList = groupMetadataByJobId(utilMetadataList); + const { artifacts, error } = useArtifacts(workflowId); const [artifactsToShow, setArtifactsToShow] = useState(new Set()); const groupedArtifacts = groupArtifacts(jobs, artifacts); - const [searchString, setSearchString] = useState(""); const [searchRes, setSearchRes] = useState<{ results: Map; @@ -144,6 +178,7 @@ export default function WorkflowBox({ results: new Map(), info: undefined, }); + useEffect(() => { getSearchRes(jobs, searchString, setSearchRes); }, [jobs, searchString]); @@ -216,6 +251,11 @@ export default function WorkflowBox({
( + `/api/list_utilization_metadata_info/${workflowId}`, + fetcher, + { + refreshInterval: 60 * 1000, // refresh every minute + // Refresh even when the user isn't looking, so that switching to the tab + // will always have fresh info. + refreshWhenHidden: true, + } + ); + + if (!workflowId) { + return { utilMetadataList: [], metaError: "No workflow ID" }; + } + + if (error != null) { + return { + utilMetadataList: [], + metaError: "Error occured while fetching util metadata", + }; + } + + if (data == null) { + return { utilMetadataList: [], metaError: "Loading..." }; + } + + if (data.metadata_list == null) { + return { utilMetadataList: [], metaError: "No metadata list found" }; + } + + return { utilMetadataList: data.metadata_list, metaError: null }; +} + function useArtifacts(workflowId: string | undefined): { artifacts: Artifact[]; error: any; @@ -260,6 +337,25 @@ function useArtifacts(workflowId: string | undefined): { return { artifacts: data, error }; } +function groupMetadataByJobId( + utilMetadataList: UtilizationMetadataInfo[] +): Map { + const grouping = new Map(); + for (const utilMetadata of utilMetadataList) { + if (!utilMetadata.job_id) { + continue; + } + + const jobId = utilMetadata.job_id.toString(); + if (grouping.has(jobId)) { + grouping.get(jobId)!.push(utilMetadata); + } else { + grouping.set(jobId, [utilMetadata]); + } + } + return grouping; +} + function groupArtifacts(jobs: JobData[], artifacts: Artifact[]) { // Group artifacts by job id if possible const jobIds = jobs.map((job) => job.id?.toString()); diff --git a/torchci/lib/utilization/fetchListUtilizationMetadataInfo.ts b/torchci/lib/utilization/fetchListUtilizationMetadataInfo.ts new file mode 100644 index 0000000000..af53fe1ec3 --- /dev/null +++ b/torchci/lib/utilization/fetchListUtilizationMetadataInfo.ts @@ -0,0 +1,36 @@ +import { queryClickhouseSaved } from "lib/clickhouse"; +import { + ListUtilizationMetadataInfoAPIResponse, + ListUtilizationMetadataInfoParams, + UTILIZATION_DEFAULT_REPO, + UtilizationMetadataInfo, +} from "./types"; +const LIST_UTIL_METADATA_INFO_QUERY_FOLDER_NAME = + "oss_ci_list_util_metadata_info"; + +export default async function fetchListUtilizationMetadataInfo( + params: ListUtilizationMetadataInfoParams +): Promise { + const meta_resp = await getUtilizationMetadataInfo( + params.workflow_id, + params.repo + ); + + return { + metadata_list: meta_resp ? meta_resp : ([] as UtilizationMetadataInfo[]), + }; +} + +async function getUtilizationMetadataInfo( + workflow_id: string, + repo: string = UTILIZATION_DEFAULT_REPO +) { + const response = await queryClickhouseSaved( + LIST_UTIL_METADATA_INFO_QUERY_FOLDER_NAME, + { + workflowId: workflow_id, + repo: repo, + } + ); + return response; +} diff --git a/torchci/lib/utilization/fetchUtilization.ts b/torchci/lib/utilization/fetchUtilization.ts index 20fa2a5b6f..01db0e934c 100644 --- a/torchci/lib/utilization/fetchUtilization.ts +++ b/torchci/lib/utilization/fetchUtilization.ts @@ -3,15 +3,15 @@ import { TimeSeriesDataPoint, TimeSeriesDbData, TimeSeriesWrapper, + UTILIZATION_DEFAULT_REPO, UtilizationAPIResponse, UtilizationMetadata, UtilizationParams, } from "./types"; -const DEFAULT_REPO = "pytorch/pytorch"; const UTIL_TS_QUERY_FOLDER_NAME = "oss_ci_util_ts"; -const UTIL_METADATA_QUERY_FOLDER_NAME = "oss_ci_util_metadata"; const UTILIZATION_TYPE = "utilization"; +const UTIL_METADATA_QUERY_FOLDER_NAME = "oss_ci_util_metadata"; export default async function fetchUtilization( params: UtilizationParams @@ -19,7 +19,8 @@ export default async function fetchUtilization( const meta_resp: UtilizationMetadata[] = await getUtilizationMetadata( params.workflow_id, params.job_id, - params.run_attempt + params.run_attempt, + params.repo ); const metadata = getLatestMetadata(meta_resp); @@ -40,7 +41,8 @@ export default async function fetchUtilization( const resp: TimeSeriesDbData[] = await getUtilTimesSeries( params.workflow_id, params.job_id, - params.run_attempt + params.run_attempt, + params.repo ); const tsMap = flattenTS(resp); @@ -61,14 +63,15 @@ export default async function fetchUtilization( async function getUtilTimesSeries( workflow_id: string, job_id: string, - run_attempt: string + run_attempt: string, + repo: string = UTILIZATION_DEFAULT_REPO ) { const response = await queryClickhouseSaved(UTIL_TS_QUERY_FOLDER_NAME, { workflowId: workflow_id, jobId: job_id, runAttempt: run_attempt, type: UTILIZATION_TYPE, - repo: DEFAULT_REPO, + repo: repo, }); return response; } @@ -76,14 +79,15 @@ async function getUtilTimesSeries( async function getUtilizationMetadata( workflow_id: string, job_id: string, - run_attempt: string + run_attempt: string, + repo: string = UTILIZATION_DEFAULT_REPO ) { const response = await queryClickhouseSaved(UTIL_METADATA_QUERY_FOLDER_NAME, { workflowId: workflow_id, jobId: job_id, runAttempt: run_attempt, type: UTILIZATION_TYPE, - repo: DEFAULT_REPO, + repo: repo, }); return response; } diff --git a/torchci/lib/utilization/types.ts b/torchci/lib/utilization/types.ts index 8f01e1aa36..a181c7e652 100644 --- a/torchci/lib/utilization/types.ts +++ b/torchci/lib/utilization/types.ts @@ -1,7 +1,10 @@ +export const UTILIZATION_DEFAULT_REPO = "pytorch/pytorch"; + export interface UtilizationParams { workflow_id: string; job_id: string; run_attempt: string; + repo?: string; } export interface TimeSeriesDbData { @@ -48,3 +51,21 @@ export interface TimeSeriesDataPoint { ts: string; value: number; } + +export interface UtilizationMetadataInfo { + workflow_id: string; + job_id: string; + run_attempt: string; + workflow_name: string; + job_name: string; + repo: string; +} + +export interface ListUtilizationMetadataInfoParams { + workflow_id: string; + repo?: string; +} + +export interface ListUtilizationMetadataInfoAPIResponse { + metadata_list: UtilizationMetadataInfo[]; +} diff --git a/torchci/pages/api/list_utilization_metadata_info/[workflowId].ts b/torchci/pages/api/list_utilization_metadata_info/[workflowId].ts new file mode 100644 index 0000000000..e032ab6f84 --- /dev/null +++ b/torchci/pages/api/list_utilization_metadata_info/[workflowId].ts @@ -0,0 +1,42 @@ +import { getErrorMessage } from "lib/error_utils"; +import fetchListUtilizationMetadataInfo from "lib/utilization/fetchListUtilizationMetadataInfo"; +import { + ListUtilizationMetadataInfoAPIResponse, + ListUtilizationMetadataInfoParams, +} from "lib/utilization/types"; +import { NextApiRequest, NextApiResponse } from "next"; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + const { workflowId } = req.query; + + // swr hook will call this api with empty query, return empty object + + if (!workflowId) { + const emptyResp: ListUtilizationMetadataInfoAPIResponse = { + metadata_list: [], + }; + return res.status(200).json(emptyResp); + } + + const params: ListUtilizationMetadataInfoParams = { + workflow_id: workflowId as string, + }; + + try { + const resp = await fetchListUtilizationMetadataInfo(params); + + if (!resp) { + const emptyResp: ListUtilizationMetadataInfoAPIResponse = { + metadata_list: [], + }; + return res.status(200).json(emptyResp); + } + return res.status(200).json(resp); + } catch (error) { + const err_msg = getErrorMessage(error); + return res.status(500).json({ error: err_msg }); + } +}