LogRowMessageDisplayedFields: increase rendering performance (#84407)

* getAllFields: refactor for improved performance

* LogRowMessageDisplayedFields: refactor line construction for performance

* AsyncIconButton: refactor to prevent infinite loops
This commit is contained in:
Matias Chomicki 2024-03-18 13:24:06 +01:00 committed by GitHub
parent a7c7a1ffed
commit 3e97999ac5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 78 additions and 69 deletions

View File

@ -1,7 +1,7 @@
import { css, cx } from '@emotion/css'; import { css, cx } from '@emotion/css';
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
import memoizeOne from 'memoize-one'; import memoizeOne from 'memoize-one';
import React, { PureComponent, useState } from 'react'; import React, { PureComponent, useEffect, useState } from 'react';
import { import {
CoreApp, CoreApp,
@ -290,7 +290,8 @@ class UnThemedLogDetailsRow extends PureComponent<Props, State> {
<AsyncIconButton <AsyncIconButton
name="search-plus" name="search-plus"
onClick={this.filterLabel} onClick={this.filterLabel}
isActive={this.isFilterLabelActive} // We purposely want to pass a new function on every render to allow the active state to be updated when log details remains open between updates.
isActive={() => this.isFilterLabelActive()}
tooltipSuffix={refIdTooltip} tooltipSuffix={refIdTooltip}
/> />
<IconButton <IconButton
@ -368,11 +369,9 @@ const AsyncIconButton = ({ isActive, tooltipSuffix, ...rest }: AsyncIconButtonPr
const [active, setActive] = useState(false); const [active, setActive] = useState(false);
const tooltip = active ? 'Remove filter' : 'Filter for value'; const tooltip = active ? 'Remove filter' : 'Filter for value';
/** useEffect(() => {
* We purposely want to run this on every render to allow the active state to be updated
* when log details remains open between updates.
*/
isActive().then(setActive); isActive().then(setActive);
}, [isActive]);
return <IconButton {...rest} variant={active ? 'primary' : undefined} tooltip={tooltip + tooltipSuffix} />; return <IconButton {...rest} variant={active ? 'primary' : undefined} tooltip={tooltip + tooltipSuffix} />;
}; };

View File

@ -25,32 +25,28 @@ export interface Props {
export const LogRowMessageDisplayedFields = React.memo((props: Props) => { export const LogRowMessageDisplayedFields = React.memo((props: Props) => {
const { row, detectedFields, getFieldLinks, wrapLogMessage, styles, mouseIsOver, pinned, ...rest } = props; const { row, detectedFields, getFieldLinks, wrapLogMessage, styles, mouseIsOver, pinned, ...rest } = props;
const fields = getAllFields(row, getFieldLinks);
const wrapClassName = wrapLogMessage ? '' : displayedFieldsStyles.noWrap; const wrapClassName = wrapLogMessage ? '' : displayedFieldsStyles.noWrap;
const fields = useMemo(() => getAllFields(row, getFieldLinks), [getFieldLinks, row]);
// only single key/value rows are filterable, so we only need the first field key for filtering // only single key/value rows are filterable, so we only need the first field key for filtering
const line = useMemo( const line = useMemo(() => {
() => let line = '';
detectedFields for (let i = 0; i < detectedFields.length; i++) {
.map((parsedKey) => { const parsedKey = detectedFields[i];
const field = fields.find((field) => { const field = fields.find((field) => {
const { keys } = field; const { keys } = field;
return keys[0] === parsedKey; return keys[0] === parsedKey;
}); });
if (field !== undefined && field !== null) { if (field) {
return `${parsedKey}=${field.values}`; line += ` ${parsedKey}=${field.values}`;
} }
if (row.labels[parsedKey] !== undefined && row.labels[parsedKey] !== null) { if (row.labels[parsedKey] !== undefined && row.labels[parsedKey] !== null) {
return `${parsedKey}=${row.labels[parsedKey]}`; line += ` ${parsedKey}=${row.labels[parsedKey]}`;
} }
}
return null; return line.trimStart();
}) }, [detectedFields, fields, row.labels]);
.filter((s) => s !== null)
.join(' '),
[detectedFields, fields, row.labels]
);
const shouldShowMenu = useMemo(() => mouseIsOver || pinned, [mouseIsOver, pinned]); const shouldShowMenu = useMemo(() => mouseIsOver || pinned, [mouseIsOver, pinned]);

View File

@ -1,5 +1,4 @@
import { partition } from 'lodash'; import { partition } from 'lodash';
import memoizeOne from 'memoize-one';
import { DataFrame, Field, FieldWithIndex, LinkModel, LogRowModel } from '@grafana/data'; import { DataFrame, Field, FieldWithIndex, LinkModel, LogRowModel } from '@grafana/data';
import { safeStringifyValue } from 'app/core/utils/explore'; import { safeStringifyValue } from 'app/core/utils/explore';
@ -18,8 +17,7 @@ export type FieldDef = {
* Returns all fields for log row which consists of fields we parse from the message itself and additional fields * Returns all fields for log row which consists of fields we parse from the message itself and additional fields
* found in the dataframe (they may contain links). * found in the dataframe (they may contain links).
*/ */
export const getAllFields = memoizeOne( export const getAllFields = (
(
row: LogRowModel, row: LogRowModel,
getFieldLinks?: ( getFieldLinks?: (
field: Field, field: Field,
@ -27,17 +25,14 @@ export const getAllFields = memoizeOne(
dataFrame: DataFrame dataFrame: DataFrame
) => Array<LinkModel<Field>> | ExploreFieldLinkModel[] ) => Array<LinkModel<Field>> | ExploreFieldLinkModel[]
) => { ) => {
const dataframeFields = getDataframeFields(row, getFieldLinks); return getDataframeFields(row, getFieldLinks);
};
return Object.values(dataframeFields);
}
);
/** /**
* A log line may contain many links that would all need to go on their own logs detail row * A log line may contain many links that would all need to go on their own logs detail row
* This iterates through and creates a FieldDef (row) per link. * This iterates through and creates a FieldDef (row) per link.
*/ */
export const createLogLineLinks = memoizeOne((hiddenFieldsWithLinks: FieldDef[]): FieldDef[] => { export const createLogLineLinks = (hiddenFieldsWithLinks: FieldDef[]): FieldDef[] => {
let fieldsWithLinksFromVariableMap: FieldDef[] = []; let fieldsWithLinksFromVariableMap: FieldDef[] = [];
hiddenFieldsWithLinks.forEach((linkField) => { hiddenFieldsWithLinks.forEach((linkField) => {
linkField.links?.forEach((link: ExploreFieldLinkModel) => { linkField.links?.forEach((link: ExploreFieldLinkModel) => {
@ -58,25 +53,21 @@ export const createLogLineLinks = memoizeOne((hiddenFieldsWithLinks: FieldDef[])
}); });
}); });
return fieldsWithLinksFromVariableMap; return fieldsWithLinksFromVariableMap;
}); };
/** /**
* creates fields from the dataframe-fields, adding data-links, when field.config.links exists * creates fields from the dataframe-fields, adding data-links, when field.config.links exists
*/ */
export const getDataframeFields = memoizeOne( export const getDataframeFields = (
(
row: LogRowModel, row: LogRowModel,
getFieldLinks?: (field: Field, rowIndex: number, dataFrame: DataFrame) => Array<LinkModel<Field>> getFieldLinks?: (field: Field, rowIndex: number, dataFrame: DataFrame) => Array<LinkModel<Field>>
): FieldDef[] => { ): FieldDef[] => {
const visibleFields = separateVisibleFields(row.dataFrame).visible; const nonEmptyVisibleFields = getNonEmptyVisibleFields(row);
const nonEmptyVisibleFields = visibleFields.filter((f) => f.values[row.rowIndex] != null);
return nonEmptyVisibleFields.map((field) => { return nonEmptyVisibleFields.map((field) => {
const links = getFieldLinks ? getFieldLinks(field, row.rowIndex, row.dataFrame) : []; const links = getFieldLinks ? getFieldLinks(field, row.rowIndex, row.dataFrame) : [];
const fieldVal = field.values[row.rowIndex]; const fieldVal = field.values[row.rowIndex];
const outputVal = const outputVal =
typeof fieldVal === 'string' || typeof fieldVal === 'number' typeof fieldVal === 'string' || typeof fieldVal === 'number' ? fieldVal.toString() : safeStringifyValue(fieldVal);
? fieldVal.toString()
: safeStringifyValue(fieldVal);
return { return {
keys: [field.name], keys: [field.name],
values: [outputVal], values: [outputVal],
@ -84,8 +75,7 @@ export const getDataframeFields = memoizeOne(
fieldIndex: field.index, fieldIndex: field.index,
}; };
}); });
} };
);
type VisOptions = { type VisOptions = {
keepTimestamp?: boolean; keepTimestamp?: boolean;
@ -148,3 +138,27 @@ export function separateVisibleFields(
return { visible, hidden }; return { visible, hidden };
} }
// Optimized version of separateVisibleFields() to only return visible fields for getAllFields()
function getNonEmptyVisibleFields(row: LogRowModel, opts?: VisOptions): FieldWithIndex[] {
const frame = row.dataFrame;
const visibleFieldIndices = getVisibleFieldIndices(frame, opts ?? {});
const visibleFields: FieldWithIndex[] = [];
for (let index = 0; index < frame.fields.length; index++) {
const field = frame.fields[index];
// ignore empty fields
if (field.values[row.rowIndex] == null) {
continue;
}
// hidden fields are always hidden
if (field.config.custom?.hidden) {
continue;
}
// fields with data-links are visible
if ((field.config.links && field.config.links.length > 0) || visibleFieldIndices.has(index)) {
visibleFields.push({ ...field, index });
}
}
return visibleFields;
}