[Profiling] link functions to flamegraph (#160548)

e213e261-0d26-44e0-8786-407f29d6fa55
This commit is contained in:
Cauê Marcondes 2023-06-28 09:40:55 +01:00 committed by GitHub
parent 384cf7864b
commit faab91c106
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 405 additions and 249 deletions

View file

@ -13,6 +13,7 @@ import {
PartialTheme, PartialTheme,
Settings, Settings,
Tooltip, Tooltip,
FlameSpec,
} from '@elastic/charts'; } from '@elastic/charts';
import { EuiFlexGroup, EuiFlexItem, useEuiTheme } from '@elastic/eui'; import { EuiFlexGroup, EuiFlexItem, useEuiTheme } from '@elastic/eui';
import { Maybe } from '@kbn/observability-plugin/common/typings'; import { Maybe } from '@kbn/observability-plugin/common/typings';
@ -27,13 +28,15 @@ import { ComparisonMode } from '../normalization_menu';
interface Props { interface Props {
id: string; id: string;
comparisonMode: ComparisonMode; comparisonMode?: ComparisonMode;
primaryFlamegraph?: ElasticFlameGraph; primaryFlamegraph?: ElasticFlameGraph;
comparisonFlamegraph?: ElasticFlameGraph; comparisonFlamegraph?: ElasticFlameGraph;
baseline?: number; baseline?: number;
comparison?: number; comparison?: number;
showInformationWindow: boolean; showInformationWindow: boolean;
toggleShowInformationWindow: () => void; toggleShowInformationWindow: () => void;
searchText?: string;
onChangeSearchText?: FlameSpec['onSearchTextChange'];
} }
export function FlameGraph({ export function FlameGraph({
@ -45,6 +48,8 @@ export function FlameGraph({
comparison, comparison,
showInformationWindow, showInformationWindow,
toggleShowInformationWindow, toggleShowInformationWindow,
searchText,
onChangeSearchText,
}: Props) { }: Props) {
const theme = useEuiTheme(); const theme = useEuiTheme();
@ -165,6 +170,8 @@ export function FlameGraph({
valueFormatter={(value) => `${value}`} valueFormatter={(value) => `${value}`}
animation={{ duration: 100 }} animation={{ duration: 100 }}
controlProviderCallback={{}} controlProviderCallback={{}}
search={searchText ? { text: searchText } : undefined}
onSearchTextChange={onChangeSearchText}
/> />
</Chart> </Chart>
</EuiFlexItem> </EuiFlexItem>

View file

@ -1,26 +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 { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
import React from 'react';
import { getCalleeFunction, getCalleeSource, StackFrameMetadata } from '../../common/profiling';
export function StackFrameSummary({ frame }: { frame: StackFrameMetadata }) {
return (
<EuiFlexGroup direction="column" gutterSize="xs">
<EuiFlexItem>
<div>
<EuiText size="s" style={{ fontWeight: 'bold', overflowWrap: 'anywhere' }}>
{getCalleeFunction(frame)}
</EuiText>
</div>
</EuiFlexItem>
<EuiFlexItem style={{ overflowWrap: 'anywhere' }}>
<EuiText size="s">{getCalleeSource(frame) || ''}</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
);
}

View file

@ -0,0 +1,51 @@
/*
* 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, EuiLink, EuiText } from '@elastic/eui';
import React from 'react';
import { getCalleeFunction, getCalleeSource, StackFrameMetadata } from '../../../common/profiling';
interface Props {
frame: StackFrameMetadata;
onFrameClick?: (functionName: string) => void;
}
function CalleeFunctionText({ calleeFunctionName }: { calleeFunctionName: string }) {
return (
<EuiText size="s" style={{ fontWeight: 'bold', overflowWrap: 'anywhere' }}>
{calleeFunctionName}
</EuiText>
);
}
export function StackFrameSummary({ frame, onFrameClick }: Props) {
const calleeFunctionName = getCalleeFunction(frame);
function handleOnClick() {
if (onFrameClick) {
onFrameClick(calleeFunctionName);
}
}
return (
<EuiFlexGroup direction="column" gutterSize="xs">
<EuiFlexItem>
<div>
{onFrameClick ? (
<EuiLink onClick={handleOnClick}>
<CalleeFunctionText calleeFunctionName={calleeFunctionName} />
</EuiLink>
) : (
<CalleeFunctionText calleeFunctionName={calleeFunctionName} />
)}
</div>
</EuiFlexItem>
<EuiFlexItem style={{ overflowWrap: 'anywhere' }}>
<EuiText size="s">{getCalleeSource(frame) || ''}</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
);
}

View file

@ -17,7 +17,6 @@ import {
Tooltip, Tooltip,
XYChartElementEvent, XYChartElementEvent,
TooltipContainer, TooltipContainer,
CustomTooltip,
} from '@elastic/charts'; } from '@elastic/charts';
import { EuiPanel } from '@elastic/eui'; import { EuiPanel } from '@elastic/eui';
import { keyBy } from 'lodash'; import { keyBy } from 'lodash';
@ -57,7 +56,7 @@ export function StackedBarChart({
const { chartsBaseTheme, chartsTheme } = useProfilingChartsTheme(); const { chartsBaseTheme, chartsTheme } = useProfilingChartsTheme();
const CustomTooltipWithSubChart: CustomTooltip = () => { function CustomTooltipWithSubChart() {
if (!highlightedSample) { if (!highlightedSample) {
return null; return null;
} }
@ -90,7 +89,7 @@ export function StackedBarChart({
</EuiPanel> </EuiPanel>
</TooltipContainer> </TooltipContainer>
); );
}; }
return ( return (
<Chart size={{ height }}> <Chart size={{ height }}>

View file

@ -146,6 +146,7 @@ interface Props {
isDifferentialView: boolean; isDifferentialView: boolean;
baselineScaleFactor?: number; baselineScaleFactor?: number;
comparisonScaleFactor?: number; comparisonScaleFactor?: number;
onFrameClick?: (functionName: string) => void;
} }
function scaleValue({ value, scaleFactor = 1 }: { value: number; scaleFactor?: number }) { function scaleValue({ value, scaleFactor = 1 }: { value: number; scaleFactor?: number }) {
@ -162,6 +163,7 @@ export function TopNFunctionsTable({
isDifferentialView, isDifferentialView,
baselineScaleFactor, baselineScaleFactor,
comparisonScaleFactor, comparisonScaleFactor,
onFrameClick,
}: Props) { }: Props) {
const [selectedRow, setSelectedRow] = useState<Row | undefined>(); const [selectedRow, setSelectedRow] = useState<Row | undefined>();
const isEstimatedA = (topNFunctions?.SamplingRate ?? 1.0) !== 1.0; const isEstimatedA = (topNFunctions?.SamplingRate ?? 1.0) !== 1.0;
@ -260,7 +262,9 @@ export function TopNFunctionsTable({
name: i18n.translate('xpack.profiling.functionsView.functionColumnLabel', { name: i18n.translate('xpack.profiling.functionsView.functionColumnLabel', {
defaultMessage: 'Function', defaultMessage: 'Function',
}), }),
render: (_, { frame }) => <StackFrameSummary frame={frame} />, render: (_, { frame }) => {
return <StackFrameSummary frame={frame} onFrameClick={onFrameClick} />;
},
width: '50%', width: '50%',
}, },
{ {

View file

@ -13,7 +13,9 @@ import { TopNFunctionSortField, topNFunctionSortFieldRt } from '../../common/fun
import { StackTracesDisplayOption, TopNType } from '../../common/stack_traces'; import { StackTracesDisplayOption, TopNType } from '../../common/stack_traces';
import { ComparisonMode, NormalizationMode } from '../components/normalization_menu'; import { ComparisonMode, NormalizationMode } from '../components/normalization_menu';
import { RedirectTo } from '../components/redirect_to'; import { RedirectTo } from '../components/redirect_to';
import { FlameGraphsView } from '../views/flame_graphs_view'; import { FlameGraphsView } from '../views/flamegraphs';
import { DifferentialFlameGraphsView } from '../views/flamegraphs/differential_flamegraphs';
import { FlameGraphView } from '../views/flamegraphs/flamegraph';
import { FunctionsView } from '../views/functions'; import { FunctionsView } from '../views/functions';
import { DifferentialTopNFunctionsView } from '../views/functions/differential_topn'; import { DifferentialTopNFunctionsView } from '../views/functions/differential_topn';
import { TopNFunctionsView } from '../views/functions/topn'; import { TopNFunctionsView } from '../views/functions/topn';
@ -109,9 +111,14 @@ const routes = {
})} })}
href="/flamegraphs/flamegraph" href="/flamegraphs/flamegraph"
> >
<Outlet /> <FlameGraphView />
</RouteBreadcrumb> </RouteBreadcrumb>
), ),
params: t.type({
query: t.partial({
searchText: t.string,
}),
}),
}, },
'/flamegraphs/differential': { '/flamegraphs/differential': {
element: ( element: (
@ -121,7 +128,7 @@ const routes = {
})} })}
href="/flamegraphs/differential" href="/flamegraphs/differential"
> >
<Outlet /> <DifferentialFlameGraphsView />
</RouteBreadcrumb> </RouteBreadcrumb>
), ),
params: t.type({ params: t.type({
@ -134,19 +141,23 @@ const routes = {
t.literal(ComparisonMode.Absolute), t.literal(ComparisonMode.Absolute),
t.literal(ComparisonMode.Relative), t.literal(ComparisonMode.Relative),
]), ]),
}),
t.partial({
normalizationMode: t.union([ normalizationMode: t.union([
t.literal(NormalizationMode.Scale), t.literal(NormalizationMode.Scale),
t.literal(NormalizationMode.Time), t.literal(NormalizationMode.Time),
]), ]),
}),
t.partial({
baseline: toNumberRt, baseline: toNumberRt,
comparison: toNumberRt, comparison: toNumberRt,
searchText: t.string,
}), }),
]), ]),
}), }),
defaults: { defaults: {
query: { query: {
comparisonRangeFrom: 'now-15m',
comparisonRangeTo: 'now',
comparisonKuery: '',
comparisonMode: ComparisonMode.Absolute, comparisonMode: ComparisonMode.Absolute,
normalizationMode: NormalizationMode.Time, normalizationMode: NormalizationMode.Time,
}, },

View file

@ -31,7 +31,7 @@ export function getFlamegraphModel({
colorSuccess, colorSuccess,
colorDanger, colorDanger,
colorNeutral, colorNeutral,
comparisonMode, comparisonMode = ComparisonMode.Absolute,
comparison, comparison,
baseline, baseline,
}: { }: {
@ -40,7 +40,7 @@ export function getFlamegraphModel({
colorSuccess: string; colorSuccess: string;
colorDanger: string; colorDanger: string;
colorNeutral: string; colorNeutral: string;
comparisonMode: ComparisonMode; comparisonMode?: ComparisonMode;
baseline?: number; baseline?: number;
comparison?: number; comparison?: number;
}): { }): {

View file

@ -1,182 +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 { EuiFlexGroup, EuiFlexItem, EuiPageHeaderContentProps } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { get } from 'lodash';
import React, { useState } from 'react';
import { useProfilingParams } from '../../hooks/use_profiling_params';
import { useProfilingRouter } from '../../hooks/use_profiling_router';
import { useProfilingRoutePath } from '../../hooks/use_profiling_route_path';
import { useTimeRange } from '../../hooks/use_time_range';
import { useTimeRangeAsync } from '../../hooks/use_time_range_async';
import { AsyncComponent } from '../../components/async_component';
import { useProfilingDependencies } from '../../components/contexts/profiling_dependencies/use_profiling_dependencies';
import { FlameGraph } from '../../components/flamegraph';
import { ProfilingAppPageTemplate } from '../../components/profiling_app_page_template';
import { RedirectTo } from '../../components/redirect_to';
import { FlameGraphSearchPanel } from './flame_graph_search_panel';
import {
ComparisonMode,
NormalizationMode,
NormalizationOptions,
} from '../../components/normalization_menu';
export function FlameGraphsView({ children }: { children: React.ReactElement }) {
const {
query,
query: { rangeFrom, rangeTo, kuery },
} = useProfilingParams('/flamegraphs/*');
const timeRange = useTimeRange({ rangeFrom, rangeTo });
const comparisonTimeRange = useTimeRange(
'comparisonRangeFrom' in query
? { rangeFrom: query.comparisonRangeFrom, rangeTo: query.comparisonRangeTo, optional: true }
: { rangeFrom: undefined, rangeTo: undefined, optional: true }
);
const comparisonKuery = 'comparisonKuery' in query ? query.comparisonKuery : '';
const comparisonMode = 'comparisonMode' in query ? query.comparisonMode : ComparisonMode.Absolute;
const normalizationMode: NormalizationMode = get(
query,
'normalizationMode',
NormalizationMode.Time
);
const baselineScale: number = get(query, 'baseline', 1);
const comparisonScale: number = get(query, 'comparison', 1);
const totalSeconds = timeRange.inSeconds.end - timeRange.inSeconds.start;
const totalComparisonSeconds =
(new Date(comparisonTimeRange.end!).getTime() -
new Date(comparisonTimeRange.start!).getTime()) /
1000;
const baselineTime = 1;
const comparisonTime = totalSeconds / totalComparisonSeconds;
const normalizationOptions: NormalizationOptions = {
baselineScale,
baselineTime,
comparisonScale,
comparisonTime,
};
const {
services: { fetchElasticFlamechart },
} = useProfilingDependencies();
const state = useTimeRangeAsync(
({ http }) => {
return Promise.all([
fetchElasticFlamechart({
http,
timeFrom: timeRange.inSeconds.start,
timeTo: timeRange.inSeconds.end,
kuery,
}),
comparisonTimeRange.inSeconds.start && comparisonTimeRange.inSeconds.end
? fetchElasticFlamechart({
http,
timeFrom: comparisonTimeRange.inSeconds.start,
timeTo: comparisonTimeRange.inSeconds.end,
kuery: comparisonKuery,
})
: Promise.resolve(undefined),
]).then(([primaryFlamegraph, comparisonFlamegraph]) => {
return {
primaryFlamegraph,
comparisonFlamegraph,
};
});
},
[
timeRange.inSeconds.start,
timeRange.inSeconds.end,
kuery,
comparisonTimeRange.inSeconds.start,
comparisonTimeRange.inSeconds.end,
comparisonKuery,
fetchElasticFlamechart,
]
);
const { data } = state;
const routePath = useProfilingRoutePath();
const profilingRouter = useProfilingRouter();
const isDifferentialView = routePath === '/flamegraphs/differential';
const tabs: Required<EuiPageHeaderContentProps>['tabs'] = [
{
label: i18n.translate('xpack.profiling.flameGraphsView.flameGraphTabLabel', {
defaultMessage: 'Flamegraph',
}),
isSelected: !isDifferentialView,
href: profilingRouter.link('/flamegraphs/flamegraph', { query }),
},
{
label: i18n.translate('xpack.profiling.flameGraphsView.differentialFlameGraphTabLabel', {
defaultMessage: 'Differential flamegraph',
}),
isSelected: isDifferentialView,
href: profilingRouter.link('/flamegraphs/differential', {
// @ts-expect-error Code gets too complicated to satisfy TS constraints
query: {
...query,
comparisonRangeFrom: query.rangeFrom,
comparisonRangeTo: query.rangeTo,
comparisonKuery: query.kuery,
},
}),
},
];
const [showInformationWindow, setShowInformationWindow] = useState(false);
function toggleShowInformationWindow() {
setShowInformationWindow((prev) => !prev);
}
if (routePath === '/flamegraphs') {
return <RedirectTo pathname="/flamegraphs/flamegraph" />;
}
const isNormalizedByTime = normalizationMode === NormalizationMode.Time;
return (
<ProfilingAppPageTemplate tabs={tabs} hideSearchBar={true}>
<EuiFlexGroup direction="column">
<EuiFlexItem grow={false}>
<FlameGraphSearchPanel
isDifferentialView={isDifferentialView}
comparisonMode={comparisonMode}
normalizationMode={normalizationMode}
normalizationOptions={normalizationOptions}
/>
</EuiFlexItem>
<EuiFlexItem>
<AsyncComponent {...state} style={{ height: '100%' }} size="xl">
<FlameGraph
id="flamechart"
primaryFlamegraph={data?.primaryFlamegraph}
comparisonFlamegraph={data?.comparisonFlamegraph}
comparisonMode={comparisonMode}
baseline={isNormalizedByTime ? baselineTime : baselineScale}
comparison={isNormalizedByTime ? comparisonTime : comparisonScale}
showInformationWindow={showInformationWindow}
toggleShowInformationWindow={toggleShowInformationWindow}
/>
</AsyncComponent>
{children}
</EuiFlexItem>
</EuiFlexGroup>
</ProfilingAppPageTemplate>
);
}

View file

@ -6,30 +6,27 @@
*/ */
import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiPanel } from '@elastic/eui'; import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiPanel } from '@elastic/eui';
import React from 'react'; import React from 'react';
import { useProfilingParams } from '../../hooks/use_profiling_params'; import { useProfilingParams } from '../../../hooks/use_profiling_params';
import { useProfilingRouter } from '../../hooks/use_profiling_router'; import { useProfilingRouter } from '../../../hooks/use_profiling_router';
import { useProfilingRoutePath } from '../../hooks/use_profiling_route_path'; import { useProfilingRoutePath } from '../../../hooks/use_profiling_route_path';
import { PrimaryAndComparisonSearchBar } from '../../components/primary_and_comparison_search_bar'; import { PrimaryAndComparisonSearchBar } from '../../../components/primary_and_comparison_search_bar';
import { PrimaryProfilingSearchBar } from '../../components/profiling_app_page_template/primary_profiling_search_bar';
import { import {
ComparisonMode, ComparisonMode,
NormalizationMode, NormalizationMode,
NormalizationOptions, NormalizationOptions,
NormalizationMenu, NormalizationMenu,
} from '../../components/normalization_menu'; } from '../../../components/normalization_menu';
import { DifferentialComparisonMode } from '../../components/differential_comparison_mode'; import { DifferentialComparisonMode } from '../../../components/differential_comparison_mode';
interface Props { interface Props {
isDifferentialView: boolean;
comparisonMode: ComparisonMode; comparisonMode: ComparisonMode;
normalizationMode: NormalizationMode; normalizationMode: NormalizationMode;
normalizationOptions: NormalizationOptions; normalizationOptions: NormalizationOptions;
} }
export function FlameGraphSearchPanel({ export function DifferentialFlameGraphSearchPanel({
comparisonMode, comparisonMode,
normalizationMode, normalizationMode,
isDifferentialView,
normalizationOptions, normalizationOptions,
}: Props) { }: Props) {
const { path, query } = useProfilingParams('/flamegraphs/*'); const { path, query } = useProfilingParams('/flamegraphs/*');
@ -77,29 +74,25 @@ export function FlameGraphSearchPanel({
} }
return ( return (
<EuiPanel hasShadow={false} color="subdued"> <EuiPanel hasShadow={false} color="subdued">
{isDifferentialView ? <PrimaryAndComparisonSearchBar /> : <PrimaryProfilingSearchBar />} <PrimaryAndComparisonSearchBar />
<EuiHorizontalRule /> <EuiHorizontalRule />
<EuiFlexGroup direction="row"> <EuiFlexGroup direction="row">
{isDifferentialView && ( <DifferentialComparisonMode
<> comparisonMode={comparisonMode}
<DifferentialComparisonMode onChange={onChangeComparisonMode}
comparisonMode={comparisonMode} />
onChange={onChangeComparisonMode} {comparisonMode === ComparisonMode.Absolute && (
/> <EuiFlexItem grow={false}>
{comparisonMode === ComparisonMode.Absolute && ( <EuiFlexGroup direction="row" gutterSize="m" alignItems="center">
<EuiFlexItem grow={false}> <EuiFlexItem grow={false}>
<EuiFlexGroup direction="row" gutterSize="m" alignItems="center"> <NormalizationMenu
<EuiFlexItem grow={false}> onChange={onChangeNormalizationMode}
<NormalizationMenu mode={normalizationMode}
onChange={onChangeNormalizationMode} options={normalizationOptions}
mode={normalizationMode} />
options={normalizationOptions}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem> </EuiFlexItem>
)} </EuiFlexGroup>
</> </EuiFlexItem>
)} )}
</EuiFlexGroup> </EuiFlexGroup>
</EuiPanel> </EuiPanel>

View file

@ -0,0 +1,144 @@
/*
* 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 } from '@elastic/eui';
import React, { useState } from 'react';
import { AsyncComponent } from '../../../components/async_component';
import { useProfilingDependencies } from '../../../components/contexts/profiling_dependencies/use_profiling_dependencies';
import { FlameGraph } from '../../../components/flamegraph';
import { NormalizationMode, NormalizationOptions } from '../../../components/normalization_menu';
import { useProfilingParams } from '../../../hooks/use_profiling_params';
import { useProfilingRouter } from '../../../hooks/use_profiling_router';
import { useProfilingRoutePath } from '../../../hooks/use_profiling_route_path';
import { useTimeRange } from '../../../hooks/use_time_range';
import { useTimeRangeAsync } from '../../../hooks/use_time_range_async';
import { DifferentialFlameGraphSearchPanel } from './differential_flame_graph_search_panel';
export function DifferentialFlameGraphsView() {
const {
query,
query: {
rangeFrom,
rangeTo,
kuery,
comparisonRangeFrom,
comparisonRangeTo,
comparisonKuery,
comparisonMode,
baseline = 1,
comparison = 1,
normalizationMode,
searchText,
},
} = useProfilingParams('/flamegraphs/differential');
const routePath = useProfilingRoutePath();
const profilingRouter = useProfilingRouter();
const [showInformationWindow, setShowInformationWindow] = useState(false);
const timeRange = useTimeRange({ rangeFrom, rangeTo });
const comparisonTimeRange = useTimeRange({
rangeFrom: comparisonRangeFrom,
rangeTo: comparisonRangeTo,
optional: true,
});
const {
services: { fetchElasticFlamechart },
} = useProfilingDependencies();
const state = useTimeRangeAsync(
({ http }) => {
return Promise.all([
fetchElasticFlamechart({
http,
timeFrom: timeRange.inSeconds.start,
timeTo: timeRange.inSeconds.end,
kuery,
}),
comparisonTimeRange.inSeconds.start && comparisonTimeRange.inSeconds.end
? fetchElasticFlamechart({
http,
timeFrom: comparisonTimeRange.inSeconds.start,
timeTo: comparisonTimeRange.inSeconds.end,
kuery: comparisonKuery,
})
: Promise.resolve(undefined),
]).then(([primaryFlamegraph, comparisonFlamegraph]) => {
return {
primaryFlamegraph,
comparisonFlamegraph,
};
});
},
[
timeRange.inSeconds.start,
timeRange.inSeconds.end,
kuery,
comparisonTimeRange.inSeconds.start,
comparisonTimeRange.inSeconds.end,
comparisonKuery,
fetchElasticFlamechart,
]
);
const totalSeconds = timeRange.inSeconds.end - timeRange.inSeconds.start;
const totalComparisonSeconds =
(new Date(comparisonTimeRange.end!).getTime() -
new Date(comparisonTimeRange.start!).getTime()) /
1000;
const baselineTime = 1;
const comparisonTime = totalSeconds / totalComparisonSeconds;
const normalizationOptions: NormalizationOptions = {
baselineScale: baseline,
baselineTime,
comparisonScale: comparison,
comparisonTime,
};
const { data } = state;
const isNormalizedByTime = normalizationMode === NormalizationMode.Time;
function toggleShowInformationWindow() {
setShowInformationWindow((prev) => !prev);
}
function handleSearchTextChange(newSearchText: string) {
// @ts-expect-error Code gets too complicated to satisfy TS constraints
profilingRouter.push(routePath, { query: { ...query, searchText: newSearchText } });
}
return (
<EuiFlexGroup direction="column">
<EuiFlexItem grow={false}>
<DifferentialFlameGraphSearchPanel
comparisonMode={comparisonMode}
normalizationMode={normalizationMode}
normalizationOptions={normalizationOptions}
/>
</EuiFlexItem>
<EuiFlexItem>
<AsyncComponent {...state} style={{ height: '100%' }} size="xl">
<FlameGraph
id="flamechart"
primaryFlamegraph={data?.primaryFlamegraph}
comparisonFlamegraph={data?.comparisonFlamegraph}
comparisonMode={comparisonMode}
baseline={isNormalizedByTime ? baselineTime : baseline}
comparison={isNormalizedByTime ? comparisonTime : comparison}
showInformationWindow={showInformationWindow}
toggleShowInformationWindow={toggleShowInformationWindow}
searchText={searchText}
onChangeSearchText={handleSearchTextChange}
/>
</AsyncComponent>
</EuiFlexItem>
</EuiFlexGroup>
);
}

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, EuiHorizontalRule, EuiPanel } from '@elastic/eui';
import React, { useState } from 'react';
import { AsyncComponent } from '../../../components/async_component';
import { useProfilingDependencies } from '../../../components/contexts/profiling_dependencies/use_profiling_dependencies';
import { FlameGraph } from '../../../components/flamegraph';
import { PrimaryProfilingSearchBar } from '../../../components/profiling_app_page_template/primary_profiling_search_bar';
import { useProfilingParams } from '../../../hooks/use_profiling_params';
import { useProfilingRouter } from '../../../hooks/use_profiling_router';
import { useProfilingRoutePath } from '../../../hooks/use_profiling_route_path';
import { useTimeRange } from '../../../hooks/use_time_range';
import { useTimeRangeAsync } from '../../../hooks/use_time_range_async';
export function FlameGraphView() {
const {
query,
query: { rangeFrom, rangeTo, kuery, searchText },
} = useProfilingParams('/flamegraphs/flamegraph');
const timeRange = useTimeRange({ rangeFrom, rangeTo });
const {
services: { fetchElasticFlamechart },
} = useProfilingDependencies();
const state = useTimeRangeAsync(
({ http }) => {
return fetchElasticFlamechart({
http,
timeFrom: timeRange.inSeconds.start,
timeTo: timeRange.inSeconds.end,
kuery,
});
},
[timeRange.inSeconds.start, timeRange.inSeconds.end, kuery, fetchElasticFlamechart]
);
const { data } = state;
const routePath = useProfilingRoutePath();
const profilingRouter = useProfilingRouter();
const [showInformationWindow, setShowInformationWindow] = useState(false);
function toggleShowInformationWindow() {
setShowInformationWindow((prev) => !prev);
}
function handleSearchTextChange(newSearchText: string) {
// @ts-expect-error Code gets too complicated to satisfy TS constraints
profilingRouter.push(routePath, { query: { ...query, searchText: newSearchText } });
}
return (
<EuiFlexGroup direction="column">
<EuiFlexItem grow={false}>
<EuiPanel hasShadow={false} color="subdued">
<PrimaryProfilingSearchBar />
<EuiHorizontalRule />
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem>
<AsyncComponent {...state} style={{ height: '100%' }} size="xl">
<FlameGraph
id="flamechart"
primaryFlamegraph={data}
showInformationWindow={showInformationWindow}
toggleShowInformationWindow={toggleShowInformationWindow}
searchText={searchText}
onChangeSearchText={handleSearchTextChange}
/>
</AsyncComponent>
</EuiFlexItem>
</EuiFlexGroup>
);
}

View file

@ -0,0 +1,57 @@
/*
* 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 { EuiPageHeaderContentProps } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { ProfilingAppPageTemplate } from '../../components/profiling_app_page_template';
import { RedirectTo } from '../../components/redirect_to';
import { useProfilingParams } from '../../hooks/use_profiling_params';
import { useProfilingRouter } from '../../hooks/use_profiling_router';
import { useProfilingRoutePath } from '../../hooks/use_profiling_route_path';
export function FlameGraphsView({ children }: { children: React.ReactElement }) {
const { query } = useProfilingParams('/flamegraphs/*');
const routePath = useProfilingRoutePath();
const profilingRouter = useProfilingRouter();
if (routePath === '/flamegraphs') {
return <RedirectTo pathname="/flamegraphs/flamegraph" />;
}
const isDifferentialView = routePath === '/flamegraphs/differential';
const tabs: Required<EuiPageHeaderContentProps>['tabs'] = [
{
label: i18n.translate('xpack.profiling.flameGraphsView.flameGraphTabLabel', {
defaultMessage: 'Flamegraph',
}),
isSelected: !isDifferentialView,
href: profilingRouter.link('/flamegraphs/flamegraph', { query }),
},
{
label: i18n.translate('xpack.profiling.flameGraphsView.differentialFlameGraphTabLabel', {
defaultMessage: 'Differential flamegraph',
}),
isSelected: isDifferentialView,
href: profilingRouter.link('/flamegraphs/differential', {
// @ts-expect-error Code gets too complicated to satisfy TS constraints
query: {
...query,
comparisonRangeFrom: query.rangeFrom,
comparisonRangeTo: query.rangeTo,
comparisonKuery: query.kuery,
},
}),
},
];
return (
<ProfilingAppPageTemplate tabs={tabs} hideSearchBar={true}>
{children}
</ProfilingAppPageTemplate>
);
}

View file

@ -135,6 +135,13 @@ export function DifferentialTopNFunctionsView() {
const isNormalizedByTime = normalizationMode === NormalizationMode.Time; const isNormalizedByTime = normalizationMode === NormalizationMode.Time;
function handleOnFrameClick(functionName: string) {
profilingRouter.push('/flamegraphs/flamegraph', {
path: {},
query: { ...query, searchText: functionName },
});
}
return ( return (
<> <>
<EuiFlexGroup direction="column"> <EuiFlexGroup direction="column">
@ -169,6 +176,7 @@ export function DifferentialTopNFunctionsView() {
totalSeconds={timeRange.inSeconds.end - timeRange.inSeconds.start} totalSeconds={timeRange.inSeconds.end - timeRange.inSeconds.start}
isDifferentialView={true} isDifferentialView={true}
baselineScaleFactor={isNormalizedByTime ? baselineTime : baseline} baselineScaleFactor={isNormalizedByTime ? baselineTime : baseline}
onFrameClick={handleOnFrameClick}
/> />
</AsyncComponent> </AsyncComponent>
</EuiFlexItem> </EuiFlexItem>
@ -196,6 +204,7 @@ export function DifferentialTopNFunctionsView() {
isDifferentialView={true} isDifferentialView={true}
baselineScaleFactor={isNormalizedByTime ? comparisonTime : comparison} baselineScaleFactor={isNormalizedByTime ? comparisonTime : comparison}
comparisonScaleFactor={isNormalizedByTime ? baselineTime : baseline} comparisonScaleFactor={isNormalizedByTime ? baselineTime : baseline}
onFrameClick={handleOnFrameClick}
/> />
</AsyncComponent> </AsyncComponent>
</EuiFlexItem> </EuiFlexItem>

View file

@ -46,6 +46,13 @@ export function TopNFunctionsView() {
const profilingRouter = useProfilingRouter(); const profilingRouter = useProfilingRouter();
function handleOnFrameClick(functionName: string) {
profilingRouter.push('/flamegraphs/flamegraph', {
path: {},
query: { ...query, searchText: functionName },
});
}
return ( return (
<> <>
<EuiFlexGroup direction="column"> <EuiFlexGroup direction="column">
@ -69,6 +76,7 @@ export function TopNFunctionsView() {
}} }}
totalSeconds={timeRange.inSeconds.end - timeRange.inSeconds.start} totalSeconds={timeRange.inSeconds.end - timeRange.inSeconds.start}
isDifferentialView={false} isDifferentialView={false}
onFrameClick={handleOnFrameClick}
/> />
</AsyncComponent> </AsyncComponent>
</EuiFlexItem> </EuiFlexItem>