Skip to content

Commit dd06bfc

Browse files
[ML] DF Analytics - auto-populate model_memory_limit (#50714)
* create modelMemoryLimit estimation endpoint. add value to form * add validation for model memory limit field * update jest tests * update validateModelMemoryLimitUnitsUtils to be more generic * add placeholder and validation with helpText to modelMemoryLimit field * update endpoint name to estimateDataFrameAnalyticsMemoryUsage for clarity * tweak modelMemoryLimitEmpty check in reducer * add tests for modelMemoryLimit validation
1 parent 10c158b commit dd06bfc

File tree

12 files changed

+212
-26
lines changed

12 files changed

+212
-26
lines changed

x-pack/legacy/plugins/ml/common/util/job_utils.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ export const ML_DATA_PREVIEW_COUNT: number;
3232

3333
export function isJobIdValid(jobId: string): boolean;
3434

35+
export function validateModelMemoryLimitUnits(
36+
modelMemoryLimit: string
37+
): { valid: boolean; messages: any[]; contains: () => boolean; find: () => void };
38+
3539
export function processCreatedBy(customSettings: { created_by?: string }): void;
3640

3741
export function mlFunctionToESAggregation(functionName: string): string | null;

x-pack/legacy/plugins/ml/common/util/job_utils.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -405,10 +405,11 @@ export function basicJobValidation(job, fields, limits, skipMmlChecks = false) {
405405

406406
if (skipMmlChecks === false) {
407407
// model memory limit
408+
const mml = job.analysis_limits && job.analysis_limits.model_memory_limit;
408409
const {
409410
messages: mmlUnitMessages,
410411
valid: mmlUnitValid,
411-
} = validateModelMemoryLimitUnits(job);
412+
} = validateModelMemoryLimitUnits(mml);
412413

413414
messages.push(...mmlUnitMessages);
414415
valid = (valid && mmlUnitValid);
@@ -494,12 +495,12 @@ export function validateModelMemoryLimit(job, limits) {
494495
};
495496
}
496497

497-
export function validateModelMemoryLimitUnits(job) {
498+
export function validateModelMemoryLimitUnits(modelMemoryLimit) {
498499
const messages = [];
499500
let valid = true;
500501

501-
if (typeof job.analysis_limits !== 'undefined' && typeof job.analysis_limits.model_memory_limit !== 'undefined') {
502-
const mml = job.analysis_limits.model_memory_limit.toUpperCase();
502+
if (modelMemoryLimit !== undefined) {
503+
const mml = modelMemoryLimit.toUpperCase();
503504
const mmlSplit = mml.match(/\d+(\w+)$/);
504505
const unit = (mmlSplit && mmlSplit.length === 2) ? mmlSplit[1] : null;
505506

x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ describe('Data Frame Analytics: <CreateAnalyticsForm />', () => {
4545
);
4646

4747
const euiFormRows = wrapper.find('EuiFormRow');
48-
expect(euiFormRows.length).toBe(6);
48+
expect(euiFormRows.length).toBe(7);
4949

5050
const row1 = euiFormRows.at(0);
5151
expect(row1.find('label').text()).toBe('Job type');

x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,21 @@ import { FormattedMessage } from '@kbn/i18n/react';
2121

2222
import { metadata } from 'ui/metadata';
2323
import { IndexPattern, INDEX_PATTERN_ILLEGAL_CHARACTERS } from 'ui/index_patterns';
24+
import { ml } from '../../../../../services/ml_api_service';
2425
import { Field, EVENT_RATE_FIELD_ID } from '../../../../../../common/types/fields';
2526

2627
import { newJobCapsService } from '../../../../../services/new_job_capabilities_service';
2728
import { useKibanaContext } from '../../../../../contexts/kibana';
2829
import { CreateAnalyticsFormProps } from '../../hooks/use_create_analytics_form';
29-
import { JOB_TYPES } from '../../hooks/use_create_analytics_form/state';
30+
import {
31+
JOB_TYPES,
32+
DEFAULT_MODEL_MEMORY_LIMIT,
33+
getJobConfigFromFormState,
34+
} from '../../hooks/use_create_analytics_form/state';
3035
import { JOB_ID_MAX_LENGTH } from '../../../../../../common/constants/validation';
3136
import { Messages } from './messages';
3237
import { JobType } from './job_type';
38+
import { mmlUnitInvalidErrorMessage } from '../../hooks/use_create_analytics_form/reducer';
3339

3440
// based on code used by `ui/index_patterns` internally
3541
// remove the space character from the list of illegal characters
@@ -73,6 +79,8 @@ export const CreateAnalyticsForm: FC<CreateAnalyticsFormProps> = ({ actions, sta
7379
jobIdInvalidMaxLength,
7480
jobType,
7581
loadingDepFieldOptions,
82+
modelMemoryLimit,
83+
modelMemoryLimitUnitValid,
7684
sourceIndex,
7785
sourceIndexNameEmpty,
7886
sourceIndexNameValid,
@@ -103,6 +111,25 @@ export const CreateAnalyticsForm: FC<CreateAnalyticsFormProps> = ({ actions, sta
103111
}
104112
};
105113

114+
const loadModelMemoryLimitEstimate = async () => {
115+
try {
116+
const jobConfig = getJobConfigFromFormState(form);
117+
delete jobConfig.dest;
118+
delete jobConfig.model_memory_limit;
119+
const resp = await ml.dataFrameAnalytics.estimateDataFrameAnalyticsMemoryUsage(jobConfig);
120+
setFormState({
121+
modelMemoryLimit: resp.expected_memory_without_disk,
122+
});
123+
} catch (e) {
124+
setFormState({
125+
modelMemoryLimit:
126+
jobType !== undefined
127+
? DEFAULT_MODEL_MEMORY_LIMIT[jobType]
128+
: DEFAULT_MODEL_MEMORY_LIMIT.outlier_detection,
129+
});
130+
}
131+
};
132+
106133
const loadDependentFieldOptions = async () => {
107134
setFormState({
108135
loadingDepFieldOptions: true,
@@ -175,6 +202,21 @@ export const CreateAnalyticsForm: FC<CreateAnalyticsFormProps> = ({ actions, sta
175202
}
176203
}, [sourceIndex, jobType, sourceIndexNameEmpty]);
177204

205+
useEffect(() => {
206+
const hasBasicRequiredFields =
207+
jobType !== undefined && sourceIndex !== '' && sourceIndexNameValid === true;
208+
209+
const hasRequiredAnalysisFields =
210+
(jobType === JOB_TYPES.REGRESSION &&
211+
dependentVariable !== '' &&
212+
trainingPercent !== undefined) ||
213+
jobType === JOB_TYPES.OUTLIER_DETECTION;
214+
215+
if (hasBasicRequiredFields && hasRequiredAnalysisFields) {
216+
loadModelMemoryLimitEstimate();
217+
}
218+
}, [jobType, sourceIndex, dependentVariable, trainingPercent]);
219+
178220
return (
179221
<EuiForm className="mlDataFrameAnalyticsCreateForm">
180222
<Messages messages={requestMessages} />
@@ -277,7 +319,7 @@ export const CreateAnalyticsForm: FC<CreateAnalyticsFormProps> = ({ actions, sta
277319
placeholder={i18n.translate(
278320
'xpack.ml.dataframe.analytics.create.sourceIndexPlaceholder',
279321
{
280-
defaultMessage: 'Choose a source index pattern or saved search.',
322+
defaultMessage: 'Choose a source index pattern.',
281323
}
282324
)}
283325
singleSelection={{ asPlainText: true }}
@@ -437,6 +479,24 @@ export const CreateAnalyticsForm: FC<CreateAnalyticsFormProps> = ({ actions, sta
437479
</EuiFormRow>
438480
</Fragment>
439481
)}
482+
<EuiFormRow
483+
label={i18n.translate('xpack.ml.dataframe.analytics.create.modelMemoryLimitLabel', {
484+
defaultMessage: 'Model memory limit',
485+
})}
486+
helpText={!modelMemoryLimitUnitValid && mmlUnitInvalidErrorMessage}
487+
>
488+
<EuiFieldText
489+
placeholder={
490+
jobType !== undefined
491+
? DEFAULT_MODEL_MEMORY_LIMIT[jobType]
492+
: DEFAULT_MODEL_MEMORY_LIMIT.outlier_detection
493+
}
494+
disabled={isJobCreated}
495+
value={modelMemoryLimit || ''}
496+
onChange={e => setFormState({ modelMemoryLimit: e.target.value })}
497+
isInvalid={modelMemoryLimit === ''}
498+
/>
499+
</EuiFormRow>
440500
<EuiFormRow
441501
isInvalid={createIndexPattern && destinationIndexPatternTitleExists}
442502
error={

x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.test.ts

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,13 @@ jest.mock('ui/index_patterns', () => ({
1818

1919
type SourceIndex = DataFrameAnalyticsConfig['source']['index'];
2020

21-
const getMockState = (index: SourceIndex) =>
21+
const getMockState = ({
22+
index,
23+
modelMemoryLimit,
24+
}: {
25+
index: SourceIndex;
26+
modelMemoryLimit?: string;
27+
}) =>
2228
merge(getInitialState(), {
2329
form: {
2430
jobIdEmpty: false,
@@ -30,6 +36,7 @@ const getMockState = (index: SourceIndex) =>
3036
source: { index },
3137
dest: { index: 'the-destination-index' },
3238
analysis: {},
39+
model_memory_limit: modelMemoryLimit,
3340
},
3441
});
3542

@@ -89,27 +96,50 @@ describe('useCreateAnalyticsForm', () => {
8996

9097
test('validateAdvancedEditor(): check index pattern variations', () => {
9198
// valid single index pattern
92-
expect(validateAdvancedEditor(getMockState('the-source-index')).isValid).toBe(true);
99+
expect(validateAdvancedEditor(getMockState({ index: 'the-source-index' })).isValid).toBe(true);
93100
// valid array with one ES index pattern
94-
expect(validateAdvancedEditor(getMockState(['the-source-index'])).isValid).toBe(true);
101+
expect(validateAdvancedEditor(getMockState({ index: ['the-source-index'] })).isValid).toBe(
102+
true
103+
);
95104
// valid array with two ES index patterns
96105
expect(
97-
validateAdvancedEditor(getMockState(['the-source-index-1', 'the-source-index-2'])).isValid
106+
validateAdvancedEditor(getMockState({ index: ['the-source-index-1', 'the-source-index-2'] }))
107+
.isValid
98108
).toBe(true);
99109
// invalid comma-separated index pattern, this is only allowed in the simple form
100110
// but not the advanced editor.
101111
expect(
102-
validateAdvancedEditor(getMockState('the-source-index-1,the-source-index-2')).isValid
112+
validateAdvancedEditor(getMockState({ index: 'the-source-index-1,the-source-index-2' }))
113+
.isValid
103114
).toBe(false);
104115
expect(
105116
validateAdvancedEditor(
106-
getMockState(['the-source-index-1,the-source-index-2', 'the-source-index-3'])
117+
getMockState({ index: ['the-source-index-1,the-source-index-2', 'the-source-index-3'] })
107118
).isValid
108119
).toBe(false);
109120
// invalid formats ("fake" TS casting to get valid TS and be able to run the tests)
110-
expect(validateAdvancedEditor(getMockState({} as SourceIndex)).isValid).toBe(false);
121+
expect(validateAdvancedEditor(getMockState({ index: {} as SourceIndex })).isValid).toBe(false);
111122
expect(
112-
validateAdvancedEditor(getMockState((undefined as unknown) as SourceIndex)).isValid
123+
validateAdvancedEditor(getMockState({ index: (undefined as unknown) as SourceIndex })).isValid
124+
).toBe(false);
125+
});
126+
127+
test('validateAdvancedEditor(): check model memory limit validation', () => {
128+
// valid model_memory_limit units
129+
expect(
130+
validateAdvancedEditor(getMockState({ index: 'the-source-index', modelMemoryLimit: '100mb' }))
131+
.isValid
132+
).toBe(true);
133+
// invalid model_memory_limit units
134+
expect(
135+
validateAdvancedEditor(
136+
getMockState({ index: 'the-source-index', modelMemoryLimit: '100bob' })
137+
).isValid
138+
).toBe(false);
139+
// invalid model_memory_limit if empty
140+
expect(
141+
validateAdvancedEditor(getMockState({ index: 'the-source-index', modelMemoryLimit: '' }))
142+
.isValid
113143
).toBe(false);
114144
});
115145
});

x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,29 @@ import { isValidIndexName } from '../../../../../../common/util/es_utils';
1313

1414
import { Action, ACTION } from './actions';
1515
import { getInitialState, getJobConfigFromFormState, State, JOB_TYPES } from './state';
16-
import { isJobIdValid } from '../../../../../../common/util/job_utils';
16+
import {
17+
isJobIdValid,
18+
validateModelMemoryLimitUnits,
19+
} from '../../../../../../common/util/job_utils';
1720
import { maxLengthValidator } from '../../../../../../common/util/validators';
18-
import { JOB_ID_MAX_LENGTH } from '../../../../../../common/constants/validation';
21+
import {
22+
JOB_ID_MAX_LENGTH,
23+
ALLOWED_DATA_UNITS,
24+
} from '../../../../../../common/constants/validation';
1925
import { getDependentVar, isRegressionAnalysis } from '../../../../common/analytics';
2026

27+
const mmlAllowedUnitsStr = `${ALLOWED_DATA_UNITS.slice(0, ALLOWED_DATA_UNITS.length - 1).join(
28+
', '
29+
)} or ${[...ALLOWED_DATA_UNITS].pop()}`;
30+
31+
export const mmlUnitInvalidErrorMessage = i18n.translate(
32+
'xpack.ml.dataframe.analytics.create.modelMemoryUnitsInvalidError',
33+
{
34+
defaultMessage: 'Model memory limit data unit unrecognized. It must be {str}',
35+
values: { str: mmlAllowedUnitsStr },
36+
}
37+
);
38+
2139
const getSourceIndexString = (state: State) => {
2240
const { jobConfig } = state;
2341

@@ -63,6 +81,12 @@ export const validateAdvancedEditor = (state: State): State => {
6381
const destinationIndexNameValid = isValidIndexName(destinationIndexName);
6482
const destinationIndexPatternTitleExists =
6583
state.indexPatternsMap[destinationIndexName] !== undefined;
84+
const mml = jobConfig.model_memory_limit;
85+
const modelMemoryLimitEmpty = mml === '';
86+
if (!modelMemoryLimitEmpty && mml !== undefined) {
87+
const { valid } = validateModelMemoryLimitUnits(mml);
88+
state.form.modelMemoryLimitUnitValid = valid;
89+
}
6690

6791
let dependentVariableEmpty = false;
6892
if (isRegressionAnalysis(jobConfig.analysis)) {
@@ -126,7 +150,27 @@ export const validateAdvancedEditor = (state: State): State => {
126150
});
127151
}
128152

153+
if (modelMemoryLimitEmpty) {
154+
state.advancedEditorMessages.push({
155+
error: i18n.translate(
156+
'xpack.ml.dataframe.analytics.create.advancedEditorMessage.modelMemoryLimitEmpty',
157+
{
158+
defaultMessage: 'The model memory limit field must not be empty.',
159+
}
160+
),
161+
message: '',
162+
});
163+
}
164+
165+
if (!state.form.modelMemoryLimitUnitValid) {
166+
state.advancedEditorMessages.push({
167+
error: mmlUnitInvalidErrorMessage,
168+
message: '',
169+
});
170+
}
171+
129172
state.isValid =
173+
state.form.modelMemoryLimitUnitValid &&
130174
!jobIdEmpty &&
131175
jobIdValid &&
132176
!jobIdExists &&
@@ -135,6 +179,7 @@ export const validateAdvancedEditor = (state: State): State => {
135179
!destinationIndexNameEmpty &&
136180
destinationIndexNameValid &&
137181
!dependentVariableEmpty &&
182+
!modelMemoryLimitEmpty &&
138183
(!destinationIndexPatternTitleExists || !createIndexPattern);
139184

140185
return state;
@@ -153,11 +198,19 @@ const validateForm = (state: State): State => {
153198
destinationIndexPatternTitleExists,
154199
createIndexPattern,
155200
dependentVariable,
201+
modelMemoryLimit,
156202
} = state.form;
157203

158204
const dependentVariableEmpty = jobType === JOB_TYPES.REGRESSION && dependentVariable === '';
205+
const modelMemoryLimitEmpty = modelMemoryLimit === '';
206+
207+
if (!modelMemoryLimitEmpty && modelMemoryLimit !== undefined) {
208+
const { valid } = validateModelMemoryLimitUnits(modelMemoryLimit);
209+
state.form.modelMemoryLimitUnitValid = valid;
210+
}
159211

160212
state.isValid =
213+
state.form.modelMemoryLimitUnitValid &&
161214
!jobIdEmpty &&
162215
jobIdValid &&
163216
!jobIdExists &&
@@ -166,6 +219,7 @@ const validateForm = (state: State): State => {
166219
!destinationIndexNameEmpty &&
167220
destinationIndexNameValid &&
168221
!dependentVariableEmpty &&
222+
!modelMemoryLimitEmpty &&
169223
(!destinationIndexPatternTitleExists || !createIndexPattern);
170224

171225
return state;

0 commit comments

Comments
 (0)