From a642a1621724b9bbdb22cda4b940c4477631d853 Mon Sep 17 00:00:00 2001 From: gurbirkalsi Date: Wed, 25 Mar 2020 18:38:12 -0400 Subject: [PATCH] Phase two result-data-sample migration Utilize scroll helper for scrolling through query until total objects are found and aggregated Assign clusters to state before querying timeseries data to trigger componentDidUpdate in TimeseriesGraph component Update mock api test with result sample case with differing measurement_title fields --- config.json.j2 | 1 + mock/api.js | 31 ++ package.json | 4 + src/components/Select/index.js | 85 ++++ src/components/TimeseriesGraph/index.js | 48 ++- src/components/TimeseriesGraph/index.test.js | 2 +- src/global.js | 2 +- src/models/dashboard.js | 55 +++ src/pages/ComparisonSelect/index.js | 57 +-- src/pages/RunComparison/index.js | 383 ++++++------------- src/services/dashboard.js | 148 +++---- src/utils/parse.js | 69 +++- 12 files changed, 492 insertions(+), 393 deletions(-) create mode 100644 src/components/Select/index.js diff --git a/config.json.j2 b/config.json.j2 index 139da6fd8..51d96b714 100644 --- a/config.json.j2 +++ b/config.json.j2 @@ -2,6 +2,7 @@ "elasticsearch": "{{ elasticsearch_url }}", "results": "{{ results_url }}", "graphql": "{{ graphql_url }}", + "result_index": "{{ result_index }}", "run_index": "{{ run_index }}", "prefix": "{{ prefix }}" } diff --git a/mock/api.js b/mock/api.js index 9513a791a..6b0d0e6ad 100644 --- a/mock/api.js +++ b/mock/api.js @@ -142,6 +142,37 @@ export const mockDataSample = [ }, }, }, + { + _source: { + run: { + id: 'test_run_id', + controller: 'test_controller', + name: 'test_run_name', + script: 'test_script', + config: 'test_config', + }, + iteration: { name: 'test_iteration_2', number: 2 }, + benchmark: { + instances: 1, + max_stddevpct: 1, + message_size_bytes: 1, + primary_metric: 'test_measurement_title', + test_type: 'stream', + }, + sample: { + closest_sample: 1, + mean: 0.1, + stddev: 0.1, + stddevpct: 1, + uid: 'test_measurement_id', + measurement_type: 'test_measurement_type', + measurement_idx: 0, + measurement_title: 'diff_measurement_title', + '@idx': 1, + name: 'sample2', + }, + }, + }, ], }, aggregations: { diff --git a/package.json b/package.json index 9680006c5..23ee136a6 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,10 @@ "@antv/g2": "^3.4.10", "@babel/runtime": "^7.3.1", "@nivo/bar": "^0.36.0", + "@patternfly/react-charts": "^5.3.19", + "@patternfly/react-core": "^3.153.2", + "@patternfly/react-icons": "^3.15.16", + "@patternfly/react-table": "^2.28.39", "ant-design-pro": "^2.1.1", "antd": "^3.16.1", "classnames": "^2.2.5", diff --git a/src/components/Select/index.js b/src/components/Select/index.js new file mode 100644 index 000000000..377b095a0 --- /dev/null +++ b/src/components/Select/index.js @@ -0,0 +1,85 @@ +import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; +import { + Select as PFSelect, + SelectOption, + SelectVariant, + SelectDirection, +} from '@patternfly/react-core'; + +export default class Select extends PureComponent { + static propTypes = { + options: PropTypes.array.isRequired, + }; + + constructor(props) { + super(props); + + this.state = { + isToggleIcon: false, + isExpanded: false, + isDisabled: false, + direction: SelectDirection.down, + }; + } + + onToggle = isExpanded => { + this.setState({ + isExpanded, + }); + }; + + clearSelection = () => { + this.setState({ + isExpanded: false, + }); + }; + + toggleDisabled = checked => { + this.setState({ + isDisabled: checked, + }); + }; + + setIcon = checked => { + this.setState({ + isToggleIcon: checked, + }); + }; + + toggleDirection = () => { + const { direction } = this.state; + + if (direction === SelectDirection.up) { + this.setState({ + direction: SelectDirection.down, + }); + } else { + this.setState({ + direction: SelectDirection.up, + }); + } + }; + + render() { + const { options, onSelect, selected } = this.props; + const { isExpanded, isDisabled, direction, isToggleIcon } = this.state; + + return ( + + {options.map(option => ( + + ))} + + ); + } +} diff --git a/src/components/TimeseriesGraph/index.js b/src/components/TimeseriesGraph/index.js index 944e76717..612acc9dc 100644 --- a/src/components/TimeseriesGraph/index.js +++ b/src/components/TimeseriesGraph/index.js @@ -2,6 +2,8 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import jschart from 'jschart'; +import Select from '@/components/Select'; + export default class TimeseriesGraph extends PureComponent { static propTypes = { dataSeriesNames: PropTypes.array.isRequired, @@ -21,6 +23,14 @@ export default class TimeseriesGraph extends PureComponent { yAxisTitle: null, }; + constructor(props) { + super(props); + + this.state = { + selectedValue: 1, + }; + } + componentDidMount = () => { const { xAxisSeries, @@ -32,39 +42,59 @@ export default class TimeseriesGraph extends PureComponent { yAxisTitle, graphOptions, } = this.props; + let { selectedValue } = this.state; + selectedValue -= 1; jschart.create_jschart(0, 'timeseries', graphId, graphName, xAxisTitle, yAxisTitle, { dynamic_chart: true, json_object: { x_axis_series: xAxisSeries, - data_series_names: dataSeriesNames, - data, + data_series_names: data.length > 0 ? data[selectedValue].timeseriesLabels : dataSeriesNames, + data: data.length > 0 ? data[selectedValue].timeseriesAggregation : data, }, ...graphOptions, }); }; - componentDidUpdate = prevProps => { + componentDidUpdate = (prevProps, prevState) => { const { data, dataSeriesNames, xAxisSeries, graphId } = this.props; + let { selectedValue } = this.state; + selectedValue -= 1; if ( JSON.stringify(prevProps.data) !== JSON.stringify(data) || JSON.stringify(prevProps.dataSeriesNames) !== JSON.stringify(dataSeriesNames) || - prevProps.xAxisSeries !== xAxisSeries + prevProps.xAxisSeries !== xAxisSeries || + prevState.selectedValue !== selectedValue ) { - jschart.chart_reload(graphId, { + jschart.chart_reload_options(graphId, { json_object: { x_axis_series: xAxisSeries, - data_series_names: dataSeriesNames, - data, + data_series_names: + data.length > 0 ? data[selectedValue].timeseriesLabels : dataSeriesNames, + data: data.length > 0 ? data[selectedValue].timeseriesAggregation : data, }, }); } }; + onSelect = (event, selection) => { + this.setState({ + selectedValue: selection, + }); + }; + render() { - const { graphId } = this.props; + const { graphId, options } = this.props; + const { selectedValue } = this.state; - return
; + return ( +
+ {options && ( + - - - - - - - -
- - this.onSelectPageSection('details')} - /> - - - this.onSelectPageSection('summary')} - /> - - - this.onSelectPageSection('timeseries')} - /> - - - this.onSelectPageSection('table')} - /> - -
-
- - - ); + const { clusters, paramKeys, loadingClusters } = this.state; return ( -
-
- -
- {exportModal} -
- - - - - {Object.keys(clusters).map(cluster => ( -
+ + + + + Run Comparison + + {paramKeys.map(run => ( + {run.name} + ))} + + + + + + Summary + + + {Object.keys(clusters).map(cluster => ( - -

{cluster}

- iteration.cluster)} - keys={clusterKeys} - enableLabel={false} - indexBy="cluster" - groupMode="grouped" - padding={0.3} - labelSkipWidth={18} - labelSkipHeight={18} - tooltip={({ id, index, value }) => ( -
- - Result - {clusters[cluster][index][id].run.name} - -
- - Sample - {clusters[cluster][index][id].sample.name} - -
- - Mean - {value} - -
- )} - borderColor="inherit:darker(1.6)" - margin={{ - top: 32, - left: 64, - bottom: 64, - right: 124, +
+ `${datum.iteration_name}: ${datum.y}`} + constrainToVisibleArea + /> + } + fixLabelOverlap + legendData={paramKeys} + legendOrientation="vertical" + legendPosition="bottom" + height={400} + themeColor={ChartThemeColor.multiOrdered} + width={1000} + padding={{ + bottom: 150, + left: 100, + right: 300, + top: 50, }} - /> - + > + + + {clusters[cluster] + .map(iteration => iteration.cluster) + .map(clusterData => ( + + {Object.entries(clusterData) + .map(entry => entry[1]) + .map(clusterItem => ( + + ))} + + ))} + +
-
- ))} -
-
-
-
-
+ ))} + + + + + + {Object.keys(clusters).map(primaryMetric => ( + + {primaryMetric} + + + + + ))} + + + ); } } diff --git a/src/services/dashboard.js b/src/services/dashboard.js index d7463e643..6aade5d10 100644 --- a/src/services/dashboard.js +++ b/src/services/dashboard.js @@ -1,3 +1,6 @@ +/* eslint-disable no-param-reassign */ +/* eslint-disable no-unused-vars */ +/* eslint-disable no-underscore-dangle */ import request from '../utils/request'; function parseMonths(datastoreConfig, index, selectedIndices) { @@ -14,6 +17,22 @@ function parseMonths(datastoreConfig, index, selectedIndices) { return indices; } +function scrollUntilEmpty(datastoreConfig, data) { + const endpoint = `${datastoreConfig.elasticsearch}/_search/scroll?scroll=1m`; + const allData = data; + + if (allData.hits.total !== allData.hits.hits.length) { + const scroll = request.post(`${endpoint}&scroll_id=${allData._scroll_id}`); + scroll.then(response => { + allData._scroll_id = response._scroll_id; + allData.hits.total = response.hits.total; + allData.hits.hits = [...allData.hits.hits, ...response.hits.hits]; + return scrollUntilEmpty(datastoreConfig, allData); + }); + } + return allData; +} + export async function queryControllers(params) { const { datastoreConfig, selectedIndices } = params; @@ -137,6 +156,7 @@ export async function queryIterationSamples(params) { iterationSampleRequests.push( request.post(endpoint, { data: { + size: 1000, query: { filtered: { query: { @@ -190,7 +210,6 @@ export async function queryIterationSamples(params) { }, }, }, - size: 10000, sort: [ { 'iteration.number': { @@ -204,13 +223,19 @@ export async function queryIterationSamples(params) { ); }); - return Promise.all(iterationSampleRequests).then(iterations => { - return iterations; + return Promise.all(iterationSampleRequests).then(async iterations => { + return Promise.all( + iterations.map(async iteration => { + iteration = await scrollUntilEmpty(datastoreConfig, iteration); + }) + ).then(() => { + return iterations; + }); }); } -export async function queryTimeseriesData(params) { - const { datastoreConfig, selectedIndices, selectedResults } = params; +export async function queryTimeseriesData(payload) { + const { datastoreConfig, selectedIndices, selectedIterations } = payload; const endpoint = `${datastoreConfig.elasticsearch}/${parseMonths( datastoreConfig, @@ -218,77 +243,56 @@ export async function queryTimeseriesData(params) { selectedIndices )}/_search?scroll=1m`; - const iterationSampleRequests = []; - selectedResults.forEach(run => { - iterationSampleRequests.push( - request.post(endpoint, { - data: { - query: { - filtered: { - query: { - multi_match: { - query: run.id, - fields: ['run.id'], - }, - }, - filter: { - term: { - _type: 'pbench-result-data', + const timeseriesRequests = []; + Object.entries(selectedIterations).forEach(([runId, run]) => { + Object.entries(run.iterations).forEach(([, iteration]) => { + Object.entries(iteration.samples).forEach(([, sample]) => { + if (sample.benchmark.primary_metric === sample.sample.measurement_title) { + timeseriesRequests.push( + request.post(endpoint, { + data: { + size: 1000, + query: { + filtered: { + query: { + query_string: { + query: `_type:pbench-result-data AND run.id:${runId} AND iteration.name:${ + iteration.name + } AND sample.measurement_type:${ + sample.sample.measurement_type + } AND sample.measurement_title:${ + sample.sample.measurement_title + } AND sample.measurement_idx:${ + sample.sample.measurement_idx + } AND sample.name:${sample.sample.name}`, + analyze_wildcard: true, + }, + }, + }, }, + sort: [ + { + '@timestamp_original': { + order: 'asc', + unmapped_type: 'boolean', + }, + }, + ], }, - }, - }, - size: 10000, - sort: [ - { - '@timestamp_original': { - order: 'asc', - unmapped_type: 'boolean', - }, - }, - ], - }, - }) - ); - }); - - return Promise.all(iterationSampleRequests).then(iterations => { - return iterations; - }); -} - -export async function queryIterations(params) { - const { datastoreConfig, selectedResults } = params; - - const iterationRequests = []; - selectedResults.forEach(result => { - let controllerDir = result['@metadata.controller_dir']; - if (controllerDir === undefined) { - controllerDir = result['run.controller']; - controllerDir = controllerDir.includes('.') - ? controllerDir.slice(0, controllerDir.indexOf('.')) - : controllerDir; - } - iterationRequests.push( - request.get( - `${datastoreConfig.results}/incoming/${encodeURI(controllerDir)}/${encodeURI( - result['run.name'] - )}/result.json`, - { getResponse: true } - ) - ); + }) + ); + } + }); + }); }); - return Promise.all(iterationRequests).then(response => { - const iterations = []; - response.forEach((iteration, index) => { - iterations.push({ - iterationData: iteration.data, - controllerName: iteration.response.url.split('/')[4], - resultName: iteration.response.url.split('/')[5], - tableId: index, - }); + return Promise.all(timeseriesRequests).then(timeseries => { + return Promise.all( + timeseries.map(async timeseriesSet => { + timeseriesSet = await scrollUntilEmpty(datastoreConfig, timeseriesSet); + }) + ).then(() => { + return timeseries; }); - return iterations; }); } diff --git a/src/utils/parse.js b/src/utils/parse.js index 26edf060d..7050e805a 100644 --- a/src/utils/parse.js +++ b/src/utils/parse.js @@ -20,51 +20,84 @@ export const filterIterations = (results, selectedParams) => { export const generateClusters = results => { const iterations = {}; - const clusterGraphKeys = new Set(); const params = new Set(); + const runNames = new Set(); Object.entries(results).forEach(([runId, result]) => { Object.entries(result.iterations).forEach(([, iteration]) => { + runNames.add(result.run_name); Object.entries(iteration.samples).forEach(([sampleId, sample]) => { + if ( + sample.sample.measurement_title !== sample.benchmark.primary_metric || + sample.sample.closest_sample !== sample.sample['@idx'] + 1 + ) { + return; + } + Object.keys(sample.benchmark).forEach(param => params.add(param)); const paramKey = Object.values(sample.benchmark).join('-'); const primaryMetric = sampleId.split('-')[0]; + const clusterId = `${runId}_${iteration.name}_${sample.sample.name}`; if (primaryMetric === sample.benchmark.primary_metric.toLowerCase()) { iterations[paramKey] = { ...iterations[paramKey], ...sample.benchmark, + clusterKeys: { + ...(iterations[paramKey] !== undefined && iterations[paramKey].clusterKeys), + [clusterId]: null, + }, + iterations: { + ...(iterations[paramKey] !== undefined && iterations[paramKey].iterations), + [iteration.name]: sample.sample.name, + }, + runIds: { + ...(iterations[paramKey] !== undefined && iterations[paramKey].runIds), + [runId]: sample.run.name, + }, cluster: { ...(iterations[paramKey] !== undefined && iterations[paramKey].cluster), - [`${runId}`]: sample.sample.mean, - [`name_${runId}`]: sample.sample.name, - [`percent_${runId}`]: sample.sample.stddevpct, - cluster: paramKey, + [clusterId]: { + y: sample.sample.mean, + key: paramKey, + iteration_name: iteration.name, + run_name: sample.run.name, + }, }, - [runId]: { + [clusterId]: { sample: sample.sample, run: sample.run, benchmark: sample.benchmark, }, }; - clusterGraphKeys.add(`${runId}`); } }); }); }); const clusters = _.groupBy(Object.values(iterations), 'primary_metric'); - Object.entries(clusters).forEach(([, cluster]) => { - Object.entries(cluster).forEach(([clusterId, data]) => { - // eslint-disable-next-line no-param-reassign - data.cluster.cluster = Number.parseInt(clusterId, 10) + 1; + const clusterKeys = new Set(); + Object.entries(clusters).forEach(([, primaryMetricCluster]) => { + Object.entries(primaryMetricCluster).forEach(([index, cluster]) => { + const clusterId = (parseInt(index, 10) + 1).toString(); + clusterKeys.add(clusterId); + Object.entries(cluster.cluster).forEach(([, clusterKey]) => { + // eslint-disable-next-line no-param-reassign + clusterKey.x = clusterId; + }); + }); + }); + + const legendData = []; + runNames.forEach(key => { + legendData.push({ + name: key, }); }); return { data: clusters, - keys: clusterGraphKeys, - params, + paramKeys: legendData, }; }; @@ -89,8 +122,12 @@ export const generateSampleTable = response => { response.forEach(run => { const iterations = {}; - const id = run.aggregations.id.buckets[0].key; const primaryMetrics = new Set(); + + if (run.hits.hits.length === 0) { + return; + } + run.hits.hits.forEach(sample => { // eslint-disable-next-line no-underscore-dangle const source = sample._source; @@ -132,7 +169,7 @@ export const generateSampleTable = response => { samples: { ...(iterations[source.iteration.name] !== undefined && iterations[source.iteration.name].samples), - ...(sampleFields.closest_sample === sampleFields['@idx'] + 1 && { + ...(sampleFields.role === 'aggregate' && { [`${primaryMetric}-${sampleFields.name}`]: { sample: { ...sampleFields }, benchmark: _.omit({ ...benchmarkFields }, benchmarkBlacklist), @@ -143,6 +180,8 @@ export const generateSampleTable = response => { }; }); + const id = run.aggregations.id.buckets[0].key; + run.aggregations.id.buckets.forEach(runId => { const iterationColumnsData = [ {