Skip to content

Commit

Permalink
[ML] Fixing wizard charts repeated loading (#44409)
Browse files Browse the repository at this point in the history
* [ML] Caching wizard chart data

* using shared chart settings hook

* fixing settings hook and switching to memoize-one

* removing custom hook
  • Loading branch information
jgowdyelastic committed Sep 2, 2019
1 parent 788b23d commit a3c2376
Show file tree
Hide file tree
Showing 17 changed files with 609 additions and 257 deletions.
Expand Up @@ -35,7 +35,6 @@ import { useKibanaContext, SavedSearchQuery } from '../../contexts/kibana';
import { kbnTypeToMLJobType } from '../../util/field_types_utils';
// @ts-ignore
import { timeBasedIndexCheck } from '../../util/index_utils';
// @ts-ignore
import { MlTimeBuckets } from '../../util/ml_time_buckets';
import { FieldRequestConfig, FieldVisConfig } from './common';
import { ActionsPanel } from './components/actions_panel';
Expand Down
Expand Up @@ -4,13 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { SavedSearch } from 'src/legacy/core_plugins/kibana/public/discover/types';
import memoizeOne from 'memoize-one';
import { isEqual } from 'lodash';
import { IndexPattern } from 'ui/index_patterns';
import { IndexPatternTitle } from '../../../../../common/types/kibana';
import { Field, SplitField, AggFieldPair } from '../../../../../common/types/fields';
import { ml } from '../../../../services/ml_api_service';
import { mlResultsService } from '../../../../services/results_service';
import { getCategoryFields } from './searches';
import { getCategoryFields as getCategoryFieldsOrig } from './searches';

type DetectorIndex = number;
export interface LineChartPoint {
Expand All @@ -20,16 +21,19 @@ export interface LineChartPoint {
type SplitFieldValue = string | null;
export type LineChartData = Record<DetectorIndex, LineChartPoint[]>;

const eq = (newArgs: any[], lastArgs: any[]) => isEqual(newArgs, lastArgs);

const newJobLineChart = memoizeOne(ml.jobs.newJobLineChart, eq);
const newJobPopulationsChart = memoizeOne(ml.jobs.newJobPopulationsChart, eq);
const getEventRateData = memoizeOne(mlResultsService.getEventRateData, eq);
const getCategoryFields = memoizeOne(getCategoryFieldsOrig, eq);

export class ChartLoader {
protected _indexPattern: IndexPattern;
protected _savedSearch: SavedSearch;
protected _indexPatternTitle: IndexPatternTitle = '';
protected _timeFieldName: string = '';
protected _query: object = {};

constructor(indexPattern: IndexPattern, savedSearch: SavedSearch, query: object) {
this._indexPattern = indexPattern;
this._savedSearch = savedSearch;
private _indexPatternTitle: IndexPatternTitle = '';
private _timeFieldName: string = '';
private _query: object = {};

constructor(indexPattern: IndexPattern, query: object) {
this._indexPatternTitle = indexPattern.title;
this._query = query;

Expand All @@ -49,7 +53,7 @@ export class ChartLoader {
if (this._timeFieldName !== '') {
const splitFieldName = splitField !== null ? splitField.name : null;

const resp = await ml.jobs.newJobLineChart(
const resp = await newJobLineChart(
this._indexPatternTitle,
this._timeFieldName,
start,
Expand All @@ -60,6 +64,9 @@ export class ChartLoader {
splitFieldName,
splitFieldValue
);
if (resp.error !== undefined) {
throw resp.error;
}
return resp.results;
}
return {};
Expand All @@ -75,7 +82,7 @@ export class ChartLoader {
if (this._timeFieldName !== '') {
const splitFieldName = splitField !== null ? splitField.name : '';

const resp = await ml.jobs.newJobPopulationsChart(
const resp = await newJobPopulationsChart(
this._indexPatternTitle,
this._timeFieldName,
start,
Expand All @@ -85,6 +92,9 @@ export class ChartLoader {
aggFieldPairs.map(getAggFieldPairNames),
splitFieldName
);
if (resp.error !== undefined) {
throw resp.error;
}
return resp.results;
}
return {};
Expand All @@ -96,14 +106,18 @@ export class ChartLoader {
intervalMs: number
): Promise<LineChartPoint[]> {
if (this._timeFieldName !== '') {
const resp = await mlResultsService.getEventRateData(
const resp = await getEventRateData(
this._indexPatternTitle,
this._query,
this._timeFieldName,
start,
end,
intervalMs * 3
);
if (resp.error !== undefined) {
throw resp.error;
}

return Object.entries(resp.results).map(([time, value]) => ({
time: +time,
value: value as number,
Expand Down
Expand Up @@ -7,6 +7,14 @@
import chrome from 'ui/chrome';
import darkTheme from '@elastic/eui/dist/eui_theme_dark.json';
import lightTheme from '@elastic/eui/dist/eui_theme_light.json';
import {
SingleMetricJobCreator,
MultiMetricJobCreator,
PopulationJobCreator,
isMultiMetricJobCreator,
isPopulationJobCreator,
} from '../../../../common/job_creator';
import { MlTimeBuckets } from '../../../../../../util/ml_time_buckets';

const IS_DARK_THEME = chrome.getUiSettingsClient().get('theme:darkMode');
const themeName = IS_DARK_THEME ? darkTheme : lightTheme;
Expand Down Expand Up @@ -50,3 +58,38 @@ export const seriesStyle = {
visible: false,
},
};

export function getChartSettings(
jobCreator: SingleMetricJobCreator | MultiMetricJobCreator | PopulationJobCreator,
chartInterval: MlTimeBuckets
) {
const cs = {
...defaultChartSettings,
intervalMs: chartInterval.getInterval().asMilliseconds(),
};

if (isPopulationJobCreator(jobCreator)) {
// for population charts, use a larger interval based on
// the calculation from MlTimeBuckets, but without the
// bar target and max bars which have been set for the
// general chartInterval
const interval = new MlTimeBuckets();
interval.setInterval('auto');
interval.setBounds(chartInterval.getBounds());
cs.intervalMs = interval.getInterval().asMilliseconds();
}

if (isMultiMetricJobCreator(jobCreator) || isPopulationJobCreator(jobCreator)) {
if (jobCreator.aggFieldPairs.length > 2 && isMultiMetricJobCreator(jobCreator)) {
cs.cols = 3;
cs.height = '150px';
cs.intervalMs = cs.intervalMs * 3;
} else if (jobCreator.aggFieldPairs.length > 1) {
cs.cols = 2;
cs.height = '200px';
cs.intervalMs = cs.intervalMs * 2;
}
}

return cs;
}
Expand Up @@ -8,29 +8,26 @@ import React, { Fragment, FC, useContext, useEffect, useState } from 'react';

import { JobCreatorContext } from '../../../job_creator_context';
import { MultiMetricJobCreator, isMultiMetricJobCreator } from '../../../../../common/job_creator';
import { Results, ModelItem, Anomaly } from '../../../../../common/results_loader';
import { LineChartData } from '../../../../../common/chart_loader';
import { DropDownLabel, DropDownProps } from '../agg_select';
import { newJobCapsService } from '../../../../../../../services/new_job_capabilities_service';
import { AggFieldPair } from '../../../../../../../../common/types/fields';
import { defaultChartSettings, ChartSettings } from '../../../charts/common/settings';
import { getChartSettings, defaultChartSettings } from '../../../charts/common/settings';
import { MetricSelector } from './metric_selector';
import { ChartGrid } from './chart_grid';
import { mlMessageBarService } from '../../../../../../../components/messagebar/messagebar_service';

interface Props {
isActive: boolean;
setIsValid: (na: boolean) => void;
}

export const MultiMetricDetectors: FC<Props> = ({ isActive, setIsValid }) => {
export const MultiMetricDetectors: FC<Props> = ({ setIsValid }) => {
const {
jobCreator: jc,
jobCreatorUpdate,
jobCreatorUpdated,
chartLoader,
chartInterval,
resultsLoader,
} = useContext(JobCreatorContext);

if (isMultiMetricJobCreator(jc) === false) {
Expand All @@ -45,14 +42,12 @@ export const MultiMetricDetectors: FC<Props> = ({ isActive, setIsValid }) => {
);
const [lineChartsData, setLineChartsData] = useState<LineChartData>({});
const [loadingData, setLoadingData] = useState(false);
const [modelData, setModelData] = useState<Record<number, ModelItem[]>>([]);
const [anomalyData, setAnomalyData] = useState<Record<number, Anomaly[]>>([]);
const [start, setStart] = useState(jobCreator.start);
const [end, setEnd] = useState(jobCreator.end);

const [chartSettings, setChartSettings] = useState(defaultChartSettings);
const [splitField, setSplitField] = useState(jobCreator.splitField);
const [fieldValues, setFieldValues] = useState<string[]>([]);
const [pageReady, setPageReady] = useState(false);

function detectorChangeHandler(selectedOptionsIn: DropDownLabel[]) {
addDetector(selectedOptionsIn);
Expand All @@ -76,17 +71,8 @@ export const MultiMetricDetectors: FC<Props> = ({ isActive, setIsValid }) => {
setAggFieldPairList([...aggFieldPairList]);
}

function setResultsWrapper(results: Results) {
setModelData(results.model);
setAnomalyData(results.anomalies);
}

useEffect(() => {
// subscribe to progress and results
const subscription = resultsLoader.subscribeToResults(setResultsWrapper);
return () => {
subscription.unsubscribe();
};
setPageReady(true);
}, []);

// watch for changes in detector list length
Expand Down Expand Up @@ -134,30 +120,12 @@ export const MultiMetricDetectors: FC<Props> = ({ isActive, setIsValid }) => {
loadCharts();
}, [fieldValues]);

function getChartSettings(): ChartSettings {
const cs = {
...defaultChartSettings,
intervalMs: chartInterval.getInterval().asMilliseconds(),
};
if (aggFieldPairList.length > 2) {
cs.cols = 3;
cs.height = '150px';
cs.intervalMs = cs.intervalMs * 3;
} else if (aggFieldPairList.length > 1) {
cs.cols = 2;
cs.height = '200px';
cs.intervalMs = cs.intervalMs * 2;
}
return cs;
}

async function loadCharts() {
const cs = getChartSettings();
setChartSettings(cs);

if (aggFieldPairList.length > 0) {
if (allDataReady()) {
setLoadingData(true);
try {
const cs = getChartSettings(jobCreator, chartInterval);
setChartSettings(cs);
const resp: LineChartData = await chartLoader.loadLineCharts(
jobCreator.start,
jobCreator.end,
Expand All @@ -175,6 +143,14 @@ export const MultiMetricDetectors: FC<Props> = ({ isActive, setIsValid }) => {
}
}

function allDataReady() {
return (
pageReady &&
aggFieldPairList.length > 0 &&
(splitField === null || (splitField !== null && fieldValues.length > 0))
);
}

return (
<Fragment>
<ChartGrid
Expand All @@ -183,21 +159,19 @@ export const MultiMetricDetectors: FC<Props> = ({ isActive, setIsValid }) => {
splitField={splitField}
fieldValues={fieldValues}
lineChartsData={lineChartsData}
modelData={modelData}
anomalyData={anomalyData}
deleteDetector={isActive ? deleteDetector : undefined}
modelData={[]}
anomalyData={[]}
deleteDetector={deleteDetector}
jobType={jobCreator.type}
loading={loadingData}
/>

{isActive && (
<MetricSelector
fields={fields}
detectorChangeHandler={detectorChangeHandler}
selectedOptions={selectedOptions}
removeOptions={aggFieldPairList}
/>
)}
<MetricSelector
fields={fields}
detectorChangeHandler={detectorChangeHandler}
selectedOptions={selectedOptions}
removeOptions={aggFieldPairList}
/>
</Fragment>
);
};

0 comments on commit a3c2376

Please sign in to comment.