[Profiling] flamegraph improvements (#153598)

Adds Annualized Co2 and dollar cost in the tooltip
<img width="587" alt="Screenshot 2023-03-24 at 1 10 49 PM"
src="https://user-images.githubusercontent.com/55978943/227594284-65e6b581-31be-42a3-aa61-cd838d926eba.png">

In the Diff flamegraph:
<img width="580" alt="Screenshot 2023-03-24 at 1 10 33 PM"
src="https://user-images.githubusercontent.com/55978943/227594322-e041eed1-1360-47bd-99b6-6ead027497b5.png">

Removed `show more information` toggle and placed it inside the tooltip.
Also displays the details of a frame inside a flyout.


https://user-images.githubusercontent.com/55978943/227952323-38625615-d2fe-40d6-949b-32e3c1cdfce4.mov
This commit is contained in:
Cauê Marcondes 2023-03-30 13:48:26 -04:00 committed by GitHub
parent 6c3badb8ec
commit c9dde2fbb3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 634 additions and 497 deletions

View file

@ -0,0 +1,60 @@
/*
* 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 { calculateImpactEstimates } from './calculate_impact_estimates';
describe('calculateImpactEstimates', () => {
it('calculates impact when countExclusive is lower than countInclusive', () => {
expect(
calculateImpactEstimates({
countExclusive: 500,
countInclusive: 1000,
totalSamples: 10000,
totalSeconds: 15 * 60, // 15m
})
).toEqual({
annualizedCo2: 17.909333333333336,
annualizedCo2NoChildren: 8.954666666666668,
annualizedCoreSeconds: 1752000,
annualizedCoreSecondsNoChildren: 876000,
annualizedDollarCost: 20.683333333333334,
annualizedDollarCostNoChildren: 10.341666666666667,
co2: 0.0005111111111111112,
co2NoChildren: 0.0002555555555555556,
coreSeconds: 50,
coreSecondsNoChildren: 25,
dollarCost: 0.0005902777777777778,
dollarCostNoChildren: 0.0002951388888888889,
percentage: 0.1,
percentageNoChildren: 0.05,
});
});
it('calculates impact', () => {
expect(
calculateImpactEstimates({
countExclusive: 1000,
countInclusive: 1000,
totalSamples: 10000,
totalSeconds: 15 * 60, // 15m
})
).toEqual({
annualizedCo2: 17.909333333333336,
annualizedCo2NoChildren: 17.909333333333336,
annualizedCoreSeconds: 1752000,
annualizedCoreSecondsNoChildren: 1752000,
annualizedDollarCost: 20.683333333333334,
annualizedDollarCostNoChildren: 20.683333333333334,
co2: 0.0005111111111111112,
co2NoChildren: 0.0005111111111111112,
coreSeconds: 50,
coreSecondsNoChildren: 50,
dollarCost: 0.0005902777777777778,
dollarCostNoChildren: 0.0005902777777777778,
percentage: 0.1,
percentageNoChildren: 0.1,
});
});
});

View file

@ -0,0 +1,66 @@
/*
* 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.
*/
const ANNUAL_SECONDS = 60 * 60 * 24 * 365;
// The assumed amortized per-core average power consumption.
const PER_CORE_WATT = 40;
// The assumed CO2 emissions per KWH (sourced from www.eia.gov)
const CO2_PER_KWH = 0.92;
// The cost of a CPU core per hour, in dollars
const CORE_COST_PER_HOUR = 0.0425;
export function calculateImpactEstimates({
countInclusive,
countExclusive,
totalSamples,
totalSeconds,
}: {
countInclusive: number;
countExclusive: number;
totalSamples: number;
totalSeconds: number;
}) {
const annualizedScaleUp = ANNUAL_SECONDS / totalSeconds;
const percentage = countInclusive / totalSamples;
const percentageNoChildren = countExclusive / totalSamples;
const totalCoreSeconds = totalSamples / 20;
const coreSeconds = totalCoreSeconds * percentage;
const annualizedCoreSeconds = coreSeconds * annualizedScaleUp;
const coreSecondsNoChildren = totalCoreSeconds * percentageNoChildren;
const annualizedCoreSecondsNoChildren = coreSecondsNoChildren * annualizedScaleUp;
const coreHours = coreSeconds / (60 * 60);
const coreHoursNoChildren = coreSecondsNoChildren / (60 * 60);
const co2 = ((PER_CORE_WATT * coreHours) / 1000.0) * CO2_PER_KWH;
const co2NoChildren = ((PER_CORE_WATT * coreHoursNoChildren) / 1000.0) * CO2_PER_KWH;
const annualizedCo2 = co2 * annualizedScaleUp;
const annualizedCo2NoChildren = co2NoChildren * annualizedScaleUp;
const dollarCost = coreHours * CORE_COST_PER_HOUR;
const annualizedDollarCost = dollarCost * annualizedScaleUp;
const dollarCostNoChildren = coreHoursNoChildren * CORE_COST_PER_HOUR;
const annualizedDollarCostNoChildren = dollarCostNoChildren * annualizedScaleUp;
return {
percentage,
percentageNoChildren,
coreSeconds,
annualizedCoreSeconds,
coreSecondsNoChildren,
annualizedCoreSecondsNoChildren,
co2,
co2NoChildren,
annualizedCo2,
annualizedCo2NoChildren,
dollarCost,
annualizedDollarCost,
dollarCostNoChildren,
annualizedDollarCostNoChildren,
};
}

View file

@ -6,7 +6,6 @@
*/
import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiPanel } from '@elastic/eui';
import React from 'react';
import { FlameGraphInformationWindowSwitch } from '.';
import { FlameGraphComparisonMode, FlameGraphNormalizationMode } from '../../../common/flamegraph';
import { useProfilingParams } from '../../hooks/use_profiling_params';
import { useProfilingRouter } from '../../hooks/use_profiling_router';
@ -21,8 +20,6 @@ interface Props {
comparisonMode: FlameGraphComparisonMode;
normalizationMode: FlameGraphNormalizationMode;
normalizationOptions: FlameGraphNormalizationOptions;
showInformationWindow: boolean;
onChangeShowInformationWindow: () => void;
}
export function FlameGraphSearchPanel({
@ -30,8 +27,6 @@ export function FlameGraphSearchPanel({
normalizationMode,
isDifferentialView,
normalizationOptions,
showInformationWindow,
onChangeShowInformationWindow,
}: Props) {
const { path, query } = useProfilingParams('/flamegraphs/*');
const routePath = useProfilingRoutePath();
@ -102,12 +97,6 @@ export function FlameGraphSearchPanel({
)}
</>
)}
<EuiFlexItem grow style={{ alignItems: 'flex-end' }}>
<FlameGraphInformationWindowSwitch
showInformationWindow={showInformationWindow}
onChange={onChangeShowInformationWindow}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
);

