Unverified Commit 3e62e60b authored by liuzhe-lz's avatar liuzhe-lz Committed by GitHub
Browse files

Refactor web UI to support incremental metric loading (#1557)

* Refactor web UI to support incremental metric loading

* refactor

* Remove host job

* Move sequence ID to NNI manager

* implement incremental loading
parent 99f7d79c
......@@ -30,11 +30,11 @@ import { Deferred } from 'ts-deferred';
import { String } from 'typescript-string-operations';
import * as component from '../../common/component';
import { NNIError, NNIErrorNames } from '../../common/errors';
import { getExperimentId, getInitTrialSequenceId } from '../../common/experimentStartupInfo';
import { getExperimentId } from '../../common/experimentStartupInfo';
import { getLogger, Logger } from '../../common/log';
import { ObservableTimer } from '../../common/observableTimer';
import {
HostJobApplicationForm, HyperParameters, JobApplicationForm, NNIManagerIpConfig, TrainingService, TrialJobApplicationForm,
HyperParameters, NNIManagerIpConfig, TrainingService, TrialJobApplicationForm,
TrialJobDetail, TrialJobMetric
} from '../../common/trainingService';
import {
......@@ -172,9 +172,7 @@ class RemoteMachineTrainingService implements TrainingService {
const deferred: Deferred<TrialJobDetail[]> = new Deferred<TrialJobDetail[]>();
for (const [key, value] of this.trialJobsMap) {
if (value.form.jobType === 'TRIAL') {
jobs.push(await this.getTrialJob(key));
}
jobs.push(await this.getTrialJob(key));
}
deferred.resolve(jobs);
......@@ -228,33 +226,26 @@ class RemoteMachineTrainingService implements TrainingService {
* @param form trial job description form
*/
// tslint:disable-next-line:informative-docs
public async submitTrialJob(form: JobApplicationForm): Promise<TrialJobDetail> {
public async submitTrialJob(form: TrialJobApplicationForm): Promise<TrialJobDetail> {
if (this.trialConfig === undefined) {
throw new Error('trial config is not initialized');
}
if (form.jobType === 'HOST') {
return this.runHostJob(<HostJobApplicationForm>form);
} else if (form.jobType === 'TRIAL') {
// Generate trial job id(random)
const trialJobId: string = uniqueString(5);
const trialWorkingFolder: string = unixPathJoin(this.remoteExpRootDir, 'trials', trialJobId);
// Generate trial job id(random)
const trialJobId: string = uniqueString(5);
const trialWorkingFolder: string = unixPathJoin(this.remoteExpRootDir, 'trials', trialJobId);
const trialJobDetail: RemoteMachineTrialJobDetail = new RemoteMachineTrialJobDetail(
trialJobId,
'WAITING',
Date.now(),
trialWorkingFolder,
form,
this.generateSequenceId()
);
this.jobQueue.push(trialJobId);
this.trialJobsMap.set(trialJobId, trialJobDetail);
const trialJobDetail: RemoteMachineTrialJobDetail = new RemoteMachineTrialJobDetail(
trialJobId,
'WAITING',
Date.now(),
trialWorkingFolder,
form
);
this.jobQueue.push(trialJobId);
this.trialJobsMap.set(trialJobId, trialJobDetail);
return Promise.resolve(trialJobDetail);
} else {
return Promise.reject(new Error(`Job form not supported: ${JSON.stringify(form)}, jobType should be HOST or TRIAL.`));
}
return Promise.resolve(trialJobDetail);
}
/**
......@@ -262,20 +253,16 @@ class RemoteMachineTrainingService implements TrainingService {
* @param trialJobId trial job id
* @param form job application form
*/
public async updateTrialJob(trialJobId: string, form: JobApplicationForm): Promise<TrialJobDetail> {
public async updateTrialJob(trialJobId: string, form: TrialJobApplicationForm): Promise<TrialJobDetail> {
const trialJobDetail: undefined | TrialJobDetail = this.trialJobsMap.get(trialJobId);
if (trialJobDetail === undefined) {
throw new Error(`updateTrialJob failed: ${trialJobId} not found`);
}
if (form.jobType === 'TRIAL') {
const rmMeta: RemoteMachineMeta | undefined = (<RemoteMachineTrialJobDetail>trialJobDetail).rmMeta;
if (rmMeta !== undefined) {
await this.writeParameterFile(trialJobId, (<TrialJobApplicationForm>form).hyperParameters, rmMeta);
} else {
throw new Error(`updateTrialJob failed: ${trialJobId} rmMeta not found`);
}
const rmMeta: RemoteMachineMeta | undefined = (<RemoteMachineTrialJobDetail>trialJobDetail).rmMeta;
if (rmMeta !== undefined) {
await this.writeParameterFile(trialJobId, form.hyperParameters, rmMeta);
} else {
throw new Error(`updateTrialJob failed: jobType ${form.jobType} not supported.`);
throw new Error(`updateTrialJob failed: ${trialJobId} rmMeta not found`);
}
return trialJobDetail;
......@@ -558,7 +545,7 @@ class RemoteMachineTrainingService implements TrainingService {
await this.allocateSSHClientForTrial(trialJobDetail);
await this.launchTrialOnScheduledMachine(
trialJobId, trialWorkingFolder, <TrialJobApplicationForm>trialJobDetail.form, rmScheduleInfo);
trialJobId, trialWorkingFolder, trialJobDetail.form, rmScheduleInfo);
trialJobDetail.status = 'RUNNING';
trialJobDetail.url = `file://${rmScheduleInfo.rmMeta.ip}:${trialWorkingFolder}`;
......@@ -628,7 +615,7 @@ class RemoteMachineTrainingService implements TrainingService {
trialWorkingFolder,
trialJobId,
getExperimentId(),
trialJobDetail.sequenceId.toString(),
trialJobDetail.form.sequenceId.toString(),
this.isMultiPhase,
unixPathJoin(trialWorkingFolder, '.nni', 'jobpid'),
command,
......@@ -657,38 +644,6 @@ class RemoteMachineTrainingService implements TrainingService {
SSHClientUtility.remoteExeCommand(`bash ${unixPathJoin(trialWorkingFolder, 'run.sh')}`, sshClient);
}
private async runHostJob(form: HostJobApplicationForm): Promise<TrialJobDetail> {
const rmMeta: RemoteMachineMeta = this.getRmMetaByHost(form.host);
const sshClientManager: SSHClientManager | undefined = this.machineSSHClientMap.get(rmMeta);
if (sshClientManager === undefined) {
throw new Error('sshClient not found.');
}
const sshClient: Client = sshClientManager.getFirstSSHClient();
const jobId: string = uniqueString(5);
const localDir: string = path.join(this.expRootDir, 'hostjobs-local', jobId);
const remoteDir: string = this.getHostJobRemoteDir(jobId);
await cpp.exec(`mkdir -p ${localDir}`);
await SSHClientUtility.remoteExeCommand(`mkdir -p ${remoteDir}`, sshClient);
const runScriptContent: string = String.Format(
HOST_JOB_SHELL_FORMAT, remoteDir, path.join(remoteDir, 'jobpid'), form.cmd, path.join(remoteDir, 'code')
);
await fs.promises.writeFile(path.join(localDir, 'run.sh'), runScriptContent, { encoding: 'utf8' });
await SSHClientUtility.copyFileToRemote(
path.join(localDir, 'run.sh'), unixPathJoin(remoteDir, 'run.sh'), sshClient);
// tslint:disable-next-line: no-floating-promises
SSHClientUtility.remoteExeCommand(`bash ${unixPathJoin(remoteDir, 'run.sh')}`, sshClient);
const jobDetail: RemoteMachineTrialJobDetail = new RemoteMachineTrialJobDetail(
jobId, 'RUNNING', Date.now(), remoteDir, form, this.generateSequenceId()
);
jobDetail.rmMeta = rmMeta;
jobDetail.startTime = Date.now();
this.trialJobsMap.set(jobId, jobDetail);
this.log.debug(`runHostJob: return: ${JSON.stringify(jobDetail)} `);
return jobDetail;
}
private getRmMetaByHost(host: string): RemoteMachineMeta {
for (const [rmMeta, client] of this.machineSSHClientMap.entries()) {
if (rmMeta.ip === host) {
......@@ -765,13 +720,7 @@ class RemoteMachineTrainingService implements TrainingService {
}
let jobpidPath: string;
if (trialJobDetail.form.jobType === 'TRIAL') {
jobpidPath = unixPathJoin(trialJobDetail.workingDirectory, '.nni', 'jobpid');
} else if (trialJobDetail.form.jobType === 'HOST') {
jobpidPath = unixPathJoin(this.getHostJobRemoteDir(jobId), 'jobpid');
} else {
throw new Error(`Job type not supported: ${trialJobDetail.form.jobType}`);
}
jobpidPath = unixPathJoin(trialJobDetail.workingDirectory, '.nni', 'jobpid');
return jobpidPath;
}
......@@ -791,14 +740,6 @@ class RemoteMachineTrainingService implements TrainingService {
await SSHClientUtility.copyFileToRemote(localFilepath, unixPathJoin(trialWorkingFolder, fileName), sshClient);
}
private generateSequenceId(): number {
if (this.trialSequenceId === -1) {
this.trialSequenceId = getInitTrialSequenceId();
}
return this.trialSequenceId++;
}
}
export { RemoteMachineTrainingService };
......@@ -76,7 +76,7 @@ describe('Unit Test for LocalTrainingService', () => {
// submit job
const form: TrialJobApplicationForm = {
jobType: 'TRIAL',
sequenceId: 0,
hyperParameters: {
value: 'mock hyperparameters',
index: 0
......@@ -95,7 +95,7 @@ describe('Unit Test for LocalTrainingService', () => {
// submit job
const form: TrialJobApplicationForm = {
jobType: 'TRIAL',
sequenceId: 0,
hyperParameters: {
value: 'mock hyperparameters',
index: 0
......@@ -121,4 +121,4 @@ describe('Unit Test for LocalTrainingService', () => {
it('Test multiphaseSupported', () => {
chai.expect(localTrainingService.isMultiPhaseJobSupported).to.be.equals(true)
})
});
\ No newline at end of file
});
......@@ -24,6 +24,7 @@ import * as chaiAsPromised from 'chai-as-promised';
import * as fs from 'fs';
import * as tmp from 'tmp';
import * as component from '../../common/component';
import { TrialJobApplicationForm } from '../../common/trainingService';
import { cleanupUnitTest, prepareUnitTest } from '../../common/utils';
import { TrialConfigMetadataKey } from '../common/trialConfigMetadataKey';
import { PAITrainingService } from '../pai/paiTrainingService';
......@@ -84,12 +85,16 @@ describe('Unit Test for PAITrainingService', () => {
console.log(`paiCluster is ${paiCluster}`)
await paiTrainingService.setClusterMetadata(TrialConfigMetadataKey.PAI_CLUSTER_CONFIG, paiCluster);
await paiTrainingService.setClusterMetadata(TrialConfigMetadataKey.TRIAL_CONFIG, paiTrialConfig);
const form: TrialJobApplicationForm = {
sequenceId: 0,
hyperParameters: { value: '', index: 0 }
};
try {
const trialDetail = await paiTrainingService.submitTrialJob({jobType : 'TRIAL'});
const trialDetail = await paiTrainingService.submitTrialJob(form);
chai.expect(trialDetail.status).to.be.equals('WAITING');
} catch(error) {
console.log('Submit job failed:' + error);
chai.assert(error)
}
});
});
\ No newline at end of file
});
......@@ -99,11 +99,11 @@ describe('Unit Test for RemoteMachineTrainingService', () => {
await remoteMachineTrainingService.setClusterMetadata(
TrialConfigMetadataKey.TRIAL_CONFIG, `{"command":"sleep 1h && echo ","codeDir":"${localCodeDir}","gpuNum":1}`);
const form: TrialJobApplicationForm = {
jobType: 'TRIAL',
hyperParameters: {
value: 'mock hyperparameters',
index: 0
}
sequenceId: 0,
hyperParameters: {
value: 'mock hyperparameters',
index: 0
}
};
const trialJob = await remoteMachineTrainingService.submitTrialJob(form);
......@@ -137,7 +137,7 @@ describe('Unit Test for RemoteMachineTrainingService', () => {
// submit job
const form: TrialJobApplicationForm = {
jobType: 'TRIAL',
sequenceId: 0,
hyperParameters: {
value: 'mock hyperparameters',
index: 0
......
import * as React from 'react';
import { Row, Col } from 'antd';
import axios from 'axios';
import { COLUMN, MANAGER_IP } from './static/const';
import { COLUMN } from './static/const';
import { EXPERIMENT, TRIALS } from './static/datamodel';
import './App.css';
import SlideBar from './components/SlideBar';
interface AppState {
interval: number;
whichPageToFresh: string;
columnList: Array<string>;
concurrency: number;
interval: number;
columnList: Array<string>;
experimentUpdateBroadcast: number;
trialsUpdateBroadcast: number;
}
class App extends React.Component<{}, AppState> {
public _isMounted: boolean;
constructor(props: {}) {
super(props);
this.state = {
interval: 10, // sendons
whichPageToFresh: '',
columnList: COLUMN,
concurrency: 1
};
}
private timerId: number | null;
changeInterval = (interval: number) => {
if (this._isMounted === true) {
this.setState(() => ({ interval: interval }));
constructor(props: {}) {
super(props);
this.state = {
interval: 10, // sendons
columnList: COLUMN,
experimentUpdateBroadcast: 0,
trialsUpdateBroadcast: 0,
};
}
}
changeFresh = (fresh: string) => {
// interval * 1000
if (this._isMounted === true) {
this.setState(() => ({ whichPageToFresh: fresh }));
async componentDidMount() {
await Promise.all([ EXPERIMENT.init(), TRIALS.init() ]);
this.setState(state => ({ experimentUpdateBroadcast: state.experimentUpdateBroadcast + 1 }));
this.setState(state => ({ trialsUpdateBroadcast: state.trialsUpdateBroadcast + 1 }));
this.timerId = window.setTimeout(this.refresh, this.state.interval * 1000);
}
}
changeColumn = (columnList: Array<string>) => {
if (this._isMounted === true) {
this.setState(() => ({ columnList: columnList }));
changeInterval = (interval: number) => {
this.setState({ interval: interval });
if (this.timerId === null && interval !== 0) {
window.setTimeout(this.refresh);
} else if (this.timerId !== null && interval === 0) {
window.clearTimeout(this.timerId);
}
}
}
changeConcurrency = (val: number) => {
if (this._isMounted === true) {
this.setState(() => ({ concurrency: val }));
// TODO: use local storage
changeColumn = (columnList: Array<string>) => {
this.setState({ columnList: columnList });
}
}
getConcurrency = () => {
axios(`${MANAGER_IP}/experiment`, {
method: 'GET'
})
.then(res => {
if (res.status === 200) {
const params = res.data.params;
if (this._isMounted) {
this.setState(() => ({ concurrency: params.trialConcurrency }));
}
render() {
const { interval, columnList, experimentUpdateBroadcast, trialsUpdateBroadcast } = this.state;
if (experimentUpdateBroadcast === 0 || trialsUpdateBroadcast === 0) {
return null; // TODO: render a loading page
}
const reactPropsChildren = React.Children.map(this.props.children, child =>
React.cloneElement(
// tslint:disable-next-line:no-any
child as React.ReactElement<any>, {
interval,
columnList, changeColumn: this.changeColumn,
experimentUpdateBroadcast,
trialsUpdateBroadcast,
})
);
return (
<Row className="nni" style={{ minHeight: window.innerHeight }}>
<Row className="header">
<Col span={1} />
<Col className="headerCon" span={22}>
<SlideBar changeInterval={this.changeInterval} />
</Col>
<Col span={1} />
</Row>
<Row className="contentBox">
<Row className="content">
{reactPropsChildren}
</Row>
</Row>
</Row>
);
}
private refresh = async () => {
const [ experimentUpdated, trialsUpdated ] = await Promise.all([ EXPERIMENT.update(), TRIALS.update() ]);
if (experimentUpdated) {
this.setState(state => ({ experimentUpdateBroadcast: state.experimentUpdateBroadcast + 1 }));
}
if (trialsUpdated) {
this.setState(state => ({ trialsUpdateBroadcast: state.trialsUpdateBroadcast + 1 }));
}
});
}
componentDidMount() {
this._isMounted = true;
this.getConcurrency();
}
if ([ 'DONE', 'ERROR', 'STOPPED' ].includes(EXPERIMENT.status)) {
// experiment finished, refresh once more to ensure consistency
if (this.state.interval > 0) {
this.setState({ interval: 0 });
this.lastRefresh();
}
componentWillUnmount() {
this._isMounted = false;
}
render() {
const { interval, whichPageToFresh, columnList, concurrency } = this.state;
const reactPropsChildren = React.Children.map(this.props.children, child =>
React.cloneElement(
// tslint:disable-next-line:no-any
child as React.ReactElement<any>, {
interval, whichPageToFresh,
columnList, changeColumn: this.changeColumn,
concurrency, changeConcurrency: this.changeConcurrency
})
);
return (
<Row className="nni" style={{ minHeight: window.innerHeight }}>
<Row className="header">
<Col span={1} />
<Col className="headerCon" span={22}>
<SlideBar changeInterval={this.changeInterval} changeFresh={this.changeFresh} />
</Col>
<Col span={1} />
</Row>
<Row className="contentBox">
<Row className="content">
{reactPropsChildren}
</Row>
</Row>
</Row>
);
}
} else if (this.state.interval !== 0) {
this.timerId = window.setTimeout(this.refresh, this.state.interval * 1000);
}
}
private async lastRefresh() {
await EXPERIMENT.update();
await TRIALS.update(true);
this.setState(state => ({ experimentUpdateBroadcast: state.experimentUpdateBroadcast + 1 }));
this.setState(state => ({ trialsUpdateBroadcast: state.trialsUpdateBroadcast + 1 }));
}
}
export default App;
......@@ -2,12 +2,13 @@ import * as React from 'react';
import { Row, Modal } from 'antd';
import ReactEcharts from 'echarts-for-react';
import IntermediateVal from '../public-child/IntermediateVal';
import { TRIALS } from '../../static/datamodel';
import '../../static/style/compare.scss';
import { TableObj, Intermedia, TooltipForIntermediate } from 'src/static/interface';
import { TableRecord, Intermedia, TooltipForIntermediate } from 'src/static/interface';
// the modal of trial compare
interface CompareProps {
compareRows: Array<TableObj>;
compareRows: Array<TableRecord>;
visible: boolean;
cancelFunc: () => void;
}
......@@ -105,11 +106,12 @@ class Compare extends React.Component<CompareProps, {}> {
// render table column ---
initColumn = () => {
const { compareRows } = this.props;
const idList: Array<string> = [];
const sequenceIdList: Array<number> = [];
const durationList: Array<number> = [];
const compareRows = this.props.compareRows.map(tableRecord => TRIALS.getTrial(tableRecord.id));
const parameterList: Array<object> = [];
let parameterKeys: Array<string> = [];
if (compareRows.length !== 0) {
......@@ -147,7 +149,7 @@ class Compare extends React.Component<CompareProps, {}> {
const temp = compareRows[index];
return (
<td className="value" key={index}>
<IntermediateVal record={temp} />
<IntermediateVal trialId={temp.id} />
</td>
);
})}
......
......@@ -58,7 +58,7 @@ class ExperimentDrawer extends React.Component<ExpDrawerProps, ExpDrawerState> {
trialMessage: trialMessagesArr
};
if (this._isCompareMount === true) {
this.setState(() => ({ experiment: JSON.stringify(result, null, 4) }));
this.setState({ experiment: JSON.stringify(result, null, 4) });
}
}
}));
......
......@@ -51,13 +51,13 @@ class LogDrawer extends React.Component<LogDrawerProps, LogDrawerState> {
setDispatcher = (value: string) => {
if (this._isLogDrawer === true) {
this.setState(() => ({ isLoadispatcher: false, dispatcherLogStr: value }));
this.setState({ isLoadispatcher: false, dispatcherLogStr: value });
}
}
setNNImanager = (val: string) => {
if (this._isLogDrawer === true) {
this.setState(() => ({ isLoading: false, nniManagerLogStr: val }));
this.setState({ isLoading: false, nniManagerLogStr: val });
}
}
......
This diff is collapsed.
......@@ -26,7 +26,6 @@ interface SliderState {
interface SliderProps extends FormComponentProps {
changeInterval: (value: number) => void;
changeFresh: (value: string) => void;
}
interface EventPer {
......@@ -35,7 +34,6 @@ interface EventPer {
class SlideBar extends React.Component<SliderProps, SliderState> {
public _isMounted = false;
public divMenu: HTMLDivElement | null;
public selectHTML: Select | null;
......@@ -57,32 +55,26 @@ class SlideBar extends React.Component<SliderProps, SliderState> {
method: 'GET'
})
.then(res => {
if (res.status === 200 && this._isMounted) {
if (res.status === 200) {
this.setState({ version: res.data });
}
});
}
handleMenuClick = (e: EventPer) => {
if (this._isMounted) { this.setState({ menuVisible: false }); }
this.setState({ menuVisible: false });
switch (e.key) {
// to see & download experiment parameters
case '1':
if (this._isMounted === true) {
this.setState(() => ({ isvisibleExperimentDrawer: true }));
}
this.setState({ isvisibleExperimentDrawer: true });
break;
// to see & download nnimanager log
case '2':
if (this._isMounted === true) {
this.setState(() => ({ activeKey: 'nnimanager', isvisibleLogDrawer: true }));
}
this.setState({ activeKey: 'nnimanager', isvisibleLogDrawer: true });
break;
// to see & download dispatcher log
case '3':
if (this._isMounted === true) {
this.setState(() => ({ isvisibleLogDrawer: true, activeKey: 'dispatcher' }));
}
this.setState({ isvisibleLogDrawer: true, activeKey: 'dispatcher' });
break;
case 'close':
case '10':
......@@ -96,13 +88,10 @@ class SlideBar extends React.Component<SliderProps, SliderState> {
}
handleVisibleChange = (flag: boolean) => {
if (this._isMounted === true) {
this.setState({ menuVisible: flag });
}
this.setState({ menuVisible: flag });
}
getInterval = (value: string) => {
if (value === 'close') {
this.props.changeInterval(0);
} else {
......@@ -203,13 +192,9 @@ class SlideBar extends React.Component<SliderProps, SliderState> {
fresh = (event: React.SyntheticEvent<EventTarget>) => {
event.preventDefault();
event.stopPropagation();
if (this._isMounted) {
this.setState({ isdisabledFresh: true }, () => {
const whichPage = window.location.pathname;
this.props.changeFresh(whichPage);
setTimeout(() => { this.setState(() => ({ isdisabledFresh: false })); }, 1000);
});
}
this.setState({ isdisabledFresh: true }, () => {
setTimeout(() => { this.setState({ isdisabledFresh: false }); }, 1000);
});
}
desktopHTML = () => {
......@@ -330,27 +315,18 @@ class SlideBar extends React.Component<SliderProps, SliderState> {
}
// close log drawer (nnimanager.dispatcher)
closeLogDrawer = () => {
if (this._isMounted === true) {
this.setState(() => ({ isvisibleLogDrawer: false, activeKey: '' }));
}
this.setState({ isvisibleLogDrawer: false, activeKey: '' });
}
// close download experiment parameters drawer
closeExpDrawer = () => {
if (this._isMounted === true) {
this.setState(() => ({ isvisibleExperimentDrawer: false }));
}
this.setState({ isvisibleExperimentDrawer: false });
}
componentDidMount() {
this._isMounted = true;
this.getNNIversion();
}
componentWillUnmount() {
this._isMounted = false;
}
render() {
const mobile = (<MediaQuery maxWidth={884}>{this.mobileHTML()}</MediaQuery>);
const tablet = (<MediaQuery minWidth={885} maxWidth={1241}>{this.tabeltHTML()}</MediaQuery>);
......@@ -376,4 +352,4 @@ class SlideBar extends React.Component<SliderProps, SliderState> {
}
}
export default Form.create<FormComponentProps>()(SlideBar);
\ No newline at end of file
export default Form.create<FormComponentProps>()(SlideBar);
This diff is collapsed.
import { Col, Row, Tooltip } from 'antd';
import * as React from 'react';
import {
Row, Col,
Tooltip
} from 'antd';
import { Experiment } from '../../static/interface';
import { EXPERIMENT } from '../../static/datamodel';
import { formatTimestamp } from '../../static/function';
interface BasicInfoProps {
trialProfile: Experiment;
status: string;
experimentUpdateBroadcast: number;
}
class BasicInfo extends React.Component<BasicInfoProps, {}> {
constructor(props: BasicInfoProps) {
super(props);
}
render() {
const { trialProfile } = this.props;
return (
<Row className="main">
<Col span={8} className="padItem basic">
<p>Name</p>
<div>{trialProfile.experName}</div>
<div>{EXPERIMENT.profile.params.experimentName}</div>
<p>ID</p>
<div>{trialProfile.id}</div>
<div>{EXPERIMENT.profile.id}</div>
</Col>
<Col span={8} className="padItem basic">
<p>Start time</p>
<div className="nowrap">
{new Date(trialProfile.startTime).toLocaleString('en-US')}
</div>
<div className="nowrap">{formatTimestamp(EXPERIMENT.profile.startTime)}</div>
<p>End time</p>
<div className="nowrap">
{
trialProfile.endTime
?
new Date(trialProfile.endTime).toLocaleString('en-US')
:
'none'
}
</div>
<div className="nowrap">{formatTimestamp(EXPERIMENT.profile.endTime)}</div>
</Col>
<Col span={8} className="padItem basic">
<p>Log directory</p>
<div className="nowrap">
<Tooltip placement="top" title={trialProfile.logDir}>
{trialProfile.logDir}
<Tooltip placement="top" title={EXPERIMENT.profile.logDir || ''}>
{EXPERIMENT.profile.logDir || 'unknown'}
</Tooltip>
</div>
<p>Training platform</p>
<div className="nowrap">
{
trialProfile.trainingServicePlatform
?
trialProfile.trainingServicePlatform
:
'none'
}
</div>
<div className="nowrap">{EXPERIMENT.profile.params.trainingServicePlatform}</div>
</Col>
</Row>
);
}
}
export default BasicInfo;
\ No newline at end of file
export default BasicInfo;
import * as React from 'react';
import { Button, Row } from 'antd';
interface ConcurrencyInputProps {
value: number;
updateValue: (val: string) => void;
}
interface ConcurrencyInputStates {
editting: boolean;
}
class ConcurrencyInput extends React.Component<ConcurrencyInputProps, ConcurrencyInputStates> {
private input = React.createRef<HTMLInputElement>();
constructor(props: ConcurrencyInputProps) {
super(props);
this.state = { editting: false };
}
save = () => {
if (this.input.current !== null) {
this.props.updateValue(this.input.current.value);
this.setState({ editting: false });
}
}
cancel = () => {
this.setState({ editting: false });
}
edit = () => {
this.setState({ editting: true });
}
render() {
if (this.state.editting) {
return (
<Row className="inputBox">
<input
type="number"
className="concurrencyInput"
defaultValue={this.props.value.toString()}
ref={this.input}
/>
<Button
type="primary"
className="tableButton editStyle"
onClick={this.save}
>
Save
</Button>
<Button
type="primary"
onClick={this.cancel}
style={{ display: 'inline-block', marginLeft: 1 }}
className="tableButton editStyle"
>
Cancel
</Button>
</Row>
);
} else {
return (
<Row className="inputBox">
<input
type="number"
className="concurrencyInput"
disabled={true}
value={this.props.value}
/>
<Button
type="primary"
className="tableButton editStyle"
onClick={this.edit}
>
Edit
</Button>
</Row>
);
}
}
}
export default ConcurrencyInput;
import * as React from 'react';
import { Row, Col, Popover, Button, message } from 'antd';
import { Row, Col, Popover, message } from 'antd';
import axios from 'axios';
import { MANAGER_IP, CONTROLTYPE } from '../../static/const';
import { Experiment, TrialNumber } from '../../static/interface';
import { MANAGER_IP } from '../../static/const';
import { EXPERIMENT, TRIALS } from '../../static/datamodel';
import { convertTime } from '../../static/function';
import ConcurrencyInput from './NumInput';
import ProgressBar from './ProgressItem';
import LogDrawer from '../Modal/LogDrawer';
import '../../static/style/progress.scss';
import '../../static/style/probar.scss';
interface ProgressProps {
trialProfile: Experiment;
concurrency: number;
trialNumber: TrialNumber;
bestAccuracy: number;
status: string;
errors: string;
changeConcurrency: (val: number) => void;
experimentUpdateBroadcast: number;
}
interface ProgressState {
btnName: string;
isEnable: boolean;
userInputVal: string; // get user input
cancelSty: string;
isShowLogDrawer: boolean;
}
class Progressed extends React.Component<ProgressProps, ProgressState> {
public conInput: HTMLInputElement | null;
public _isMounted = false;
constructor(props: ProgressProps) {
super(props);
this.state = {
btnName: 'Edit',
isEnable: true,
userInputVal: this.props.trialProfile.runConcurren.toString(),
cancelSty: 'none',
isShowLogDrawer: false
};
}
editTrialConcurrency = () => {
const { btnName } = this.state;
if (this._isMounted) {
if (btnName === 'Edit') {
// user click edit
this.setState(() => ({
isEnable: false,
btnName: 'Save',
cancelSty: 'inline-block'
}));
} else {
// user click save button
axios(`${MANAGER_IP}/experiment`, {
method: 'GET'
})
.then(rese => {
if (rese.status === 200) {
const { userInputVal } = this.state;
const experimentFile = rese.data;
const trialConcurrency = experimentFile.params.trialConcurrency;
if (userInputVal !== undefined) {
if (userInputVal === trialConcurrency.toString() || userInputVal === '0') {
message.destroy();
message.info(
`trialConcurrency's value is ${trialConcurrency}, you did not modify it`, 2);
} else {
experimentFile.params.trialConcurrency = parseInt(userInputVal, 10);
// rest api, modify trial concurrency value
axios(`${MANAGER_IP}/experiment`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
data: experimentFile,
params: {
update_type: CONTROLTYPE[1]
}
}).then(res => {
if (res.status === 200) {
message.destroy();
message.success(`Update ${CONTROLTYPE[1].toLocaleLowerCase()}
successfully`);
this.props.changeConcurrency(parseInt(userInputVal, 10));
}
})
.catch(error => {
if (error.response.status === 500) {
if (error.response.data.error) {
message.error(error.response.data.error);
} else {
message.error(
`Update ${CONTROLTYPE[1].toLocaleLowerCase()} failed`);
}
}
});
// btn -> edit
this.setState(() => ({
btnName: 'Edit',
isEnable: true,
cancelSty: 'none'
}));
}
}
}
});
}
}
}
cancelFunction = () => {
const { trialProfile } = this.props;
if (this._isMounted) {
this.setState(
() => ({
btnName: 'Edit',
isEnable: true,
cancelSty: 'none',
}));
editTrialConcurrency = async (userInput: string) => {
if (!userInput.match(/^[1-9]\d*$/)) {
message.error('Please enter a positive integer!', 2);
return;
}
if (this.conInput !== null) {
this.conInput.value = trialProfile.runConcurren.toString();
const newConcurrency = parseInt(userInput, 10);
if (newConcurrency === this.props.concurrency) {
message.info(`Trial concurrency has not changed`, 2);
return;
}
}
getUserTrialConcurrency = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
if (value.match(/^[1-9]\d*$/) || value === '') {
this.setState(() => ({
userInputVal: value
}));
} else {
message.error('Please enter a positive integer!', 2);
if (this.conInput !== null) {
const { trialProfile } = this.props;
this.conInput.value = trialProfile.runConcurren.toString();
const newProfile = Object.assign({}, EXPERIMENT.profile);
newProfile.params.trialConcurrency = newConcurrency;
// rest api, modify trial concurrency value
try {
const res = await axios.put(`${MANAGER_IP}/experiment`, newProfile, {
params: { update_type: 'TRIAL_CONCURRENCY' }
});
if (res.status === 200) {
message.success(`Successfully updated trial concurrency`);
// NOTE: should we do this earlier in favor of poor networks?
this.props.changeConcurrency(newConcurrency);
}
} catch (error) {
if (error.response && error.response.data.error) {
message.error(`Failed to update trial concurrency\n${error.response.data.error}`);
} else if (error.response) {
message.error(`Failed to update trial concurrency\nServer responsed ${error.response.status}`);
} else if (error.message) {
message.error(`Failed to update trial concurrency\n${error.message}`);
} else {
message.error(`Failed to update trial concurrency\nUnknown error`);
}
}
}
isShowDrawer = () => {
if (this._isMounted === true) {
this.setState(() => ({ isShowLogDrawer: true }));
}
this.setState({ isShowLogDrawer: true });
}
closeDrawer = () => {
if (this._isMounted === true) {
this.setState(() => ({ isShowLogDrawer: false }));
}
this.setState({ isShowLogDrawer: false });
}
componentWillReceiveProps() {
const { trialProfile } = this.props;
if (this.conInput !== null) {
this.conInput.value = trialProfile.runConcurren.toString();
}
}
render() {
const { bestAccuracy } = this.props;
const { isShowLogDrawer } = this.state;
componentDidMount() {
this._isMounted = true;
}
const count = TRIALS.countStatus();
const stoppedCount = count.get('USER_CANCELED')! + count.get('SYS_CANCELED')! + count.get('EARLY_STOPPED')!;
const bar2 = count.get('RUNNING')! + count.get('SUCCEEDED')! + count.get('FAILED')! + stoppedCount;
componentWillUnmount() {
this._isMounted = false;
}
const bar2Percent = (bar2 / EXPERIMENT.profile.params.maxTrialNum) * 100;
const percent = (EXPERIMENT.profile.execDuration / EXPERIMENT.profile.params.maxExecDuration) * 100;
const remaining = convertTime(EXPERIMENT.profile.params.maxExecDuration - EXPERIMENT.profile.execDuration);
const maxDuration = convertTime(EXPERIMENT.profile.params.maxExecDuration);
const maxTrialNum = convertTime(EXPERIMENT.profile.params.maxTrialNum);
const execDuration = convertTime(EXPERIMENT.profile.execDuration);
render() {
const { trialProfile, trialNumber, bestAccuracy, status, errors } = this.props;
const { isEnable, btnName, cancelSty, isShowLogDrawer } = this.state;
const bar2 = trialNumber.totalCurrentTrial - trialNumber.waitTrial - trialNumber.unknowTrial;
const bar2Percent = (bar2 / trialProfile.MaxTrialNum) * 100;
const percent = (trialProfile.execDuration / trialProfile.maxDuration) * 100;
const runDuration = convertTime(trialProfile.execDuration);
const temp = trialProfile.maxDuration - trialProfile.execDuration;
let remaining;
let errorContent;
if (temp < 0) {
remaining = '0';
} else {
remaining = convertTime(temp);
}
if (errors !== '') {
if (EXPERIMENT.error) {
errorContent = (
<div className="errors">
{errors}
{EXPERIMENT.error}
<div><a href="#" onClick={this.isShowDrawer}>Learn about</a></div>
</div>
);
......@@ -196,9 +103,9 @@ class Progressed extends React.Component<ProgressProps, ProgressState> {
<Row className="basic lineBasic">
<p>Status</p>
<div className="status">
<span className={status}>{status}</span>
<span className={EXPERIMENT.status}>{EXPERIMENT.status}</span>
{
status === 'ERROR'
EXPERIMENT.status === 'ERROR'
?
<Popover
placement="rightTop"
......@@ -216,26 +123,26 @@ class Progressed extends React.Component<ProgressProps, ProgressState> {
<ProgressBar
who="Duration"
percent={percent}
description={runDuration}
bgclass={status}
maxString={`Max duration: ${convertTime(trialProfile.maxDuration)}`}
description={execDuration}
bgclass={EXPERIMENT.status}
maxString={`Max duration: ${maxDuration}`}
/>
<ProgressBar
who="Trial numbers"
percent={bar2Percent}
description={bar2.toString()}
bgclass={status}
maxString={`Max trial number: ${trialProfile.MaxTrialNum}`}
bgclass={EXPERIMENT.status}
maxString={`Max trial number: ${maxTrialNum}`}
/>
<Row className="basic colorOfbasic mess">
<p>Best metric</p>
<div>{bestAccuracy.toFixed(6)}</div>
<div>{isNaN(bestAccuracy) ? 'N/A' : bestAccuracy.toFixed(6)}</div>
</Row>
<Row className="mess">
<Col span={6}>
<Row className="basic colorOfbasic">
<p>Spent</p>
<div>{convertTime(trialProfile.execDuration)}</div>
<div>{execDuration}</div>
</Row>
</Col>
<Col span={6}>
......@@ -247,54 +154,32 @@ class Progressed extends React.Component<ProgressProps, ProgressState> {
<Col span={12}>
{/* modify concurrency */}
<p>Concurrency</p>
<Row className="inputBox">
<input
type="number"
disabled={isEnable}
onChange={this.getUserTrialConcurrency}
className="concurrencyInput"
ref={(input) => this.conInput = input}
/>
<Button
type="primary"
className="tableButton editStyle"
onClick={this.editTrialConcurrency}
>{btnName}
</Button>
<Button
type="primary"
onClick={this.cancelFunction}
style={{ display: cancelSty, marginLeft: 1 }}
className="tableButton editStyle"
>
Cancel
</Button>
</Row>
<ConcurrencyInput value={this.props.concurrency} updateValue={this.editTrialConcurrency} />
</Col>
</Row>
<Row className="mess">
<Col span={6}>
<Row className="basic colorOfbasic">
<p>Running</p>
<div>{trialNumber.runTrial}</div>
<div>{count.get('RUNNING')}</div>
</Row>
</Col>
<Col span={6}>
<Row className="basic colorOfbasic">
<p>Succeeded</p>
<div>{trialNumber.succTrial}</div>
<div>{count.get('SUCCEEDED')}</div>
</Row>
</Col>
<Col span={6}>
<Row className="basic">
<p>Stopped</p>
<div>{trialNumber.stopTrial}</div>
<div>{stoppedCount}</div>
</Row>
</Col>
<Col span={6}>
<Row className="basic">
<p>Failed</p>
<div>{trialNumber.failTrial}</div>
<div>{count.get('FAILED')}</div>
</Row>
</Col>
</Row>
......@@ -309,4 +194,4 @@ class Progressed extends React.Component<ProgressProps, ProgressState> {
}
}
export default Progressed;
\ No newline at end of file
export default Progressed;
......@@ -2,131 +2,83 @@ import * as React from 'react';
import { Table } from 'antd';
import OpenRow from '../public-child/OpenRow';
import DefaultMetric from '../public-child/DefaultMetrc';
import { TableObj } from '../../static/interface';
import { TRIALS } from '../../static/datamodel';
import { TableRecord } from '../../static/interface';
import { convertDuration } from '../../static/function';
import '../../static/style/tableStatus.css';
import '../../static/style/openRow.scss';
interface SuccessTableProps {
tableSource: Array<TableObj>;
trainingPlatform: string;
logCollection: boolean;
multiphase: boolean;
trialIds: string[];
}
class SuccessTable extends React.Component<SuccessTableProps, {}> {
public _isMounted = false;
function openRow(record: TableRecord) {
return (
<OpenRow trialId={record.id} />
);
}
class SuccessTable extends React.Component<SuccessTableProps, {}> {
constructor(props: SuccessTableProps) {
super(props);
}
openRow = (record: TableObj) => {
const { trainingPlatform, logCollection, multiphase } = this.props;
return (
<OpenRow
trainingPlatform={trainingPlatform}
record={record}
logCollection={logCollection}
multiphase={multiphase}
/>
);
}
componentDidMount() {
this._isMounted = true;
}
componentWillUnmount() {
this._isMounted = false;
}
render() {
const { tableSource } = this.props;
let bgColor = '';
const columns = [{
title: 'Trial No.',
dataIndex: 'sequenceId',
key: 'sequenceId',
width: 140,
className: 'tableHead'
}, {
title: 'ID',
dataIndex: 'id',
key: 'id',
width: 60,
className: 'tableHead leftTitle',
render: (text: string, record: TableObj) => {
return (
<div>{record.id}</div>
);
},
}, {
title: 'Duration',
dataIndex: 'duration',
key: 'duration',
width: 140,
sorter: (a: TableObj, b: TableObj) => (a.duration as number) - (b.duration as number),
render: (text: string, record: TableObj) => {
let duration;
if (record.duration !== undefined) {
// duration is nagative number(-1) & 0-1
if (record.duration > 0 && record.duration < 1 || record.duration < 0) {
duration = `${record.duration}s`;
} else {
duration = convertDuration(record.duration);
}
} else {
duration = 0;
const columns = [
{
title: 'Trial No.',
dataIndex: 'sequenceId',
width: 140,
className: 'tableHead'
}, {
title: 'ID',
dataIndex: 'id',
width: 60,
className: 'tableHead leftTitle',
render: (text: string, record: TableRecord) => {
return (
<div>{record.id}</div>
);
},
}, {
title: 'Duration',
dataIndex: 'duration',
width: 140,
render: (text: string, record: TableRecord) => {
return (
<div className="durationsty"><div>{convertDuration(record.duration)}</div></div>
);
},
}, {
title: 'Status',
dataIndex: 'status',
width: 150,
className: 'tableStatus',
render: (text: string, record: TableRecord) => {
return (
<div className={`${record.status} commonStyle`}>{record.status}</div>
);
}
return (
<div className="durationsty"><div>{duration}</div></div>
);
},
}, {
title: 'Status',
dataIndex: 'status',
key: 'status',
width: 150,
className: 'tableStatus',
render: (text: string, record: TableObj) => {
bgColor = record.status;
return (
<div className={`${bgColor} commonStyle`}>
{record.status}
</div>
);
}
}, {
title: 'Default metric',
dataIndex: 'acc',
key: 'acc',
sorter: (a: TableObj, b: TableObj) => {
if (a.acc !== undefined && b.acc !== undefined) {
return JSON.parse(a.acc.default) - JSON.parse(b.acc.default);
} else {
return NaN;
}, {
title: 'Default metric',
dataIndex: 'accuracy',
render: (text: string, record: TableRecord) => {
return (
<DefaultMetric trialId={record.id} />
);
}
},
render: (text: string, record: TableObj) => {
return (
<DefaultMetric record={record} />
);
}
}];
];
return (
<div className="tabScroll" >
<Table
columns={columns}
expandedRowRender={this.openRow}
dataSource={tableSource}
expandedRowRender={openRow}
dataSource={TRIALS.table(this.props.trialIds)}
className="commonTableStyle"
pagination={false}
/>
</div >
</div>
);
}
}
......
import * as React from 'react';
import MonacoEditor from 'react-monaco-editor';
import { MONACO } from '../../static/const';
import { EXPERIMENT } from '../../static/datamodel';
interface TrialInfoProps {
experiment: object;
experimentUpdateBroadcast: number;
concurrency: number;
}
class TrialInfo extends React.Component<TrialInfoProps, {}> {
......@@ -12,32 +14,21 @@ class TrialInfo extends React.Component<TrialInfoProps, {}> {
super(props);
}
componentWillReceiveProps(nextProps: TrialInfoProps) {
const experiments = nextProps.experiment;
Object.keys(experiments).map(key => {
switch (key) {
case 'id':
case 'logDir':
case 'startTime':
case 'endTime':
experiments[key] = undefined;
break;
case 'params':
const params = experiments[key];
Object.keys(params).map(item => {
if (item === 'experimentName' || item === 'searchSpace'
|| item === 'trainingServicePlatform') {
params[item] = undefined;
}
});
break;
default:
render() {
const blacklist = [
'id', 'logDir', 'startTime', 'endTime',
'experimentName', 'searchSpace', 'trainingServicePlatform'
];
// tslint:disable-next-line:no-any
const filter = (key: string, val: any) => {
if (key === 'trialConcurrency') {
return this.props.concurrency;
}
});
}
return blacklist.includes(key) ? undefined : val;
};
const profile = JSON.stringify(EXPERIMENT.profile, filter, 2);
render() {
const { experiment } = this.props;
// FIXME: highlight not working?
return (
<div className="profile">
<MonacoEditor
......@@ -45,7 +36,7 @@ class TrialInfo extends React.Component<TrialInfoProps, {}> {
height="361"
language="json"
theme="vs-light"
value={JSON.stringify(experiment, null, 2)}
value={profile}
options={MONACO}
/>
</div>
......
import * as React from 'react';
import { TableObj } from '../../static/interface';
import { TRIALS } from '../../static/datamodel';
import { formatAccuracy } from '../../static/function';
interface DefaultMetricProps {
record: TableObj;
trialId: string;
}
class DefaultMetric extends React.Component<DefaultMetricProps, {}> {
constructor(props: DefaultMetricProps) {
super(props);
}
render() {
const { record } = this.props;
let accuracy;
if (record.acc !== undefined) {
accuracy = record.acc.default;
}
let wei = 0;
if (accuracy !== undefined) {
if (accuracy.toString().indexOf('.') !== -1) {
wei = accuracy.toString().length - accuracy.toString().indexOf('.') - 1;
}
}
const accuracy = TRIALS.getTrial(this.props.trialId).accuracy;
return (
<div>
{
record.acc !== undefined && record.acc.default !== undefined
?
wei > 6
?
JSON.parse(record.acc.default).toFixed(6)
:
record.acc.default
:
'--'
}
</div>
<div>{accuracy !== undefined ? formatAccuracy(accuracy) : '--'}</div>
);
}
}
export default DefaultMetric;
\ No newline at end of file
export default DefaultMetric;
import * as React from 'react';
import { TableObj } from '../../static/interface';
import { TRIALS } from '../../static/datamodel';
interface IntermediateValProps {
record: TableObj;
trialId: string;
}
class IntermediateVal extends React.Component<IntermediateValProps, {}> {
constructor(props: IntermediateValProps) {
super(props);
}
render() {
const { record } = this.props;
const interArr = record.description.intermediate;
let lastVal;
let wei = 0;
if (interArr !== undefined) {
lastVal = interArr[interArr.length - 1];
}
let result: string = JSON.stringify(lastVal);
if (lastVal !== undefined) {
if (lastVal.toString().indexOf('.') !== -1) {
wei = lastVal.toString().length - lastVal.toString().indexOf('.') - 1;
if (wei > 6) {
result = `${lastVal.toFixed(6)}`;
}
}
// some trial haven't final result
if (record.acc !== undefined) {
if (record.acc.default !== undefined) {
result = `${result} (FINAL)`;
}
} else {
result = `${result} (LATEST)`;
}
} else {
result = '--';
}
return (
<div>{result}</div>
<div>{TRIALS.getTrial(this.props.trialId).formatLatestAccuracy()}</div>
);
}
}
......
......@@ -2,7 +2,8 @@ import * as React from 'react';
import * as copy from 'copy-to-clipboard';
import PaiTrialLog from '../public-child/PaiTrialLog';
import TrialLog from '../public-child/TrialLog';
import { TableObj } from '../../static/interface';
import { EXPERIMENT, TRIALS } from '../../static/datamodel';
import { Trial } from '../../static/model/trial';
import { Row, Tabs, Button, message, Modal } from 'antd';
import { MANAGER_IP } from '../../static/const';
import '../../static/style/overview.scss';
......@@ -11,10 +12,7 @@ import JSONTree from 'react-json-tree';
const TabPane = Tabs.TabPane;
interface OpenRowProps {
trainingPlatform: string;
record: TableObj;
logCollection: boolean;
multiphase: boolean;
trialId: string;
}
interface OpenRowState {
......@@ -24,7 +22,6 @@ interface OpenRowState {
class OpenRow extends React.Component<OpenRowProps, OpenRowState> {
public _isMounted: boolean;
constructor(props: OpenRowProps) {
super(props);
this.state = {
......@@ -33,20 +30,16 @@ class OpenRow extends React.Component<OpenRowProps, OpenRowState> {
};
}
showFormatModal = (record: TableObj) => {
showFormatModal = (trial: Trial) => {
// get copy parameters
const params = JSON.stringify(record.description.parameters, null, 4);
const params = JSON.stringify(trial.info.hyperParameters, null, 4);
// open modal with format string
if (this._isMounted === true) {
this.setState(() => ({ isShowFormatModal: true, formatStr: params }));
}
this.setState({ isShowFormatModal: true, formatStr: params });
}
hideFormatModal = () => {
// close modal, destroy state format string data
if (this._isMounted === true) {
this.setState(() => ({ isShowFormatModal: false, formatStr: '' }));
}
this.setState({ isShowFormatModal: false, formatStr: '' });
}
copyParams = () => {
......@@ -62,68 +55,54 @@ class OpenRow extends React.Component<OpenRowProps, OpenRowState> {
this.hideFormatModal();
}
componentDidMount() {
this._isMounted = true;
}
componentWillUnmount() {
this._isMounted = false;
}
render() {
const { trainingPlatform, record, logCollection, multiphase } = this.props;
const { isShowFormatModal, formatStr } = this.state;
const trialId = this.props.trialId;
const trial = TRIALS.getTrial(trialId);
let isClick = false;
let isHasParameters = true;
if (record.description.parameters.error) {
isHasParameters = false;
}
const openRowDataSource = record.description.parameters;
const trialink: string = `${MANAGER_IP}/trial-jobs/${record.id}`;
const logPathRow = record.description.logPath !== undefined
?
record.description.logPath
:
'This trial\'s log path are not available.';
const trialLink: string = `${MANAGER_IP}/trial-jobs/${trialId}`;
const logPathRow = trial.info.logPath || 'This trial\'s log path is not available.';
const multiProgress = trial.info.hyperParameters === undefined ? 0 : trial.info.hyperParameters.length;
return (
<Row className="openRowContent hyperpar">
<Tabs tabPosition="left" className="card">
<TabPane tab="Parameters" key="1">
{
multiphase
EXPERIMENT.multiPhase
?
<Row className="link">
Trails for multiphase experiment will return a set of parameters,
we are listing the latest parameter in webportal.
<br />
For the entire parameter set, please refer to the following "
<a href={trialink} target="_blank">{trialink}</a>".
<a href={trialLink} target="_blank">{trialLink}</a>".
<br/>
Current Phase: {record.description.multiProgress}.
Current Phase: {multiProgress}.
</Row>
:
<div />
}
{
isHasParameters
trial.info.hyperParameters !== undefined
?
<Row id="description">
<Row className="bgHyper">
{
isClick
?
<pre>{JSON.stringify(openRowDataSource, null, 4)}</pre>
<pre>{JSON.stringify(trial.info.hyperParameters, null, 4)}</pre>
:
<JSONTree
hideRoot={true}
shouldExpandNode={() => true} // default expandNode
getItemString={() => (<span />)} // remove the {} items
data={openRowDataSource}
data={trial.info.hyperParameters}
/>
}
</Row>
<Row className="copy">
<Button
onClick={this.showFormatModal.bind(this, record)}
onClick={this.showFormatModal.bind(this, trial)}
>
Copy as json
</Button>
......@@ -138,15 +117,16 @@ class OpenRow extends React.Component<OpenRowProps, OpenRowState> {
</TabPane>
<TabPane tab="Log" key="2">
{
trainingPlatform !== 'local'
// FIXME: this should not be handled in web UI side
EXPERIMENT.trainingServicePlatform !== 'local'
?
<PaiTrialLog
logStr={logPathRow}
id={record.id}
logCollection={logCollection}
id={trialId}
logCollection={EXPERIMENT.logCollectionEnabled}
/>
:
<TrialLog logStr={logPathRow} id={record.id} />
<TrialLog logStr={logPathRow} id={trialId} />
}
</TabPane>
</Tabs>
......@@ -170,4 +150,4 @@ class OpenRow extends React.Component<OpenRowProps, OpenRowState> {
}
}
export default OpenRow;
\ No newline at end of file
export default OpenRow;
import * as React from 'react';
import { Switch } from 'antd';
import ReactEcharts from 'echarts-for-react';
import { filterByStatus } from '../../static/function';
import { TableObj, DetailAccurPoint, TooltipForAccuracy } from '../../static/interface';
import { EXPERIMENT, TRIALS } from '../../static/datamodel';
import { Trial } from '../../static/model/trial';
import { TooltipForAccuracy } from '../../static/interface';
require('echarts/lib/chart/scatter');
require('echarts/lib/component/tooltip');
require('echarts/lib/component/title');
interface DefaultPointProps {
showSource: Array<TableObj>;
height: number;
whichGraph: string;
optimize: string;
trialIds: string[];
visible: boolean;
trialsUpdateBroadcast: number;
}
interface DefaultPointState {
defaultSource: object;
accNodata: string;
succeedTrials: number;
isViewBestCurve: boolean;
bestCurveEnabled: boolean;
}
class DefaultPoint extends React.Component<DefaultPointProps, DefaultPointState> {
public _isDefaultMounted = false;
constructor(props: DefaultPointProps) {
super(props);
this.state = {
defaultSource: {},
accNodata: '',
succeedTrials: 10000000,
isViewBestCurve: false
};
}
defaultMetric = (succeedSource: Array<TableObj>, isCurve: boolean) => {
const { optimize } = this.props;
const accSource: Array<DetailAccurPoint> = [];
const drawSource: Array<TableObj> = succeedSource.filter(filterByStatus);
const lengthOfSource = drawSource.length;
const tooltipDefault = lengthOfSource === 0 ? 'No data' : '';
if (this._isDefaultMounted === true) {
this.setState(() => ({
succeedTrials: lengthOfSource,
accNodata: tooltipDefault
}));
}
if (lengthOfSource === 0) {
const nullGraph = {
grid: {
left: '8%'
},
xAxis: {
name: 'Trial',
type: 'category',
},
yAxis: {
name: 'Default metric',
type: 'value',
}
};
if (this._isDefaultMounted === true) {
this.setState(() => ({
defaultSource: nullGraph
}));
}
} else {
const resultList: Array<number | object>[] = [];
// lineListDefault: [[sequenceId, default metric], []]
const lineListDefault: Array<number>[] = [];
Object.keys(drawSource).map(item => {
const temp = drawSource[item];
if (temp.acc !== undefined) {
if (temp.acc.default !== undefined) {
const searchSpace = temp.description.parameters;
lineListDefault.push([temp.sequenceId, temp.acc.default]);
accSource.push({
acc: temp.acc.default,
index: temp.sequenceId,
searchSpace: searchSpace
});
}
}
});
// deal with best metric line
const bestCurve: Array<number | object>[] = []; // best curve data source
if (lineListDefault[0] !== undefined) {
bestCurve.push([lineListDefault[0][0], lineListDefault[0][1], accSource[0].searchSpace]);
}
if (optimize === 'maximize') {
for (let i = 1; i < lineListDefault.length; i++) {
const val = lineListDefault[i][1];
const latest = bestCurve[bestCurve.length - 1][1];
if (val >= latest) {
bestCurve.push([lineListDefault[i][0], val, accSource[i].searchSpace]);
} else {
bestCurve.push([lineListDefault[i][0], latest, accSource[i].searchSpace]);
}
}
} else {
for (let i = 1; i < lineListDefault.length; i++) {
const val = lineListDefault[i][1];
const latest = bestCurve[bestCurve.length - 1][1];
if (val <= latest) {
bestCurve.push([lineListDefault[i][0], val, accSource[i].searchSpace]);
} else {
bestCurve.push([lineListDefault[i][0], latest, accSource[i].searchSpace]);
}
}
}
Object.keys(accSource).map(item => {
const items = accSource[item];
let temp: Array<number | object>;
temp = [items.index, items.acc, items.searchSpace];
resultList.push(temp);
});
// isViewBestCurve: false show default metric graph
// isViewBestCurve: true show best curve
if (isCurve === true) {
if (this._isDefaultMounted === true) {
this.setState(() => ({
defaultSource: this.drawBestcurve(bestCurve, resultList)
}));
}
} else {
if (this._isDefaultMounted === true) {
this.setState(() => ({
defaultSource: this.drawDefaultMetric(resultList)
}));
}
}
}
}
drawBestcurve = (realDefault: Array<number | object>[], resultList: Array<number | object>[]) => {
return {
grid: {
left: '8%'
},
tooltip: {
trigger: 'item',
enterable: true,
position: function (point: Array<number>, data: TooltipForAccuracy) {
if (data.data[0] < realDefault.length / 2) {
return [point[0], 80];
} else {
return [point[0] - 300, 80];
}
},
formatter: function (data: TooltipForAccuracy) {
const result = '<div class="tooldetailAccuracy">' +
'<div>Trial No.: ' + data.data[0] + '</div>' +
'<div>Optimization curve: ' + data.data[1] + '</div>' +
'<div>Parameters: ' +
'<pre>' + JSON.stringify(data.data[2], null, 4) + '</pre>' +
'</div>' +
'</div>';
return result;
}
},
xAxis: {
name: 'Trial',
type: 'category',
},
yAxis: {
name: 'Default metric',
type: 'value',
scale: true
},
series: [
{
type: 'line',
lineStyle: { color: '#FF6600' },
data: realDefault
},
{
symbolSize: 6,
type: 'scatter',
data: resultList
}]
};
}
drawDefaultMetric = (resultList: Array<number | object>[]) => {
return {
grid: {
left: '8%'
},
tooltip: {
trigger: 'item',
enterable: true,
position: function (point: Array<number>, data: TooltipForAccuracy) {
if (data.data[0] < resultList.length / 2) {
return [point[0], 80];
} else {
return [point[0] - 300, 80];
}
},
formatter: function (data: TooltipForAccuracy) {
const result = '<div class="tooldetailAccuracy">' +
'<div>Trial No.: ' + data.data[0] + '</div>' +
'<div>Default metric: ' + data.data[1] + '</div>' +
'<div>Parameters: ' +
'<pre>' + JSON.stringify(data.data[2], null, 4) + '</pre>' +
'</div>' +
'</div>';
return result;
}
},
xAxis: {
name: 'Trial',
type: 'category',
},
yAxis: {
name: 'Default metric',
type: 'value',
scale: true
},
series: [{
symbolSize: 6,
type: 'scatter',
data: resultList
}]
};
this.state = { bestCurveEnabled: false };
}
loadDefault = (checked: boolean) => {
// checked: true show best metric curve
const { showSource } = this.props;
if (this._isDefaultMounted === true) {
this.defaultMetric(showSource, checked);
// ** deal with data and then update view layer
this.setState(() => ({ isViewBestCurve: checked }));
}
}
// update parent component state
componentWillReceiveProps(nextProps: DefaultPointProps) {
const { whichGraph, showSource } = nextProps;
const { isViewBestCurve } = this.state;
if (whichGraph === '1') {
this.defaultMetric(showSource, isViewBestCurve);
}
this.setState({ bestCurveEnabled: checked });
}
shouldComponentUpdate(nextProps: DefaultPointProps, nextState: DefaultPointState) {
const { whichGraph } = nextProps;
if (whichGraph === '1') {
const { succeedTrials, isViewBestCurve } = nextState;
const succTrial = this.state.succeedTrials;
const isViewBestCurveBefore = this.state.isViewBestCurve;
if (isViewBestCurveBefore !== isViewBestCurve) {
return true;
}
if (succeedTrials !== succTrial) {
return true;
}
}
// only whichGraph !== '1', default metric can't update
return false;
}
componentDidMount() {
this._isDefaultMounted = true;
}
componentWillUnmount() {
this._isDefaultMounted = false;
return nextProps.visible;
}
render() {
const { height } = this.props;
const { defaultSource, accNodata } = this.state;
const graph = this.generateGraph();
const accNodata = (graph === EmptyGraph ? 'No data' : '');
return (
<div>
<div className="default-metric">
......@@ -282,10 +45,10 @@ class DefaultPoint extends React.Component<DefaultPointProps, DefaultPointState>
</div>
</div>
<ReactEcharts
option={defaultSource}
option={graph}
style={{
width: '100%',
height: height,
height: 402,
margin: '0 auto',
}}
theme="my_theme"
......@@ -295,6 +58,102 @@ class DefaultPoint extends React.Component<DefaultPointProps, DefaultPointState>
</div>
);
}
private generateGraph() {
const trials = TRIALS.getTrials(this.props.trialIds).filter(trial => trial.sortable);
if (trials.length === 0) {
return EmptyGraph;
}
const graph = generateGraphConfig(trials[trials.length - 1].sequenceId);
if (this.state.bestCurveEnabled) {
(graph as any).series = [ generateBestCurveSeries(trials), generateScatterSeries(trials) ];
} else {
(graph as any).series = [ generateScatterSeries(trials) ];
}
return graph;
}
}
const EmptyGraph = {
grid: {
left: '8%'
},
xAxis: {
name: 'Trial',
type: 'category',
},
yAxis: {
name: 'Default metric',
type: 'value',
}
};
function generateGraphConfig(maxSequenceId: number) {
return {
grid: {
left: '8%',
},
tooltip: {
trigger: 'item',
enterable: true,
position: (point: Array<number>, data: TooltipForAccuracy) => (
[ (data.data[0] < maxSequenceId ? point[0] : (point[0] - 300)), 80 ]
),
formatter: (data: TooltipForAccuracy) => (
'<div class="tooldetailAccuracy">' +
'<div>Trial No.: ' + data.data[0] + '</div>' +
'<div>Default metric: ' + data.data[1] + '</div>' +
'<div>Parameters: <pre>' + JSON.stringify(data.data[2], null, 4) + '</pre></div>' +
'</div>'
),
},
xAxis: {
name: 'Trial',
type: 'category',
},
yAxis: {
name: 'Default metric',
type: 'value',
scale: true,
},
series: undefined,
};
}
function generateScatterSeries(trials: Trial[]) {
const data = trials.map(trial => [
trial.sequenceId,
trial.accuracy,
trial.info.hyperParameters,
]);
return {
symbolSize: 6,
type: 'scatter',
data,
};
}
function generateBestCurveSeries(trials: Trial[]) {
let best = trials[0];
const data = [[ best.sequenceId, best.accuracy, best.info.hyperParameters ]];
for (let i = 1; i < trials.length; i++) {
const trial = trials[i];
const delta = trial.accuracy! - best.accuracy!;
const better = (EXPERIMENT.optimizeMode === 'minimize') ? (delta < 0) : (delta > 0);
if (better) {
data.push([ trial.sequenceId, trial.accuracy, trial.info.hyperParameters ]);
best = trial;
} else {
data.push([ trial.sequenceId, best.accuracy, trial.info.hyperParameters ]);
}
}
return {
type: 'line',
lineStyle: { color: '#FF6600' },
data,
};
}
export default DefaultPoint;
\ No newline at end of file
export default DefaultPoint;
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment