mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[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:
parent
ed9987592e
commit
08d85554b9
7 changed files with 252 additions and 59 deletions
|
@ -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);
|
||||
|
|
54
x-pack/plugins/profiling/common/frame_type_colors.ts
Normal file
54
x-pack/plugins/profiling/common/frame_type_colors.ts
Normal 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,
|
||||
];
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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} />;
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
34
x-pack/plugins/profiling/public/components/legend.tsx
Normal file
34
x-pack/plugins/profiling/public/components/legend.tsx
Normal 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>
|
||||
);
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue