From 3e97999ac5a3270798a4fe330557ca49b5eba654 Mon Sep 17 00:00:00 2001 From: Matias Chomicki Date: Mon, 18 Mar 2024 13:24:06 +0100 Subject: [PATCH] LogRowMessageDisplayedFields: increase rendering performance (#84407) * getAllFields: refactor for improved performance * LogRowMessageDisplayedFields: refactor line construction for performance * AsyncIconButton: refactor to prevent infinite loops --- .../logs/components/LogDetailsRow.tsx | 13 ++- .../LogRowMessageDisplayedFields.tsx | 40 ++++---- .../app/features/logs/components/logParser.ts | 94 +++++++++++-------- 3 files changed, 78 insertions(+), 69 deletions(-) diff --git a/public/app/features/logs/components/LogDetailsRow.tsx b/public/app/features/logs/components/LogDetailsRow.tsx index ac1ef650e5e..37fab48ce9b 100644 --- a/public/app/features/logs/components/LogDetailsRow.tsx +++ b/public/app/features/logs/components/LogDetailsRow.tsx @@ -1,7 +1,7 @@ import { css, cx } from '@emotion/css'; import { isEqual } from 'lodash'; import memoizeOne from 'memoize-one'; -import React, { PureComponent, useState } from 'react'; +import React, { PureComponent, useEffect, useState } from 'react'; import { CoreApp, @@ -290,7 +290,8 @@ class UnThemedLogDetailsRow extends PureComponent { this.isFilterLabelActive()} tooltipSuffix={refIdTooltip} /> { + isActive().then(setActive); + }, [isActive]); return ; }; diff --git a/public/app/features/logs/components/LogRowMessageDisplayedFields.tsx b/public/app/features/logs/components/LogRowMessageDisplayedFields.tsx index 2a9d154b1e8..18837d92929 100644 --- a/public/app/features/logs/components/LogRowMessageDisplayedFields.tsx +++ b/public/app/features/logs/components/LogRowMessageDisplayedFields.tsx @@ -25,32 +25,28 @@ export interface Props { export const LogRowMessageDisplayedFields = React.memo((props: Props) => { const { row, detectedFields, getFieldLinks, wrapLogMessage, styles, mouseIsOver, pinned, ...rest } = props; - const fields = getAllFields(row, getFieldLinks); 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 - const line = useMemo( - () => - detectedFields - .map((parsedKey) => { - const field = fields.find((field) => { - const { keys } = field; - return keys[0] === parsedKey; - }); + const line = useMemo(() => { + let line = ''; + for (let i = 0; i < detectedFields.length; i++) { + const parsedKey = detectedFields[i]; + const field = fields.find((field) => { + const { keys } = field; + return keys[0] === parsedKey; + }); - if (field !== undefined && field !== null) { - return `${parsedKey}=${field.values}`; - } + if (field) { + line += ` ${parsedKey}=${field.values}`; + } - if (row.labels[parsedKey] !== undefined && row.labels[parsedKey] !== null) { - return `${parsedKey}=${row.labels[parsedKey]}`; - } - - return null; - }) - .filter((s) => s !== null) - .join(' '), - [detectedFields, fields, row.labels] - ); + if (row.labels[parsedKey] !== undefined && row.labels[parsedKey] !== null) { + line += ` ${parsedKey}=${row.labels[parsedKey]}`; + } + } + return line.trimStart(); + }, [detectedFields, fields, row.labels]); const shouldShowMenu = useMemo(() => mouseIsOver || pinned, [mouseIsOver, pinned]); diff --git a/public/app/features/logs/components/logParser.ts b/public/app/features/logs/components/logParser.ts index 7e541ca3e53..6f412f070f1 100644 --- a/public/app/features/logs/components/logParser.ts +++ b/public/app/features/logs/components/logParser.ts @@ -1,5 +1,4 @@ import { partition } from 'lodash'; -import memoizeOne from 'memoize-one'; import { DataFrame, Field, FieldWithIndex, LinkModel, LogRowModel } from '@grafana/data'; import { safeStringifyValue } from 'app/core/utils/explore'; @@ -18,26 +17,22 @@ export type FieldDef = { * 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). */ -export const getAllFields = memoizeOne( - ( - row: LogRowModel, - getFieldLinks?: ( - field: Field, - rowIndex: number, - dataFrame: DataFrame - ) => Array> | ExploreFieldLinkModel[] - ) => { - const dataframeFields = getDataframeFields(row, getFieldLinks); - - return Object.values(dataframeFields); - } -); +export const getAllFields = ( + row: LogRowModel, + getFieldLinks?: ( + field: Field, + rowIndex: number, + dataFrame: DataFrame + ) => Array> | ExploreFieldLinkModel[] +) => { + return getDataframeFields(row, getFieldLinks); +}; /** * 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. */ -export const createLogLineLinks = memoizeOne((hiddenFieldsWithLinks: FieldDef[]): FieldDef[] => { +export const createLogLineLinks = (hiddenFieldsWithLinks: FieldDef[]): FieldDef[] => { let fieldsWithLinksFromVariableMap: FieldDef[] = []; hiddenFieldsWithLinks.forEach((linkField) => { linkField.links?.forEach((link: ExploreFieldLinkModel) => { @@ -58,34 +53,29 @@ export const createLogLineLinks = memoizeOne((hiddenFieldsWithLinks: FieldDef[]) }); }); return fieldsWithLinksFromVariableMap; -}); +}; /** * creates fields from the dataframe-fields, adding data-links, when field.config.links exists */ -export const getDataframeFields = memoizeOne( - ( - row: LogRowModel, - getFieldLinks?: (field: Field, rowIndex: number, dataFrame: DataFrame) => Array> - ): FieldDef[] => { - const visibleFields = separateVisibleFields(row.dataFrame).visible; - const nonEmptyVisibleFields = visibleFields.filter((f) => f.values[row.rowIndex] != null); - return nonEmptyVisibleFields.map((field) => { - const links = getFieldLinks ? getFieldLinks(field, row.rowIndex, row.dataFrame) : []; - const fieldVal = field.values[row.rowIndex]; - const outputVal = - typeof fieldVal === 'string' || typeof fieldVal === 'number' - ? fieldVal.toString() - : safeStringifyValue(fieldVal); - return { - keys: [field.name], - values: [outputVal], - links: links, - fieldIndex: field.index, - }; - }); - } -); +export const getDataframeFields = ( + row: LogRowModel, + getFieldLinks?: (field: Field, rowIndex: number, dataFrame: DataFrame) => Array> +): FieldDef[] => { + const nonEmptyVisibleFields = getNonEmptyVisibleFields(row); + return nonEmptyVisibleFields.map((field) => { + const links = getFieldLinks ? getFieldLinks(field, row.rowIndex, row.dataFrame) : []; + const fieldVal = field.values[row.rowIndex]; + const outputVal = + typeof fieldVal === 'string' || typeof fieldVal === 'number' ? fieldVal.toString() : safeStringifyValue(fieldVal); + return { + keys: [field.name], + values: [outputVal], + links: links, + fieldIndex: field.index, + }; + }); +}; type VisOptions = { keepTimestamp?: boolean; @@ -148,3 +138,27 @@ export function separateVisibleFields( 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; +}