[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

@ -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",

View file

@ -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"],

View file

@ -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);
});
});

View file

@ -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;
}

View 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);
});
});

View 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;
}

View file

@ -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();
});
});

View file

@ -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;
}

View 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);
});
});

View 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]
);
}

View file

@ -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`,
};
}

View file

@ -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
}`;
}

View file

@ -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

View file

@ -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={() => {

View file

@ -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;
}

View file

@ -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;

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;
}

View file

@ -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"