mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[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:
parent
6c3badb8ec
commit
c9dde2fbb3
14 changed files with 634 additions and 497 deletions
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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),
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
}
|
184
x-pack/plugins/profiling/public/components/flamegraph/index.tsx
Normal file
184
x-pack/plugins/profiling/public/components/flamegraph/index.tsx
Normal 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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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));
|
||||
|
||||
|
|
|
@ -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.)",
|
||||
|
|
|
@ -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(子を除く)",
|
||||
|
|
|
@ -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": "年化核心-秒(不包括子项)",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue