[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,
Settings,
Tooltip,
FlameSpec,
} from '@elastic/charts';
import { EuiFlexGroup, EuiFlexItem, useEuiTheme } from '@elastic/eui';
import { Maybe } from '@kbn/observability-plugin/common/typings';
@ -27,13 +28,15 @@ import { ComparisonMode } from '../normalization_menu';
interface Props {
id: string;
comparisonMode: ComparisonMode;
comparisonMode?: ComparisonMode;
primaryFlamegraph?: ElasticFlameGraph;
comparisonFlamegraph?: ElasticFlameGraph;
baseline?: number;
comparison?: number;
showInformationWindow: boolean;
toggleShowInformationWindow: () => void;
searchText?: string;
onChangeSearchText?: FlameSpec['onSearchTextChange'];
}
export function FlameGraph({
@ -45,6 +48,8 @@ export function FlameGraph({
comparison,
showInformationWindow,
toggleShowInformationWindow,
searchText,
onChangeSearchText,
}: Props) {
const theme = useEuiTheme();
@ -165,6 +170,8 @@ export function FlameGraph({
valueFormatter={(value) => `${value}`}
animation={{ duration: 100 }}
controlProviderCallback={{}}
search={searchText ? { text: searchText } : undefined}
onSearchTextChange={onChangeSearchText}
/>
</Chart>
</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,
XYChartElementEvent,
TooltipContainer,
CustomTooltip,
} from '@elastic/charts';
import { EuiPanel } from '@elastic/eui';
import { keyBy } from 'lodash';
@ -57,7 +56,7 @@ export function StackedBarChart({
const { chartsBaseTheme, chartsTheme } = useProfilingChartsTheme();
const CustomTooltipWithSubChart: CustomTooltip = () => {
function CustomTooltipWithSubChart() {
if (!highlightedSample) {
return null;
}
@ -90,7 +89,7 @@ export function StackedBarChart({
</EuiPanel>
</TooltipContainer>
);
};
}
return (
<Chart size={{ height }}>

View file

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

View file

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

View file

@ -31,7 +31,7 @@ export function getFlamegraphModel({
colorSuccess,
colorDanger,
colorNeutral,
comparisonMode,
comparisonMode = ComparisonMode.Absolute,
comparison,
baseline,
}: {
@ -40,7 +40,7 @@ export function getFlamegraphModel({
colorSuccess: string;
colorDanger: string;
colorNeutral: string;
comparisonMode: ComparisonMode;
comparisonMode?: ComparisonMode;
baseline?: 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 React 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 { PrimaryAndComparisonSearchBar } from '../../components/primary_and_comparison_search_bar';
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 { PrimaryAndComparisonSearchBar } from '../../../components/primary_and_comparison_search_bar';
import {
ComparisonMode,
NormalizationMode,
NormalizationOptions,
NormalizationMenu,
} from '../../components/normalization_menu';
import { DifferentialComparisonMode } from '../../components/differential_comparison_mode';
} from '../../../components/normalization_menu';
import { DifferentialComparisonMode } from '../../../components/differential_comparison_mode';
interface Props {
isDifferentialView: boolean;
comparisonMode: ComparisonMode;
normalizationMode: NormalizationMode;
normalizationOptions: NormalizationOptions;
}
export function FlameGraphSearchPanel({
export function DifferentialFlameGraphSearchPanel({
comparisonMode,
normalizationMode,
isDifferentialView,
normalizationOptions,
}: Props) {
const { path, query } = useProfilingParams('/flamegraphs/*');
@ -77,29 +74,25 @@ export function FlameGraphSearchPanel({
}
return (
<EuiPanel hasShadow={false} color="subdued">
{isDifferentialView ? <PrimaryAndComparisonSearchBar /> : <PrimaryProfilingSearchBar />}
<PrimaryAndComparisonSearchBar />
<EuiHorizontalRule />
<EuiFlexGroup direction="row">
{isDifferentialView && (
<>
<DifferentialComparisonMode
comparisonMode={comparisonMode}
onChange={onChangeComparisonMode}
/>
{comparisonMode === ComparisonMode.Absolute && (
<DifferentialComparisonMode
comparisonMode={comparisonMode}
onChange={onChangeComparisonMode}
/>
{comparisonMode === ComparisonMode.Absolute && (
<EuiFlexItem grow={false}>
<EuiFlexGroup direction="row" gutterSize="m" alignItems="center">
<EuiFlexItem grow={false}>
<EuiFlexGroup direction="row" gutterSize="m" alignItems="center">
<EuiFlexItem grow={false}>
<NormalizationMenu
onChange={onChangeNormalizationMode}
mode={normalizationMode}
options={normalizationOptions}
/>
</EuiFlexItem>
</EuiFlexGroup>
<NormalizationMenu
onChange={onChangeNormalizationMode}
mode={normalizationMode}
options={normalizationOptions}
/>
</EuiFlexItem>
)}
</>
</EuiFlexGroup>
</EuiFlexItem>
)}
</EuiFlexGroup>
</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;
function handleOnFrameClick(functionName: string) {
profilingRouter.push('/flamegraphs/flamegraph', {
path: {},
query: { ...query, searchText: functionName },
});
}
return (
<>
<EuiFlexGroup direction="column">
@ -169,6 +176,7 @@ export function DifferentialTopNFunctionsView() {
totalSeconds={timeRange.inSeconds.end - timeRange.inSeconds.start}
isDifferentialView={true}
baselineScaleFactor={isNormalizedByTime ? baselineTime : baseline}
onFrameClick={handleOnFrameClick}
/>
</AsyncComponent>
</EuiFlexItem>
@ -196,6 +204,7 @@ export function DifferentialTopNFunctionsView() {
isDifferentialView={true}
baselineScaleFactor={isNormalizedByTime ? comparisonTime : comparison}
comparisonScaleFactor={isNormalizedByTime ? baselineTime : baseline}
onFrameClick={handleOnFrameClick}
/>
</AsyncComponent>
</EuiFlexItem>

View file

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