Graphite: Limit number of suggestions displayed in Graphite drop downs (#42056) (#42231)

* Limit number of suggestions displayed in Graphite dropdowns

* Use limit API to reduce number of loaded tags for autocomplete

* Make tests more explicit

(cherry picked from commit 8725d3d7e0)

Co-authored-by: Piotr Jamróz <pm.jamroz@gmail.com>
This commit is contained in:
Grot (@grafanabot) 2021-11-24 14:51:16 -05:00 committed by GitHub
parent 0b0962ea13
commit 5bfe95499e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 91 additions and 9 deletions

View File

@ -3,7 +3,12 @@ import gfunc from '../gfunc';
import { TemplateSrvStub } from 'test/specs/helpers';
import { silenceConsoleOutput } from 'test/core/utils/silenceConsoleOutput';
import { actions } from '../state/actions';
import { getAltSegmentsSelectables, getTagsSelectables, getTagsAsSegmentsSelectables } from '../state/providers';
import {
getAltSegmentsSelectables,
getTagsSelectables,
getTagsAsSegmentsSelectables,
getTagValuesSelectables,
} from '../state/providers';
import { GraphiteSegment } from '../types';
import { createStore } from '../state/store';
@ -37,6 +42,7 @@ describe('Graphite actions', async () => {
waitForFuncDefsLoaded: jest.fn(() => Promise.resolve(null)),
createFuncInstance: gfunc.createFuncInstance,
getTagsAutoComplete: jest.fn().mockReturnValue(Promise.resolve([])),
getTagValuesAutoComplete: jest.fn().mockReturnValue(Promise.resolve([])),
},
target: { target: 'aliasByNode(scaleToSeconds(test.prod.*,1),2)' },
} as any;
@ -210,18 +216,23 @@ describe('Graphite actions', async () => {
});
});
it('current time range is passed when getting list of tags when editing', async () => {
it('current time range and limit is passed when getting list of tags when editing', async () => {
const currentRange = { from: 0, to: 1 };
ctx.state.range = currentRange;
await getTagsSelectables(ctx.state, 0, 'any');
expect(ctx.state.datasource.getTagsAutoComplete).toBeCalledWith([], 'any', { range: currentRange });
expect(ctx.state.datasource.getTagsAutoComplete).toBeCalledWith([], 'any', { range: currentRange, limit: 5000 });
});
it('current time range is passed when getting list of tags for adding', async () => {
it('current time range and limit is passed when getting list of tags for adding', async () => {
const currentRange = { from: 0, to: 1 };
ctx.state.range = currentRange;
await getTagsAsSegmentsSelectables(ctx.state, 'any');
expect(ctx.state.datasource.getTagsAutoComplete).toBeCalledWith([], 'any', { range: currentRange });
expect(ctx.state.datasource.getTagsAutoComplete).toBeCalledWith([], 'any', { range: currentRange, limit: 5000 });
});
it('limit is passed when getting list of tag values', async () => {
await getTagValuesSelectables(ctx.state, { key: 'key', operator: '=', value: 'value' }, 1, 'test');
expect(ctx.state.datasource.getTagValuesAutoComplete).toBeCalledWith([], 'key', 'test', { limit: 5000 });
});
describe('when autocomplete for metric names is not available', () => {
@ -472,4 +483,46 @@ describe('Graphite actions', async () => {
expect(ctx.state.target.target).toEqual(expected);
});
});
describe('when auto-completing over a large set of tags and metrics', () => {
const manyMetrics: Array<{ text: string }> = [],
max = 20000;
beforeEach(() => {
for (let i = 0; i < max; i++) {
manyMetrics.push({ text: `metric${i}` });
}
ctx.state.datasource.metricFindQuery = jest.fn().mockReturnValue(Promise.resolve(manyMetrics));
ctx.state.datasource.getTagsAutoComplete = jest.fn((_tag, _prefix, { limit }) => {
const tags = [];
for (let i = 0; i < limit; i++) {
tags.push({ text: `tag${i}` });
}
return tags;
});
});
it('uses limited metrics and tags list', async () => {
ctx.state.supportsTags = true;
const segments = await getAltSegmentsSelectables(ctx.state, 0, '');
expect(segments).toHaveLength(10000);
expect(segments[0].value!.value).toBe('*'); // * - is a fixed metric name, always added at the top
expect(segments[4999].value!.value).toBe('metric4998');
expect(segments[5000].value!.value).toBe('tag: tag0');
expect(segments[9999].value!.value).toBe('tag: tag4999');
});
it('uses correct limit for metrics and tags list when tags are not supported', async () => {
ctx.state.supportsTags = false;
const segments = await getAltSegmentsSelectables(ctx.state, 0, '');
expect(segments).toHaveLength(5000);
expect(segments[0].value!.value).toBe('*'); // * - is a fixed metric name, always added at the top
expect(segments[4999].value!.value).toBe('metric4998');
});
it('uses limited metrics when adding more metrics', async () => {
const segments = await getAltSegmentsSelectables(ctx.state, 1, '');
expect(segments).toHaveLength(5000);
});
});
});

View File

@ -10,6 +10,15 @@ import { GraphiteSegment, GraphiteTag, GraphiteTagOperator } from '../types';
import { mapSegmentsToSelectables, mapStringsToSelectables } from '../components/helpers';
import { SelectableValue } from '@grafana/data';
/**
* All auto-complete lists are updated while typing. To avoid performance issues we do not render more
* than MAX_SUGGESTIONS limits in a single dropdown.
*
* MAX_SUGGESTIONS is per metrics and tags separately. On the very first dropdown where metrics and tags are
* combined together meaning it may end up with max of 2 * MAX_SUGGESTIONS items in total.
*/
const MAX_SUGGESTIONS = 5000;
/**
* Providers are hooks for views to provide temporal data for autocomplete. They don't modify the state.
*/
@ -72,8 +81,10 @@ async function getAltSegments(
});
});
// add wildcard option
// add wildcard option and limit number of suggestions (API doesn't support limiting
// hence we are doing it here)
altSegments.unshift({ value: '*', expandable: true });
altSegments.splice(MAX_SUGGESTIONS);
if (state.supportsTags && index === 0) {
removeTaggedEntry(altSegments);
@ -88,6 +99,10 @@ async function getAltSegments(
return [];
}
/**
* Get the list of segments with tags and metrics. Suggestions are reduced in getAltSegments and addAltTagSegments so in case
* we hit MAX_SUGGESTIONS limit there are always some tags and metrics shown.
*/
export async function getAltSegmentsSelectables(
state: GraphiteQueryEditorState,
index: number,
@ -106,7 +121,10 @@ export function getTagOperatorsSelectables(): Array<SelectableValue<GraphiteTagO
async function getTags(state: GraphiteQueryEditorState, index: number, tagPrefix: string): Promise<string[]> {
try {
const tagExpressions = state.queryModel.renderTagExpressions(index);
const values = await state.datasource.getTagsAutoComplete(tagExpressions, tagPrefix, { range: state.range });
const values = await state.datasource.getTagsAutoComplete(tagExpressions, tagPrefix, {
range: state.range,
limit: MAX_SUGGESTIONS,
});
const altTags = map(values, 'text');
altTags.splice(0, 0, state.removeTagValue);
@ -134,7 +152,10 @@ async function getTagsAsSegments(state: GraphiteQueryEditorState, tagPrefix: str
let tagsAsSegments: GraphiteSegment[];
try {
const tagExpressions = state.queryModel.renderTagExpressions();
const values = await state.datasource.getTagsAutoComplete(tagExpressions, tagPrefix, { range: state.range });
const values = await state.datasource.getTagsAutoComplete(tagExpressions, tagPrefix, {
range: state.range,
limit: MAX_SUGGESTIONS,
});
tagsAsSegments = map(values, (val) => {
return {
value: val.text,
@ -150,6 +171,9 @@ async function getTagsAsSegments(state: GraphiteQueryEditorState, tagPrefix: str
return tagsAsSegments;
}
/**
* Get list of tags, used when adding additional tags (first tag is selected from a joined list of metrics and tags)
*/
export async function getTagsAsSegmentsSelectables(
state: GraphiteQueryEditorState,
tagPrefix: string
@ -165,7 +189,9 @@ async function getTagValues(
): Promise<string[]> {
const tagExpressions = state.queryModel.renderTagExpressions(index);
const tagKey = tag.key;
const values = await state.datasource.getTagValuesAutoComplete(tagExpressions, tagKey, valuePrefix, {});
const values = await state.datasource.getTagValuesAutoComplete(tagExpressions, tagKey, valuePrefix, {
limit: MAX_SUGGESTIONS,
});
const altValues = map(values, 'text');
// Add template variables as additional values
eachRight(state.templateSrv.getVariables(), (variable) => {

View File

@ -150,6 +150,9 @@ export class ContextSrvStub {
export function TemplateSrvStub(this: any) {
this.variables = [];
this.getVariables = function () {
return this.variables;
};
this.templateSettings = { interpolate: /\[\[([\s\S]+?)\]\]/g };
this.data = {};
this.replace = (text: string) => {