[Profiling] Auto-abort requests (#142750)

This commit is contained in:
Dario Gieselaar 2022-10-06 09:17:14 +02:00 committed by GitHub
parent 115039d452
commit c05190e516
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 204 additions and 134 deletions

View file

@ -43,35 +43,40 @@ export function FlameGraphsView({ children }: { children: React.ReactElement })
services: { fetchElasticFlamechart },
} = useProfilingDependencies();
const state = useTimeRangeAsync(() => {
return Promise.all([
fetchElasticFlamechart({
timeFrom: new Date(timeRange.start).getTime() / 1000,
timeTo: new Date(timeRange.end).getTime() / 1000,
kuery,
}),
comparisonTimeRange.start && comparisonTimeRange.end
? fetchElasticFlamechart({
timeFrom: new Date(comparisonTimeRange.start).getTime() / 1000,
timeTo: new Date(comparisonTimeRange.end).getTime() / 1000,
kuery: comparisonKuery,
})
: Promise.resolve(undefined),
]).then(([primaryFlamegraph, comparisonFlamegraph]) => {
return {
primaryFlamegraph,
comparisonFlamegraph,
};
});
}, [
timeRange.start,
timeRange.end,
kuery,
comparisonTimeRange.start,
comparisonTimeRange.end,
comparisonKuery,
fetchElasticFlamechart,
]);
const state = useTimeRangeAsync(
({ http }) => {
return Promise.all([
fetchElasticFlamechart({
http,
timeFrom: new Date(timeRange.start).getTime() / 1000,
timeTo: new Date(timeRange.end).getTime() / 1000,
kuery,
}),
comparisonTimeRange.start && comparisonTimeRange.end
? fetchElasticFlamechart({
http,
timeFrom: new Date(comparisonTimeRange.start).getTime() / 1000,
timeTo: new Date(comparisonTimeRange.end).getTime() / 1000,
kuery: comparisonKuery,
})
: Promise.resolve(undefined),
]).then(([primaryFlamegraph, comparisonFlamegraph]) => {
return {
primaryFlamegraph,
comparisonFlamegraph,
};
});
},
[
timeRange.start,
timeRange.end,
kuery,
comparisonTimeRange.start,
comparisonTimeRange.end,
comparisonKuery,
fetchElasticFlamechart,
]
);
const { data } = state;
@ -173,7 +178,6 @@ export function FlameGraphsView({ children }: { children: React.ReactElement })
<AsyncComponent {...state} style={{ height: '100%' }} size="xl">
<FlameGraph
id="flamechart"
height={'100%'}
primaryFlamegraph={data?.primaryFlamegraph}
comparisonFlamegraph={data?.comparisonFlamegraph}
comparisonMode={comparisonMode}

View file

