[Profiling] Move additional flamegraph calculations into UI (#142415)

* Remove total and sampled traces from API

* Remove Samples array from flamegraph API

These values are redundant with CountInclusive so could be removed
without issue.

* Remove totalCount and eventsIndex

These values are no longer needed.

* Remove samples from callee tree

* Refactor columnar view model into separate file

* Add more lazy-loaded flamegraph calculations

* Fix spacing in frame label

* Remove frame information API

* Improve test coverage

* Fix type error

* Replace fnv-plus with custom 64-bit FNV1-a

* Add exceptions for linting errors

* Add workaround for frame type truncation bug

* Replace prior workaround for truncation bug

This fix supercedes the prior workaround and addresses the truncation at
its source.

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Joseph Crail 2022-10-05 06:10:38 -07:00 committed by GitHub
parent 276cd3d0ef
commit c888aca9b4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 547 additions and 518 deletions

View file

@ -9,7 +9,7 @@ import { schema } from '@kbn/config-schema';
import { RouteRegisterParameters } from '.';
import { getRoutePaths } from '../../common';
import { createCalleeTree } from '../../common/callee';
import { createFlameGraph } from '../../common/flamegraph';
import { createBaseFlameGraph } from '../../common/flamegraph';
import { createProfilingEsClient } from '../utils/create_profiling_es_client';
import { withProfilingSpan } from '../utils/with_profiling_span';
import { getClient } from './compat';
@ -42,20 +42,13 @@ export function registerFlameChartSearchRoute({ router, logger }: RouteRegisterP
});
const totalSeconds = timeTo - timeFrom;
const {
stackTraces,
executables,
stackFrames,
eventsIndex,
totalCount,
totalFrames,
stackTraceEvents,
} = await getExecutablesAndStackTraces({
logger,
client: createProfilingEsClient({ request, esClient }),
filter,
sampleSize: targetSampleSize,
});
const { stackTraceEvents, stackTraces, executables, stackFrames, totalFrames } =
await getExecutablesAndStackTraces({
logger,
client: createProfilingEsClient({ request, esClient }),
filter,
sampleSize: targetSampleSize,
});
const flamegraph = await withProfilingSpan('create_flamegraph', async () => {
const t0 = Date.now();
@ -68,23 +61,8 @@ export function registerFlameChartSearchRoute({ router, logger }: RouteRegisterP
);
logger.info(`creating callee tree took ${Date.now() - t0} ms`);
// sampleRate is 1/5^N, with N being the downsampled index the events were fetched from.
// N=0: full events table (sampleRate is 1)
// N=1: downsampled by 5 (sampleRate is 0.2)
// ...
// totalCount is the sum(Count) of all events in the filter range in the
// downsampled index we were looking at.
// To estimate how many events we have in the full events index: totalCount / sampleRate.
// Do the same for single entries in the events array.
const t1 = Date.now();
const fg = createFlameGraph(
tree,
totalSeconds,
Math.floor(totalCount / eventsIndex.sampleRate),
totalCount
);
const fg = createBaseFlameGraph(tree, totalSeconds);
logger.info(`creating flamegraph took ${Date.now() - t1} ms`);
return fg;

View file

@ -1,102 +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 { schema } from '@kbn/config-schema';
import { Logger } from '@kbn/logging';
import { RouteRegisterParameters } from '.';
import { getRoutePaths } from '../../common';
import {
createStackFrameMetadata,
Executable,
StackFrame,
StackFrameMetadata,
} from '../../common/profiling';
import { createProfilingEsClient, ProfilingESClient } from '../utils/create_profiling_es_client';
import { mgetStackFrames, mgetExecutables } from './stacktrace';
async function getFrameInformation({
frameID,
executableID,
logger,
client,
}: {
frameID: string;
executableID: string;
logger: Logger;
client: ProfilingESClient;
}): Promise<StackFrameMetadata | undefined> {
const [stackFrames, executables] = await Promise.all([
mgetStackFrames({
logger,
client,
stackFrameIDs: new Set([frameID]),
}),
mgetExecutables({
logger,
client,
executableIDs: new Set([executableID]),
}),
]);
const frame = Array.from(stackFrames.values())[0] as StackFrame | undefined;
const executable = Array.from(executables.values())[0] as Executable | undefined;
if (frame) {
return createStackFrameMetadata({
FrameID: frameID,
FileID: executableID,
SourceFilename: frame.FileName,
FunctionName: frame.FunctionName,
ExeFileName: executable?.FileName,
});
}
}
export function registerFrameInformationRoute(params: RouteRegisterParameters) {
const { logger, router } = params;
const routePaths = getRoutePaths();
router.get(
{
path: routePaths.FrameInformation,
validate: {
query: schema.object({
frameID: schema.string(),
executableID: schema.string(),
}),
},
},
async (context, request, response) => {
const { frameID, executableID } = request.query;
const client = createProfilingEsClient({
request,
esClient: (await context.core).elasticsearch.client.asCurrentUser,
});
try {
const frame = await getFrameInformation({
frameID,
executableID,
logger,
client,
});
return response.ok({ body: frame });
} catch (error: any) {
logger.error(error);
return response.custom({
statusCode: error.statusCode ?? 500,
body: {
message: error.message ?? 'An internal server error occured',
},
});
}
}
);
}

View file

@ -13,7 +13,6 @@ import {
} from '../types';
import { registerFlameChartSearchRoute } from './flamechart';
import { registerFrameInformationRoute } from './frames';
import { registerTopNFunctionsSearchRoute } from './functions';
import {
@ -41,5 +40,4 @@ export function registerRoutes(params: RouteRegisterParameters) {
registerTraceEventsTopNHostsSearchRoute(params);
registerTraceEventsTopNStackTracesSearchRoute(params);
registerTraceEventsTopNThreadsSearchRoute(params);
registerFrameInformationRoute(params);
}

View file

@ -118,6 +118,14 @@ describe('Stack trace operations', () => {
}
});
test('runLengthDecode with larger output than available input', () => {
const bytes = Buffer.from([0x5, 0x0, 0x2, 0x2]);
const decoded = [0, 0, 0, 0, 0, 2, 2];
const expected = decoded.concat(Array(decoded.length).fill(0));
expect(runLengthDecode(bytes, expected.length)).toEqual(expected);
});
test('runLengthDecode without optional parameter', () => {
const tests: Array<{
bytes: Buffer;

View file

@ -122,6 +122,17 @@ export function runLengthDecode(input: Buffer, outputSize?: number): number[] {
}
}
// Due to truncation of the frame types for stacktraces longer than 255,
// the expected output size and the actual decoded size can be different.
// Ordinarily, these two values should be the same.
//
// We have decided to fill in the remainder of the output array with zeroes
// as a reasonable default. Without this step, the output array would have
// undefined values.
for (let i = idx; i < size; i++) {
output[i] = 0;
}
return output;
}