[Profiling] Show values of highlighted sample in TopN chart (#147431)

Closes https://github.com/elastic/prodfiler/issues/2842
This commit is contained in:
Dario Gieselaar 2022-12-21 11:38:42 +01:00 committed by GitHub
parent 9f54ad46c6
commit d01b5de252
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 106 additions and 41 deletions

View file

@ -19,11 +19,13 @@ export const OTHER_BUCKET_LABEL = i18n.translate('xpack.profiling.topn.otherBuck
export interface CountPerTime {
Timestamp: number;
Percentage: number;
Count: number | null;
}
export interface TopNSample extends CountPerTime {
Category: string;
Percentage: number;
}
export interface TopNSamples {
@ -157,6 +159,8 @@ export function createTopNSamples(
// from the total
const otherFrameCountsByTimestamp = new Map<number, number>();
const totalFrameCountsByTimestamp = new Map<number, number>();
let addOtherBucket = false;
for (let i = 0; i < response.over_time.buckets.length; i++) {
@ -170,6 +174,8 @@ export function createTopNSamples(
}
otherFrameCountsByTimestamp.set(timestamp, valueForOtherBucket);
totalFrameCountsByTimestamp.set(timestamp, bucket.count.value ?? 0);
}
// Only add the 'other' bucket if at least one value per timestamp is > 0
@ -190,10 +196,12 @@ export function createTopNSamples(
for (const category of bucketsByCategories.keys()) {
const frameCountsByTimestamp = bucketsByCategories.get(category);
for (const timestamp of timestamps) {
const count = frameCountsByTimestamp.get(timestamp) ?? 0;
const sample: TopNSample = {
Timestamp: timestamp,
Count: frameCountsByTimestamp.get(timestamp) ?? 0,
Count: count,
Category: category,
Percentage: (count / totalFrameCountsByTimestamp.get(timestamp)!) * 100,
};
samples.push(sample);
}
@ -233,6 +241,7 @@ export function groupSamplesByCategory({
}
const series = seriesByCategory.get(sample.Category)!;
series.push({
Percentage: sample.Percentage,
Timestamp: sample.Timestamp,
Count: sample.Count,
});

View file

@ -44,6 +44,7 @@ export const ChartGrid: React.FC<ChartGridProps> = ({ limit, charts, showFrames
metadata={subchart.Metadata}
height={200}
data={subchart.Series}
sample={null}
showAxes
onShowMoreClick={() => {
setSelectedSubchart(subchart);
@ -71,6 +72,7 @@ export const ChartGrid: React.FC<ChartGridProps> = ({ limit, charts, showFrames
metadata={selectedSubchart.Metadata}
height={200}
data={selectedSubchart.Series}
sample={null}
showAxes
onShowMoreClick={null}
showFrames={showFrames}

View file

@ -8,7 +8,7 @@ import { EuiButton, EuiButtonGroup, EuiFlexGroup, EuiFlexItem, EuiPanel } from '
import { i18n } from '@kbn/i18n';
import React, { useState } from 'react';
import { StackTracesDisplayOption, TopNType } from '../../../common/stack_traces';
import { groupSamplesByCategory, TopNResponse, TopNSubchart } from '../../../common/topn';
import { groupSamplesByCategory, TopNResponse, TopNSample } from '../../../common/topn';
import { useProfilingParams } from '../../hooks/use_profiling_params';
import { useProfilingRouter } from '../../hooks/use_profiling_router';
import { useProfilingRoutePath } from '../../hooks/use_profiling_route_path';
@ -78,9 +78,12 @@ export function StackTracesView() {
[topNType, timeRange.start, timeRange.end, fetchTopN, kuery]
);
const [highlightedSubchart, setHighlightedSubchart] = useState<TopNSubchart | undefined>(
undefined
);
const [highlightedSample, setHighlightedSample] = useState<TopNSample | null>(null);
const highlightedSubchart =
(highlightedSample &&
state.data?.charts.find((chart) => chart.Category === highlightedSample?.Category)) ||
null;
const { data } = state;
@ -143,14 +146,13 @@ export function StackTracesView() {
},
});
}}
onSampleClick={(sample) => {
setHighlightedSubchart(
data?.charts.find((subchart) => subchart.Category === sample.Category)
);
onSampleOver={(sample) => {
setHighlightedSample(sample);
}}
onSampleOut={() => {
setHighlightedSubchart(undefined);
setHighlightedSample(null);
}}
highlightedSample={highlightedSample}
highlightedSubchart={highlightedSubchart}
showFrames={topNType === TopNType.Traces}
/>

View file

@ -26,10 +26,15 @@ import { useProfilingChartsTheme } from '../hooks/use_profiling_charts_theme';
import { asPercentage } from '../utils/formatters/as_percentage';
import { SubChart } from './subchart';
function SubchartTooltip({
const SubchartTooltip = ({
highlightedSubchart,
highlightedSample,
showFrames,
}: TooltipInfo & { highlightedSubchart: TopNSubchart; showFrames: boolean }) {
}: TooltipInfo & {
highlightedSubchart: TopNSubchart;
highlightedSample: TopNSample | null;
showFrames: boolean;
}) => {
// max tooltip width - 2 * padding (16px)
const width = 224;
return (
@ -39,8 +44,9 @@ function SubchartTooltip({
color={highlightedSubchart.Color}
category={highlightedSubchart.Category}
label={highlightedSubchart.Label}
percentage={highlightedSubchart.Percentage}
data={highlightedSubchart.Series}
percentage={highlightedSubchart.Percentage}
sample={highlightedSample}
showFrames={showFrames}
/* we don't show metadata in tooltips */
metadata={[]}
@ -52,15 +58,16 @@ function SubchartTooltip({
/>
</EuiPanel>
);
}
};
export interface StackedBarChartProps {
height: number;
asPercentages: boolean;
onBrushEnd: (range: { rangeFrom: string; rangeTo: string }) => void;
onSampleClick: (sample: TopNSample) => void;
onSampleOver: (sample: TopNSample) => void;
onSampleOut: () => void;
highlightedSubchart?: TopNSubchart;
highlightedSample: TopNSample | null;
highlightedSubchart: TopNSubchart | null;
charts: TopNSubchart[];
showFrames: boolean;
}
@ -69,8 +76,9 @@ export const StackedBarChart: React.FC<StackedBarChartProps> = ({
height,
asPercentages,
onBrushEnd,
onSampleClick,
onSampleOut,
onSampleOver,
highlightedSample,
highlightedSubchart,
charts,
showFrames,
@ -79,6 +87,17 @@ export const StackedBarChart: React.FC<StackedBarChartProps> = ({
const { chartsBaseTheme, chartsTheme } = useProfilingChartsTheme();
const customTooltip = highlightedSubchart
? (props: TooltipInfo) => (
<SubchartTooltip
{...props}
highlightedSubchart={highlightedSubchart!}
highlightedSample={highlightedSample}
showFrames={showFrames}
/>
)
: () => <></>;
return (
<Chart size={{ height }}>
<Settings
@ -92,30 +111,15 @@ export const StackedBarChart: React.FC<StackedBarChartProps> = ({
}}
baseTheme={chartsBaseTheme}
theme={chartsTheme}
onElementClick={(events) => {
onElementOver={(events) => {
const [value] = events[0] as XYChartElementEvent;
onSampleClick(value.datum as TopNSample);
}}
onElementOver={() => {
onSampleOut();
onSampleOver(value.datum as TopNSample);
}}
onElementOut={() => {
onSampleOut();
}}
/>
<Tooltip
customTooltip={
highlightedSubchart
? (props) => (
<SubchartTooltip
{...props}
showFrames={showFrames}
highlightedSubchart={highlightedSubchart}
/>
)
: () => <></>
}
/>
<Tooltip customTooltip={customTooltip} />
{charts.map((chart) => (
<HistogramBarSeries
key={chart.Category}

View file

@ -6,10 +6,13 @@
*/
import {
AnnotationDomainType,
AreaSeries,
Axis,
Chart,
CurveType,
LineAnnotation,
Position,
ScaleType,
Settings,
timeFormatter,
@ -20,6 +23,7 @@ import {
EuiFlexGroup,
EuiFlexItem,
EuiHorizontalRule,
EuiIcon,
EuiLink,
EuiSpacer,
EuiText,
@ -29,11 +33,12 @@ import { i18n } from '@kbn/i18n';
import React from 'react';
import { StackFrameMetadata } from '../../common/profiling';
import { getFieldNameForTopNType, TopNType } from '../../common/stack_traces';
import { CountPerTime, OTHER_BUCKET_LABEL } from '../../common/topn';
import { CountPerTime, OTHER_BUCKET_LABEL, TopNSample } from '../../common/topn';
import { useKibanaTimeZoneSetting } from '../hooks/use_kibana_timezone_setting';
import { useProfilingChartsTheme } from '../hooks/use_profiling_charts_theme';
import { useProfilingParams } from '../hooks/use_profiling_params';
import { useProfilingRouter } from '../hooks/use_profiling_router';
import { asNumber } from '../utils/formatters/as_number';
import { asPercentage } from '../utils/formatters/as_percentage';
import { StackFrameSummary } from './stack_frame_summary';
@ -52,6 +57,7 @@ export interface SubChartProps {
style?: React.ComponentProps<typeof EuiFlexGroup>['style'];
showFrames: boolean;
padTitle: boolean;
sample: TopNSample | null;
}
const NUM_DISPLAYED_FRAMES = 5;
@ -71,6 +77,7 @@ export const SubChart: React.FC<SubChartProps> = ({
style,
showFrames,
padTitle,
sample,
}) => {
const theme = useEuiTheme();
@ -196,7 +203,23 @@ export const SubChart: React.FC<SubChartProps> = ({
)}
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="s">{asPercentage(percentage / 100)}</EuiText>
<EuiFlexGroup direction="column" gutterSize="xs" alignItems="flexEnd">
{sample ? (
<EuiFlexItem>
<EuiText size="m">{asPercentage(sample.Percentage / 100)}</EuiText>
</EuiFlexItem>
) : null}
<EuiFlexItem>
<EuiText size={sample ? 'xs' : 's'}>
{sample
? i18n.translate('xpack.profiling.stackFrames.subChart.avg', {
defaultMessage: 'avg. {percentage}',
values: { percentage: asPercentage(percentage / 100) },
})
: asPercentage(percentage / 100)}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
@ -220,6 +243,23 @@ export const SubChart: React.FC<SubChartProps> = ({
curve={CurveType.CURVE_STEP_AFTER}
color={color}
/>
{sample ? (
<LineAnnotation
id="highlighted_sample"
domainType={AnnotationDomainType.XDomain}
dataValues={[{ dataValue: sample.Timestamp }]}
style={{
line: {
strokeWidth: 2,
dash: [4, 4],
opacity: 0.5,
},
}}
marker={<EuiIcon type="dot" />}
markerPosition={Position.Top}
hideTooltips
/>
) : null}
{showAxes ? (
<Axis
id="bottom-axis"
@ -252,10 +292,12 @@ export const SubChart: React.FC<SubChartProps> = ({
backgroundColor: `rgba(255, 255, 255, 0.75)`,
}}
>
{i18n.translate('xpack.profiling.maxValue', {
defaultMessage: 'Max: {max}',
values: { max: Math.max(...data.map((value) => value.Count ?? 0)) },
})}
{sample
? asNumber(sample.Count!)
: i18n.translate('xpack.profiling.maxValue', {
defaultMessage: 'Max: {max}',
values: { max: asNumber(Math.max(...data.map((value) => value.Count ?? 0))) },
})}
</div>
) : null}
</EuiFlexItem>

View file

@ -5,7 +5,13 @@
* 2.0.
*/
import { NOT_AVAILABLE_LABEL } from '../../../common';
export function asNumber(value: number): string {
if (isNaN(value)) {
return NOT_AVAILABLE_LABEL;
}
if (value === 0) {
return '0';
}