[Profiling] Flamegraph legend (#147910)

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Closes https://github.com/elastic/prodfiler/issues/2810
This commit is contained in:
Dario Gieselaar 2023-01-16 14:45:54 +01:00 committed by GitHub
parent ed9987592e
commit 08d85554b9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 252 additions and 59 deletions

View file

@ -0,0 +1,91 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
import React from 'react';
import { i18n } from '@kbn/i18n';
import { asPercentage } from '../../utils/formatters/as_percentage';
import { Legend, LegendItem } from '../legend';
export function FlameGraphLegend({
legendItems,
asScale,
}: {
legendItems: LegendItem[];
asScale: boolean;
}) {
if (asScale) {
return (
<EuiFlexGroup direction="column" alignItems="flexStart">
<EuiFlexItem grow={false}>
<EuiFlexGroup direction="column" gutterSize="s">
<EuiFlexItem grow={false}>
<EuiFlexGroup direction="row">
<EuiFlexItem>
<EuiText textAlign="center" size="s">
{i18n.translate('xpack.profiling.flameGraphLegend.improvement', {
defaultMessage: 'Improvement',
})}
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
<EuiText textAlign="center" size="s">
{i18n.translate('xpack.profiling.flameGraphLegend.regression', {
defaultMessage: 'Regression',
})}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup direction="row">
<EuiFlexItem grow={false}>
<EuiFlexGroup direction="row" gutterSize="s">
<EuiFlexItem grow={false}>
<EuiText size="s">+{asPercentage(1)}</EuiText>
</EuiFlexItem>
<EuiFlexItem style={{ width: legendItems.length * 20 }}>
<EuiFlexGroup direction="row" gutterSize="none">
{legendItems.map(({ color, label }) => {
return (
<EuiFlexItem
style={{ backgroundColor: color, justifyContent: 'center' }}
>
{label ? (
<EuiText
size="xs"
style={{
verticalAlign: 'center',
whiteSpace: 'nowrap',
paddingLeft: 8,
paddingRight: 8,
}}
>
{label}
</EuiText>
) : (
''
)}
</EuiFlexItem>
);
})}
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="s">{asPercentage(-1)}</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
);
}
return <Legend legendItems={legendItems} />;
}

View file

@ -24,6 +24,7 @@ import { ElasticFlameGraph, FlameGraphComparisonMode } from '../../common/flameg
import { asPercentage } from '../utils/formatters/as_percentage';
import { getFlamegraphModel } from '../utils/get_flamegraph_model';
import { FlamegraphInformationWindow } from './flame_graphs_view/flamegraph_information_window';
import { FlameGraphLegend } from './flame_graphs_view/flame_graph_legend';
function TooltipRow({
value,
@ -321,6 +322,9 @@ export const FlameGraph: React.FC<FlameGraphProps> = ({
) : undefined}
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<FlameGraphLegend legendItems={columnarData.legendItems} asScale={!!comparisonFlamegraph} />
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -0,0 +1,34 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText } from '@elastic/eui';
import React from 'react';
export interface LegendItem {
color: string;
label: string;
}
export function Legend({ legendItems }: { legendItems: LegendItem[] }) {
return (
<EuiFlexGroup direction="row" gutterSize="m">
{legendItems.map(({ color, label }) => {
return (
<EuiFlexItem key={label} grow={false}>
<EuiFlexGroup direction="row" gutterSize="xs">
<EuiFlexItem grow={false}>
<EuiIcon type="dot" color={color} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="xs">{label}</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
);
})}
</EuiFlexGroup>
);
}

View file

@ -4,10 +4,14 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { ColumnarViewModel } from '@elastic/charts';
import d3 from 'd3';
import { sum, uniqueId } from 'lodash';
import { createColumnarViewModel, rgbToRGBA } from '../../../common/columnar_view_model';
import { compact, sum, uniqueId, range } from 'lodash';
import { i18n } from '@kbn/i18n';
import { createColumnarViewModel } from '../../../common/columnar_view_model';
import { ElasticFlameGraph, FlameGraphComparisonMode } from '../../../common/flamegraph';
import { FRAME_TYPE_COLOR_MAP, rgbToRGBA } from '../../../common/frame_type_colors';
import { describeFrameType, FrameType } from '../../../common/profiling';
import { getInterpolationValue } from './get_interpolation_value';
const nullColumnarViewModel = {
@ -34,17 +38,72 @@ export function getFlamegraphModel({
colorDanger: string;
colorNeutral: string;
comparisonMode: FlameGraphComparisonMode;
}) {
}): {
key: string;
viewModel: ColumnarViewModel;
comparisonNodesById: Record<string, { CountInclusive: number; CountExclusive: number }>;
legendItems: Array<{ label: string; color: string }>;
} {
const comparisonNodesById: Record<string, { CountInclusive: number; CountExclusive: number }> =
{};
if (!primaryFlamegraph || !primaryFlamegraph.Label || primaryFlamegraph.Label.length === 0) {
return { key: uniqueId(), viewModel: nullColumnarViewModel, comparisonNodesById };
return {
key: uniqueId(),
viewModel: nullColumnarViewModel,
comparisonNodesById,
legendItems: [],
};
}
const viewModel = createColumnarViewModel(primaryFlamegraph, comparisonFlamegraph === undefined);
if (comparisonFlamegraph) {
let legendItems: Array<{ label: string; color: string }>;
if (!comparisonFlamegraph) {
const usedFrameTypes = new Set([...primaryFlamegraph.FrameType]);
legendItems = compact(
Object.entries(FRAME_TYPE_COLOR_MAP).map(([frameTypeKey, colors]) => {
const frameType = Number(frameTypeKey) as FrameType;
return usedFrameTypes.has(frameType)
? {
color: `#${colors[0].toString(16)}`,
label: describeFrameType(frameType),
}
: undefined;
})
);
} else {
const positiveChangeInterpolator = d3.interpolateRgb(colorNeutral, colorSuccess);
const negativeChangeInterpolator = d3.interpolateRgb(colorNeutral, colorDanger);
function getColor(interpolationValue: number) {
const nodeColor =
interpolationValue >= 0
? positiveChangeInterpolator(interpolationValue)
: negativeChangeInterpolator(Math.abs(interpolationValue));
return nodeColor;
}
legendItems = range(1, -1, -0.2)
.concat(-1)
.map((value) => {
const rounded = Math.round(value * 100) / 100;
const color = getColor(rounded);
return {
color,
label:
rounded === 0
? i18n.translate('xpack.profiling.flamegraphModel.noChange', {
defaultMessage: 'No change',
})
: '',
};
});
comparisonFlamegraph.ID.forEach((nodeID, index) => {
comparisonNodesById[nodeID] = {
CountInclusive: comparisonFlamegraph.CountInclusive[index],
@ -52,10 +111,6 @@ export function getFlamegraphModel({
};
});
const positiveChangeInterpolator = d3.interpolateRgb(colorNeutral, colorSuccess);
const negativeChangeInterpolator = d3.interpolateRgb(colorNeutral, colorDanger);
// per @thomasdullien:
// In "relative" mode: Take the percentage of CPU time consumed by block A and subtract
// the percentage of CPU time consumed by block B. If the number is positive, linearly
@ -100,10 +155,7 @@ export function getFlamegraphModel({
denominator
);
const nodeColor =
interpolationValue >= 0
? positiveChangeInterpolator(interpolationValue)
: negativeChangeInterpolator(Math.abs(interpolationValue));
const nodeColor = getColor(interpolationValue);
const rgba = rgbToRGBA(Number(nodeColor.replace('#', '0x')));
viewModel.color.set(rgba, 4 * index);
@ -114,5 +166,6 @@ export function getFlamegraphModel({
key: uniqueId(),
viewModel,
comparisonNodesById,
legendItems,
};
}