mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[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:
parent
276cd3d0ef
commit
c888aca9b4
22 changed files with 547 additions and 518 deletions
|
@ -486,7 +486,6 @@
|
|||
"fast-deep-equal": "^3.1.1",
|
||||
"fflate": "^0.6.9",
|
||||
"file-saver": "^1.3.8",
|
||||
"fnv-plus": "^1.3.1",
|
||||
"font-awesome": "4.7.0",
|
||||
"formik": "^2.2.9",
|
||||
"fp-ts": "^2.3.1",
|
||||
|
@ -820,7 +819,6 @@
|
|||
"@types/fetch-mock": "^7.3.1",
|
||||
"@types/file-saver": "^2.0.0",
|
||||
"@types/flot": "^0.0.31",
|
||||
"@types/fnv-plus": "^1.3.0",
|
||||
"@types/geojson": "7946.0.7",
|
||||
"@types/getos": "^3.0.0",
|
||||
"@types/gulp": "^4.0.6",
|
||||
|
|
|
@ -210,7 +210,7 @@
|
|||
},
|
||||
{
|
||||
"groupName": "Profiling",
|
||||
"matchPackageNames": ["fnv-plus", "peggy", "@types/dagre", "@types/fnv-plus"],
|
||||
"matchPackageNames": ["peggy", "@types/dagre"],
|
||||
"reviewers": ["team:profiling-ui"],
|
||||
"matchBaseBranches": ["main"],
|
||||
"labels": ["release_note:skip", "backport:skip"],
|
||||
|
|
|
@ -10,15 +10,28 @@ import { createCalleeTree } from './callee';
|
|||
|
||||
import { events, stackTraces, stackFrames, executables } from './__fixtures__/stacktraces';
|
||||
|
||||
const totalSamples = sum([...events.values()]);
|
||||
const totalFrames = sum([...stackTraces.values()].map((trace) => trace.FrameIDs.length));
|
||||
const tree = createCalleeTree(events, stackTraces, stackFrames, executables, totalFrames);
|
||||
|
||||
describe('Callee operations', () => {
|
||||
test('1', () => {
|
||||
const totalSamples = sum([...events.values()]);
|
||||
const totalFrames = sum([...stackTraces.values()].map((trace) => trace.FrameIDs.length));
|
||||
|
||||
const tree = createCalleeTree(events, stackTraces, stackFrames, executables, totalFrames);
|
||||
|
||||
expect(tree.Samples[0]).toEqual(totalSamples);
|
||||
test('inclusive count of root equals total sampled stacktraces', () => {
|
||||
expect(tree.CountInclusive[0]).toEqual(totalSamples);
|
||||
});
|
||||
|
||||
test('inclusive count for each node should be greater than or equal to its children', () => {
|
||||
const allGreaterThanOrEqual = tree.Edges.map(
|
||||
(children, i) =>
|
||||
tree.CountInclusive[i] >= sum([...children.values()].map((j) => tree.CountInclusive[j]))
|
||||
);
|
||||
expect(allGreaterThanOrEqual).toBeTruthy();
|
||||
});
|
||||
|
||||
test('exclusive count of root is zero', () => {
|
||||
expect(tree.CountExclusive[0]).toEqual(0);
|
||||
});
|
||||
|
||||
test('tree de-duplicates sibling nodes', () => {
|
||||
expect(tree.Size).toEqual(totalFrames - 2);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,20 +5,15 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import fnv from 'fnv-plus';
|
||||
|
||||
import { createFrameGroupID, FrameGroupID } from './frame_group';
|
||||
import {
|
||||
createStackFrameMetadata,
|
||||
emptyExecutable,
|
||||
emptyStackFrame,
|
||||
emptyStackTrace,
|
||||
Executable,
|
||||
FileID,
|
||||
getCalleeLabel,
|
||||
StackFrame,
|
||||
StackFrameID,
|
||||
StackFrameMetadata,
|
||||
StackTrace,
|
||||
StackTraceID,
|
||||
} from './profiling';
|
||||
|
@ -29,84 +24,19 @@ export interface CalleeTree {
|
|||
Size: number;
|
||||
Edges: Array<Map<FrameGroupID, NodeID>>;
|
||||
|
||||
ID: string[];
|
||||
FileID: string[];
|
||||
FrameType: number[];
|
||||
FrameID: StackFrameID[];
|
||||
FileID: FileID[];
|
||||
Label: string[];
|
||||
ExeFilename: string[];
|
||||
AddressOrLine: number[];
|
||||
FunctionName: string[];
|
||||
FunctionOffset: number[];
|
||||
SourceFilename: string[];
|
||||
SourceLine: number[];
|
||||
|
||||
Samples: number[];
|
||||
CountInclusive: number[];
|
||||
CountExclusive: number[];
|
||||
}
|
||||
|
||||
function initCalleeTree(capacity: number): CalleeTree {
|
||||
const metadata = createStackFrameMetadata();
|
||||
const frameGroupID = createFrameGroupID(
|
||||
metadata.FileID,
|
||||
metadata.AddressOrLine,
|
||||
metadata.ExeFileName,
|
||||
metadata.SourceFilename,
|
||||
metadata.FunctionName
|
||||
);
|
||||
const tree: CalleeTree = {
|
||||
Size: 1,
|
||||
Edges: new Array(capacity),
|
||||
ID: new Array(capacity),
|
||||
FrameType: new Array(capacity),
|
||||
FrameID: new Array(capacity),
|
||||
FileID: new Array(capacity),
|
||||
Label: new Array(capacity),
|
||||
Samples: new Array(capacity),
|
||||
CountInclusive: new Array(capacity),
|
||||
CountExclusive: new Array(capacity),
|
||||
};
|
||||
|
||||
tree.Edges[0] = new Map<FrameGroupID, NodeID>();
|
||||
|
||||
tree.ID[0] = fnv.fast1a64utf(frameGroupID).toString();
|
||||
tree.FrameType[0] = metadata.FrameType;
|
||||
tree.FrameID[0] = metadata.FrameID;
|
||||
tree.FileID[0] = metadata.FileID;
|
||||
tree.Label[0] = 'root: Represents 100% of CPU time.';
|
||||
tree.Samples[0] = 0;
|
||||
tree.CountInclusive[0] = 0;
|
||||
tree.CountExclusive[0] = 0;
|
||||
|
||||
return tree;
|
||||
}
|
||||
|
||||
function insertNode(
|
||||
tree: CalleeTree,
|
||||
parent: NodeID,
|
||||
metadata: StackFrameMetadata,
|
||||
frameGroupID: FrameGroupID,
|
||||
samples: number
|
||||
) {
|
||||
const node = tree.Size;
|
||||
|
||||
tree.Edges[parent].set(frameGroupID, node);
|
||||
tree.Edges[node] = new Map<FrameGroupID, NodeID>();
|
||||
|
||||
tree.ID[node] = fnv.fast1a64utf(`${tree.ID[parent]}${frameGroupID}`).toString();
|
||||
tree.FrameType[node] = metadata.FrameType;
|
||||
tree.FrameID[node] = metadata.FrameID;
|
||||
tree.FileID[node] = metadata.FileID;
|
||||
tree.Label[node] = getCalleeLabel(metadata);
|
||||
tree.Samples[node] = samples;
|
||||
tree.CountInclusive[node] = 0;
|
||||
tree.CountExclusive[node] = 0;
|
||||
|
||||
tree.Size++;
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
// createCalleeTree creates a tree from the trace results, the number of
|
||||
// times that the trace has been seen, and the respective metadata.
|
||||
//
|
||||
// The resulting data structure contains all of the data, but is not yet in the
|
||||
// form most easily digestible by others.
|
||||
export function createCalleeTree(
|
||||
events: Map<StackTraceID, number>,
|
||||
stackTraces: Map<StackTraceID, StackTrace>,
|
||||
|
@ -114,7 +44,35 @@ export function createCalleeTree(
|
|||
executables: Map<FileID, Executable>,
|
||||
totalFrames: number
|
||||
): CalleeTree {
|
||||
const tree = initCalleeTree(totalFrames);
|
||||
const tree: CalleeTree = {
|
||||
Size: 1,
|
||||
Edges: new Array(totalFrames),
|
||||
FileID: new Array(totalFrames),
|
||||
FrameType: new Array(totalFrames),
|
||||
ExeFilename: new Array(totalFrames),
|
||||
AddressOrLine: new Array(totalFrames),
|
||||
FunctionName: new Array(totalFrames),
|
||||
FunctionOffset: new Array(totalFrames),
|
||||
SourceFilename: new Array(totalFrames),
|
||||
SourceLine: new Array(totalFrames),
|
||||
|
||||
CountInclusive: new Array(totalFrames),
|
||||
CountExclusive: new Array(totalFrames),
|
||||
};
|
||||
|
||||
tree.Edges[0] = new Map<FrameGroupID, NodeID>();
|
||||
|
||||
tree.FileID[0] = '';
|
||||
tree.FrameType[0] = 0;
|
||||
tree.ExeFilename[0] = '';
|
||||
tree.AddressOrLine[0] = 0;
|
||||
tree.FunctionName[0] = '';
|
||||
tree.FunctionOffset[0] = 0;
|
||||
tree.SourceFilename[0] = '';
|
||||
tree.SourceLine[0] = 0;
|
||||
|
||||
tree.CountInclusive[0] = 0;
|
||||
tree.CountExclusive[0] = 0;
|
||||
|
||||
const sortedStackTraceIDs = new Array<StackTraceID>();
|
||||
for (const trace of stackTraces.keys()) {
|
||||
|
@ -139,7 +97,9 @@ export function createCalleeTree(
|
|||
const samples = events.get(stackTraceID) ?? 0;
|
||||
|
||||
let currentNode = 0;
|
||||
tree.Samples[currentNode] += samples;
|
||||
|
||||
tree.CountInclusive[currentNode] += samples;
|
||||
tree.CountExclusive[currentNode] = 0;
|
||||
|
||||
for (let i = 0; i < lenStackTrace; i++) {
|
||||
const frameID = stackTrace.FrameIDs[i];
|
||||
|
@ -159,25 +119,27 @@ export function createCalleeTree(
|
|||
let node = tree.Edges[currentNode].get(frameGroupID);
|
||||
|
||||
if (node === undefined) {
|
||||
const metadata = createStackFrameMetadata({
|
||||
FrameID: frameID,
|
||||
FileID: fileID,
|
||||
AddressOrLine: addressOrLine,
|
||||
FrameType: stackTrace.Types[i],
|
||||
FunctionName: frame.FunctionName,
|
||||
FunctionOffset: frame.FunctionOffset,
|
||||
SourceLine: frame.LineNumber,
|
||||
SourceFilename: frame.FileName,
|
||||
ExeFileName: executable.FileName,
|
||||
});
|
||||
node = tree.Size;
|
||||
|
||||
node = insertNode(tree, currentNode, metadata, frameGroupID, samples);
|
||||
tree.FileID[node] = fileID;
|
||||
tree.FrameType[node] = stackTrace.Types[i];
|
||||
tree.ExeFilename[node] = executable.FileName;
|
||||
tree.AddressOrLine[node] = addressOrLine;
|
||||
tree.FunctionName[node] = frame.FunctionName;
|
||||
tree.FunctionOffset[node] = frame.FunctionOffset;
|
||||
tree.SourceLine[node] = frame.LineNumber;
|
||||
tree.SourceFilename[node] = frame.FileName;
|
||||
tree.CountInclusive[node] = samples;
|
||||
tree.CountExclusive[node] = 0;
|
||||
|
||||
tree.Edges[currentNode].set(frameGroupID, node);
|
||||
tree.Edges[node] = new Map<FrameGroupID, NodeID>();
|
||||
|
||||
tree.Size++;
|
||||
} else {
|
||||
tree.Samples[node] += samples;
|
||||
tree.CountInclusive[node] += samples;
|
||||
}
|
||||
|
||||
tree.CountInclusive[node] += samples;
|
||||
|
||||
if (i === lenStackTrace - 1) {
|
||||
// Leaf frame: sum up counts for exclusive CPU.
|
||||
tree.CountExclusive[node] += samples;
|
||||
|
@ -186,8 +148,5 @@ export function createCalleeTree(
|
|||
}
|
||||
}
|
||||
|
||||
tree.CountExclusive[0] = 0;
|
||||
tree.CountInclusive[0] = tree.Samples[0];
|
||||
|
||||
return tree;
|
||||
}
|
||||
|
|
32
x-pack/plugins/profiling/common/columnar_view_model.test.ts
Normal file
32
x-pack/plugins/profiling/common/columnar_view_model.test.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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 { sum } from 'lodash';
|
||||
import { createCalleeTree } from './callee';
|
||||
import { createColumnarViewModel } from './columnar_view_model';
|
||||
import { createBaseFlameGraph, createFlameGraph } from './flamegraph';
|
||||
|
||||
import { events, stackTraces, stackFrames, executables } from './__fixtures__/stacktraces';
|
||||
|
||||
const totalFrames = sum([...stackTraces.values()].map((trace) => trace.FrameIDs.length));
|
||||
|
||||
const tree = createCalleeTree(events, stackTraces, stackFrames, executables, totalFrames);
|
||||
const graph = createFlameGraph(createBaseFlameGraph(tree, 60));
|
||||
|
||||
describe('Columnar view model operations', () => {
|
||||
test('color values are generated by default', () => {
|
||||
const viewModel = createColumnarViewModel(graph);
|
||||
|
||||
expect(sum(viewModel.color)).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('color values are not generated when disabled', () => {
|
||||
const viewModel = createColumnarViewModel(graph, false);
|
||||
|
||||
expect(sum(viewModel.color)).toEqual(0);
|
||||
});
|
||||
});
|
136
x-pack/plugins/profiling/common/columnar_view_model.ts
Normal file
136
x-pack/plugins/profiling/common/columnar_view_model.ts
Normal file
|
@ -0,0 +1,136 @@
|
|||
/*
|
||||
* 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 { ColumnarViewModel } from '@elastic/charts';
|
||||
|
||||
import { ElasticFlameGraph } from './flamegraph';
|
||||
|
||||
/*
|
||||
* Helper to calculate the color of a given block to be drawn. The desirable outcomes of this are:
|
||||
* Each of the following frame types should get a different set of color hues:
|
||||
*
|
||||
* 0 = Unsymbolized frame
|
||||
* 1 = Python
|
||||
* 2 = PHP
|
||||
* 3 = Native
|
||||
* 4 = Kernel
|
||||
* 5 = JVM/Hotspot
|
||||
* 6 = Ruby
|
||||
* 7 = Perl
|
||||
* 8 = JavaScript
|
||||
*
|
||||
* This is most easily achieved by mapping frame types to different color variations, using
|
||||
* the x-position we can use different colors for adjacent blocks while keeping a similar hue
|
||||
*
|
||||
* Taken originally from prodfiler_ui/src/helpers/Pixi/frameTypeToColors.tsx
|
||||
*/
|
||||
const frameTypeToColors = [
|
||||
[0xfd8484, 0xfd9d9d, 0xfeb5b5, 0xfecece],
|
||||
[0xfcae6b, 0xfdbe89, 0xfdcea6, 0xfedfc4],
|
||||
[0xfcdb82, 0xfde29b, 0xfde9b4, 0xfef1cd],
|
||||
[0x6dd0dc, 0x8ad9e3, 0xa7e3ea, 0xc5ecf1],
|
||||
[0x7c9eff, 0x96b1ff, 0xb0c5ff, 0xcbd8ff],
|
||||
[0x65d3ac, 0x84dcbd, 0xa3e5cd, 0xc1edde],
|
||||
[0xd79ffc, 0xdfb2fd, 0xe7c5fd, 0xefd9fe],
|
||||
[0xf98bb9, 0xfaa2c7, 0xfbb9d5, 0xfdd1e3],
|
||||
[0xcbc3e3, 0xd5cfe8, 0xdfdbee, 0xeae7f3],
|
||||
];
|
||||
|
||||
function frameTypeToRGB(frameType: number, x: number): number {
|
||||
return frameTypeToColors[frameType][x % 4];
|
||||
}
|
||||
|
||||
export function rgbToRGBA(rgb: number): number[] {
|
||||
return [
|
||||
Math.floor(rgb / 65536) / 255,
|
||||
(Math.floor(rgb / 256) % 256) / 255,
|
||||
(rgb % 256) / 255,
|
||||
1.0,
|
||||
];
|
||||
}
|
||||
|
||||
function normalize(n: number, lower: number, upper: number): number {
|
||||
return (n - lower) / (upper - lower);
|
||||
}
|
||||
|
||||
// createColumnarViewModel normalizes the columnar representation into a form
|
||||
// consumed by the flamegraph in the UI.
|
||||
export function createColumnarViewModel(
|
||||
flamegraph: ElasticFlameGraph,
|
||||
assignColors: boolean = true
|
||||
): ColumnarViewModel {
|
||||
const numNodes = flamegraph.Size;
|
||||
const xs = new Float32Array(numNodes);
|
||||
const ys = new Float32Array(numNodes);
|
||||
|
||||
const queue = [{ x: 0, depth: 1, node: 0 }];
|
||||
|
||||
while (queue.length > 0) {
|
||||
const { x, depth, node } = queue.pop()!;
|
||||
|
||||
xs[node] = x;
|
||||
ys[node] = depth;
|
||||
|
||||
// For a deterministic result we have to walk the callees in a deterministic
|
||||
// order. A deterministic result allows deterministic UI views, something
|
||||
// that users expect.
|
||||
const children = flamegraph.Edges[node].sort((n1, n2) => {
|
||||
if (flamegraph.CountInclusive[n1] > flamegraph.CountInclusive[n2]) {
|
||||
return -1;
|
||||
}
|
||||
if (flamegraph.CountInclusive[n1] < flamegraph.CountInclusive[n2]) {
|
||||
return 1;
|
||||
}
|
||||
return flamegraph.ID[n1].localeCompare(flamegraph.ID[n2]);
|
||||
});
|
||||
|
||||
let delta = 0;
|
||||
for (const child of children) {
|
||||
delta += flamegraph.CountInclusive[child];
|
||||
}
|
||||
|
||||
for (let i = children.length - 1; i >= 0; i--) {
|
||||
delta -= flamegraph.CountInclusive[children[i]];
|
||||
queue.push({ x: x + delta, depth: depth + 1, node: children[i] });
|
||||
}
|
||||
}
|
||||
|
||||
const colors = new Float32Array(numNodes * 4);
|
||||
|
||||
if (assignColors) {
|
||||
for (let i = 0; i < numNodes; i++) {
|
||||
const rgba = rgbToRGBA(frameTypeToRGB(flamegraph.FrameType[i], xs[i]));
|
||||
colors.set(rgba, 4 * i);
|
||||
}
|
||||
}
|
||||
|
||||
const position = new Float32Array(numNodes * 2);
|
||||
const maxX = flamegraph.CountInclusive[0];
|
||||
const maxY = ys.reduce((max, n) => (n > max ? n : max), 0);
|
||||
|
||||
for (let i = 0; i < numNodes; i++) {
|
||||
const j = 2 * i;
|
||||
position[j] = normalize(xs[i], 0, maxX);
|
||||
position[j + 1] = normalize(maxY - ys[i], 0, maxY);
|
||||
}
|
||||
|
||||
const size = new Float32Array(numNodes);
|
||||
|
||||
for (let i = 0; i < numNodes; i++) {
|
||||
size[i] = normalize(flamegraph.CountInclusive[i], 0, maxX);
|
||||
}
|
||||
|
||||
return {
|
||||
label: flamegraph.Label.slice(0, numNodes),
|
||||
value: Float64Array.from(flamegraph.CountInclusive.slice(0, numNodes)),
|
||||
color: colors,
|
||||
position0: position,
|
||||
position1: position,
|
||||
size0: size,
|
||||
size1: size,
|
||||
} as ColumnarViewModel;
|
||||
}
|
|
@ -7,26 +7,36 @@
|
|||
|
||||
import { sum } from 'lodash';
|
||||
import { createCalleeTree } from './callee';
|
||||
import { createColumnarViewModel, createFlameGraph } from './flamegraph';
|
||||
import { createBaseFlameGraph, createFlameGraph } from './flamegraph';
|
||||
|
||||
import { events, stackTraces, stackFrames, executables } from './__fixtures__/stacktraces';
|
||||
|
||||
const totalFrames = sum([...stackTraces.values()].map((trace) => trace.FrameIDs.length));
|
||||
const tree = createCalleeTree(events, stackTraces, stackFrames, executables, totalFrames);
|
||||
const baseFlamegraph = createBaseFlameGraph(tree, 60);
|
||||
const flamegraph = createFlameGraph(baseFlamegraph);
|
||||
|
||||
describe('Flamegraph operations', () => {
|
||||
test('1', () => {
|
||||
const totalSamples = sum([...events.values()]);
|
||||
const totalFrames = sum([...stackTraces.values()].map((trace) => trace.FrameIDs.length));
|
||||
test('base flamegraph has non-zero total seconds', () => {
|
||||
expect(baseFlamegraph.TotalSeconds).toEqual(60);
|
||||
});
|
||||
|
||||
const tree = createCalleeTree(events, stackTraces, stackFrames, executables, totalFrames);
|
||||
const graph = createFlameGraph(tree, 60, totalSamples, totalSamples);
|
||||
test('base flamegraph has one more node than the number of edges', () => {
|
||||
const numEdges = baseFlamegraph.Edges.flatMap((edge) => edge).length;
|
||||
|
||||
expect(graph.Size).toEqual(totalFrames - 2);
|
||||
expect(numEdges).toEqual(baseFlamegraph.Size - 1);
|
||||
});
|
||||
|
||||
const viewModel1 = createColumnarViewModel(graph);
|
||||
test('all flamegraph IDs are the same non-zero length', () => {
|
||||
// 16 is the length of a 64-bit FNV-1a hash encoded to a hex string
|
||||
const allSameLengthIDs = flamegraph.ID.every((id) => id.length === 16);
|
||||
|
||||
expect(sum(viewModel1.color)).toBeGreaterThan(0);
|
||||
expect(allSameLengthIDs).toBeTruthy();
|
||||
});
|
||||
|
||||
const viewModel2 = createColumnarViewModel(graph, false);
|
||||
test('all flamegraph labels are non-empty', () => {
|
||||
const allNonEmptyLabels = flamegraph.Label.every((id) => id.length > 0);
|
||||
|
||||
expect(sum(viewModel2.color)).toEqual(0);
|
||||
expect(allNonEmptyLabels).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,106 +5,54 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ColumnarViewModel } from '@elastic/charts';
|
||||
|
||||
import { CalleeTree } from './callee';
|
||||
|
||||
export interface ElasticFlameGraph {
|
||||
Size: number;
|
||||
Edges: number[][];
|
||||
|
||||
ID: string[];
|
||||
FrameType: number[];
|
||||
FrameID: string[];
|
||||
ExecutableID: string[];
|
||||
Label: string[];
|
||||
|
||||
Samples: number[];
|
||||
CountInclusive: number[];
|
||||
CountExclusive: number[];
|
||||
|
||||
TotalSeconds: number;
|
||||
TotalTraces: number;
|
||||
SampledTraces: number;
|
||||
}
|
||||
import { createFrameGroupID } from './frame_group';
|
||||
import { fnv1a64 } from './hash';
|
||||
import { createStackFrameMetadata, getCalleeLabel } from './profiling';
|
||||
|
||||
export enum FlameGraphComparisonMode {
|
||||
Absolute = 'absolute',
|
||||
Relative = 'relative',
|
||||
}
|
||||
|
||||
/*
|
||||
* Helper to calculate the color of a given block to be drawn. The desirable outcomes of this are:
|
||||
* Each of the following frame types should get a different set of color hues:
|
||||
*
|
||||
* 0 = Unsymbolized frame
|
||||
* 1 = Python
|
||||
* 2 = PHP
|
||||
* 3 = Native
|
||||
* 4 = Kernel
|
||||
* 5 = JVM/Hotspot
|
||||
* 6 = Ruby
|
||||
* 7 = Perl
|
||||
* 8 = JavaScript
|
||||
*
|
||||
* This is most easily achieved by mapping frame types to different color variations, using
|
||||
* the x-position we can use different colors for adjacent blocks while keeping a similar hue
|
||||
*
|
||||
* Taken originally from prodfiler_ui/src/helpers/Pixi/frameTypeToColors.tsx
|
||||
*/
|
||||
const frameTypeToColors = [
|
||||
[0xfd8484, 0xfd9d9d, 0xfeb5b5, 0xfecece],
|
||||
[0xfcae6b, 0xfdbe89, 0xfdcea6, 0xfedfc4],
|
||||
[0xfcdb82, 0xfde29b, 0xfde9b4, 0xfef1cd],
|
||||
[0x6dd0dc, 0x8ad9e3, 0xa7e3ea, 0xc5ecf1],
|
||||
[0x7c9eff, 0x96b1ff, 0xb0c5ff, 0xcbd8ff],
|
||||
[0x65d3ac, 0x84dcbd, 0xa3e5cd, 0xc1edde],
|
||||
[0xd79ffc, 0xdfb2fd, 0xe7c5fd, 0xefd9fe],
|
||||
[0xf98bb9, 0xfaa2c7, 0xfbb9d5, 0xfdd1e3],
|
||||
[0xcbc3e3, 0xd5cfe8, 0xdfdbee, 0xeae7f3],
|
||||
];
|
||||
export interface BaseFlameGraph {
|
||||
Size: number;
|
||||
Edges: number[][];
|
||||
|
||||
function frameTypeToRGB(frameType: number, x: number): number {
|
||||
return frameTypeToColors[frameType][x % 4];
|
||||
FileID: string[];
|
||||
FrameType: number[];
|
||||
ExeFilename: string[];
|
||||
AddressOrLine: number[];
|
||||
FunctionName: string[];
|
||||
FunctionOffset: number[];
|
||||
SourceFilename: string[];
|
||||
SourceLine: number[];
|
||||
|
||||
CountInclusive: number[];
|
||||
CountExclusive: number[];
|
||||
|
||||
TotalSeconds: number;
|
||||
}
|
||||
|
||||
export function rgbToRGBA(rgb: number): number[] {
|
||||
return [
|
||||
Math.floor(rgb / 65536) / 255,
|
||||
(Math.floor(rgb / 256) % 256) / 255,
|
||||
(rgb % 256) / 255,
|
||||
1.0,
|
||||
];
|
||||
}
|
||||
|
||||
function normalize(n: number, lower: number, upper: number): number {
|
||||
return (n - lower) / (upper - lower);
|
||||
}
|
||||
|
||||
// createFlameGraph encapsulates the tree representation into a serialized form.
|
||||
export function createFlameGraph(
|
||||
tree: CalleeTree,
|
||||
totalSeconds: number,
|
||||
totalTraces: number,
|
||||
sampledTraces: number
|
||||
): ElasticFlameGraph {
|
||||
const graph: ElasticFlameGraph = {
|
||||
// createBaseFlameGraph encapsulates the tree representation into a serialized form.
|
||||
export function createBaseFlameGraph(tree: CalleeTree, totalSeconds: number): BaseFlameGraph {
|
||||
const graph: BaseFlameGraph = {
|
||||
Size: tree.Size,
|
||||
Edges: new Array<number[]>(tree.Size),
|
||||
|
||||
ID: tree.ID.slice(0, tree.Size),
|
||||
Label: tree.Label.slice(0, tree.Size),
|
||||
FrameID: tree.FrameID.slice(0, tree.Size),
|
||||
FileID: tree.FileID.slice(0, tree.Size),
|
||||
FrameType: tree.FrameType.slice(0, tree.Size),
|
||||
ExecutableID: tree.FileID.slice(0, tree.Size),
|
||||
ExeFilename: tree.ExeFilename.slice(0, tree.Size),
|
||||
AddressOrLine: tree.AddressOrLine.slice(0, tree.Size),
|
||||
FunctionName: tree.FunctionName.slice(0, tree.Size),
|
||||
FunctionOffset: tree.FunctionOffset.slice(0, tree.Size),
|
||||
SourceFilename: tree.SourceFilename.slice(0, tree.Size),
|
||||
SourceLine: tree.SourceLine.slice(0, tree.Size),
|
||||
|
||||
Samples: tree.Samples.slice(0, tree.Size),
|
||||
CountInclusive: tree.CountInclusive.slice(0, tree.Size),
|
||||
CountExclusive: tree.CountExclusive.slice(0, tree.Size),
|
||||
|
||||
TotalSeconds: totalSeconds,
|
||||
TotalTraces: totalTraces,
|
||||
SampledTraces: sampledTraces,
|
||||
};
|
||||
|
||||
for (let i = 0; i < tree.Size; i++) {
|
||||
|
@ -120,80 +68,79 @@ export function createFlameGraph(
|
|||
return graph;
|
||||
}
|
||||
|
||||
// createColumnarViewModel normalizes the columnar representation into a form
|
||||
// consumed by the flamegraph in the UI.
|
||||
export function createColumnarViewModel(
|
||||
flamegraph: ElasticFlameGraph,
|
||||
assignColors: boolean = true
|
||||
): ColumnarViewModel {
|
||||
const numNodes = flamegraph.Size;
|
||||
const xs = new Float32Array(numNodes);
|
||||
const ys = new Float32Array(numNodes);
|
||||
|
||||
const queue = [{ x: 0, depth: 1, node: 0 }];
|
||||
|
||||
while (queue.length > 0) {
|
||||
const { x, depth, node } = queue.pop()!;
|
||||
|
||||
xs[node] = x;
|
||||
ys[node] = depth;
|
||||
|
||||
// For a deterministic result we have to walk the callees in a deterministic
|
||||
// order. A deterministic result allows deterministic UI views, something
|
||||
// that users expect.
|
||||
const children = flamegraph.Edges[node].sort((n1, n2) => {
|
||||
if (flamegraph.Samples[n1] > flamegraph.Samples[n2]) {
|
||||
return -1;
|
||||
}
|
||||
if (flamegraph.Samples[n1] < flamegraph.Samples[n2]) {
|
||||
return 1;
|
||||
}
|
||||
return flamegraph.ID[n1].localeCompare(flamegraph.ID[n2]);
|
||||
});
|
||||
|
||||
let delta = 0;
|
||||
for (const child of children) {
|
||||
delta += flamegraph.Samples[child];
|
||||
}
|
||||
|
||||
for (let i = children.length - 1; i >= 0; i--) {
|
||||
delta -= flamegraph.Samples[children[i]];
|
||||
queue.push({ x: x + delta, depth: depth + 1, node: children[i] });
|
||||
}
|
||||
}
|
||||
|
||||
const colors = new Float32Array(numNodes * 4);
|
||||
|
||||
if (assignColors) {
|
||||
for (let i = 0; i < numNodes; i++) {
|
||||
const rgba = rgbToRGBA(frameTypeToRGB(flamegraph.FrameType[i], xs[i]));
|
||||
colors.set(rgba, 4 * i);
|
||||
}
|
||||
}
|
||||
|
||||
const position = new Float32Array(numNodes * 2);
|
||||
const maxX = flamegraph.Samples[0];
|
||||
const maxY = ys.reduce((max, n) => (n > max ? n : max), 0);
|
||||
|
||||
for (let i = 0; i < numNodes; i++) {
|
||||
const j = 2 * i;
|
||||
position[j] = normalize(xs[i], 0, maxX);
|
||||
position[j + 1] = normalize(maxY - ys[i], 0, maxY);
|
||||
}
|
||||
|
||||
const size = new Float32Array(numNodes);
|
||||
|
||||
for (let i = 0; i < numNodes; i++) {
|
||||
size[i] = normalize(flamegraph.Samples[i], 0, maxX);
|
||||
}
|
||||
|
||||
return {
|
||||
label: flamegraph.Label.slice(0, numNodes),
|
||||
value: Float64Array.from(flamegraph.Samples.slice(0, numNodes)),
|
||||
color: colors,
|
||||
position0: position,
|
||||
position1: position,
|
||||
size0: size,
|
||||
size1: size,
|
||||
} as ColumnarViewModel;
|
||||
export interface ElasticFlameGraph extends BaseFlameGraph {
|
||||
ID: string[];
|
||||
Label: string[];
|
||||
}
|
||||
|
||||
// createFlameGraph combines the base flamegraph with CPU-intensive values.
|
||||
// This allows us to create a flamegraph in two steps (e.g. first on the server
|
||||
// and finally in the browser).
|
||||
export function createFlameGraph(base: BaseFlameGraph): ElasticFlameGraph {
|
||||
const graph: ElasticFlameGraph = {
|
||||
Size: base.Size,
|
||||
Edges: base.Edges,
|
||||
|
||||
FileID: base.FileID,
|
||||
FrameType: base.FrameType,
|
||||
ExeFilename: base.ExeFilename,
|
||||
AddressOrLine: base.AddressOrLine,
|
||||
FunctionName: base.FunctionName,
|
||||
FunctionOffset: base.FunctionOffset,
|
||||
SourceFilename: base.SourceFilename,
|
||||
SourceLine: base.SourceLine,
|
||||
|
||||
CountInclusive: base.CountInclusive,
|
||||
CountExclusive: base.CountExclusive,
|
||||
|
||||
ID: new Array<string>(base.Size),
|
||||
Label: new Array<string>(base.Size),
|
||||
|
||||
TotalSeconds: base.TotalSeconds,
|
||||
};
|
||||
|
||||
const rootFrameGroupID = createFrameGroupID(
|
||||
graph.FileID[0],
|
||||
graph.AddressOrLine[0],
|
||||
graph.ExeFilename[0],
|
||||
graph.SourceFilename[0],
|
||||
graph.FunctionName[0]
|
||||
);
|
||||
|
||||
graph.ID[0] = fnv1a64(new TextEncoder().encode(rootFrameGroupID));
|
||||
|
||||
const queue = [0];
|
||||
while (queue.length > 0) {
|
||||
const parent = queue.pop()!;
|
||||
for (const child of graph.Edges[parent]) {
|
||||
const frameGroupID = createFrameGroupID(
|
||||
graph.FileID[child],
|
||||
graph.AddressOrLine[child],
|
||||
graph.ExeFilename[child],
|
||||
graph.SourceFilename[child],
|
||||
graph.FunctionName[child]
|
||||
);
|
||||
const bytes = new TextEncoder().encode(graph.ID[parent] + frameGroupID);
|
||||
graph.ID[child] = fnv1a64(bytes);
|
||||
queue.push(child);
|
||||
}
|
||||
}
|
||||
|
||||
graph.Label[0] = 'root: Represents 100% of CPU time.';
|
||||
|
||||
for (let i = 1; i < graph.Size; i++) {
|
||||
const metadata = createStackFrameMetadata({
|
||||
FileID: graph.FileID[i],
|
||||
FrameType: graph.FrameType[i],
|
||||
ExeFileName: graph.ExeFilename[i],
|
||||
AddressOrLine: graph.AddressOrLine[i],
|
||||
FunctionName: graph.FunctionName[i],
|
||||
FunctionOffset: graph.FunctionOffset[i],
|
||||
SourceFilename: graph.SourceFilename[i],
|
||||
SourceLine: graph.SourceLine[i],
|
||||
});
|
||||
graph.Label[i] = getCalleeLabel(metadata);
|
||||
}
|
||||
|
||||
return graph;
|
||||
}
|
||||
|
|
49
x-pack/plugins/profiling/common/hash.test.ts
Normal file
49
x-pack/plugins/profiling/common/hash.test.ts
Normal file
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* 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 { fnv1a64 } from './hash';
|
||||
|
||||
function toUint8Array(s: string): Uint8Array {
|
||||
return new TextEncoder().encode(s);
|
||||
}
|
||||
|
||||
describe('FNV-1a hashing operations', () => {
|
||||
test('empty', () => {
|
||||
const input = toUint8Array('');
|
||||
const expected = 'cbf29ce484222325';
|
||||
|
||||
expect(fnv1a64(input)).toEqual(expected);
|
||||
});
|
||||
|
||||
test('simple', () => {
|
||||
const input = toUint8Array('hello world');
|
||||
const expected = '779a65e7023cd2e7';
|
||||
|
||||
expect(fnv1a64(input)).toEqual(expected);
|
||||
});
|
||||
|
||||
test('long', () => {
|
||||
const input = toUint8Array('Llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch');
|
||||
const expected = '7673401f09f26b0d';
|
||||
|
||||
expect(fnv1a64(input)).toEqual(expected);
|
||||
});
|
||||
|
||||
test('unicode double quotation marks', () => {
|
||||
const input = toUint8Array('trace:comm = “hello”');
|
||||
const expected = '8dada3d28d75245c';
|
||||
|
||||
expect(fnv1a64(input)).toEqual(expected);
|
||||
});
|
||||
|
||||
test('unicode spaces', () => {
|
||||
const input = toUint8Array('trace:comm\u2000=\u2001"hello"\u3000');
|
||||
const expected = '2cdcbb43ff62f74f';
|
||||
|
||||
expect(fnv1a64(input)).toEqual(expected);
|
||||
});
|
||||
});
|
79
x-pack/plugins/profiling/common/hash.ts
Normal file
79
x-pack/plugins/profiling/common/hash.ts
Normal file
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// prettier-ignore
|
||||
const lowerHex = [
|
||||
'00', '01', '02', '03', '04', '05', '06', '07', '08', '09', '0a', '0b', '0c', '0d', '0e', '0f',
|
||||
'10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '1a', '1b', '1c', '1d', '1e', '1f',
|
||||
'20', '21', '22', '23', '24', '25', '26', '27', '28', '29', '2a', '2b', '2c', '2d', '2e', '2f',
|
||||
'30', '31', '32', '33', '34', '35', '36', '37', '38', '39', '3a', '3b', '3c', '3d', '3e', '3f',
|
||||
'40', '41', '42', '43', '44', '45', '46', '47', '48', '49', '4a', '4b', '4c', '4d', '4e', '4f',
|
||||
'50', '51', '52', '53', '54', '55', '56', '57', '58', '59', '5a', '5b', '5c', '5d', '5e', '5f',
|
||||
'60', '61', '62', '63', '64', '65', '66', '67', '68', '69', '6a', '6b', '6c', '6d', '6e', '6f',
|
||||
'70', '71', '72', '73', '74', '75', '76', '77', '78', '79', '7a', '7b', '7c', '7d', '7e', '7f',
|
||||
'80', '81', '82', '83', '84', '85', '86', '87', '88', '89', '8a', '8b', '8c', '8d', '8e', '8f',
|
||||
'90', '91', '92', '93', '94', '95', '96', '97', '98', '99', '9a', '9b', '9c', '9d', '9e', '9f',
|
||||
'a0', 'a1', 'a2', 'a3', 'a4', 'a5', 'a6', 'a7', 'a8', 'a9', 'aa', 'ab', 'ac', 'ad', 'ae', 'af',
|
||||
'b0', 'b1', 'b2', 'b3', 'b4', 'b5', 'b6', 'b7', 'b8', 'b9', 'ba', 'bb', 'bc', 'bd', 'be', 'bf',
|
||||
'c0', 'c1', 'c2', 'c3', 'c4', 'c5', 'c6', 'c7', 'c8', 'c9', 'ca', 'cb', 'cc', 'cd', 'ce', 'cf',
|
||||
'd0', 'd1', 'd2', 'd3', 'd4', 'd5', 'd6', 'd7', 'd8', 'd9', 'da', 'db', 'dc', 'dd', 'de', 'df',
|
||||
'e0', 'e1', 'e2', 'e3', 'e4', 'e5', 'e6', 'e7', 'e8', 'e9', 'ea', 'eb', 'ec', 'ed', 'ee', 'ef',
|
||||
'f0', 'f1', 'f2', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8', 'f9', 'fa', 'fb', 'fc', 'fd', 'fe', 'ff',
|
||||
];
|
||||
|
||||
// fnv1a64 computes a 64-bit hash of a byte array using the FNV-1a hash function [1].
|
||||
//
|
||||
// Due to the lack of a native uint64 in JavaScript, we operate on 64-bit values using an array
|
||||
// of 4 uint16s instead. This method follows Knuth's Algorithm M in section 4.3.1 [2] using a
|
||||
// modified multiword multiplication implementation described in [3]. The modifications include:
|
||||
//
|
||||
// * rewrite default algorithm for the special case m = n = 4
|
||||
// * unroll loops
|
||||
// * simplify expressions
|
||||
// * create pre-computed lookup table for serialization to hexadecimal
|
||||
//
|
||||
// 1. https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function
|
||||
// 2. Knuth, Donald E. The Art of Computer Programming, Volume 2, Third Edition: Seminumerical
|
||||
// Algorithms. Addison-Wesley, 1998.
|
||||
// 3. Warren, Henry S. Hacker's Delight. Upper Saddle River, NJ: Addison-Wesley, 2013.
|
||||
|
||||
/* eslint no-bitwise: ["error", { "allow": ["^=", ">>", "&"] }] */
|
||||
export function fnv1a64(bytes: Uint8Array): string {
|
||||
const n = bytes.length;
|
||||
let [h0, h1, h2, h3] = [0x2325, 0x8422, 0x9ce4, 0xcbf2];
|
||||
let [t0, t1, t2, t3] = [0, 0, 0, 0];
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
h0 ^= bytes[i];
|
||||
|
||||
t0 = h0 * 0x01b3;
|
||||
t1 = h1 * 0x01b3;
|
||||
t2 = h2 * 0x01b3;
|
||||
t3 = h3 * 0x01b3;
|
||||
|
||||
t1 += t0 >> 16;
|
||||
t2 += t1 >> 16;
|
||||
t2 += h0 * 0x0100;
|
||||
t3 += h1 * 0x0100;
|
||||
|
||||
h0 = t0 & 0xffff;
|
||||
h1 = t1 & 0xffff;
|
||||
h2 = t2 & 0xffff;
|
||||
h3 = (t3 + (t2 >> 16)) & 0xffff;
|
||||
}
|
||||
|
||||
return (
|
||||
lowerHex[h3 >> 8] +
|
||||
lowerHex[h3 & 0xff] +
|
||||
lowerHex[h2 >> 8] +
|
||||
lowerHex[h2 & 0xff] +
|
||||
lowerHex[h1 >> 8] +
|
||||
lowerHex[h1 & 0xff] +
|
||||
lowerHex[h0 >> 8] +
|
||||
lowerHex[h0 & 0xff]
|
||||
);
|
||||
}
|
|
@ -27,7 +27,6 @@ export function getRoutePaths() {
|
|||
TopNThreads: `${BASE_ROUTE_PATH}/topn/threads`,
|
||||
TopNTraces: `${BASE_ROUTE_PATH}/topn/traces`,
|
||||
Flamechart: `${BASE_ROUTE_PATH}/flamechart`,
|
||||
FrameInformation: `${BASE_ROUTE_PATH}/frame_information`,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -187,7 +187,7 @@ export function getCalleeLabel(metadata: StackFrameMetadata) {
|
|||
if (metadata.FunctionName !== '') {
|
||||
const sourceFilename = metadata.SourceFilename;
|
||||
const sourceURL = sourceFilename ? sourceFilename.split('/').pop() : '';
|
||||
return `${getExeFileName(metadata)}: ${getFunctionName(metadata)} in ${sourceURL} #${
|
||||
return `${getExeFileName(metadata)}: ${getFunctionName(metadata)} in ${sourceURL}#${
|
||||
metadata.SourceLine
|
||||
}`;
|
||||
}
|
||||
|
|
|
@ -9,7 +9,6 @@ import {
|
|||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiHorizontalRule,
|
||||
EuiLoadingSpinner,
|
||||
EuiPanel,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
|
@ -17,7 +16,6 @@ import {
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { NOT_AVAILABLE_LABEL } from '../../../common';
|
||||
import { AsyncStatus } from '../../hooks/use_async';
|
||||
import { getImpactRows } from './get_impact_rows';
|
||||
|
||||
interface Props {
|
||||
|
@ -31,7 +29,6 @@ interface Props {
|
|||
totalSamples: number;
|
||||
totalSeconds: number;
|
||||
onClose: () => void;
|
||||
status: AsyncStatus;
|
||||
}
|
||||
|
||||
function KeyValueList({ rows }: { rows: Array<{ label: string; value: React.ReactNode }> }) {
|
||||
|
@ -61,11 +58,9 @@ function KeyValueList({ rows }: { rows: Array<{ label: string; value: React.Reac
|
|||
function FlamegraphFrameInformationPanel({
|
||||
children,
|
||||
onClose,
|
||||
status,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
onClose: () => void;
|
||||
status: AsyncStatus;
|
||||
}) {
|
||||
return (
|
||||
<EuiPanel style={{ width: 400, maxHeight: '100%', overflow: 'auto' }} hasBorder>
|
||||
|
@ -83,11 +78,6 @@ function FlamegraphFrameInformationPanel({
|
|||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
{status === AsyncStatus.Loading ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLoadingSpinner />
|
||||
</EuiFlexItem>
|
||||
) : undefined}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
|
@ -101,16 +91,10 @@ function FlamegraphFrameInformationPanel({
|
|||
);
|
||||
}
|
||||
|
||||
export function FlamegraphInformationWindow({
|
||||
onClose,
|
||||
frame,
|
||||
totalSamples,
|
||||
totalSeconds,
|
||||
status,
|
||||
}: Props) {
|
||||
export function FlamegraphInformationWindow({ onClose, frame, totalSamples, totalSeconds }: Props) {
|
||||
if (!frame) {
|
||||
return (
|
||||
<FlamegraphFrameInformationPanel status={status} onClose={onClose}>
|
||||
<FlamegraphFrameInformationPanel onClose={onClose}>
|
||||
<EuiText>
|
||||
{i18n.translate('xpack.profiling.flamegraphInformationWindow.selectFrame', {
|
||||
defaultMessage: 'Click on a frame to display more information',
|
||||
|
@ -130,7 +114,7 @@ export function FlamegraphInformationWindow({
|
|||
});
|
||||
|
||||
return (
|
||||
<FlamegraphFrameInformationPanel status={status} onClose={onClose}>
|
||||
<FlamegraphFrameInformationPanel onClose={onClose}>
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem>
|
||||
<KeyValueList
|
||||
|
|
|
@ -12,10 +12,8 @@ import { Maybe } from '@kbn/observability-plugin/common/typings';
|
|||
import { isNumber } from 'lodash';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { ElasticFlameGraph, FlameGraphComparisonMode } from '../../common/flamegraph';
|
||||
import { useAsync } from '../hooks/use_async';
|
||||
import { asPercentage } from '../utils/formatters/as_percentage';
|
||||
import { getFlamegraphModel } from '../utils/get_flamegraph_model';
|
||||
import { useProfilingDependencies } from './contexts/profiling_dependencies/use_profiling_dependencies';
|
||||
import { FlamegraphInformationWindow } from './flame_graphs_view/flamegraph_information_window';
|
||||
|
||||
function TooltipRow({
|
||||
|
@ -162,10 +160,6 @@ export const FlameGraph: React.FC<FlameGraphProps> = ({
|
|||
}) => {
|
||||
const theme = useEuiTheme();
|
||||
|
||||
const {
|
||||
services: { fetchFrameInformation },
|
||||
} = useProfilingDependencies();
|
||||
|
||||
const columnarData = useMemo(() => {
|
||||
return getFlamegraphModel({
|
||||
primaryFlamegraph,
|
||||
|
@ -193,38 +187,13 @@ export const FlameGraph: React.FC<FlameGraphProps> = ({
|
|||
|
||||
const [highlightedVmIndex, setHighlightedVmIndex] = useState<number | undefined>(undefined);
|
||||
|
||||
const highlightedFrameQueryParams = useMemo(() => {
|
||||
if (!primaryFlamegraph || highlightedVmIndex === undefined || highlightedVmIndex === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const frameID = primaryFlamegraph.FrameID[highlightedVmIndex];
|
||||
const executableID = primaryFlamegraph.ExecutableID[highlightedVmIndex];
|
||||
|
||||
return {
|
||||
frameID,
|
||||
executableID,
|
||||
};
|
||||
}, [primaryFlamegraph, highlightedVmIndex]);
|
||||
|
||||
const { data: highlightedFrame, status: highlightedFrameStatus } = useAsync(() => {
|
||||
if (!highlightedFrameQueryParams) {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
return fetchFrameInformation({
|
||||
frameID: highlightedFrameQueryParams.frameID,
|
||||
executableID: highlightedFrameQueryParams.executableID,
|
||||
});
|
||||
}, [highlightedFrameQueryParams, fetchFrameInformation]);
|
||||
|
||||
const selected: undefined | React.ComponentProps<typeof FlamegraphInformationWindow>['frame'] =
|
||||
primaryFlamegraph && highlightedFrame && highlightedVmIndex !== undefined
|
||||
primaryFlamegraph && highlightedVmIndex !== undefined
|
||||
? {
|
||||
exeFileName: highlightedFrame.ExeFileName,
|
||||
sourceFileName: highlightedFrame.SourceFilename,
|
||||
functionName: highlightedFrame.FunctionName,
|
||||
countInclusive: primaryFlamegraph.Samples[highlightedVmIndex],
|
||||
exeFileName: primaryFlamegraph.ExeFilename[highlightedVmIndex],
|
||||
sourceFileName: primaryFlamegraph.SourceFilename[highlightedVmIndex],
|
||||
functionName: primaryFlamegraph.FunctionName[highlightedVmIndex],
|
||||
countInclusive: primaryFlamegraph.CountInclusive[highlightedVmIndex],
|
||||
countExclusive: primaryFlamegraph.CountExclusive[highlightedVmIndex],
|
||||
}
|
||||
: undefined;
|
||||
|
@ -271,7 +240,7 @@ export const FlameGraph: React.FC<FlameGraphProps> = ({
|
|||
|
||||
const valueIndex = props.values[0].valueAccessor as number;
|
||||
const label = primaryFlamegraph.Label[valueIndex];
|
||||
const samples = primaryFlamegraph.Samples[valueIndex];
|
||||
const samples = primaryFlamegraph.CountInclusive[valueIndex];
|
||||
const countInclusive = primaryFlamegraph.CountInclusive[valueIndex];
|
||||
const countExclusive = primaryFlamegraph.CountExclusive[valueIndex];
|
||||
const nodeID = primaryFlamegraph.ID[valueIndex];
|
||||
|
@ -287,8 +256,8 @@ export const FlameGraph: React.FC<FlameGraphProps> = ({
|
|||
comparisonCountInclusive={comparisonNode?.CountInclusive}
|
||||
comparisonCountExclusive={comparisonNode?.CountExclusive}
|
||||
totalSamples={totalSamples}
|
||||
comparisonTotalSamples={comparisonFlamegraph?.Samples[0]}
|
||||
comparisonSamples={comparisonNode?.Samples}
|
||||
comparisonTotalSamples={comparisonFlamegraph?.CountInclusive[0]}
|
||||
comparisonSamples={comparisonNode?.CountInclusive}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
@ -309,7 +278,6 @@ export const FlameGraph: React.FC<FlameGraphProps> = ({
|
|||
<EuiFlexItem grow={false}>
|
||||
<FlamegraphInformationWindow
|
||||
frame={selected}
|
||||
status={highlightedFrameStatus}
|
||||
totalSeconds={primaryFlamegraph?.TotalSeconds ?? 0}
|
||||
totalSamples={totalSamples}
|
||||
onClose={() => {
|
||||
|
|
|
@ -7,9 +7,8 @@
|
|||
|
||||
import { CoreStart, HttpFetchQuery } from '@kbn/core/public';
|
||||
import { getRoutePaths } from '../common';
|
||||
import { ElasticFlameGraph } from '../common/flamegraph';
|
||||
import { BaseFlameGraph, createFlameGraph, ElasticFlameGraph } from '../common/flamegraph';
|
||||
import { TopNFunctions } from '../common/functions';
|
||||
import { StackFrameMetadata } from '../common/profiling';
|
||||
import { TopNResponse } from '../common/topn';
|
||||
|
||||
export interface Services {
|
||||
|
@ -31,10 +30,6 @@ export interface Services {
|
|||
timeTo: number;
|
||||
kuery: string;
|
||||
}) => Promise<ElasticFlameGraph>;
|
||||
fetchFrameInformation: (params: {
|
||||
frameID: string;
|
||||
executableID: string;
|
||||
}) => Promise<StackFrameMetadata>;
|
||||
}
|
||||
|
||||
export function getServices(core: CoreStart): Services {
|
||||
|
@ -96,24 +91,8 @@ export function getServices(core: CoreStart): Services {
|
|||
timeTo,
|
||||
kuery,
|
||||
};
|
||||
return await core.http.get(paths.Flamechart, { query });
|
||||
} catch (e) {
|
||||
return e;
|
||||
}
|
||||
},
|
||||
fetchFrameInformation: async ({
|
||||
frameID,
|
||||
executableID,
|
||||
}: {
|
||||
frameID: string;
|
||||
executableID: string;
|
||||
}) => {
|
||||
try {
|
||||
const query: HttpFetchQuery = {
|
||||
frameID,
|
||||
executableID,
|
||||
};
|
||||
return await core.http.get(paths.FrameInformation, { query });
|
||||
const baseFlamegraph: BaseFlameGraph = await core.http.get(paths.Flamechart, { query });
|
||||
return createFlameGraph(baseFlamegraph);
|
||||
} catch (e) {
|
||||
return e;
|
||||
}
|
||||
|
|
|
@ -6,12 +6,8 @@
|
|||
*/
|
||||
import d3 from 'd3';
|
||||
import { sum, uniqueId } from 'lodash';
|
||||
import {
|
||||
createColumnarViewModel,
|
||||
ElasticFlameGraph,
|
||||
FlameGraphComparisonMode,
|
||||
rgbToRGBA,
|
||||
} from '../../../common/flamegraph';
|
||||
import { createColumnarViewModel, rgbToRGBA } from '../../../common/columnar_view_model';
|
||||
import { ElasticFlameGraph, FlameGraphComparisonMode } from '../../../common/flamegraph';
|
||||
import { getInterpolationValue } from './get_interpolation_value';
|
||||
|
||||
const nullColumnarViewModel = {
|
||||
|
@ -39,10 +35,8 @@ export function getFlamegraphModel({
|
|||
colorNeutral: string;
|
||||
comparisonMode: FlameGraphComparisonMode;
|
||||
}) {
|
||||
const comparisonNodesById: Record<
|
||||
string,
|
||||
{ Samples: number; CountInclusive: number; CountExclusive: number }
|
||||
> = {};
|
||||
const comparisonNodesById: Record<string, { CountInclusive: number; CountExclusive: number }> =
|
||||
{};
|
||||
|
||||
if (!primaryFlamegraph || !primaryFlamegraph.Label || primaryFlamegraph.Label.length === 0) {
|
||||
return { key: uniqueId(), viewModel: nullColumnarViewModel, comparisonNodesById };
|
||||
|
@ -53,7 +47,6 @@ export function getFlamegraphModel({
|
|||
if (comparisonFlamegraph) {
|
||||
comparisonFlamegraph.ID.forEach((nodeID, index) => {
|
||||
comparisonNodesById[nodeID] = {
|
||||
Samples: comparisonFlamegraph.Samples[index],
|
||||
CountInclusive: comparisonFlamegraph.CountInclusive[index],
|
||||
CountExclusive: comparisonFlamegraph.CountExclusive[index],
|
||||
};
|
||||
|
@ -88,8 +81,8 @@ export function getFlamegraphModel({
|
|||
: primaryFlamegraph.TotalSeconds / comparisonFlamegraph.TotalSeconds;
|
||||
|
||||
primaryFlamegraph.ID.forEach((nodeID, index) => {
|
||||
const samples = primaryFlamegraph.Samples[index];
|
||||
const comparisonSamples = comparisonNodesById[nodeID]?.Samples as number | undefined;
|
||||
const samples = primaryFlamegraph.CountInclusive[index];
|
||||
const comparisonSamples = comparisonNodesById[nodeID]?.CountInclusive as number | undefined;
|
||||
|
||||
const foreground =
|
||||
comparisonMode === FlameGraphComparisonMode.Absolute ? samples : samples / totalSamples;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
10
yarn.lock
10
yarn.lock
|
@ -6523,11 +6523,6 @@
|
|||
dependencies:
|
||||
"@types/jquery" "*"
|
||||
|
||||
"@types/fnv-plus@^1.3.0":
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/fnv-plus/-/fnv-plus-1.3.0.tgz#0f43f0b7e7b4b24de3a1cab69bfa009508f4c084"
|
||||
integrity sha512-ijls8MsO6Q9JUSd5w1v4y2ijM6S4D/nmOyI/FwcepvrZfym0wZhLdYGFD5TJID7tga0O3I7SmtK69RzpSJ1Fcw==
|
||||
|
||||
"@types/fs-extra@^8.0.0":
|
||||
version "8.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-8.1.1.tgz#1e49f22d09aa46e19b51c0b013cb63d0d923a068"
|
||||
|
@ -15499,11 +15494,6 @@ fn.name@1.x.x:
|
|||
resolved "https://registry.yarnpkg.com/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc"
|
||||
integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==
|
||||
|
||||
fnv-plus@^1.3.1:
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/fnv-plus/-/fnv-plus-1.3.1.tgz#c34cb4572565434acb08ba257e4044ce2b006d67"
|
||||
integrity sha512-Gz1EvfOneuFfk4yG458dJ3TLJ7gV19q3OM/vVvvHf7eT02Hm1DleB4edsia6ahbKgAYxO9gvyQ1ioWZR+a00Yw==
|
||||
|
||||
focus-lock@^0.11.2:
|
||||
version "0.11.2"
|
||||
resolved "https://registry.yarnpkg.com/focus-lock/-/focus-lock-0.11.2.tgz#aeef3caf1cea757797ac8afdebaec8fd9ab243ed"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue