From c7adc3d3484fcd8a671139bb51c61a655f06bdee Mon Sep 17 00:00:00 2001 From: abikki Date: Wed, 7 Aug 2024 11:07:00 +0530 Subject: [PATCH 01/14] Experiments filter by projects --- .../components/ExperimentListView.tsx | 36 +++++-- .../components/HomePage.tsx | 10 +- .../components/MetricsPlotPanel.tsx | 2 +- .../components/ProjectListView.tsx | 96 +++++++++++++++++++ 4 files changed, 134 insertions(+), 10 deletions(-) create mode 100644 mlflow/server/js/src/experiment-tracking/components/ProjectListView.tsx diff --git a/mlflow/server/js/src/experiment-tracking/components/ExperimentListView.tsx b/mlflow/server/js/src/experiment-tracking/components/ExperimentListView.tsx index 44f675fc91..7eb0a5d514 100644 --- a/mlflow/server/js/src/experiment-tracking/components/ExperimentListView.tsx +++ b/mlflow/server/js/src/experiment-tracking/components/ExperimentListView.tsx @@ -28,6 +28,7 @@ import { RenameExperimentModal } from './modals/RenameExperimentModal'; import { IconButton } from '../../common/components/IconButton'; import { withRouterNext } from '../../common/utils/withRouterNext'; import { ExperimentEntity } from '../types'; +import ProjectListView, { filterExperimentsByProject } from './ProjectListView'; type Props = { activeExperimentIds: string[]; @@ -39,11 +40,12 @@ type State = any; export class ExperimentListView extends Component { list: any; - + selectedExperiments: string[] = JSON.parse(localStorage.getItem('selected-experiments') || '[]'); state = { - checkedKeys: this.props.activeExperimentIds, + checkedKeys: JSON.parse(localStorage.getItem('selected-experiments') || '[]'), hidden: false, searchInput: '', + project: localStorage.getItem('mlflow-exp-project') || 'All', showCreateExperimentModal: false, showDeleteExperimentModal: false, showRenameExperimentModal: false, @@ -55,19 +57,23 @@ export class ExperimentListView extends Component { this.list = ref; }; - componentDidUpdate = () => { + componentDidUpdate = (prevProps: Props) => { // Ensure the filter is applied if (this.list) { this.list.forceUpdateGrid(); } - }; + const exps = JSON.parse(localStorage.getItem('selected-experiments') || '[]'); + if (prevProps.activeExperimentIds.length !== exps.length) { + this.pushExperimentRoute(); + } + } filterExperiments = (searchInput: any) => { - const { experiments } = this.props; + const experiments = filterExperimentsByProject(this.props.experiments, this.state.project) const lowerCasedSearchInput = searchInput.toLowerCase(); return lowerCasedSearchInput === '' - ? this.props.experiments - : experiments.filter(({ name }) => name.toLowerCase().includes(lowerCasedSearchInput)); + ? experiments + : experiments.filter(({ name }: any) => name.toLowerCase().includes(lowerCasedSearchInput)); }; handleSearchInputChange = (event: any) => { @@ -76,6 +82,14 @@ export class ExperimentListView extends Component { }); }; + handleProjectChange = (value: any) => { + const experiments = filterExperimentsByProject(this.props.experiments, value) + localStorage.setItem('mlflow-exp-project', value); + this.setState((prevState: any, props: any) => { + return {project: value, checkedKeys: experiments.length ?[experiments[0].experiment_id] : [] }; + }, this.pushExperimentRoute); + }; + updateSelectedExperiment = (experimentId: any, experimentName: any) => { this.setState({ selectedExperimentId: experimentId, @@ -125,6 +139,9 @@ export class ExperimentListView extends Component { this.updateSelectedExperiment('0', ''); }; + persistSelectedExperiemnts = (selected: any) => { + localStorage.setItem('selected-experiments', JSON.stringify(selected)); + } // Add a key if it does not exist, remove it if it does // Always keep at least one experiment checked if it is only the active one. handleCheck = (isChecked: any, key: any) => { @@ -136,11 +153,13 @@ export class ExperimentListView extends Component { if (isChecked === false && props.activeExperimentIds.length !== 1) { checkedKeys = props.activeExperimentIds.filter((i: any) => i !== key); } + this.persistSelectedExperiemnts(checkedKeys); return { checkedKeys: checkedKeys }; }, this.pushExperimentRoute); }; pushExperimentRoute = () => { + this.persistSelectedExperiemnts(this.state.checkedKeys); if (this.state.checkedKeys.length > 0) { const route = this.state.checkedKeys.length === 1 @@ -257,6 +276,8 @@ export class ExperimentListView extends Component { experimentId={this.state.selectedExperimentId} experimentName={this.state.selectedExperimentName} /> +
+
Experiments @@ -276,6 +297,7 @@ export class ExperimentListView extends Component { />
+ { - const sorted = [...experiments].sort(Utils.compareExperiments); - return sorted.find(({ lifecycleStage }) => lifecycleStage === 'active'); + const selectedProject = localStorage.getItem('mlflow-exp-project'); + let filteredExperiments = experiments + if(selectedProject){ + filteredExperiments = filterExperimentsByProject(experiments, selectedProject); + } + const sorted = [...filteredExperiments].sort(Utils.compareExperiments); + return sorted.find(({ lifecycleStage }) => lifecycleStage === 'active' ); }; const HomePage = () => { diff --git a/mlflow/server/js/src/experiment-tracking/components/MetricsPlotPanel.tsx b/mlflow/server/js/src/experiment-tracking/components/MetricsPlotPanel.tsx index 69d45a2f24..66fa13acc0 100644 --- a/mlflow/server/js/src/experiment-tracking/components/MetricsPlotPanel.tsx +++ b/mlflow/server/js/src/experiment-tracking/components/MetricsPlotPanel.tsx @@ -32,7 +32,7 @@ export const METRICS_PLOT_POLLING_INTERVAL_MS = 10 * 1000; // 10 seconds // A run is considered as 'hanging' if its status is 'RUNNING' but its latest metric was logged // prior to this threshold. The metrics plot doesn't automatically update hanging runs. export const METRICS_PLOT_HANGING_RUN_THRESHOLD_MS = 3600 * 24 * 7 * 1000; // 1 week -const MAXIMUM_METRIC_DATA_POINTS = 100_000; +const MAXIMUM_METRIC_DATA_POINTS = 1_000_000; const GET_METRIC_HISTORY_MAX_RESULTS = 25000; export const convertMetricsToCsv = (metrics: any) => { diff --git a/mlflow/server/js/src/experiment-tracking/components/ProjectListView.tsx b/mlflow/server/js/src/experiment-tracking/components/ProjectListView.tsx new file mode 100644 index 0000000000..3049a08176 --- /dev/null +++ b/mlflow/server/js/src/experiment-tracking/components/ProjectListView.tsx @@ -0,0 +1,96 @@ +import 'react-virtualized/styles.css'; + +import { + DialogCombobox, + DialogComboboxContent, + DialogComboboxOptionList, + DialogComboboxOptionListSelectItem, + DialogComboboxTrigger, + Typography, +} from '@databricks/design-system'; +import React, { Component } from 'react'; + +import { ExperimentEntity } from '../types'; +import { css } from '@emotion/react'; + +type Props = { + experiments: ExperimentEntity[]; + project: string; + handleProjectChange: any; +}; + +type State = any; + +export const filterExperimentsByProject = (experiments:any, selectedProject:any) =>{ + if (selectedProject === "All") { + return experiments; + } else if (selectedProject === "Default") { + return experiments.filter( + (experiment:any) => !experiment.tags || !experiment.tags.some((tag:any) => tag.key === "project") + ); + } else { + return experiments.filter( + (experiment:any) => experiment.tags && experiment.tags.some((tag:any) => tag.key === "project" && tag.value === selectedProject) + ); + } +} +export class ProjectListView extends Component { + + listProjects = () => { + const { experiments } = this.props; + const projects = experiments + .filter(experiment => { + const projectTag = experiment.tags && experiment.tags.find((tag :any) => tag.key === "project"); + return projectTag !== undefined; + }) + .map(experiment => { + const projectTag = experiment.tags.find((tag:any) => tag.key === "project"); + return projectTag ? projectTag.value : null; + }); + return ['All','Default', ...new Set(projects)]; + }; + + render() { + const projects = this.listProjects(); + return ( +
+ + Projects + + + + + + {projects.map((project:any) => ( + this.props.handleProjectChange(project)} + > + {project} + + ))} + + + +
+ ); + } +} + +const classNames = { + projectsContainer: { + marginBottom: '8px', + width:'100%' + }, +}; + +export default ProjectListView; From 7f0b167640e658052cee0228ea8d5aeac92350ba Mon Sep 17 00:00:00 2001 From: abikki Date: Wed, 7 Aug 2024 11:40:35 +0530 Subject: [PATCH 02/14] Run metrics - number of sample to render --- .../run-page/RunViewMetricChart.tsx | 9 ++- .../run-page/RunViewMetricCharts.tsx | 75 ++++++++++++++++++- .../run-page/RunViewMetricHistoryChart.tsx | 2 + .../hooks/useSampledMetricHistory.tsx | 2 +- .../sdk/SampledMetricHistoryService.ts | 3 +- 5 files changed, 83 insertions(+), 8 deletions(-) diff --git a/mlflow/server/js/src/experiment-tracking/components/run-page/RunViewMetricChart.tsx b/mlflow/server/js/src/experiment-tracking/components/run-page/RunViewMetricChart.tsx index c6f4b428a1..9f68a96e5f 100644 --- a/mlflow/server/js/src/experiment-tracking/components/run-page/RunViewMetricChart.tsx +++ b/mlflow/server/js/src/experiment-tracking/components/run-page/RunViewMetricChart.tsx @@ -57,6 +57,8 @@ export interface RunViewMetricChartProps { * Reference to a overarching refresh manager (entity that will trigger refresh of subscribed charts) */ chartRefreshManager: ChartRefreshManager; + maxResults: number; + showPoint: boolean; } /** @@ -72,6 +74,8 @@ export const RunViewMetricChart = ({ onMoveUp, onMoveDown, chartRefreshManager, + maxResults, + showPoint }: RunViewMetricChartProps) => { const { dragHandleRef, dragPreviewRef, dropTargetRef, isDragging, isOver } = useDragAndDropElement({ dragGroupKey, @@ -97,7 +101,7 @@ export const RunViewMetricChart = ({ metricKeys, enabled: isInViewport, range: stepRange, - maxResults: 320, + maxResults: maxResults, }); const { metricsHistory, error } = resultsByRunUuid[runInfo.runUuid ?? '']?.[metricKey] || {}; @@ -128,7 +132,7 @@ export const RunViewMetricChart = ({ }); } return () => {}; - }, [chartRefreshManager, refresh, isInViewport]); + }, [chartRefreshManager, refresh, isInViewport, maxResults]); const yRange = useRef<[number, number] | undefined>(undefined); @@ -197,6 +201,7 @@ export const RunViewMetricChart = ({ // cases the plotly will use last known range. xRange={xRange} yRange={yRange.current} + showPoint={showPoint} /> ); }; diff --git a/mlflow/server/js/src/experiment-tracking/components/run-page/RunViewMetricCharts.tsx b/mlflow/server/js/src/experiment-tracking/components/run-page/RunViewMetricCharts.tsx index 54706b7598..dbb13cd139 100644 --- a/mlflow/server/js/src/experiment-tracking/components/run-page/RunViewMetricCharts.tsx +++ b/mlflow/server/js/src/experiment-tracking/components/run-page/RunViewMetricCharts.tsx @@ -7,9 +7,17 @@ import { Spacer, Spinner, useDesignSystemTheme, + DialogCombobox, + DialogComboboxContent, + DialogComboboxOptionList, + DialogComboboxOptionListCheckboxItem, + DialogComboboxOptionListSelectItem, + DialogComboboxOptionListSearch, + DialogComboboxTrigger, + Switch, } from '@databricks/design-system'; import { compact, mapValues, values } from 'lodash'; -import { useMemo, useState } from 'react'; +import { useMemo, useState, useEffect } from 'react'; import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; import { useSelector } from 'react-redux'; import { getGridColumnSetup } from '../../../common/utils/CssGrid.utils'; @@ -57,12 +65,16 @@ const RunViewMetricChartsSection = ({ runInfo, chartRefreshManager, onReorderChart, + maxResults, + showPoint, }: { metricKeys: string[]; search: string; runInfo: RunInfoEntity | UseGetRunQueryResponseRunInfo; onReorderChart: (sourceChartKey: string, targetChartKey: string) => void; chartRefreshManager: ChartRefreshManager; + maxResults: number; + showPoint: boolean; }) => { const { theme } = useDesignSystemTheme(); @@ -76,13 +88,13 @@ const RunViewMetricChartsSection = ({ const gridSetup = useMemo( () => ({ ...getGridColumnSetup({ - maxColumns: 3, + maxColumns: maxResults > 320 ? 1 : 3, gap: theme.spacing.lg, additionalBreakpoints: [{ breakpointWidth: 3 * 720, minColumnWidthForBreakpoint: 720 }], }), overflow: 'hidden', }), - [theme], + [theme, maxResults], ); return filteredMetricKeys.length ? ( @@ -101,6 +113,8 @@ const RunViewMetricChartsSection = ({ onMoveDown={() => moveChartDown(metricKey)} onMoveUp={() => moveChartUp(metricKey)} chartRefreshManager={chartRefreshManager} + maxResults={maxResults} + showPoint={showPoint} /> ))} @@ -137,8 +151,11 @@ export const RunViewMetricCharts = ({ }); const [search, setSearch] = useState(''); + const prevSample = localStorage.getItem('mlflow-run-chart-default-samples') || "320" + const [maxSteps, setMaxSteps] = useState(parseInt(prevSample)); + const [showPoint, setShowPoint] = useState(false); const { formatMessage } = useIntl(); - + const maxSamples = [320, 500, 1000, 2500] const { orderedMetricKeys, onReorderChart } = useOrderedCharts(metricKeys, 'RunView' + mode, runInfo.runUuid ?? ''); const noMetricsRecorded = !metricKeys.length; @@ -156,6 +173,11 @@ export const RunViewMetricCharts = ({ ); }); + // on samples to render change, refresh all charts + useEffect(() => { + chartRefreshManager.refreshAllCharts(); + }, [maxSteps]); + return (
@@ -173,6 +195,49 @@ export const RunViewMetricCharts = ({ description: 'Run page > Charts tab > Filter metric charts input > placeholder', })} /> + + + + + {maxSamples.map((sample) => { + return ( + { + setMaxSteps(sample); + localStorage.setItem('mlflow-run-chart-default-samples', sample.toString()); + }} + > + {sample} + + ); + })} + + + +
+
+ +
+ setShowPoint(!showPoint)} + /> +