diff --git a/torchci/components/benchmark_v3/components/benchmarkSideBar/BenchmarkTopBar.tsx b/torchci/components/benchmark_v3/components/benchmarkSideBar/BenchmarkTopBar.tsx index 2447ab29f4..516edcf555 100644 --- a/torchci/components/benchmark_v3/components/benchmarkSideBar/BenchmarkTopBar.tsx +++ b/torchci/components/benchmark_v3/components/benchmarkSideBar/BenchmarkTopBar.tsx @@ -4,13 +4,16 @@ import { BenchmarkUIConfigHandler } from "components/benchmark_v3/configs/benchm import { BenchmarkReportFeatureNotification } from "../benchmarkRegressionReport/BenchmarkReportFeatureNotification"; import { BenchmarkReportFeatureSidePanel } from "../benchmarkRegressionReport/BenchmarkReportFeatureSidePanel"; import { CommitWorflowSelectSection } from "./components/commits/CommitWorkfowSelectSection"; +import { SingleCommitSelectSelection } from "./components/commits/SingleCommitSelectSelection"; export function BenchmarkTopBar({ config, title = "", + mode = "default", }: { config: BenchmarkUIConfigHandler; title?: string; + mode?: string; }) { const reportFeature = config.raw.dataRender?.sideRender?.RegressionReportFeature; @@ -41,9 +44,12 @@ export function BenchmarkTopBar({ )} - - - + {reportFeature && } + {reportFeature && ( + + )} + {mode == "default" && } + {mode == "single" && } ); diff --git a/torchci/components/benchmark_v3/components/benchmarkSideBar/components/SideBarMainSection.tsx b/torchci/components/benchmark_v3/components/benchmarkSideBar/components/SideBarMainSection.tsx index 24358eb956..becfb1d86f 100644 --- a/torchci/components/benchmark_v3/components/benchmarkSideBar/components/SideBarMainSection.tsx +++ b/torchci/components/benchmark_v3/components/benchmarkSideBar/components/SideBarMainSection.tsx @@ -107,6 +107,7 @@ export function SideBarMainSection() { branchOptionType, enableMultiBranchOption, enableSamplingSetting, + enableSamplingFeature, commitMainOptions, revertMainOptions, } = useDashboardSelector((s) => ({ @@ -115,6 +116,7 @@ export function SideBarMainSection() { stagedLbranch: s.stagedLbranch, stagedRbranch: s.stagedRbranch, stagedMaxSampling: s.stagedMaxSampling, + enableSamplingFeature: s.enableSamplingFeature, enableMultiBranchOption: s.enableMultiBranchOption, branchOptionType: s.branchOptionType, @@ -253,15 +255,19 @@ export function SideBarMainSection() { end={stagedTime.end} gap={0} /> - {/* Fetch Settings */} - - Fetch Settings - + {enableSamplingFeature && ( + <> + {/* Fetch Settings */} + + Fetch Settings{" "} + + + )} {showSamplinginfo && ( {`Data Sampling: subsample from ${sampling_info?.origin ?? 0} to ${ diff --git a/torchci/components/benchmark_v3/components/benchmarkSideBar/components/commits/BranchDropdown.tsx b/torchci/components/benchmark_v3/components/benchmarkSideBar/components/commits/BranchDropdown.tsx index d68a4a142f..c8da8fe1c8 100644 --- a/torchci/components/benchmark_v3/components/benchmarkSideBar/components/commits/BranchDropdown.tsx +++ b/torchci/components/benchmark_v3/components/benchmarkSideBar/components/commits/BranchDropdown.tsx @@ -49,7 +49,6 @@ export function BranchDropdowns({ return ( - {type} {empty ? ( No branch is found, please select other features. diff --git a/torchci/components/benchmark_v3/components/benchmarkSideBar/components/commits/SingleCommitSelectSelection.tsx b/torchci/components/benchmark_v3/components/benchmarkSideBar/components/commits/SingleCommitSelectSelection.tsx new file mode 100644 index 0000000000..1d01d82009 --- /dev/null +++ b/torchci/components/benchmark_v3/components/benchmarkSideBar/components/commits/SingleCommitSelectSelection.tsx @@ -0,0 +1,210 @@ +import OpenInNewIcon from "@mui/icons-material/OpenInNew"; +import { Typography } from "@mui/material"; +import { Box, Stack } from "@mui/system"; +import { useBenchmarkBook } from "components/benchmark_v3/configs/benchmark_config_book"; +import { QueryParameterConverterInputs } from "components/benchmark_v3/configs/utils/dataBindingRegistration"; +import { CenteredLoader } from "components/common/LoadingIcon"; +import { + UMDenseCommitDropdown, + UMDenseSingleButton, +} from "components/uiModules/UMDenseComponents"; +import { useBenchmarkCommitsData } from "lib/benchmark/api_helper/fe/hooks"; +import { useDashboardSelector } from "lib/benchmark/store/benchmark_dashboard_provider"; +import { BenchmarkCommitMeta } from "lib/benchmark/store/benchmark_regression_store"; +import { stateToQuery } from "lib/helpers/urlQuery"; +import { NextRouter, useRouter } from "next/router"; +import { useEffect, useState } from "react"; + +export function SingleCommitSelectSelection() { + const { + repo, + type, + benchmarkName, + benchmarkId, + committedTime, + committedFilters, + lcommit, + committedLBranch, + committedMaxSampling, + enableSamplingSetting, + setLcommit, + } = useDashboardSelector((s) => ({ + type: s.type, + benchmarkId: s.benchmarkId, + committedTime: s.committedTime, + committedFilters: s.committedFilters, + committedMaxSampling: s.committedMaxSampling, + enableSamplingSetting: s.enableSamplingSetting, + repo: s.repo, + benchmarkName: s.benchmarkName, + lcommit: s.lcommit, + rcommit: s.rcommit, + committedLBranch: s.committedLbranch, + setLcommit: s.setLcommit, + })); + + const [leftList, setLeftList] = useState([]); + const getConfig = useBenchmarkBook((s) => s.getConfig); + const config = getConfig(benchmarkId, type); + const dataBinding = config.dataBinding; + const required_filter_fields = config.raw?.required_filter_fields ?? []; + + const ready = + !!committedTime?.start && + !!committedTime?.end && + !!committedLBranch && + committedLBranch.length > 0 && + (enableSamplingSetting ? committedMaxSampling : true) && + required_filter_fields.every((k) => !!committedFilters[k]); + + // Fetch data + const branches = [...new Set([committedLBranch].filter((b) => b.length > 0))]; + + // Convert to query params + const params = dataBinding.toQueryParams({ + repo: repo, + benchmarkName: benchmarkName, + branches, + timeRange: committedTime, + filters: committedFilters, + maxSampling: enableSamplingSetting ? committedMaxSampling : undefined, + } as QueryParameterConverterInputs); + if (!params) { + throw new Error(`Failed to convert to query params for ${benchmarkId}`); + } + + const queryParams: any | null = ready ? params : null; + + // Fetch data + const { data, isLoading, error } = useBenchmarkCommitsData( + benchmarkId, + queryParams + ); + + useEffect(() => { + if (isLoading || !data) return; + + const groups = data?.data?.branch ?? []; + const branchMap = convertToBranchMap(groups); + + const L: BenchmarkCommitMeta[] = branchMap[committedLBranch] ?? []; + + // update list + setLeftList(L); + + if (L.length === 0) return; + + // check if user has selected a commit that is not in the list + const lSelected = lcommit?.workflow_id ?? null; + + const lHas = !!lSelected && L.some((c) => c.workflow_id === lSelected); + + // rule left and right both pick left option + const nextAutoL = lHas ? lSelected : L[0]?.workflow_id ?? null; + + if (!lHas) { + setLcommit( + nextAutoL ? L.find((c) => c.workflow_id === nextAutoL) ?? null : null + ); + } + }, [isLoading, data, committedLBranch, lcommit?.workflow_id, setLcommit]); + + if (error) return
Error: {error.message}
; + if (isLoading || !data) return ; + + return ( + + + Commit: + + + {lcommit?.branch}: + + { + setLcommit(c); + }} + /> + + + ); +} + +export const convertToBranchMap = ( + raw: any[] +): Record => { + return raw.reduce((acc, g) => { + const branch = g?.group_info?.branch ?? "unknown"; + acc[branch] = g.rows.map((r: any) => ({ + commit: r.commit, + workflow_id: String(r.workflow_id), + date: r.date, + branch, + })); + return acc; + }, {} as Record); +}; + +export function NavigateToDashboardButton({ + benchmarkId, + commit, +}: { + benchmarkId: string; + commit: BenchmarkCommitMeta | null; +}) { + const router = useRouter(); + if (!commit) { + return <>; + } + return ( + } + sx={{ + whiteSpace: "nowrap", + }} + > + View {commit.workflow_id} ({commit.commit.slice(0, 7)}) in Dashboard + + ); +} + +export function toDashboardUrl( + benchmarkId: string, + commit: BenchmarkCommitMeta, + router: NextRouter +) { + const pathname = `/benchmark/v3/dashboard/${benchmarkId}`; + const lcommit: BenchmarkCommitMeta = commit; + const rcommit: BenchmarkCommitMeta = commit; + const reformattedPrams = stateToQuery({ + lcommit, + rcommit, + }); + + const nextDashboardMainQuery = { + ...router.query, // keep existing params + ...reformattedPrams, + renderGroupId: "main", + }; + const params = new URLSearchParams( + Object.entries(nextDashboardMainQuery) + .filter(([_, v]) => v != null && v !== "") + .map(([k, v]) => [k, String(v)]) + ); + const url = `${pathname}?${params.toString()}`; + return url; +} diff --git a/torchci/components/benchmark_v3/components/common/BenchmarkLinkButton.tsx b/torchci/components/benchmark_v3/components/common/BenchmarkLinkButton.tsx new file mode 100644 index 0000000000..255813d25e --- /dev/null +++ b/torchci/components/benchmark_v3/components/common/BenchmarkLinkButton.tsx @@ -0,0 +1,43 @@ +import type { ButtonProps } from "@mui/material/Button"; +import Button from "@mui/material/Button"; +import { styled } from "@mui/material/styles"; +import * as React from "react"; + +/** Anchor-style MUI Button with proper typing for href/target/rel. */ +export interface BenchmarkLinkButtonProps + extends Omit, "component"> { + href: string; + /** Default: "_self". Use "_blank" for new tab. */ + target?: "_self" | "_blank" | "_parent" | "_top"; + /** Default: added automatically for _blank */ + rel?: string; +} + +export const LinkButton = React.forwardRef< + HTMLAnchorElement, + BenchmarkLinkButtonProps +>(({ href, target = "_self", rel, ...props }, ref) => { + // Security for new-tab links + const finalRel = target === "_blank" ? rel ?? "noopener noreferrer" : rel; + return ( + + JSX.Element; }) { @@ -167,6 +170,7 @@ export function RenderRawContent({ type={type} component={component} buttonName={buttonName} + sx={buttonSx} /> ); } diff --git a/torchci/components/benchmark_v3/components/dataRender/auto/autoComponents.tsx b/torchci/components/benchmark_v3/components/dataRender/auto/autoComponents.tsx index 78fc593f75..727823870f 100644 --- a/torchci/components/benchmark_v3/components/dataRender/auto/autoComponents.tsx +++ b/torchci/components/benchmark_v3/components/dataRender/auto/autoComponents.tsx @@ -11,8 +11,10 @@ import BenchmarkRawDataTable from "../components/benchmarkTimeSeries/components/ import { LOG_PREFIX } from "components/benchmark/common"; import { UIRenderConfig } from "components/benchmark_v3/configs/config_book_types"; +import { useRouter } from "next/router"; import { BenchmarkLogSidePanelWrapper } from "../../common/BenchmarkLogViewer/BenchmarkSidePanel"; -import { RenderRawContent } from "../../common/RawContentDialog"; +import BenchmarkSingleDataTable from "../components/benchmarkTimeSeries/components/BenchmarkSingleDataTable"; +import { BenchmarkSingleViewNavigation } from "../components/benchmarkTimeSeries/components/BenchmarkSingleViewNatigation"; import BenchmarkTimeSeriesChartGroup from "../components/benchmarkTimeSeries/components/BenchmarkTimeSeriesChart/BenchmarkTimeSeriesChartGroup"; import { ComparisonTable } from "../components/benchmarkTimeSeries/components/BenchmarkTimeSeriesComparisonSection/BenchmarkTimeSeriesComparisonTable/ComparisonTable"; @@ -269,7 +271,6 @@ export function AutoBenchmarkPairwiseTable({ config }: AutoComponentProps) { return ( - ; + } + return ( + + ); +} + export function AutoBenchmarkLogs({ config }: AutoComponentProps) { const ctx = useBenchmarkCommittedContext(); @@ -557,7 +590,7 @@ export function AutoBenchmarkRawDataTable({ config }: AutoComponentProps) { return ( - + !!ctx.committedFilters[k]); + + const dataBinding = ctx?.configHandler.dataBinding; + const uiRenderConfig = config as UIRenderConfig; + + const branches = [ + ...new Set([ctx.committedLbranch].filter((b) => b.length > 0)), + ]; + const workflows = ctx.lcommit?.workflow_id ? [ctx.lcommit?.workflow_id] : []; + + // convert to the query params + const params = dataBinding.toQueryParams({ + repo: ctx.repo, + branches: branches, + benchmarkName: ctx.benchmarkName, + timeRange: ctx.committedTime, + filters: ctx.committedFilters, + maxSampling: ctx.committedMaxSampling, + workflows, + }); + + const queryParams: any | null = ready ? params : null; + // fetch the bechmark data + const { + data: resp, + isLoading, + error, + } = useBenchmarkTimeSeriesData(ctx.benchmarkId, queryParams, ["table"]); + + if (isLoading) { + return ( + + ); + } + + if (error) { + return ( + + (AutoBenchmarkSingleDataTable){error.message} + + ); + } + + if (!resp?.data?.data) { + return
no data
; + } + const data = resp?.data?.data; + return ( + + + + + + ); +} + export function collectJobGroupInfoUniques( rows: any[], fields: string[] diff --git a/torchci/components/benchmark_v3/components/dataRender/components/benchmarkTimeSeries/components/BenchmarkRawDataTable.tsx b/torchci/components/benchmark_v3/components/dataRender/components/benchmarkTimeSeries/components/BenchmarkRawDataTable.tsx index ea92ef8529..a2a9e52ca5 100644 --- a/torchci/components/benchmark_v3/components/dataRender/components/benchmarkTimeSeries/components/BenchmarkRawDataTable.tsx +++ b/torchci/components/benchmark_v3/components/dataRender/components/benchmarkTimeSeries/components/BenchmarkRawDataTable.tsx @@ -1,4 +1,4 @@ -import { Button, Tooltip, Typography } from "@mui/material"; +import { Tooltip, Typography } from "@mui/material"; import { Box } from "@mui/system"; import { DataGrid, @@ -7,6 +7,7 @@ import { useGridApiRef, } from "@mui/x-data-grid"; import { RenderRawContent } from "components/benchmark_v3/components/common/RawContentDialog"; +import { UMDenseSingleButton } from "components/uiModules/UMDenseComponents"; import Link from "next/link"; import { useMemo } from "react"; import { @@ -62,21 +63,30 @@ export default function BenchmarkRawDataTable({ [allColumns, config] ); + const tableRenderingBook = config?.renderOptions?.tableRenderingBook as + | Record + | undefined; + + const columnVisibilityModel = Object.fromEntries( + Object.entries(tableRenderingBook ?? {}) + .filter(([_, v]) => v?.hide) + .map(([k]) => [k, false]) + ); + return ( {title?.text} {title?.description && ( {title.description} )} - {isDebug && ( - - )} - + { + return ToRawTableRow(config, data); + }, [data]); + + const allColumns = useMemo(() => { + const s = new Set(); + rows.forEach((r) => + r.rowItem.forEach((i: any) => { + Object.keys(i ?? {}).forEach((k) => { + const groupItem = i[k]; + // helps debuging if the group item has more than one data item + if (groupItem?.data?.length > 1) { + groupItem.data.forEach((d: any, idx: number) => { + s.add(`${k}||${idx}`); + }); + } else { + s.add(k); + } + }); + }) + ); + const auto = Array.from(s).sort(); + return auto; + }, [rows]); + + const columns: GridColDef[] = useMemo( + () => getTableConlumnRendering(config, allColumns), + [allColumns, config] + ); + + const tableRenderingBook = config?.renderOptions?.tableRenderingBook as + | Record + | undefined; + + const columnVisibilityModel = Object.fromEntries( + Object.entries(tableRenderingBook ?? {}) + .filter(([_, v]) => v?.hide) + .map(([k]) => [k, false]) + ); + + return ( + + {title?.text} + {title?.description && ( + {title.description} + )} + + + + + ); +} + +/** + * function to get the table column rendering logics + * + * @param config + * @param metricFields + * @returns + */ +function getTableConlumnRendering( + config: any, + metricFields: string[] = [] +): GridColDef[] { + const metadataColumns: any[] = [ + { + field: "workflow_run", + headerName: "Workflow Run", + minWidth: 140, + valueGetter: (_value: any, row: any) => { + const wf = row?.workflow_id ?? ""; + const job = row?.job_id ?? ""; + return `${wf}/${job}`; + }, + valueFormatter: (value: any, row: any) => { + return value ?? ""; + }, + renderCell: (params: GridRenderCellParams) => { + const tooltipText = `navigate to github page for job ${params.value} + }`; + return ( + + + {params.value} + + + ); + }, + }, + { + field: "commit", + headerName: "Commit", + renderCell: (params: GridRenderCellParams) => { + const tooltipText = `navigate to job run in hud commit page`; + return ( + + + + {String(params.value)} + + + + ); + }, + }, + ]; + + const metadata = config?.extraMetadata ?? []; + const metadataCols: GridColDef[] = metadata + .filter((k: any) => !!k.field) // skip fields that are not defined + .map((k: any) => ({ + field: k.field, + headerName: k.displayName ?? k.field, + renderCell: (p: any) => ( + {p.row[k.field]} + ), + })); + + const metricCols: GridColDef[] = metricFields.map((field) => ({ + field, + headerName: formatHeaderName( + field, + config?.renderOptions?.tableRenderingBook + ), + flex: 0.5, + sortable: false, + filterable: false, + valueGetter: (_value: any, row: any) => { + const data = row.rowItem; + if (data.length == 0) { + return undefined; + } + let fieldName = field; + let idx = 0; + if (field.split("||").length > 1) { + idx = Number(field.split("||")[1]); + fieldName = field.split("||")[0]; + } + const value = data[0][fieldName]?.data[idx]?.value; + return value; + }, + valueFormatter: (value: any, row: any) => { + let fieldName = field; + const rc = getBenchmarkTimeSeriesComparisionTableRenderingConfig( + fieldName, + config + ); + + if (!value) { + return "missing data"; + } + return renderBasedOnUnitConifg(value, rc?.unit); + }, + renderCell: (params: GridRenderCellParams) => { + if (config?.renderOptions?.highlightPolicy) { + const policy = config?.renderOptions?.highlightPolicy; + return renderHighlight(policy, params); + } + return {params.formattedValue ?? ""}; + }, + })); + + return [...metadataColumns, ...metadataCols, ...metricCols]; +} + +function renderHighlight( + highlightPolicy: any, + params: GridRenderCellParams +) { + if (highlightPolicy.direction != "row") { + return {params.formattedValue ?? ""}; + } + const policy = highlightPolicy?.policy ?? "max"; + const regex = highlightPolicy?.regex; + const highlighColor = highlightPolicy?.color ?? GOOD_COLOR; + const highlight = shouldHighlightCellByRowExtrema(params, policy, regex); + return ( + + {params.formattedValue ?? params.value ?? ""} + + ); +} + +/** + * Transform the data into a table row item for rendering + * @param config + * @param data + * @returns + */ +export function ToRawTableRow(config: any, data: any) { + const m = new Map(); + for (const d of data ?? []) { + const i = d.group_info; + const wf = String(i?.workflow_id ?? ""); + const jobId = String(i?.job_id ?? ""); + const sourceRepo = i?.repo ?? ""; + const hudCommitUrl = `/${sourceRepo}/commit/${ + i?.commit ?? "" + }#${jobId}-box`; + const gitRepoUrl = `https://github.com/${sourceRepo}`; + const jobUrl = `${gitRepoUrl}/actions/runs/${wf}/job/${jobId}`; + const rawData = d.data ?? []; + const { key } = groupKeyAndLabel(i); + if (!m.has(key)) { + m.set(key, { + ...i, + job_id: jobId, + workflow_id: wf, + commit: i?.commit ?? "", + commit_url: hudCommitUrl, + job_url: jobUrl, + repo: String(i?.repo ?? ""), + timestamp: i?.granularity_bucket ?? "", + id: key, + rowItem: [], + }); + } + m.get(key)!.rowItem.push(rawData); + } + return Array.from(m.values()); +} + +export function shouldHighlightCellByRowExtrema( + params: GridRenderCellParams, + policy: "min" | "max", + regexString?: string +): boolean { + const rowItems = params.row?.rowItem; + if (!Array.isArray(rowItems) || rowItems.length === 0) return false; + + // parse "field||idx" -> base name + index + const [baseField, idxStr] = String(params.field).split("||"); + const idx = Number.isFinite(Number(idxStr)) ? Number(idxStr) : 0; + + // flatten rowItem[0] + const root = rowItems[0] ?? {}; + const flattened: Record = {}; + + for (const key of Object.keys(root)) { + const arr = root[key]?.data; + if (!Array.isArray(arr) || arr.length <= idx) continue; + const v = arr[idx]?.value; + if (typeof v === "number" && Number.isFinite(v)) { + flattened[key] = v; + } + } + + const current = flattened[baseField]; + + if (current == null) return false; + + // optional regex filter + let regex: RegExp | null = null; + if (regexString) { + try { + regex = new RegExp(regexString); + } catch { + regex = null; + } + } + + const entries = Object.entries(flattened).filter(([k]) => + regex ? regex.test(k) : true + ); + + if (entries.length === 0) return false; + + const values = entries.map(([, v]) => v); + const minV = Math.min(...values); + const maxV = Math.max(...values); + + return policy === "min" ? current === minV : current === maxV; +} diff --git a/torchci/components/benchmark_v3/components/dataRender/components/benchmarkTimeSeries/components/BenchmarkSingleViewNatigation.tsx b/torchci/components/benchmark_v3/components/dataRender/components/benchmarkTimeSeries/components/BenchmarkSingleViewNatigation.tsx new file mode 100644 index 0000000000..bfe0fcf8a6 --- /dev/null +++ b/torchci/components/benchmark_v3/components/dataRender/components/benchmarkTimeSeries/components/BenchmarkSingleViewNatigation.tsx @@ -0,0 +1,87 @@ +import { Typography } from "@mui/material"; +import { Box } from "@mui/system"; +import { BenchmarkLinkButton } from "components/benchmark_v3/components/common/BenchmarkLinkButton"; +import { UIRenderConfig } from "components/benchmark_v3/configs/config_book_types"; +import { BenchmarkCommitMeta } from "lib/benchmark/store/benchmark_regression_store"; +import { stateToQuery } from "lib/helpers/urlQuery"; +import { NextRouter, useRouter } from "next/router"; + +export function BenchmarkSingleViewNavigation({ + benchmarkId, + lcommit, + rcommit, + config, + title, +}: { + benchmarkId: string; + lcommit?: BenchmarkCommitMeta | null; + rcommit?: BenchmarkCommitMeta | null; + title?: { + text?: string; + description?: string; + }; + config: UIRenderConfig; +}) { + const router = useRouter(); + + const uiRenderConfig = config as UIRenderConfig; + + if (!lcommit || !rcommit) { + return <>; + } + + return ( + + {title?.text && {title?.text}} + {title?.description && ( + {title.description} + )} + + {lcommit.workflow_id} ({lcommit.commit.slice(0, 7)}) + {" "} + + {rcommit.workflow_id} ({rcommit.commit.slice(0, 7)}) + + + ); +} + +export function toSingleViewUrl( + benchmarkId: string, + commit: BenchmarkCommitMeta, + router: NextRouter +) { + const pathname = `/benchmark/v3/single/${benchmarkId}`; + const lcommit: BenchmarkCommitMeta = commit; + const rcommit: BenchmarkCommitMeta = { + commit: "", + date: "", + workflow_id: "", + branch: "", + }; + const reformattedPrams = stateToQuery({ + lcommit, + rcommit, + }); + + const nextDashboardMainQuery = { + ...router.query, // keep existing params + ...reformattedPrams, + renderGroupId: "main", + }; + const params = new URLSearchParams( + Object.entries(nextDashboardMainQuery) + .filter(([_, v]) => v != null && v !== "") + .map(([k, v]) => [k, String(v)]) + ); + const url = `${pathname}?${params.toString()}`; + return url; +} diff --git a/torchci/components/benchmark_v3/components/dataRender/components/benchmarkTimeSeries/components/BenchmarkTimeSeriesComparisonSection/BenchmarkTimeSeriesComparisonTable/ComparisonTable.tsx b/torchci/components/benchmark_v3/components/dataRender/components/benchmarkTimeSeries/components/BenchmarkTimeSeriesComparisonSection/BenchmarkTimeSeriesComparisonTable/ComparisonTable.tsx index 6a723896f5..044043d78f 100644 --- a/torchci/components/benchmark_v3/components/dataRender/components/benchmarkTimeSeries/components/BenchmarkTimeSeriesComparisonSection/BenchmarkTimeSeriesComparisonTable/ComparisonTable.tsx +++ b/torchci/components/benchmark_v3/components/dataRender/components/benchmarkTimeSeries/components/BenchmarkTimeSeriesComparisonSection/BenchmarkTimeSeriesComparisonTable/ComparisonTable.tsx @@ -6,6 +6,7 @@ import { GridRowModel, useGridApiRef, } from "@mui/x-data-grid"; +import { RenderRawContent } from "components/benchmark_v3/components/common/RawContentDialog"; import { SelectionDialog } from "components/benchmark_v3/components/common/SelectionDialog"; import { useMemo, useState } from "react"; import { ComparisonTableConfig } from "../../../helper"; @@ -87,11 +88,15 @@ export function ComparisonTable({ [allColumns, lWorkflowId, rWorkflowId, title] ); + const tableRenderingBook = config?.renderOptions?.tableRenderingBook as + | Record + | undefined; const columnVisibilityModel = Object.fromEntries( - Object.entries(config?.renderOptions?.tableRenderingBook ?? {}) - .filter(([_, v]) => v?.hide === true) + Object.entries(tableRenderingBook ?? {}) + .filter(([_, v]) => v?.hide) .map(([k]) => [k, false]) ); + return ( {title.text} @@ -101,8 +106,24 @@ export function ComparisonTable({ {lWorkflowId} - {rWorkflowId} + {!config?.disableExport && ( )} + ()((set, get) => ({ predefined: PREDEFINED_BENCHMARK_CONFIG, temps: {}, - initTempConfig: ( id, type: BenchmarkPageType = BenchmarkPageType.DashboardPage, @@ -147,13 +149,16 @@ export const useBenchmarkBook = create()((set, get) => ({ case BenchmarkPageType.DashboardPage: defaultConfig = defaultDashboardBenchmarkUIConfig; break; + case BenchmarkPageType.SinglePage: + defaultConfig = defaultSingleBenchmarkUIConfig; + break; default: throw new Error( - `Cannot create default page, We currently only support default Dashboard Page, but you request page type: ${type}` + `Cannot create default page, We currently only support default Dashboard Page and Single Page, but you request page type: ${type}` ); } const cfg: BenchmarkUIConfig = { - ...defaultDashboardBenchmarkUIConfig, + ...defaultConfig, type, benchmarkId: id, apiId: params.apiId ?? id, @@ -181,6 +186,7 @@ export const useBenchmarkBook = create()((set, get) => ({ [id]: updatedGroup, }, }); + console.log("initialed", cfg); return cfg; }, @@ -204,12 +210,14 @@ export const useBenchmarkBook = create()((set, get) => ({ if (!type) throw new Error("getConfig: type is required"); const { predefined, temps } = get(); - const group = predefined[id] ?? temps[id]; - const cfg = group?.[type]; + const pg = predefined[id]; + const tmpg = temps[id]; + + const cfg = pg?.[type] ?? tmpg?.[type]; if (!cfg) throw new Error( `No config found for id: ${id} and ${type}, Group: ${ - group ? "found the group" : "missing the group" + pg || tmpg ? "found the group" : "missing the group" }` ); return new BenchmarkUIConfigHandler(cfg); diff --git a/torchci/components/benchmark_v3/configs/config_book_types.ts b/torchci/components/benchmark_v3/configs/config_book_types.ts index e161eecb36..6e9dea2823 100644 --- a/torchci/components/benchmark_v3/configs/config_book_types.ts +++ b/torchci/components/benchmark_v3/configs/config_book_types.ts @@ -46,6 +46,7 @@ export type BenchmarkUIConfigFilterConstarintConfig = { export type UIRenderConfig = { title?: string; // title of the component to render + description?: string; // description of the component to render id?: string; // id of the component to render type: string; // type of the component to render config: any; // config of the component to render diff --git a/torchci/components/benchmark_v3/configs/teams/compilers/config.ts b/torchci/components/benchmark_v3/configs/teams/compilers/config.ts index 554e5172dc..823661646e 100644 --- a/torchci/components/benchmark_v3/configs/teams/compilers/config.ts +++ b/torchci/components/benchmark_v3/configs/teams/compilers/config.ts @@ -193,6 +193,7 @@ export const COMPILTER_PRECOMPUTE_BENCHMARK_INITIAL = { }, lbranch: "main", rbranch: "main", + enableSamplingFeature: true, maxSampling: 110, // max number of job run results to show in the table, this avoid out of memory issue }; diff --git a/torchci/components/benchmark_v3/configs/teams/defaults/default_single_view_config.ts b/torchci/components/benchmark_v3/configs/teams/defaults/default_single_view_config.ts new file mode 100644 index 0000000000..1859ec5e11 --- /dev/null +++ b/torchci/components/benchmark_v3/configs/teams/defaults/default_single_view_config.ts @@ -0,0 +1,54 @@ +import dayjs from "dayjs"; +import utc from "dayjs/plugin/utc"; +import { BenchmarkUIConfig } from "../../config_book_types"; +dayjs.extend(utc); + +export const DEFAULT_SINGLE_VIEW_ID = "default-single"; + +// The initial config for the compiler benchmark regression page +export const DEFAULT_SINGLE_VIEW_BENCHMARK_INITIAL = { + time: { + start: dayjs.utc().startOf("day").subtract(7, "day"), + end: dayjs.utc().endOf("day"), + }, + filters: {}, + lbranch: "main", + rbranch: "main", +}; + +export const DEFAULT_TABLE_METADATA_COLUMNS = [ + { + field: "branch", + displayName: "Branch", + }, + { + field: "device", + displayName: "Hardware type", + }, + { + field: "arch", + displayName: "Hardware model", + }, +] as const; + +export const defaultSingleBenchmarkUIConfig: BenchmarkUIConfig | any = { + benchmarkId: DEFAULT_SINGLE_VIEW_ID, + apiId: DEFAULT_SINGLE_VIEW_ID, + title: "Default Single View", + dataBinding: { + initial: DEFAULT_SINGLE_VIEW_BENCHMARK_INITIAL, + required_filter_fields: [], + }, + dataRender: { + type: "auto", + renders: [ + { + type: "AutoBenchmarkSingleDataTable", + title: "Single Table", + config: { + extraMetadata: DEFAULT_TABLE_METADATA_COLUMNS, + }, + }, + ], + }, +}; diff --git a/torchci/components/benchmark_v3/configs/teams/helion/config.ts b/torchci/components/benchmark_v3/configs/teams/helion/config.ts index c62cb79913..4236085a6e 100644 --- a/torchci/components/benchmark_v3/configs/teams/helion/config.ts +++ b/torchci/components/benchmark_v3/configs/teams/helion/config.ts @@ -1,5 +1,6 @@ import { BenchmarkUIConfig } from "../../config_book_types"; import { DEFAULT_DASHBOARD_BENCHMARK_INITIAL } from "../defaults/default_dashboard_config"; +import { DEFAULT_SINGLE_VIEW_BENCHMARK_INITIAL } from "../defaults/default_single_view_config"; export const PYTORCH_HELION_BENCHMARK_ID = "pytorch_helion"; @@ -9,6 +10,10 @@ const initialOptions = { }; const COMPARISON_TABLE_METADATA_COLUMNS = [ + { + field: "branch", + displayName: "branch", + }, { field: "device", displayName: "Hardware type", @@ -17,10 +22,6 @@ const COMPARISON_TABLE_METADATA_COLUMNS = [ field: "arch", displayName: "Hardware model", }, - { - field: "branch", - displayName: "branch", - }, ] as const; const RENDER_MAPPING_BOOK = { @@ -52,6 +53,37 @@ const RENDER_MAPPING_BOOK = { hide: true, }, }; + +export const PytorchHelionSingleConfig: BenchmarkUIConfig | any = { + benchmarkId: PYTORCH_HELION_BENCHMARK_ID, + apiId: "pytorch_helion", + title: "Helion Single View", + dataBinding: { + initial: DEFAULT_SINGLE_VIEW_BENCHMARK_INITIAL, + required_filter_fields: [], + }, + dataRender: { + type: "auto", + renders: [ + { + type: "AutoBenchmarkSingleDataTable", + title: "Single Run Table", + config: { + extraMetadata: COMPARISON_TABLE_METADATA_COLUMNS, + renderOptions: { + tableRenderingBook: RENDER_MAPPING_BOOK, + highlightPolicy: { + direction: "row", + regex: "_speedup$", + policy: "max", + }, + }, + }, + }, + ], + }, +}; + export const PytorchHelionDashboardConfig: BenchmarkUIConfig = { benchmarkId: PYTORCH_HELION_BENCHMARK_ID, apiId: "pytorch_helion", @@ -123,6 +155,11 @@ export const PytorchHelionDashboardConfig: BenchmarkUIConfig = { }, }, renders: [ + { + type: "AutoBenchmarkSingleViewNavigation", + description: "See single view for left and right runs", + config: {}, + }, { type: "AutoBenchmarkPairwiseTable", title: "Comparison Table", diff --git a/torchci/components/benchmark_v3/configs/utils/autoRegistration.tsx b/torchci/components/benchmark_v3/configs/utils/autoRegistration.tsx index 1a5cc9f708..a6becc517c 100644 --- a/torchci/components/benchmark_v3/configs/utils/autoRegistration.tsx +++ b/torchci/components/benchmark_v3/configs/utils/autoRegistration.tsx @@ -2,6 +2,8 @@ import { AutoBenchmarkLogs, AutoBenchmarkPairwiseTable, AutoBenchmarkRawDataTable, + AutoBenchmarkSingleDataTable, + AutoBenchmarkSingleViewNavigation, AutoBenchmarkTimeSeriesChartGroup, AutoBenchmarkTimeSeriesTable, } from "components/benchmark_v3/components/dataRender/auto/autoComponents"; @@ -49,12 +51,18 @@ export class AutoComponentRegistry { AutoBenchmarkPairwiseTable: { Component: AutoBenchmarkPairwiseTable, }, + AutoBenchmarkSingleViewNavigation: { + Component: AutoBenchmarkSingleViewNavigation, + }, AutoBenchmarkTimeSeriesChartGroup: { Component: AutoBenchmarkTimeSeriesChartGroup, }, AutoBenchmarkRawDataTable: { Component: AutoBenchmarkRawDataTable, }, + AutoBenchmarkSingleDataTable: { + Component: AutoBenchmarkSingleDataTable, + }, AutoBenchmarkLogs: { Component: AutoBenchmarkLogs, }, diff --git a/torchci/components/benchmark_v3/pages/BenchmarkSinglePage.tsx b/torchci/components/benchmark_v3/pages/BenchmarkSinglePage.tsx new file mode 100644 index 0000000000..cf1b4593ae --- /dev/null +++ b/torchci/components/benchmark_v3/pages/BenchmarkSinglePage.tsx @@ -0,0 +1,70 @@ +import { Box } from "@mui/system"; +import { + BenchmarkUIConfigHandler, + useBenchmarkBook, +} from "components/benchmark_v3/configs/benchmark_config_book"; +import LoadingPage from "components/common/LoadingPage"; +import dayjs from "dayjs"; +import utc from "dayjs/plugin/utc"; +import { BenchmarkDashboardStoreProvider } from "lib/benchmark/store/benchmark_dashboard_provider"; +import { useRouter } from "next/router"; +import { useEffect, useState } from "react"; +import BenchmarkSideBar from "../components/benchmarkSideBar/BenchmarkSideBar"; +import { BenchmarkTopBar } from "../components/benchmarkSideBar/BenchmarkTopBar"; +import { BenchmarkIdNotRegisterError } from "../components/common/BenchmarkIdNotRegisterError"; +import { BenchmarkPageType } from "../configs/config_book_types"; +import { getBenchmarkIdMappingItem } from "../configs/configurations"; +dayjs.extend(utc); + +export default function BenchmarkSinglePage({ + benchmarkId, + type, +}: { + benchmarkId: string; + type: BenchmarkPageType; +}) { + const router = useRouter(); + + // ensure config will read the config from the store if it's predefined, + // otherwise it will create a new config based on default template + const ensureConfig = useBenchmarkBook((s) => s.ensureConfig); + const [config, setConfig] = useState(); + + useEffect(() => { + if (!router.isReady) return; + const configHandler = ensureConfig(benchmarkId, type, {}); + setConfig(configHandler); + }, [router.isReady, benchmarkId]); + + if (!config) return ; + + const mappingItem = getBenchmarkIdMappingItem(benchmarkId); + if (!mappingItem) { + return ( + + ); + } + + const Comp = config.getDataRenderComponent(); + return ( + + + + + + + + + + + + ); +} diff --git a/torchci/components/uiModules/UMDenseComponents.tsx b/torchci/components/uiModules/UMDenseComponents.tsx index 0480685d37..827240cc81 100644 --- a/torchci/components/uiModules/UMDenseComponents.tsx +++ b/torchci/components/uiModules/UMDenseComponents.tsx @@ -1,5 +1,6 @@ import { Button, + ButtonProps, FormControl, InputLabel, MenuItem, @@ -29,6 +30,16 @@ export const UMDenseButtonLight = styled(Button)(({ theme }) => ({ textTransform: "none", // optional: avoids uppercase })); +export const UMDenseSingleButton = styled(Button)(({ theme }) => ({ + px: 0.5, + py: 0, + mx: 1, + minWidth: "auto", + lineHeight: 2, + fontSize: "0.75rem", + textTransform: "none", +})); + // Reusable dense menu style (affects the dropdown list items) export const DENSE_MENU_STYLE = { // shrink the whole list diff --git a/torchci/lib/benchmark/store/benchmark_regression_store.ts b/torchci/lib/benchmark/store/benchmark_regression_store.ts index 9c2a913fd8..085d0da792 100644 --- a/torchci/lib/benchmark/store/benchmark_regression_store.ts +++ b/torchci/lib/benchmark/store/benchmark_regression_store.ts @@ -36,6 +36,7 @@ export interface BenchmarkDashboardState { committedLbranch: string; committedRbranch: string; + enableSamplingFeature?: boolean; enableSamplingSetting?: boolean; // max sampling threshold, if null, no limit. // otherwise, we subsampling data in backend to fit the limit during the data @@ -112,6 +113,7 @@ export function createDashboardStore(initial: { rcommit?: BenchmarkCommitMeta | null; renderGroupId?: string; maxSampling?: number; + enableSamplingFeature?: boolean; enableMultiBranchOption?: boolean; }) { const idItem = BENCHMARK_ID_MAPPING[initial.benchmarkId]; @@ -132,11 +134,19 @@ export function createDashboardStore(initial: { branchOptionType: "single", // set only with initial config - enableSamplingSetting: (initial.maxSampling ?? 0) > 0, + enableSamplingFeature: initial.enableSamplingFeature, + enableSamplingSetting: + initial?.enableSamplingFeature && + initial?.maxSampling && + initial?.maxSampling > 0 + ? true + : false, // max sampling threshold, if null, no limit. // otherwise, we subsampling data in backend to fit the limit during the data // the min sampling threshold is 10 - committedMaxSampling: initial.maxSampling, + committedMaxSampling: initial.enableSamplingFeature + ? initial.maxSampling + : undefined, // todo(elainewy): may allow user to set a different max sampling threshold based on their needs stagedMaxSampling: initial.maxSampling, diff --git a/torchci/pages/benchmark/v3/single/[id].tsx b/torchci/pages/benchmark/v3/single/[id].tsx new file mode 100644 index 0000000000..76aaf6e6d5 --- /dev/null +++ b/torchci/pages/benchmark/v3/single/[id].tsx @@ -0,0 +1,14 @@ +import { Alert } from "@mui/material"; +import { BenchmarkPageType } from "components/benchmark_v3/configs/config_book_types"; +import BenchmarkSinglePage from "components/benchmark_v3/pages/BenchmarkSinglePage"; +import { useRouter } from "next/router"; + +export default function Page() { + const router = useRouter(); + const { id } = router.query; + const type: BenchmarkPageType = BenchmarkPageType.SinglePage; + if (!id) { + return Cannot find the page ; + } + return ; +}