Skip to content

Commit

Permalink
[Profiling] Move additional flamegraph calculations into UI (#142415)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
jbcrail and kibanamachine authored Oct 5, 2022
1 parent 276cd3d commit c888aca
Show file tree
Hide file tree
Showing 22 changed files with 531 additions and 502 deletions.
2 changes: 0 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion renovate.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down
25 changes: 19 additions & 6 deletions x-pack/plugins/profiling/common/callee.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
test('inclusive count of root equals total sampled stacktraces', () => {
expect(tree.CountInclusive[0]).toEqual(totalSamples);
});

const tree = createCalleeTree(events, stackTraces, stackFrames, executables, totalFrames);
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();
});

expect(tree.Samples[0]).toEqual(totalSamples);
expect(tree.CountInclusive[0]).toEqual(totalSamples);
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);
});
});
153 changes: 56 additions & 97 deletions x-pack/plugins/profiling/common/callee.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -29,93 +24,56 @@ 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
);
export function createCalleeTree(
events: Map<StackTraceID, number>,
stackTraces: Map<StackTraceID, StackTrace>,
stackFrames: Map<StackFrameID, StackFrame>,
executables: Map<FileID, Executable>,
totalFrames: number
): CalleeTree {
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),
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.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.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;

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>,
stackFrames: Map<StackFrameID, StackFrame>,
executables: Map<FileID, Executable>,
totalFrames: number
): CalleeTree {
const tree = initCalleeTree(totalFrames);

const sortedStackTraceIDs = new Array<StackTraceID>();
for (const trace of stackTraces.keys()) {
sortedStackTraceIDs.push(trace);
Expand All @@ -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];
Expand All @@ -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 = insertNode(tree, currentNode, metadata, frameGroupID, samples);
node = tree.Size;

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;
Expand All @@ -186,8 +148,5 @@ export function createCalleeTree(
}
}

tree.CountExclusive[0] = 0;
tree.CountInclusive[0] = tree.Samples[0];

return tree;
}
32 changes: 32 additions & 0 deletions x-pack/plugins/profiling/common/columnar_view_model.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
Loading

0 comments on commit c888aca

Please sign in to comment.