@ -145,7 +145,6 @@ function FlameGraphTooltip({
export interface FlameGraphProps {
id: string;
height: number | string;
comparisonMode: FlameGraphComparisonMode;
primaryFlamegraph?: ElasticFlameGraph;
comparisonFlamegraph?: ElasticFlameGraph;
@ -153,7 +152,6 @@ export interface FlameGraphProps {
export const FlameGraph: React.FC<FlameGraphProps> = ({
id,
height,
comparisonMode,
primaryFlamegraph,
comparisonFlamegraph,

View file

@ -42,28 +42,36 @@ export function FunctionsView({ children }: { children: React.ReactElement }) {
services: { fetchTopNFunctions },
} = useProfilingDependencies();
const state = useTimeRangeAsync(() => {
return fetchTopNFunctions({
timeFrom: new Date(timeRange.start).getTime() / 1000,
timeTo: new Date(timeRange.end).getTime() / 1000,
startIndex: 0,
endIndex: 1000,
kuery,
});
}, [timeRange.start, timeRange.end, kuery, fetchTopNFunctions]);
const state = useTimeRangeAsync(
({ http }) => {
return fetchTopNFunctions({
http,
timeFrom: new Date(timeRange.start).getTime() / 1000,
timeTo: new Date(timeRange.end).getTime() / 1000,
startIndex: 0,
endIndex: 1000,
kuery,
});
},
[timeRange.start, timeRange.end, kuery, fetchTopNFunctions]
);
const comparisonState = useTimeRangeAsync(() => {
if (!comparisonTimeRange.start || !comparisonTimeRange.end) {
return undefined;
}
return fetchTopNFunctions({
timeFrom: new Date(comparisonTimeRange.start).getTime() / 1000,
timeTo: new Date(comparisonTimeRange.end).getTime() / 1000,
startIndex: 0,
endIndex: 1000,
kuery: comparisonKuery,
});
}, [comparisonTimeRange.start, comparisonTimeRange.end, comparisonKuery, fetchTopNFunctions]);
const comparisonState = useTimeRangeAsync(
({ http }) => {
if (!comparisonTimeRange.start || !comparisonTimeRange.end) {
return undefined;
}
return fetchTopNFunctions({
http,
timeFrom: new Date(comparisonTimeRange.start).getTime() / 1000,
timeTo: new Date(comparisonTimeRange.end).getTime() / 1000,
startIndex: 0,
endIndex: 1000,
kuery: comparisonKuery,
});
},
[comparisonTimeRange.start, comparisonTimeRange.end, comparisonKuery, fetchTopNFunctions]
);
const routePath = useProfilingRoutePath() as
| '/functions'

View file

@ -50,29 +50,33 @@ export function StackTracesView() {
rangeTo,
});
const state = useTimeRangeAsync(() => {
if (!topNType) {
return Promise.resolve({ charts: [], metadata: {} });
}
return fetchTopN({
type: topNType,
timeFrom: new Date(timeRange.start).getTime() / 1000,
timeTo: new Date(timeRange.end).getTime() / 1000,
kuery,
}).then((response: TopNResponse) => {
const totalCount = response.TotalCount;
const samples = response.TopN;
const charts = groupSamplesByCategory({
samples,
totalCount,
metadata: response.Metadata,
labels: response.Labels,
const state = useTimeRangeAsync(
({ http }) => {
if (!topNType) {
return Promise.resolve({ charts: [], metadata: {} });
}
return fetchTopN({
http,
type: topNType,
timeFrom: new Date(timeRange.start).getTime() / 1000,
timeTo: new Date(timeRange.end).getTime() / 1000,
kuery,
}).then((response: TopNResponse) => {
const totalCount = response.TotalCount;
const samples = response.TopN;
const charts = groupSamplesByCategory({
samples,
totalCount,
metadata: response.Metadata,
labels: response.Labels,
});
return {
charts,
};
});
return {
charts,
};
});
}, [topNType, timeRange.start, timeRange.end, fetchTopN, kuery]);
},
[topNType, timeRange.start, timeRange.end, fetchTopN, kuery]
);
const [highlightedSubchart, setHighlightedSubchart] = useState<TopNSubchart | undefined>(
undefined

View file

@ -4,8 +4,10 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { HttpStart } from '@kbn/core-http-browser';
import { useEffect, useState } from 'react';
import { HttpFetchOptions, HttpHandler, HttpStart } from '@kbn/core-http-browser';
import { AbortError } from '@kbn/kibana-utils-plugin/common';
import { useEffect, useRef, useState } from 'react';
import { Overwrite, ValuesType } from 'utility-types';
import { useProfilingDependencies } from '../components/contexts/profiling_dependencies/use_profiling_dependencies';
export enum AsyncStatus {
@ -20,8 +22,22 @@ export interface AsyncState<T> {
status: AsyncStatus;
}
const HTTP_METHODS = ['fetch', 'get', 'post', 'put', 'delete', 'patch'] as const;
type HttpMethod = ValuesType<typeof HTTP_METHODS>;
type AutoAbortedHttpMethod = (
path: string,
options: Omit<HttpFetchOptions, 'signal'>
) => ReturnType<HttpHandler>;
export type AutoAbortedHttpService = Overwrite<
HttpStart,
Record<HttpMethod, AutoAbortedHttpMethod>
>;
export type UseAsync = <T>(
fn: ({ http }: { http: HttpStart }) => Promise<T> | undefined,
fn: ({ http }: { http: AutoAbortedHttpService }) => Promise<T> | undefined,
dependencies: any[]
) => AsyncState<T>;
@ -37,8 +53,30 @@ export const useAsync: UseAsync = (fn, dependencies) => {
const { data, error } = asyncState;
const controllerRef = useRef(new AbortController());
useEffect(() => {
const returnValue = fn({ http });
controllerRef.current.abort();
controllerRef.current = new AbortController();
const autoAbortedMethods = {} as Record<HttpMethod, AutoAbortedHttpMethod>;
for (const key of HTTP_METHODS) {
autoAbortedMethods[key] = (path, options) => {
return http[key](path, { ...options, signal: controllerRef.current.signal }).catch(
(err) => {
if (err.name === 'AbortError') {
// return never-resolving promise
return new Promise(() => {});
}
throw err;
}
);
};
}
const returnValue = fn({ http: { ...http, ...autoAbortedMethods } });
if (returnValue === undefined) {
setAsyncState({
@ -63,13 +101,23 @@ export const useAsync: UseAsync = (fn, dependencies) => {
});
returnValue.catch((nextError) => {
if (nextError instanceof AbortError) {
return;
}
setAsyncState({
status: AsyncStatus.Settled,
error: nextError,
});
throw nextError;
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [http, ...dependencies]);
useEffect(() => {
return () => {
controllerRef.current.abort();
};
}, []);
return asyncState;
};

View file

@ -90,7 +90,7 @@ export class ProfilingPlugin implements Plugin {
unknown
];
const profilingFetchServices = getServices(coreStart);
const profilingFetchServices = getServices();
const { renderApp } = await import('./app');
function pushKueryToSubject(location: Location) {

View file

@ -4,21 +4,23 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { CoreStart, HttpFetchQuery } from '@kbn/core/public';
import { HttpFetchQuery } from '@kbn/core/public';
import { getRoutePaths } from '../common';
import { BaseFlameGraph, createFlameGraph, ElasticFlameGraph } from '../common/flamegraph';
import { TopNFunctions } from '../common/functions';
import { TopNResponse } from '../common/topn';
import { AutoAbortedHttpService } from './hooks/use_async';
export interface Services {
fetchTopN: (params: {
http: AutoAbortedHttpService;
type: string;
timeFrom: number;
timeTo: number;
kuery: string;
}) => Promise<TopNResponse>;
fetchTopNFunctions: (params: {
http: AutoAbortedHttpService;
timeFrom: number;
timeTo: number;
startIndex: number;
@ -26,42 +28,31 @@ export interface Services {
kuery: string;
}) => Promise<TopNFunctions>;
fetchElasticFlamechart: (params: {
http: AutoAbortedHttpService;
timeFrom: number;
timeTo: number;
kuery: string;
}) => Promise<ElasticFlameGraph>;
}
export function getServices(core: CoreStart): Services {
export function getServices(): Services {
const paths = getRoutePaths();
return {
fetchTopN: async ({ type, timeFrom, timeTo, kuery }) => {
fetchTopN: async ({ http, type, timeFrom, timeTo, kuery }) => {
try {
const query: HttpFetchQuery = {
timeFrom,
timeTo,
kuery,
};
return await core.http.get(`${paths.TopN}/${type}`, { query });
return await http.get(`${paths.TopN}/${type}`, { query });
} catch (e) {
return e;
}
},
fetchTopNFunctions: async ({
timeFrom,
timeTo,
startIndex,
endIndex,
kuery,
}: {
timeFrom: number;
timeTo: number;
startIndex: number;
endIndex: number;
kuery: string;
}) => {
fetchTopNFunctions: async ({ http, timeFrom, timeTo, startIndex, endIndex, kuery }) => {
try {
const query: HttpFetchQuery = {
timeFrom,
@ -70,28 +61,20 @@ export function getServices(core: CoreStart): Services {
endIndex,
kuery,
};
return await core.http.get(paths.TopNFunctions, { query });
return await http.get(paths.TopNFunctions, { query });
} catch (e) {
return e;
}
},
fetchElasticFlamechart: async ({
timeFrom,
timeTo,
kuery,
}: {
timeFrom: number;
timeTo: number;
kuery: string;
}) => {
fetchElasticFlamechart: async ({ http, timeFrom, timeTo, kuery }) => {
try {
const query: HttpFetchQuery = {
timeFrom,
timeTo,
kuery,
};
const baseFlamegraph: BaseFlameGraph = await core.http.get(paths.Flamechart, { query });
const baseFlamegraph = (await http.get(paths.Flamechart, { query })) as BaseFlameGraph;
return createFlameGraph(baseFlamegraph);
} catch (e) {
return e;