Skip to content

Commit 27cb511

Browse files
authored
[Utilization] Add url link to utilization report page (#6285)
add utilization report page navigation in workflowbox if there is any exist and ready the working demo depends on pr change: pytorch/pytorch#145310 working demo: https://torchci-1arxcelhf-fbopensource.vercel.app/pr/145310 I won't submit this util the [pr](pytorch/pytorch#145310) is sumbitted also checked such as execuTorch so far nothing is broken
1 parent 8aa8a37 commit 27cb511

File tree

7 files changed

+233
-15
lines changed

7 files changed

+233
-15
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"params": {
3+
"workflowId": "UInt64",
4+
"repo": "String"
5+
},
6+
"tests":[]
7+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
SELECT
2+
workflow_id,
3+
job_id,
4+
workflow_name,
5+
job_name,
6+
run_attempt,
7+
repo
8+
FROM
9+
misc.oss_ci_utilization_metadata
10+
WHERE
11+
workflow_id = { workflowId: UInt64}
12+
AND repo = {repo: String }

torchci/components/WorkflowBox.tsx

Lines changed: 103 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
1+
import { Button, styled } from "@mui/material";
12
import styles from "components/commit.module.css";
23
import { fetcher } from "lib/GeneralUtils";
34
import { isFailedJob } from "lib/jobUtils";
45
import { getSearchRes, LogSearchResult } from "lib/searchLogs";
56
import { Artifact, IssueData, JobData } from "lib/types";
7+
import {
8+
ListUtilizationMetadataInfoAPIResponse,
9+
UtilizationMetadataInfo,
10+
} from "lib/utilization/types";
611
import React, { useEffect, useState } from "react";
712
import useSWR from "swr";
813
import { getConclusionSeverityForSorting } from "../lib/JobClassifierUtil";
@@ -25,14 +30,22 @@ function sortJobsByConclusion(jobA: JobData, jobB: JobData): number {
2530
return ("" + jobA.jobName).localeCompare("" + jobB.jobName); // the '' forces the type to be a string
2631
}
2732

33+
const JobButton = styled(Button)({
34+
fontSize: "8px",
35+
padding: "0 1px 0 1px",
36+
color: "green",
37+
margin: "2px",
38+
});
2839
function WorkflowJobSummary({
2940
job,
41+
utilMetadata,
3042
artifacts,
3143
artifactsToShow,
3244
setArtifactsToShow,
3345
unstableIssues,
3446
}: {
3547
job: JobData;
48+
utilMetadata?: UtilizationMetadataInfo[];
3649
artifacts?: Artifact[];
3750
artifactsToShow: Set<string>;
3851
setArtifactsToShow: any;
@@ -73,15 +86,34 @@ function WorkflowJobSummary({
7386

7487
if (hasArtifacts) {
7588
subInfo.push(
76-
<a onClick={() => setArtifactsToShowHelper()}>Show artifacts</a>
89+
<JobButton variant="outlined" onClick={() => setArtifactsToShowHelper()}>
90+
artifacts
91+
</JobButton>
7792
);
7893
}
79-
8094
if (job.logUrl) {
8195
subInfo.push(
82-
<a target="_blank" rel="noreferrer" href={job.logUrl}>
96+
<JobButton variant="outlined" href={job.logUrl}>
8397
Raw logs
84-
</a>
98+
</JobButton>
99+
);
100+
}
101+
if (utilMetadata && utilMetadata.length > 0) {
102+
if (utilMetadata.length > 1) {
103+
console.log(
104+
`Multiple util metadata found for job ${job.id}, currently only showing the first one`
105+
);
106+
}
107+
const m = utilMetadata[0];
108+
subInfo.push(
109+
<>
110+
<JobButton
111+
variant="outlined"
112+
href={`/utilization/${m.workflow_id}/${m.job_id}/${m.run_attempt}`}
113+
>
114+
Utilization Report{" "}
115+
</JobButton>
116+
</>
85117
);
86118
}
87119

@@ -95,7 +127,7 @@ function WorkflowJobSummary({
95127
return (
96128
<span key={ind}>
97129
{info}
98-
{ind < subInfo.length - 1 && ", "}
130+
{ind < subInfo.length - 1 && " "}
99131
</span>
100132
);
101133
})}
@@ -124,18 +156,20 @@ export default function WorkflowBox({
124156
setWide: any;
125157
repoFullName: string;
126158
}) {
159+
const workflowId = jobs[0].workflowId;
127160
const isFailed = jobs.some(isFailedJob) !== false;
128161
const workflowClass = isFailed
129162
? styles.workflowBoxFail
130163
: styles.workflowBoxSuccess;
131164

132-
const workflowId = jobs[0].workflowId;
133165
const anchorName = encodeURIComponent(workflowName.toLowerCase());
134166

167+
const { utilMetadataList } = useUtilMetadata(workflowId);
168+
const groupUtilMetadataList = groupMetadataByJobId(utilMetadataList);
169+
135170
const { artifacts, error } = useArtifacts(workflowId);
136171
const [artifactsToShow, setArtifactsToShow] = useState(new Set<string>());
137172
const groupedArtifacts = groupArtifacts(jobs, artifacts);
138-
139173
const [searchString, setSearchString] = useState("");
140174
const [searchRes, setSearchRes] = useState<{
141175
results: Map<string, LogSearchResult>;
@@ -144,6 +178,7 @@ export default function WorkflowBox({
144178
results: new Map(),
145179
info: undefined,
146180
});
181+
147182
useEffect(() => {
148183
getSearchRes(jobs, searchString, setSearchRes);
149184
}, [jobs, searchString]);
@@ -216,6 +251,11 @@ export default function WorkflowBox({
216251
<div key={job.id} id={`${job.id}-box`}>
217252
<WorkflowJobSummary
218253
job={job}
254+
utilMetadata={
255+
job.id
256+
? groupUtilMetadataList.get(job.id.toString())
257+
: undefined
258+
}
219259
artifacts={groupedArtifacts?.get(job.id?.toString())}
220260
artifactsToShow={artifactsToShow}
221261
setArtifactsToShow={setArtifactsToShow}
@@ -236,6 +276,43 @@ export default function WorkflowBox({
236276
);
237277
}
238278

279+
function useUtilMetadata(workflowId: string | undefined): {
280+
utilMetadataList: UtilizationMetadataInfo[];
281+
metaError: any;
282+
} {
283+
const { data, error } = useSWR<ListUtilizationMetadataInfoAPIResponse>(
284+
`/api/list_utilization_metadata_info/${workflowId}`,
285+
fetcher,
286+
{
287+
refreshInterval: 60 * 1000, // refresh every minute
288+
// Refresh even when the user isn't looking, so that switching to the tab
289+
// will always have fresh info.
290+
refreshWhenHidden: true,
291+
}
292+
);
293+
294+
if (!workflowId) {
295+
return { utilMetadataList: [], metaError: "No workflow ID" };
296+
}
297+
298+
if (error != null) {
299+
return {
300+
utilMetadataList: [],
301+
metaError: "Error occured while fetching util metadata",
302+
};
303+
}
304+
305+
if (data == null) {
306+
return { utilMetadataList: [], metaError: "Loading..." };
307+
}
308+
309+
if (data.metadata_list == null) {
310+
return { utilMetadataList: [], metaError: "No metadata list found" };
311+
}
312+
313+
return { utilMetadataList: data.metadata_list, metaError: null };
314+
}
315+
239316
function useArtifacts(workflowId: string | undefined): {
240317
artifacts: Artifact[];
241318
error: any;
@@ -260,6 +337,25 @@ function useArtifacts(workflowId: string | undefined): {
260337
return { artifacts: data, error };
261338
}
262339

340+
function groupMetadataByJobId(
341+
utilMetadataList: UtilizationMetadataInfo[]
342+
): Map<string, UtilizationMetadataInfo[]> {
343+
const grouping = new Map<string, UtilizationMetadataInfo[]>();
344+
for (const utilMetadata of utilMetadataList) {
345+
if (!utilMetadata.job_id) {
346+
continue;
347+
}
348+
349+
const jobId = utilMetadata.job_id.toString();
350+
if (grouping.has(jobId)) {
351+
grouping.get(jobId)!.push(utilMetadata);
352+
} else {
353+
grouping.set(jobId, [utilMetadata]);
354+
}
355+
}
356+
return grouping;
357+
}
358+
263359
function groupArtifacts(jobs: JobData[], artifacts: Artifact[]) {
264360
// Group artifacts by job id if possible
265361
const jobIds = jobs.map((job) => job.id?.toString());
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { queryClickhouseSaved } from "lib/clickhouse";
2+
import {
3+
ListUtilizationMetadataInfoAPIResponse,
4+
ListUtilizationMetadataInfoParams,
5+
UTILIZATION_DEFAULT_REPO,
6+
UtilizationMetadataInfo,
7+
} from "./types";
8+
const LIST_UTIL_METADATA_INFO_QUERY_FOLDER_NAME =
9+
"oss_ci_list_util_metadata_info";
10+
11+
export default async function fetchListUtilizationMetadataInfo(
12+
params: ListUtilizationMetadataInfoParams
13+
): Promise<ListUtilizationMetadataInfoAPIResponse> {
14+
const meta_resp = await getUtilizationMetadataInfo(
15+
params.workflow_id,
16+
params.repo
17+
);
18+
19+
return {
20+
metadata_list: meta_resp ? meta_resp : ([] as UtilizationMetadataInfo[]),
21+
};
22+
}
23+
24+
async function getUtilizationMetadataInfo(
25+
workflow_id: string,
26+
repo: string = UTILIZATION_DEFAULT_REPO
27+
) {
28+
const response = await queryClickhouseSaved(
29+
LIST_UTIL_METADATA_INFO_QUERY_FOLDER_NAME,
30+
{
31+
workflowId: workflow_id,
32+
repo: repo,
33+
}
34+
);
35+
return response;
36+
}

torchci/lib/utilization/fetchUtilization.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,24 @@ import {
33
TimeSeriesDataPoint,
44
TimeSeriesDbData,
55
TimeSeriesWrapper,
6+
UTILIZATION_DEFAULT_REPO,
67
UtilizationAPIResponse,
78
UtilizationMetadata,
89
UtilizationParams,
910
} from "./types";
1011

11-
const DEFAULT_REPO = "pytorch/pytorch";
1212
const UTIL_TS_QUERY_FOLDER_NAME = "oss_ci_util_ts";
13-
const UTIL_METADATA_QUERY_FOLDER_NAME = "oss_ci_util_metadata";
1413
const UTILIZATION_TYPE = "utilization";
14+
const UTIL_METADATA_QUERY_FOLDER_NAME = "oss_ci_util_metadata";
1515

1616
export default async function fetchUtilization(
1717
params: UtilizationParams
1818
): Promise<UtilizationAPIResponse | null> {
1919
const meta_resp: UtilizationMetadata[] = await getUtilizationMetadata(
2020
params.workflow_id,
2121
params.job_id,
22-
params.run_attempt
22+
params.run_attempt,
23+
params.repo
2324
);
2425

2526
const metadata = getLatestMetadata(meta_resp);
@@ -40,7 +41,8 @@ export default async function fetchUtilization(
4041
const resp: TimeSeriesDbData[] = await getUtilTimesSeries(
4142
params.workflow_id,
4243
params.job_id,
43-
params.run_attempt
44+
params.run_attempt,
45+
params.repo
4446
);
4547
const tsMap = flattenTS(resp);
4648

@@ -61,29 +63,31 @@ export default async function fetchUtilization(
6163
async function getUtilTimesSeries(
6264
workflow_id: string,
6365
job_id: string,
64-
run_attempt: string
66+
run_attempt: string,
67+
repo: string = UTILIZATION_DEFAULT_REPO
6568
) {
6669
const response = await queryClickhouseSaved(UTIL_TS_QUERY_FOLDER_NAME, {
6770
workflowId: workflow_id,
6871
jobId: job_id,
6972
runAttempt: run_attempt,
7073
type: UTILIZATION_TYPE,
71-
repo: DEFAULT_REPO,
74+
repo: repo,
7275
});
7376
return response;
7477
}
7578

7679
async function getUtilizationMetadata(
7780
workflow_id: string,
7881
job_id: string,
79-
run_attempt: string
82+
run_attempt: string,
83+
repo: string = UTILIZATION_DEFAULT_REPO
8084
) {
8185
const response = await queryClickhouseSaved(UTIL_METADATA_QUERY_FOLDER_NAME, {
8286
workflowId: workflow_id,
8387
jobId: job_id,
8488
runAttempt: run_attempt,
8589
type: UTILIZATION_TYPE,
86-
repo: DEFAULT_REPO,
90+
repo: repo,
8791
});
8892
return response;
8993
}

torchci/lib/utilization/types.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
export const UTILIZATION_DEFAULT_REPO = "pytorch/pytorch";
2+
13
export interface UtilizationParams {
24
workflow_id: string;
35
job_id: string;
46
run_attempt: string;
7+
repo?: string;
58
}
69

710
export interface TimeSeriesDbData {
@@ -48,3 +51,21 @@ export interface TimeSeriesDataPoint {
4851
ts: string;
4952
value: number;
5053
}
54+
55+
export interface UtilizationMetadataInfo {
56+
workflow_id: string;
57+
job_id: string;
58+
run_attempt: string;
59+
workflow_name: string;
60+
job_name: string;
61+
repo: string;
62+
}
63+
64+
export interface ListUtilizationMetadataInfoParams {
65+
workflow_id: string;
66+
repo?: string;
67+
}
68+
69+
export interface ListUtilizationMetadataInfoAPIResponse {
70+
metadata_list: UtilizationMetadataInfo[];
71+
}

0 commit comments

Comments
 (0)