Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automatically update metric plots for in-progress runs #2099 #5017

Merged
merged 21 commits into from Dec 2, 2021
Merged
@@ -1,7 +1,7 @@
import React from 'react';
import _ from 'lodash';
import { QuestionCircleOutlined } from '@ant-design/icons';
import { Radio, Switch, TreeSelect, Tooltip } from 'antd';
import { Radio, Switch, TreeSelect, Tooltip, Progress } from 'antd';
import PropTypes from 'prop-types';
import { CHART_TYPE_LINE } from './MetricsPlotPanel';
import { LineSmoothSlider } from './LineSmoothSlider';
Expand Down Expand Up @@ -31,6 +31,8 @@ export class MetricsPlotControlsImpl extends React.Component {
yAxisLogScale: PropTypes.bool.isRequired,
showPoint: PropTypes.bool.isRequired,
intl: PropTypes.shape({ formatMessage: PropTypes.func.isRequired }).isRequired,
numRuns: PropTypes.number.isRequired,
numCompletedRuns: PropTypes.number.isRequired,
};

handleMetricsSelectFilterChange = (text, option) =>
Expand All @@ -46,7 +48,14 @@ export class MetricsPlotControlsImpl extends React.Component {
};

render() {
const { chartType, yAxisLogScale, initialLineSmoothness, showPoint } = this.props;
const {
chartType,
yAxisLogScale,
initialLineSmoothness,
showPoint,
numRuns,
numCompletedRuns,
} = this.props;
const wrapperStyle = chartType === CHART_TYPE_LINE ? styles.linechartControlsWrapper : {};
const lineSmoothnessTooltipText = (
<FormattedMessage
Expand All @@ -55,10 +64,34 @@ export class MetricsPlotControlsImpl extends React.Component {
description='Helpful tooltip message to help with line smoothness for the metrics plot'
/>
);
const completedRunsTooltipText = (
<FormattedMessage
// eslint-disable-next-line max-len
defaultMessage='MLflow UI periodically fetches metric histories for active runs and updates the metrics plot.'
description='Helpful tooltip message to explain the automatic metrics plot update'
/>
);
return (
<div className='plot-controls' style={wrapperStyle}>
{chartType === CHART_TYPE_LINE ? (
<div>
<div className='inline-control'>
<div className='control-label'>
<FormattedMessage
defaultMessage='Completed Runs'
// eslint-disable-next-line max-len
description='Label for the progress bar to show the number of completed runs'
/>{' '}
<Tooltip title={completedRunsTooltipText}>
<QuestionCircleOutlined />
</Tooltip>
<Progress
percent={Math.round((100 * numCompletedRuns) / numRuns)}
format={() => `${numCompletedRuns}/${numRuns}`}
status='normal'
/>
</div>
</div>
<div className='inline-control'>
<div className='control-label'>
<FormattedMessage
Expand Down
Expand Up @@ -2,11 +2,11 @@ import React from 'react';
import { connect } from 'react-redux';
import Utils from '../../common/utils/Utils';
import RequestStateWrapper from '../../common/components/RequestStateWrapper';
import { getMetricHistoryApi } from '../actions';
import { getMetricHistoryApi, getRunApi } from '../actions';
import PropTypes from 'prop-types';
import _ from 'lodash';
import { MetricsPlotView } from './MetricsPlotView';
import { getRunTags } from '../reducers/Reducers';
import { getRunTags, getRunInfo } from '../reducers/Reducers';
import {
MetricsPlotControls,
X_AXIS_WALL,
Expand All @@ -22,10 +22,16 @@ import { getUUID } from '../../common/utils/ActionUtils';
export const CHART_TYPE_LINE = 'line';
export const CHART_TYPE_BAR = 'bar';

// Polling interval
export const METRICS_PLOT_POLLING_INTERVAL_MS = 5000;
Copy link
Collaborator

@dbczumar dbczumar Nov 29, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we increase this to 10 seconds? 5 seems aggressive.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, what happens if the refresh fails? Does the page crash?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we increase this to 10 seconds? 5 seems aggressive.

Sure!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, what happens if the refresh fails? Does the page crash?

Let me test.

Copy link
Member

@harupy harupy Nov 30, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when-request-fails.mov
  • The page doesn't crash.
  • The page keeps polling.

// Stop polling when the polling duration exceeds this value
export const METRICS_PLOT_POLLING_DURATION_MS = 3600 * 1000; // 1 hour
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should only stop polling when there's no new data.

Copy link
Member

@harupy harupy Nov 30, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dbczumar I remember we discussed that we should set an appropriate polling threshold when a run never ends, but not setting such a threshold sounds ok to me because runs end in most cases.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Offline discussion: check the timestamp of the last metric, and if it's more than 1 week, then we won't refresh.


export class MetricsPlotPanel extends React.Component {
static propTypes = {
experimentId: PropTypes.string.isRequired,
runUuids: PropTypes.arrayOf(PropTypes.string).isRequired,
completedRunUuids: PropTypes.arrayOf(PropTypes.string).isRequired,
metricKey: PropTypes.string.isRequired,
// A map of { runUuid : { metricKey: value } }
latestMetricsByRunUuid: PropTypes.object.isRequired,
Expand All @@ -34,6 +40,7 @@ export class MetricsPlotPanel extends React.Component {
// An array of { metricKey, history, runUuid, runDisplayName }
metricsWithRunInfoAndHistory: PropTypes.arrayOf(PropTypes.object).isRequired,
getMetricHistoryApi: PropTypes.func.isRequired,
getRunApi: PropTypes.func.isRequired,
location: PropTypes.object.isRequired,
history: PropTypes.object.isRequired,
runDisplayNames: PropTypes.arrayOf(PropTypes.string).isRequired,
Expand Down Expand Up @@ -71,11 +78,65 @@ export class MetricsPlotPanel extends React.Component {
popoverX: 0,
popoverY: 0,
popoverRunItems: [],
focused: true,
};
this.displayPopover = false;
this.intervalId = null;
this.mountedAt = null;
this.loadMetricHistory(this.props.runUuids, this.getUrlState().selectedMetricKeys);
}

onFocus = () => {
this.setState({ focused: true });
};

onBlur = () => {
this.setState({ focused: false });
};

clearIntervalIfExists = () => {
if (this.intervalId) {
clearInterval(this.intervalId);
}
};

allRunsCompleted = () => {
return this.props.completedRunUuids.length === this.props.runUuids.length;
};

exceedsPollingDuration = () => {
if (!this.mountedAt) {
return false;
}
return new Date().getTime() - this.mountedAt >= METRICS_PLOT_POLLING_DURATION_MS;
};

componentDidMount() {
this.mountedAt = new Date().getTime();
window.addEventListener('focus', this.onFocus);
window.addEventListener('blur', this.onBlur);

if (!this.allRunsCompleted()) {
this.intervalId = setInterval(() => {
if (this.state.focused) {
const { completedRunUuids, runUuids } = this.props;
const uncompletedRuns = _.difference(runUuids, completedRunUuids);
this.loadMetricHistory(uncompletedRuns, this.getUrlState().selectedMetricKeys);
this.loadRuns(uncompletedRuns);
}
if (this.exceedsPollingDuration() || this.allRunsCompleted()) {
this.clearIntervalIfExists();
}
}, METRICS_PLOT_POLLING_INTERVAL_MS);
}
}

componentWillUnmount() {
window.removeEventListener('focus', this.onFocus);
window.removeEventListener('blur', this.onBlur);
this.clearIntervalIfExists();
}

getUrlState() {
return Utils.getMetricPlotStateFromUrl(this.props.location.search);
}
Expand Down Expand Up @@ -149,6 +210,16 @@ export class MetricsPlotPanel extends React.Component {
return requestIds;
};

loadRuns = (runUuids) => {
const requestIds = [];
runUuids.forEach((runUuid) => {
const id = getUUID();
this.props.getRunApi(runUuid);
requestIds.push(id);
});
return requestIds;
};

getMetrics = () => {
/* eslint-disable no-param-reassign */
const state = this.getUrlState();
Expand Down Expand Up @@ -471,6 +542,8 @@ export class MetricsPlotPanel extends React.Component {
return (
<div className='metrics-plot-container'>
<MetricsPlotControls
numRuns={this.props.runUuids.length}
numCompletedRuns={this.props.completedRunUuids.length}
distinctMetricKeys={distinctMetricKeys}
selectedXAxis={selectedXAxis}
selectedMetricKeys={selectedMetricKeys}
Expand Down Expand Up @@ -534,6 +607,9 @@ const mapStateToProps = (state, ownProps) => {
return latestMetrics ? Object.keys(latestMetrics) : [];
});
const distinctMetricKeys = [...new Set(metricKeys)].sort();
const completedRunUuids = runUuids.filter(
(runUuid) => getRunInfo(runUuid, state).status !== 'RUNNING',
);

const runDisplayNames = [];

Expand Down Expand Up @@ -561,9 +637,10 @@ const mapStateToProps = (state, ownProps) => {
latestMetricsByRunUuid,
distinctMetricKeys,
metricsWithRunInfoAndHistory,
completedRunUuids,
};
};

const mapDispatchToProps = { getMetricHistoryApi };
const mapDispatchToProps = { getMetricHistoryApi, getRunApi };

export default withRouter(connect(mapStateToProps, mapDispatchToProps)(MetricsPlotPanel));