[Profiling] Normalize samples by time for differential flamegraph (#152158)

This PR appropriately scales the samples in the differential
flamegraph's tooltip when time-normalized mode is selected.

Fixes https://github.com/elastic/prodfiler/issues/3038

### Notes

* Respective values for the normalization menu and differential
flamegraph are now defined in the parent view so that both elements
remain in sync. Previously only the normalization menu had the scaling
factors for time-normalized mode, thus, the tooltip in the differential
flamegraph was not accurate.
* The prior scaling factors for scale-normalized mode are remembered as
long as a user is on the differential flamegraph. Thus, a user can
update the time ranges, the format (Abs vs Rel), and the normalization
mode without losing their previously chosen scale-normalized values.
* The time-normalized scaling factors continue to remain immutable.
* Due to artifacts related to floating-point division, the adjusted
samples may not be whole integers.
This commit is contained in:
Joseph Crail 2023-02-27 01:29:19 -08:00 committed by GitHub
parent da4307e80e
commit ff02c4e124
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 83 additions and 82 deletions

View file

@ -15,7 +15,7 @@ import {
EuiTitle,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { pick } from 'lodash';
import { get } from 'lodash';
import React, { useState } from 'react';
import { FlameGraphComparisonMode, FlameGraphNormalizationMode } from '../../../common/flamegraph';
import { useProfilingParams } from '../../hooks/use_profiling_params';
@ -85,9 +85,31 @@ export function FlameGraphsView({ children }: { children: React.ReactElement })
const comparisonMode =
'comparisonMode' in query ? query.comparisonMode : FlameGraphComparisonMode.Absolute;
const normalizationMode = 'normalizationMode' in query ? query.normalizationMode : undefined;
const baseline = 'baseline' in query ? query.baseline : 1;
const comparison = 'comparison' in query ? query.comparison : 1;
const normalizationMode: FlameGraphNormalizationMode = get(
query,
'normalizationMode',
FlameGraphNormalizationMode.Time
);
const baselineScale: number = get(query, 'baseline', 1);
const comparisonScale: number = get(query, 'comparison', 1);
const totalSeconds =
(new Date(timeRange.end).getTime() - new Date(timeRange.start).getTime()) / 1000;
const totalComparisonSeconds =
(new Date(comparisonTimeRange.end!).getTime() -
new Date(comparisonTimeRange.start!).getTime()) /
1000;
const baselineTime = 1;
const comparisonTime = totalSeconds / totalComparisonSeconds;
const normalizationOptions: FlameGraphNormalizationOptions = {
baselineScale,
baselineTime,
comparisonScale,
comparisonTime,
};
const {
services: { fetchElasticFlamechart },
@ -247,33 +269,25 @@ export function FlameGraphsView({ children }: { children: React.ReactElement })
<EuiFlexGroup direction="row" gutterSize="m" alignItems="center">
<EuiFlexItem grow={false}>
<NormalizationMenu
onChange={(options) => {
onChange={(mode, options) => {
profilingRouter.push(routePath, {
path: routePath,
query: {
...query,
...pick(options, 'baseline', 'comparison'),
normalizationMode: options.mode,
},
query:
mode === FlameGraphNormalizationMode.Scale
? {
...query,
baseline: options.baselineScale,
comparison: options.comparisonScale,
normalizationMode: mode,
}
: {
...query,
normalizationMode: mode,
},
});
}}
totalSeconds={
(new Date(timeRange.end).getTime() - new Date(timeRange.start).getTime()) / 1000
}
comparisonTotalSeconds={
(new Date(comparisonTimeRange.end!).getTime() -
new Date(comparisonTimeRange.start!).getTime()) /
1000
}
options={
(normalizationMode === FlameGraphNormalizationMode.Time
? { mode: FlameGraphNormalizationMode.Time }
: {
mode: FlameGraphNormalizationMode.Scale,
baseline,
comparison,
}) as FlameGraphNormalizationOptions
}
mode={normalizationMode}
options={normalizationOptions}
/>
</EuiFlexItem>
</EuiFlexGroup>
@ -308,8 +322,16 @@ export function FlameGraphsView({ children }: { children: React.ReactElement })
primaryFlamegraph={data?.primaryFlamegraph}
comparisonFlamegraph={data?.comparisonFlamegraph}
comparisonMode={comparisonMode}
baseline={baseline}
comparison={comparison}
baseline={
normalizationMode === FlameGraphNormalizationMode.Time
? baselineTime
: baselineScale
}
comparison={
normalizationMode === FlameGraphNormalizationMode.Time
? comparisonTime
: comparisonScale
}
showInformationWindow={showInformationWindow}
onInformationWindowClose={() => {
setShowInformationWindow(false);

View file

@ -27,19 +27,17 @@ import { i18n } from '@kbn/i18n';
import React, { useEffect, useState } from 'react';
import { FlameGraphNormalizationMode } from '../../../common/flamegraph';
export type FlameGraphNormalizationOptions =
| {
mode: FlameGraphNormalizationMode.Scale;
baseline: number;
comparison: number;
}
| { mode: FlameGraphNormalizationMode.Time };
export interface FlameGraphNormalizationOptions {
baselineScale: number;
baselineTime: number;
comparisonScale: number;
comparisonTime: number;
}
interface Props {
mode: FlameGraphNormalizationMode;
options: FlameGraphNormalizationOptions;
totalSeconds: number;
comparisonTotalSeconds: number;
onChange: (options: FlameGraphNormalizationOptions) => void;
onChange: (mode: FlameGraphNormalizationMode, options: FlameGraphNormalizationOptions) => void;
}
const SCALE_LABEL = i18n.translate('xpack.profiling.flameGraphNormalizationMenu.scale', {
@ -57,19 +55,6 @@ const NORMALIZE_BY_LABEL = i18n.translate(
}
);
function getScaleFactorsBasedOnTime({
totalSeconds,
comparisonTotalSeconds,
}: {
totalSeconds: number;
comparisonTotalSeconds: number;
}) {
return {
baseline: 1,
comparison: totalSeconds / comparisonTotalSeconds,
};
}
export function NormalizationMenu(props: Props) {
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
@ -78,26 +63,18 @@ export function NormalizationMenu(props: Props) {
const baselineScaleFactorInputId = useGeneratedHtmlId({ prefix: 'baselineScaleFactor' });
const comparisonScaleFactorInputId = useGeneratedHtmlId({ prefix: 'comparisonScaleFactor' });
const [mode, setMode] = useState(props.mode);
const [options, setOptions] = useState(props.options);
useEffect(() => {
setMode(props.mode);
setOptions(props.options);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
props.options.mode,
// @ts-expect-error can't refine because ESLint will complain
props.options.baseline,
// @ts-expect-error can't refine because ESLint will complain
props.options.comparison,
]);
}, [props.mode, props.options]);
const { baseline, comparison } =
options.mode === FlameGraphNormalizationMode.Time
? getScaleFactorsBasedOnTime({
comparisonTotalSeconds: props.comparisonTotalSeconds,
totalSeconds: props.totalSeconds,
})
: { comparison: options.comparison, baseline: options.baseline };
mode === FlameGraphNormalizationMode.Time
? { comparison: options.comparisonTime, baseline: options.baselineTime }
: { comparison: options.comparisonScale, baseline: options.baselineScale };
return (
<EuiPopover
@ -133,7 +110,7 @@ export function NormalizationMenu(props: Props) {
padding: '0 16px',
}}
>
{props.options.mode === FlameGraphNormalizationMode.Scale ? SCALE_LABEL : TIME_LABEL}
{props.mode === FlameGraphNormalizationMode.Scale ? SCALE_LABEL : TIME_LABEL}
</EuiFlexItem>
</EuiFormControlLayout>
}
@ -183,17 +160,12 @@ export function NormalizationMenu(props: Props) {
buttonSize="compressed"
isFullWidth
onChange={(id, value) => {
setOptions((prevOptions) => ({
...prevOptions,
...(id === FlameGraphNormalizationMode.Time
? { mode: FlameGraphNormalizationMode.Time }
: { mode: FlameGraphNormalizationMode.Scale, baseline: 1, comparison: 1 }),
}));
setMode(id as FlameGraphNormalizationMode);
}}
legend={i18n.translate('xpack.profiling.flameGraphNormalizationMode.selectModeLegend', {
defaultMessage: 'Select a normalization mode for the flamegraph',
})}
idSelected={options.mode}
idSelected={mode}
options={[
{
id: FlameGraphNormalizationMode.Scale,
@ -223,9 +195,14 @@ export function NormalizationMenu(props: Props) {
id={baselineScaleFactorInputId}
value={baseline}
onChange={(e) => {
setOptions((prevOptions) => ({ ...prevOptions, baseline: e.target.valueAsNumber }));
if (mode === FlameGraphNormalizationMode.Scale) {
setOptions((prevOptions) => ({
...prevOptions,
baselineScale: e.target.valueAsNumber,
}));
}
}}
disabled={options.mode === FlameGraphNormalizationMode.Time}
disabled={mode === FlameGraphNormalizationMode.Time}
/>
</EuiFormControlLayout>
<EuiSpacer size="m" />
@ -246,18 +223,20 @@ export function NormalizationMenu(props: Props) {
id={comparisonScaleFactorInputId}
value={comparison}
onChange={(e) => {
setOptions((prevOptions) => ({
...prevOptions,
comparison: e.target.valueAsNumber,
}));
if (mode === FlameGraphNormalizationMode.Scale) {
setOptions((prevOptions) => ({
...prevOptions,
comparisonScale: e.target.valueAsNumber,
}));
}
}}
disabled={options.mode === FlameGraphNormalizationMode.Time}
disabled={mode === FlameGraphNormalizationMode.Time}
/>
</EuiFormControlLayout>
<EuiSpacer size="m" />
<EuiButton
onClick={() => {
props.onChange(options);
props.onChange(mode, options);
setIsPopoverOpen(false);
}}
fullWidth