View file

@ -4,15 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import {
EuiButtonIcon,
EuiFlexGroup,
EuiFlexItem,
EuiHorizontalRule,
EuiPanel,
EuiText,
EuiTitle,
} from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiText, EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { getImpactRows } from './get_impact_rows';
@ -32,7 +24,6 @@ interface Props {
};
totalSamples: number;
totalSeconds: number;
onClose: () => void;
}
function KeyValueList({ rows }: { rows: Array<{ label: string; value: React.ReactNode }> }) {
@ -59,46 +50,31 @@ function KeyValueList({ rows }: { rows: Array<{ label: string; value: React.Reac
);
}
function FlamegraphFrameInformationPanel({
children,
onClose,
}: {
children: React.ReactNode;
onClose: () => void;
}) {
function FlamegraphFrameInformationPanel({ children }: { children: React.ReactNode }) {
return (
<EuiPanel style={{ width: 400, maxHeight: '100%', overflow: 'auto' }} hasBorder>
<EuiFlexGroup direction="column" gutterSize="m">
<EuiFlexItem>
<EuiFlexGroup direction="row">
<EuiFlexItem grow>
<EuiFlexGroup direction="row" gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
<EuiTitle size="xs">
<h2>
{i18n.translate('xpack.profiling.flameGraphInformationWindowTitle', {
defaultMessage: 'Frame information',
})}
</h2>
</EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonIcon iconType="cross" onClick={() => onClose()} />
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>{children}</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
<EuiFlexGroup direction="column" gutterSize="m">
<EuiFlexItem>
<EuiFlexGroup direction="row" gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
<EuiTitle size="xs">
<h2>
{i18n.translate('xpack.profiling.flameGraphInformationWindowTitle', {
defaultMessage: 'Frame information',
})}
</h2>
</EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>{children}</EuiFlexItem>
</EuiFlexGroup>
);
}
export function FlamegraphInformationWindow({ onClose, frame, totalSamples, totalSeconds }: Props) {
export function FlamegraphInformationWindow({ frame, totalSamples, totalSeconds }: Props) {
if (!frame) {
return (
<FlamegraphFrameInformationPanel onClose={onClose}>
<FlamegraphFrameInformationPanel>
<EuiText>
{i18n.translate('xpack.profiling.flamegraphInformationWindow.selectFrame', {
defaultMessage: 'Click on a frame to display more information',
@ -138,7 +114,7 @@ export function FlamegraphInformationWindow({ onClose, frame, totalSamples, tota
});
return (
<FlamegraphFrameInformationPanel onClose={onClose}>
<FlamegraphFrameInformationPanel>
<EuiFlexGroup direction="column">
<EuiFlexItem>
<KeyValueList rows={informationRows} />

View file

@ -11,17 +11,7 @@ import { asDuration } from '../../utils/formatters/as_duration';
import { asNumber } from '../../utils/formatters/as_number';
import { asPercentage } from '../../utils/formatters/as_percentage';
import { asWeight } from '../../utils/formatters/as_weight';
const ANNUAL_SECONDS = 60 * 60 * 24 * 365;
// The assumed amortized per-core average power consumption.
const PER_CORE_WATT = 40;
// The assumed CO2 emissions per KWH (sourced from www.eia.gov)
const CO2_PER_KWH = 0.92;
// The cost of a CPU core per hour, in dollars
const CORE_COST_PER_HOUR = 0.0425;
import { calculateImpactEstimates } from './calculate_impact_estimates';
export function getImpactRows({
countInclusive,
@ -34,37 +24,40 @@ export function getImpactRows({
totalSamples: number;
totalSeconds: number;
}) {
const percentage = countInclusive / totalSamples;
const percentageNoChildren = countExclusive / totalSamples;
const totalCoreSeconds = totalSamples / 20;
const coreSeconds = totalCoreSeconds * percentage;
const coreSecondsNoChildren = totalCoreSeconds * percentageNoChildren;
const coreHours = coreSeconds / (60 * 60);
const coreHoursNoChildren = coreSecondsNoChildren / (60 * 60);
const annualizedScaleUp = ANNUAL_SECONDS / totalSeconds;
const co2 = ((PER_CORE_WATT * coreHours) / 1000.0) * CO2_PER_KWH;
const co2NoChildren = ((PER_CORE_WATT * coreHoursNoChildren) / 1000.0) * CO2_PER_KWH;
const annualizedCo2 = co2 * annualizedScaleUp;
const annualizedCo2NoChildren = co2NoChildren * annualizedScaleUp;
const dollarCost = coreHours * CORE_COST_PER_HOUR;
const dollarCostNoChildren = coreHoursNoChildren * CORE_COST_PER_HOUR;
const {
percentage,
percentageNoChildren,
coreSeconds,
annualizedCoreSeconds,
coreSecondsNoChildren,
co2,
co2NoChildren,
annualizedCo2,
annualizedCo2NoChildren,
dollarCost,
dollarCostNoChildren,
annualizedDollarCost,
annualizedDollarCostNoChildren,
annualizedCoreSecondsNoChildren,
} = calculateImpactEstimates({
countInclusive,
countExclusive,
totalSamples,
totalSeconds,
});
const impactRows = [
{
label: i18n.translate(
'xpack.profiling.flameGraphInformationWindow.percentageCpuTimeInclusiveLabel',
{
defaultMessage: '% of CPU time',
}
{ defaultMessage: '% of CPU time' }
),
value: asPercentage(percentage),
},
{
label: i18n.translate(
'xpack.profiling.flameGraphInformationWindow.percentageCpuTimeExclusiveLabel',
{
defaultMessage: '% of CPU time (excl. children)',
}
{ defaultMessage: '% of CPU time (excl. children)' }
),
value: asPercentage(percentageNoChildren),
},
@ -83,38 +76,30 @@ export function getImpactRows({
{
label: i18n.translate(
'xpack.profiling.flameGraphInformationWindow.coreSecondsInclusiveLabel',
{
defaultMessage: 'Core-seconds',
}
{ defaultMessage: 'Core-seconds' }
),
value: asDuration(coreSeconds),
},
{
label: i18n.translate(
'xpack.profiling.flameGraphInformationWindow.coreSecondsExclusiveLabel',
{
defaultMessage: 'Core-seconds (excl. children)',
}
{ defaultMessage: 'Core-seconds (excl. children)' }
),
value: asDuration(coreSecondsNoChildren),
},
{
label: i18n.translate(
'xpack.profiling.flameGraphInformationWindow.annualizedCoreSecondsInclusiveLabel',
{
defaultMessage: 'Annualized core-seconds',
}
{ defaultMessage: 'Annualized core-seconds' }
),
value: asDuration(coreSeconds * annualizedScaleUp),
value: asDuration(annualizedCoreSeconds),
},
{
label: i18n.translate(
'xpack.profiling.flameGraphInformationWindow.annualizedCoreSecondsExclusiveLabel',
{
defaultMessage: 'Annualized core-seconds (excl. children)',
}
{ defaultMessage: 'Annualized core-seconds (excl. children)' }
),
value: asDuration(coreSecondsNoChildren * annualizedScaleUp),
value: asDuration(annualizedCoreSecondsNoChildren),
},
{
label: i18n.translate(
@ -128,65 +113,51 @@ export function getImpactRows({
{
label: i18n.translate(
'xpack.profiling.flameGraphInformationWindow.co2EmissionExclusiveLabel',
{
defaultMessage: 'CO2 emission (excl. children)',
}
{ defaultMessage: 'CO2 emission (excl. children)' }
),
value: asWeight(co2NoChildren),
},
{
label: i18n.translate(
'xpack.profiling.flameGraphInformationWindow.annualizedCo2InclusiveLabel',
{
defaultMessage: 'Annualized CO2',
}
{ defaultMessage: 'Annualized CO2' }
),
value: asWeight(annualizedCo2),
},
{
label: i18n.translate(
'xpack.profiling.flameGraphInformationWindow.annualizedCo2ExclusiveLabel',
{
defaultMessage: 'Annualized CO2 (excl. children)',
}
{ defaultMessage: 'Annualized CO2 (excl. children)' }
),
value: asWeight(annualizedCo2NoChildren),
},
{
label: i18n.translate(
'xpack.profiling.flameGraphInformationWindow.dollarCostInclusiveLabel',
{
defaultMessage: 'Dollar cost',
}
{ defaultMessage: 'Dollar cost' }
),
value: asCost(dollarCost),
},
{
label: i18n.translate(
'xpack.profiling.flameGraphInformationWindow.dollarCostExclusiveLabel',
{
defaultMessage: 'Dollar cost (excl. children)',
}
{ defaultMessage: 'Dollar cost (excl. children)' }
),
value: asCost(dollarCostNoChildren),
},
{
label: i18n.translate(
'xpack.profiling.flameGraphInformationWindow.annualizedDollarCostInclusiveLabel',
{
defaultMessage: 'Annualized dollar cost',
}
{ defaultMessage: 'Annualized dollar cost' }
),
value: asCost(dollarCost * annualizedScaleUp),
value: asCost(annualizedDollarCost),
},
{
label: i18n.translate(
'xpack.profiling.flameGraphInformationWindow.annualizedDollarCostExclusiveLabel',
{
defaultMessage: 'Annualized dollar cost (excl. children)',
}
{ defaultMessage: 'Annualized dollar cost (excl. children)' }
),
value: asCost(dollarCostNoChildren * annualizedScaleUp),
value: asCost(annualizedDollarCostNoChildren),
},
];

View file

@ -4,7 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiFlexGroup, EuiFlexItem, EuiPageHeaderContentProps, EuiSwitch } from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiPageHeaderContentProps } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { get } from 'lodash';
import React, { useState } from 'react';
@ -22,24 +22,6 @@ import { RedirectTo } from '../redirect_to';
import { FlameGraphSearchPanel } from './flame_graph_search_panel';
import { FlameGraphNormalizationOptions } from './normalization_menu';
export function FlameGraphInformationWindowSwitch({
showInformationWindow,
onChange,
}: {
showInformationWindow: boolean;
onChange: () => void;
}) {
return (
<EuiSwitch
checked={showInformationWindow}
onChange={onChange}
label={i18n.translate('xpack.profiling.flameGraph.showInformationWindow', {
defaultMessage: 'Show information window',
})}
/>
);
}
export function FlameGraphsView({ children }: { children: React.ReactElement }) {
const {
query,
@ -174,8 +156,6 @@ export function FlameGraphsView({ children }: { children: React.ReactElement })
comparisonMode={comparisonMode}
normalizationMode={normalizationMode}
normalizationOptions={normalizationOptions}
showInformationWindow={showInformationWindow}
onChangeShowInformationWindow={toggleShowInformationWindow}
/>
</EuiFlexItem>
<EuiFlexItem>
@ -196,9 +176,7 @@ export function FlameGraphsView({ children }: { children: React.ReactElement })
: comparisonScale
}
showInformationWindow={showInformationWindow}
onInformationWindowClose={() => {
setShowInformationWindow(false);
}}
toggleShowInformationWindow={toggleShowInformationWindow}
/>
</AsyncComponent>
{children}

View file

@ -1,345 +0,0 @@
/*
* 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 {
Chart,
Datum,
Flame,
FlameLayerValue,
PartialTheme,
Settings,
TooltipContainer,
} from '@elastic/charts';
import {
EuiFlexGroup,
EuiFlexItem,
EuiPanel,
EuiSpacer,
EuiText,
EuiTextColor,
useEuiTheme,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { Maybe } from '@kbn/observability-plugin/common/typings';
import { isNumber } from 'lodash';
import React, { useEffect, useMemo, useState } from 'react';
import { ElasticFlameGraph, FlameGraphComparisonMode } from '../../common/flamegraph';
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,
label,
comparison,
formatAsPercentage,
showChange,
}: {
value: number;
label: string;
comparison?: number;
formatAsPercentage: boolean;
showChange: boolean;
}) {
const valueLabel = formatAsPercentage ? asPercentage(Math.abs(value)) : value.toString();
const comparisonLabel =
formatAsPercentage && isNumber(comparison) ? asPercentage(comparison) : comparison?.toString();
let diff: number | undefined;
let diffLabel = '';
let color = '';
if (isNumber(comparison)) {
if (showChange) {
color = value < comparison ? 'danger' : 'success';
if (formatAsPercentage) {
// CPU percent values
diff = comparison - value;
diffLabel =
'(' + (diff > 0 ? '+' : diff < 0 ? '-' : '') + asPercentage(Math.abs(diff)) + ')';
} else {
// Sample counts
diff = 1 - comparison / value;
diffLabel =
'(' + (diff > 0 ? '-' : diff < 0 ? '+' : '') + asPercentage(Math.abs(diff)) + ')';
}
if (Math.abs(diff) < 0.0001) {
diffLabel = '';
}
}
}
return (
<EuiFlexItem style={{ width: 256, overflowWrap: 'anywhere' }}>
<EuiFlexGroup direction="row" gutterSize="xs">
<EuiFlexItem style={{}}>
<EuiText size="xs">
<strong>{label}</strong>
</EuiText>
<EuiText size="xs" style={{ marginLeft: '20px' }}>
{comparison !== undefined
? i18n.translate('xpack.profiling.flameGraphTooltip.valueLabel', {
defaultMessage: `{value} vs {comparison}`,
values: {
value: valueLabel,
comparison: comparisonLabel,
},
})
: valueLabel}
<EuiTextColor color={color}> {diffLabel}</EuiTextColor>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="xs" />
</EuiFlexItem>
);
}
function FlameGraphTooltip({
isRoot,
label,
countInclusive,
countExclusive,
totalSamples,
baselineScaleFactor,
comparisonScaleFactor,
comparisonCountInclusive,
comparisonCountExclusive,
comparisonTotalSamples,
}: {
isRoot: boolean;
label: string;
countInclusive: number;
countExclusive: number;
totalSamples: number;
baselineScaleFactor?: number;
comparisonScaleFactor?: number;
comparisonCountInclusive?: number;
comparisonCountExclusive?: number;
comparisonTotalSamples?: number;
}) {
return (
<TooltipContainer>
<EuiPanel>
<EuiFlexGroup
direction="column"
gutterSize="m"
style={{
overflowWrap: 'anywhere',
}}
>
<EuiFlexItem>{label}</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup direction="column" gutterSize="xs">
{isRoot === false && (
<>
<TooltipRow
label={i18n.translate('xpack.profiling.flameGraphTooltip.inclusiveCpuLabel', {
defaultMessage: `CPU incl. subfunctions`,
})}
value={countInclusive / totalSamples}
comparison={
isNumber(comparisonCountInclusive) && isNumber(comparisonTotalSamples)
? comparisonCountInclusive / comparisonTotalSamples
: undefined
}
formatAsPercentage
showChange
/>
<TooltipRow
label={i18n.translate('xpack.profiling.flameGraphTooltip.exclusiveCpuLabel', {
defaultMessage: `CPU`,
})}
value={countExclusive / totalSamples}
comparison={
isNumber(comparisonCountExclusive) && isNumber(comparisonTotalSamples)
? comparisonCountExclusive / comparisonTotalSamples
: undefined
}
formatAsPercentage
showChange
/>
</>
)}
<TooltipRow
label={i18n.translate('xpack.profiling.flameGraphTooltip.samplesLabel', {
defaultMessage: `Samples`,
})}
value={
isNumber(baselineScaleFactor)
? countInclusive * baselineScaleFactor
: countInclusive
}
comparison={
isNumber(comparisonCountInclusive) && isNumber(comparisonScaleFactor)
? comparisonCountInclusive * comparisonScaleFactor
: undefined
}
formatAsPercentage={false}
showChange
/>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
</TooltipContainer>
);
}
export interface FlameGraphProps {
id: string;
comparisonMode: FlameGraphComparisonMode;
primaryFlamegraph?: ElasticFlameGraph;
comparisonFlamegraph?: ElasticFlameGraph;
baseline?: number;
comparison?: number;
showInformationWindow: boolean;
onInformationWindowClose: () => void;
}
export const FlameGraph: React.FC<FlameGraphProps> = ({
id,
comparisonMode,
primaryFlamegraph,
comparisonFlamegraph,
baseline,
comparison,
showInformationWindow,
onInformationWindowClose,
}) => {
const theme = useEuiTheme();
const columnarData = useMemo(() => {
return getFlamegraphModel({
primaryFlamegraph,
comparisonFlamegraph,
colorSuccess: theme.euiTheme.colors.success,
colorDanger: theme.euiTheme.colors.danger,
colorNeutral: theme.euiTheme.colors.lightShade,
comparisonMode,
baseline,
comparison,
});
}, [
primaryFlamegraph,
comparisonFlamegraph,
theme.euiTheme.colors.success,
theme.euiTheme.colors.danger,
theme.euiTheme.colors.lightShade,
comparisonMode,
baseline,
comparison,
]);
const chartTheme: PartialTheme = {
chartMargins: { top: 0, left: 0, bottom: 0, right: 0 },
chartPaddings: { left: 0, right: 0, top: 0, bottom: 0 },
};
const totalSamples = columnarData.viewModel.value[0];
const [highlightedVmIndex, setHighlightedVmIndex] = useState<number | undefined>(undefined);
const selected: undefined | React.ComponentProps<typeof FlamegraphInformationWindow>['frame'] =
primaryFlamegraph && highlightedVmIndex !== undefined
? {
fileID: primaryFlamegraph.FileID[highlightedVmIndex],
frameType: primaryFlamegraph.FrameType[highlightedVmIndex],
exeFileName: primaryFlamegraph.ExeFilename[highlightedVmIndex],
addressOrLine: primaryFlamegraph.AddressOrLine[highlightedVmIndex],
functionName: primaryFlamegraph.FunctionName[highlightedVmIndex],
sourceFileName: primaryFlamegraph.SourceFilename[highlightedVmIndex],
sourceLine: primaryFlamegraph.SourceLine[highlightedVmIndex],
countInclusive: primaryFlamegraph.CountInclusive[highlightedVmIndex],
countExclusive: primaryFlamegraph.CountExclusive[highlightedVmIndex],
}
: undefined;
useEffect(() => {
setHighlightedVmIndex(undefined);
}, [columnarData.key]);
return (
<EuiFlexGroup direction="column">
<EuiFlexItem>
<EuiFlexGroup direction="row">
{columnarData.viewModel.label.length > 0 && (
<EuiFlexItem grow>
<Chart key={columnarData.key}>
<Settings
theme={chartTheme}
onElementClick={(elements) => {
const selectedElement = elements[0] as Maybe<FlameLayerValue>;
if (Number.isNaN(selectedElement?.vmIndex)) {
setHighlightedVmIndex(undefined);
} else {
setHighlightedVmIndex(selectedElement!.vmIndex);
}
}}
tooltip={{
customTooltip: (props) => {
if (!primaryFlamegraph) {
return <></>;
}
const valueIndex = props.values[0].valueAccessor as number;
const label = primaryFlamegraph.Label[valueIndex];
const countInclusive = primaryFlamegraph.CountInclusive[valueIndex];
const countExclusive = primaryFlamegraph.CountExclusive[valueIndex];
const nodeID = primaryFlamegraph.ID[valueIndex];
const comparisonNode = columnarData.comparisonNodesById[nodeID];
return (
<FlameGraphTooltip
isRoot={valueIndex === 0}
label={label}
countInclusive={countInclusive}
countExclusive={countExclusive}
comparisonCountInclusive={comparisonNode?.CountInclusive}
comparisonCountExclusive={comparisonNode?.CountExclusive}
totalSamples={totalSamples}
comparisonTotalSamples={comparisonFlamegraph?.CountInclusive[0]}
baselineScaleFactor={baseline}
comparisonScaleFactor={comparison}
/>
);
},
}}
/>
<Flame
id={id}
columnarData={columnarData.viewModel}
valueAccessor={(d: Datum) => d.value as number}
valueFormatter={(value) => `${value}`}
animation={{ duration: 100 }}
controlProviderCallback={{}}
/>
</Chart>
</EuiFlexItem>
)}
{showInformationWindow ? (
<EuiFlexItem grow={false}>
<FlamegraphInformationWindow
frame={selected}
totalSeconds={primaryFlamegraph?.TotalSeconds ?? 0}
totalSamples={totalSamples}
onClose={() => {
onInformationWindowClose();
}}
/>
</EuiFlexItem>
) : undefined}
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<FlameGraphLegend legendItems={columnarData.legendItems} asScale={!!comparisonFlamegraph} />
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -0,0 +1,180 @@
/*
* 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 { TooltipContainer } from '@elastic/charts';
import {
EuiButtonEmpty,
EuiFlexGroup,
EuiFlexItem,
EuiHorizontalRule,
EuiIcon,
EuiPanel,
EuiText,
useEuiTheme,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { isNumber } from 'lodash';
import React from 'react';
import { asCost } from '../../utils/formatters/as_cost';
import { asPercentage } from '../../utils/formatters/as_percentage';
import { asWeight } from '../../utils/formatters/as_weight';
import { calculateImpactEstimates } from '../flame_graphs_view/calculate_impact_estimates';
import { TooltipRow } from './tooltip_row';
interface Props {
isRoot: boolean;
label: string;
countInclusive: number;
countExclusive: number;
totalSamples: number;
totalSeconds: number;
baselineScaleFactor?: number;
comparisonScaleFactor?: number;
comparisonCountInclusive?: number;
comparisonCountExclusive?: number;
comparisonTotalSamples?: number;
comparisonTotalSeconds?: number;
onShowMoreClick?: () => void;
}
export function FlameGraphTooltip({
isRoot,
label,
countInclusive,
countExclusive,
totalSamples,
totalSeconds,
baselineScaleFactor,
comparisonScaleFactor,
comparisonCountInclusive,
comparisonCountExclusive,
comparisonTotalSamples,
comparisonTotalSeconds,
onShowMoreClick,
}: Props) {
const theme = useEuiTheme();
const impactEstimates = calculateImpactEstimates({
countExclusive,
countInclusive,
totalSamples,
totalSeconds,
});
const comparisonImpactEstimates =
isNumber(comparisonCountExclusive) &&
isNumber(comparisonCountInclusive) &&
isNumber(comparisonTotalSamples) &&
isNumber(comparisonTotalSeconds)
? calculateImpactEstimates({
countExclusive: comparisonCountExclusive,
countInclusive: comparisonCountInclusive,
totalSamples: comparisonTotalSamples,
totalSeconds: comparisonTotalSeconds,
})
: undefined;
return (
<TooltipContainer>
<EuiPanel paddingSize="s">
<EuiFlexGroup direction="column" gutterSize="xs">
<EuiFlexItem>{label}</EuiFlexItem>
<EuiHorizontalRule margin="none" style={{ background: theme.euiTheme.border.color }} />
{isRoot === false && (
<>
<TooltipRow
label={i18n.translate('xpack.profiling.flameGraphTooltip.inclusiveCpuLabel', {
defaultMessage: `CPU incl. subfunctions`,
})}
value={impactEstimates.percentage}
comparison={comparisonImpactEstimates?.percentage}
formatValue={asPercentage}
showDifference
formatDifferenceAsPercentage
/>
<TooltipRow
label={i18n.translate('xpack.profiling.flameGraphTooltip.exclusiveCpuLabel', {
defaultMessage: `CPU`,
})}
value={impactEstimates.percentageNoChildren}
comparison={comparisonImpactEstimates?.percentageNoChildren}
showDifference
formatDifferenceAsPercentage
formatValue={asPercentage}
/>
</>
)}
<TooltipRow
label={i18n.translate('xpack.profiling.flameGraphTooltip.samplesLabel', {
defaultMessage: `Samples`,
})}
value={
isNumber(baselineScaleFactor) ? countInclusive * baselineScaleFactor : countInclusive
}
comparison={
isNumber(comparisonCountInclusive) && isNumber(comparisonScaleFactor)
? comparisonCountInclusive * comparisonScaleFactor
: undefined
}
showDifference
formatDifferenceAsPercentage={false}
/>
<TooltipRow
label={i18n.translate('xpack.profiling.flameGraphTooltip.annualizedCo2', {
defaultMessage: `Annualized CO2`,
})}
value={impactEstimates.annualizedCo2}
comparison={comparisonImpactEstimates?.annualizedCo2}
formatValue={asWeight}
showDifference
formatDifferenceAsPercentage={false}
/>
<TooltipRow
label={i18n.translate('xpack.profiling.flameGraphTooltip.annualizedDollarCost', {
defaultMessage: `Annualized dollar cost`,
})}
value={impactEstimates.annualizedDollarCost}
comparison={comparisonImpactEstimates?.annualizedDollarCost}
formatValue={asCost}
showDifference
formatDifferenceAsPercentage={false}
/>
{onShowMoreClick && (
<>
<EuiHorizontalRule
margin="none"
style={{ background: theme.euiTheme.border.color }}
/>
<EuiFlexItem>
<EuiButtonEmpty size="s" iconType="inspect" onClick={onShowMoreClick}>
<EuiText size="xs">
{i18n.translate('xpack.profiling.flameGraphTooltip.showMoreButton', {
defaultMessage: `Show more information`,
})}
</EuiText>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup gutterSize="xs">
<EuiFlexItem grow={false}>
<EuiIcon type="iInCircle" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText color="subdued" size="xs">
{i18n.translate('xpack.profiling.flameGraphTooltip.rightClickTip', {
defaultMessage: `Right-click to pin tooltip`,
})}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</>
)}
</EuiFlexGroup>
</EuiPanel>
</TooltipContainer>
);
}

View file

@ -0,0 +1,184 @@
/*
* 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 { Chart, Datum, Flame, FlameLayerValue, PartialTheme, Settings } from '@elastic/charts';
import { EuiFlexGroup, EuiFlexItem, EuiFlyout, EuiFlyoutBody, useEuiTheme } from '@elastic/eui';
import { Maybe } from '@kbn/observability-plugin/common/typings';
import React, { useEffect, useMemo, useState } from 'react';
import { ElasticFlameGraph, FlameGraphComparisonMode } from '../../../common/flamegraph';
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';
import { FlameGraphTooltip } from './flamegraph_tooltip';
interface Props {
id: string;
comparisonMode: FlameGraphComparisonMode;
primaryFlamegraph?: ElasticFlameGraph;
comparisonFlamegraph?: ElasticFlameGraph;
baseline?: number;
comparison?: number;
showInformationWindow: boolean;
toggleShowInformationWindow: () => void;
}
export function FlameGraph({
id,
comparisonMode,
primaryFlamegraph,
comparisonFlamegraph,
baseline,
comparison,
showInformationWindow,
toggleShowInformationWindow,
}: Props) {
const theme = useEuiTheme();
const columnarData = useMemo(() => {
return getFlamegraphModel({
primaryFlamegraph,
comparisonFlamegraph,
colorSuccess: theme.euiTheme.colors.success,
colorDanger: theme.euiTheme.colors.danger,
colorNeutral: theme.euiTheme.colors.lightShade,
comparisonMode,
baseline,
comparison,
});
}, [
primaryFlamegraph,
comparisonFlamegraph,
theme.euiTheme.colors.success,
theme.euiTheme.colors.danger,
theme.euiTheme.colors.lightShade,
comparisonMode,
baseline,
comparison,
]);
const chartTheme: PartialTheme = {
chartMargins: { top: 0, left: 0, bottom: 0, right: 0 },
chartPaddings: { left: 0, right: 0, top: 0, bottom: 0 },
tooltip: { maxWidth: 500 },
};
const totalSamples = columnarData.viewModel.value[0];
const [highlightedVmIndex, setHighlightedVmIndex] = useState<number | undefined>(undefined);
const selected: undefined | React.ComponentProps<typeof FlamegraphInformationWindow>['frame'] =
primaryFlamegraph && highlightedVmIndex !== undefined
? {
fileID: primaryFlamegraph.FileID[highlightedVmIndex],
frameType: primaryFlamegraph.FrameType[highlightedVmIndex],
exeFileName: primaryFlamegraph.ExeFilename[highlightedVmIndex],
addressOrLine: primaryFlamegraph.AddressOrLine[highlightedVmIndex],
functionName: primaryFlamegraph.FunctionName[highlightedVmIndex],
sourceFileName: primaryFlamegraph.SourceFilename[highlightedVmIndex],
sourceLine: primaryFlamegraph.SourceLine[highlightedVmIndex],
countInclusive: primaryFlamegraph.CountInclusive[highlightedVmIndex],
countExclusive: primaryFlamegraph.CountExclusive[highlightedVmIndex],
}
: undefined;
useEffect(() => {
setHighlightedVmIndex(undefined);
}, [columnarData.key]);
return (
<>
<EuiFlexGroup direction="column">
<EuiFlexItem>
<EuiFlexGroup direction="row">
{columnarData.viewModel.label.length > 0 && (
<EuiFlexItem grow>
<Chart key={columnarData.key}>
<Settings
theme={chartTheme}
onElementClick={(elements) => {
const selectedElement = elements[0] as Maybe<FlameLayerValue>;
if (Number.isNaN(selectedElement?.vmIndex)) {
setHighlightedVmIndex(undefined);
} else {
setHighlightedVmIndex(selectedElement!.vmIndex);
}
}}
tooltip={{
actions: [{ label: '', onSelect: () => {} }],
customTooltip: (props) => {
if (!primaryFlamegraph) {
return <></>;
}
const valueIndex = props.values[0].valueAccessor as number;
const label = primaryFlamegraph.Label[valueIndex];
const countInclusive = primaryFlamegraph.CountInclusive[valueIndex];
const countExclusive = primaryFlamegraph.CountExclusive[valueIndex];
const totalSeconds = primaryFlamegraph.TotalSeconds;
const nodeID = primaryFlamegraph.ID[valueIndex];
const comparisonNode = columnarData.comparisonNodesById[nodeID];
return (
<FlameGraphTooltip
isRoot={valueIndex === 0}
label={label}
countInclusive={countInclusive}
countExclusive={countExclusive}
totalSamples={totalSamples}
totalSeconds={totalSeconds}
comparisonCountInclusive={comparisonNode?.CountInclusive}
comparisonCountExclusive={comparisonNode?.CountExclusive}
comparisonTotalSamples={comparisonFlamegraph?.CountInclusive[0]}
comparisonTotalSeconds={comparisonFlamegraph?.TotalSeconds}
baselineScaleFactor={baseline}
comparisonScaleFactor={comparison}
onShowMoreClick={() => {
if (!showInformationWindow) {
toggleShowInformationWindow();
}
setHighlightedVmIndex(valueIndex);
}}
/>
);
},
}}
/>
<Flame
id={id}
columnarData={columnarData.viewModel}
valueAccessor={(d: Datum) => d.value as number}
valueFormatter={(value) => `${value}`}
animation={{ duration: 100 }}
controlProviderCallback={{}}
/>
</Chart>
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<FlameGraphLegend
legendItems={columnarData.legendItems}
asScale={!!comparisonFlamegraph}
/>
</EuiFlexItem>
</EuiFlexGroup>
{showInformationWindow && (
<EuiFlyout onClose={toggleShowInformationWindow} size="s">
<EuiFlyoutBody>
<FlamegraphInformationWindow
frame={selected}
totalSeconds={primaryFlamegraph?.TotalSeconds ?? 0}
totalSamples={totalSamples}
/>
</EuiFlyoutBody>
</EuiFlyout>
)}
</>
);
}

View file

@ -0,0 +1,81 @@
/*
* 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, EuiTextColor } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { isNumber } from 'lodash';
import React from 'react';
import { asPercentage } from '../../utils/formatters/as_percentage';
export function TooltipRow({
value,
label,
comparison,
formatDifferenceAsPercentage,
showDifference,
formatValue,
}: {
value: number;
label: string;
comparison?: number;
formatDifferenceAsPercentage: boolean;
showDifference: boolean;
formatValue?: (value: number) => string;
}) {
const valueLabel = formatValue ? formatValue(Math.abs(value)) : value.toString();
const comparisonLabel =
formatValue && isNumber(comparison) ? formatValue(comparison) : comparison?.toString();
let diff: number | undefined;
let diffLabel = '';
let color = '';
if (isNumber(comparison)) {
if (showDifference) {
color = value < comparison ? 'danger' : 'success';
if (formatDifferenceAsPercentage) {
// CPU percent values
diff = comparison - value;
diffLabel =
'(' + (diff > 0 ? '+' : diff < 0 ? '-' : '') + asPercentage(Math.abs(diff)) + ')';
} else {
// Sample counts
diff = 1 - comparison / value;
diffLabel =
'(' + (diff > 0 ? '-' : diff < 0 ? '+' : '') + asPercentage(Math.abs(diff)) + ')';
}
if (Math.abs(diff) < 0.0001) {
diffLabel = '';
}
}
}
return (
<EuiFlexItem>
<EuiFlexGroup direction="row" gutterSize="xs">
<EuiFlexItem grow={false}>
<EuiText size="xs">
<strong>{label}:</strong>
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
<EuiText size="xs">
{comparison !== undefined
? i18n.translate('xpack.profiling.flameGraphTooltip.valueLabel', {
defaultMessage: `{value} vs {comparison}`,
values: {
value: valueLabel,
comparison: comparisonLabel,
},
})
: valueLabel}
<EuiTextColor color={color}> {diffLabel}</EuiTextColor>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
);
}

View file

@ -10,7 +10,7 @@ import { asNumber } from './as_number';
const ONE_POUND_TO_A_KILO = 0.45359237;
export function asWeight(valueInPounds: number) {
export function asWeight(valueInPounds: number): string {
const lbs = asNumber(valueInPounds);
const kgs = asNumber(Number(valueInPounds * ONE_POUND_TO_A_KILO));

View file

@ -26413,7 +26413,6 @@
"xpack.profiling.breadcrumb.topnFunctions": "N premiers",
"xpack.profiling.checkSetup.setupFailureToastTitle": "Impossible de terminer la configuration",
"xpack.profiling.featureRegistry.profilingFeatureName": "Universal Profiling",
"xpack.profiling.flameGraph.showInformationWindow": "Afficher la fenêtre d'informations",
"xpack.profiling.flameGraphInformationWindow.annualizedCo2ExclusiveLabel": "CO2 annualisé (enfants excl.)",
"xpack.profiling.flameGraphInformationWindow.annualizedCo2InclusiveLabel": "CO2 annualisé",
"xpack.profiling.flameGraphInformationWindow.annualizedCoreSecondsExclusiveLabel": "Cœurs-secondes annualisé (enfants excl.)",

View file

@ -26394,7 +26394,6 @@
"xpack.profiling.breadcrumb.topnFunctions": "トップ N",
"xpack.profiling.checkSetup.setupFailureToastTitle": "設定を完了できませんでした",
"xpack.profiling.featureRegistry.profilingFeatureName": "ユニバーサルプロファイリング",
"xpack.profiling.flameGraph.showInformationWindow": "情報ウィンドウを表示",
"xpack.profiling.flameGraphInformationWindow.annualizedCo2ExclusiveLabel": "年間換算CO2子を除く",
"xpack.profiling.flameGraphInformationWindow.annualizedCo2InclusiveLabel": "年間換算CO2",
"xpack.profiling.flameGraphInformationWindow.annualizedCoreSecondsExclusiveLabel": "年間換算core-seconds子を除く",

View file

@ -26410,7 +26410,6 @@
"xpack.profiling.breadcrumb.topnFunctions": "排名前 N",
"xpack.profiling.checkSetup.setupFailureToastTitle": "无法完成设置",
"xpack.profiling.featureRegistry.profilingFeatureName": "Universal Profiling",
"xpack.profiling.flameGraph.showInformationWindow": "显示信息窗口",
"xpack.profiling.flameGraphInformationWindow.annualizedCo2ExclusiveLabel": "年化 CO2不包括子项",
"xpack.profiling.flameGraphInformationWindow.annualizedCo2InclusiveLabel": "年化 CO2",
"xpack.profiling.flameGraphInformationWindow.annualizedCoreSecondsExclusiveLabel": "年化核心-秒(不包括子项)",