[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

@ -8,52 +8,7 @@
import { ColumnarViewModel } from '@elastic/charts';
import { ElasticFlameGraph } from './flamegraph';
/*
* Helper to calculate the color of a given block to be drawn. The desirable outcomes of this are:
* Each of the following frame types should get a different set of color hues:
*
* 0 = Unsymbolized frame
* 1 = Python
* 2 = PHP
* 3 = Native
* 4 = Kernel
* 5 = JVM/Hotspot
* 6 = Ruby
* 7 = Perl
* 8 = JavaScript
* 9 = PHP JIT
*
* This is most easily achieved by mapping frame types to different color variations, using
* the x-position we can use different colors for adjacent blocks while keeping a similar hue
*
* Taken originally from prodfiler_ui/src/helpers/Pixi/frameTypeToColors.tsx
*/
const frameTypeToColors = [
[0xfd8484, 0xfd9d9d, 0xfeb5b5, 0xfecece],
[0xfcae6b, 0xfdbe89, 0xfdcea6, 0xfedfc4],
[0xfcdb82, 0xfde29b, 0xfde9b4, 0xfef1cd],
[0x6dd0dc, 0x8ad9e3, 0xa7e3ea, 0xc5ecf1],
[0x7c9eff, 0x96b1ff, 0xb0c5ff, 0xcbd8ff],
[0x65d3ac, 0x84dcbd, 0xa3e5cd, 0xc1edde],
[0xd79ffc, 0xdfb2fd, 0xe7c5fd, 0xefd9fe],
[0xf98bb9, 0xfaa2c7, 0xfbb9d5, 0xfdd1e3],
[0xcbc3e3, 0xd5cfe8, 0xdfdbee, 0xeae7f3],
[0xccfc82, 0xd1fc8e, 0xd6fc9b, 0xdbfca7],
];
function frameTypeToRGB(frameType: number, x: number): number {
return frameTypeToColors[frameType][x % 4];
}
export function rgbToRGBA(rgb: number): number[] {
return [
Math.floor(rgb / 65536) / 255,
(Math.floor(rgb / 256) % 256) / 255,
(rgb % 256) / 255,
1.0,
];
}
import { frameTypeToRGB, rgbToRGBA } from './frame_type_colors';
function normalize(n: number, lower: number, upper: number): number {
return (n - lower) / (upper - lower);

View file

@ -0,0 +1,54 @@
/*
* 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 { FrameType } from './profiling';
/*
* Helper to calculate the color of a given block to be drawn. The desirable outcomes of this are:
* Each of the following frame types should get a different set of color hues:
*
* 0 = Unsymbolized frame
* 1 = Python
* 2 = PHP
* 3 = Native
* 4 = Kernel
* 5 = JVM/Hotspot
* 6 = Ruby
* 7 = Perl
* 8 = JavaScript
* 9 = PHP JIT
*
* This is most easily achieved by mapping frame types to different color variations, using
* the x-position we can use different colors for adjacent blocks while keeping a similar hue
*
* Taken originally from prodfiler_ui/src/helpers/Pixi/frameTypeToColors.tsx
*/
export const FRAME_TYPE_COLOR_MAP = {
[FrameType.Unsymbolized]: [0xfd8484, 0xfd9d9d, 0xfeb5b5, 0xfecece],
[FrameType.Python]: [0xfcae6b, 0xfdbe89, 0xfdcea6, 0xfedfc4],
[FrameType.PHP]: [0xfcdb82, 0xfde29b, 0xfde9b4, 0xfef1cd],
[FrameType.Native]: [0x6dd0dc, 0x8ad9e3, 0xa7e3ea, 0xc5ecf1],
[FrameType.Kernel]: [0x7c9eff, 0x96b1ff, 0xb0c5ff, 0xcbd8ff],
[FrameType.JVM]: [0x65d3ac, 0x84dcbd, 0xa3e5cd, 0xc1edde],
[FrameType.Ruby]: [0xd79ffc, 0xdfb2fd, 0xe7c5fd, 0xefd9fe],
[FrameType.Perl]: [0xf98bb9, 0xfaa2c7, 0xfbb9d5, 0xfdd1e3],
[FrameType.JavaScript]: [0xcbc3e3, 0xd5cfe8, 0xdfdbee, 0xeae7f3],
[FrameType.PHPJIT]: [0xccfc82, 0xd1fc8e, 0xd6fc9b, 0xdbfca7],
};
export function frameTypeToRGB(frameType: FrameType, x: number): number {
return FRAME_TYPE_COLOR_MAP[frameType][x % 4];
}
export function rgbToRGBA(rgb: number): number[] {
return [
Math.floor(rgb / 65536) / 255,
(Math.floor(rgb / 256) % 256) / 255,
(rgb % 256) / 255,
1.0,
];
}

View file

@ -59,6 +59,7 @@ export enum FrameType {
Ruby,
Perl,
JavaScript,
PHPJIT,
}
const frameTypeDescriptions = {
@ -71,6 +72,7 @@ const frameTypeDescriptions = {
[FrameType.Ruby]: 'Ruby',
[FrameType.Perl]: 'Perl',
[FrameType.JavaScript]: 'JavaScript',
[FrameType.PHPJIT]: 'PHP JIT',
};
export function describeFrameType(ft: FrameType): string {

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,
};
}