[Profiling] Reduce CPU usage for flamegraph and TopN function (#141017)

* Add instrumentation for flamegraph

I inlined the construction of the flamegraph into the respective route
so that we could add fine-grained instrumentation. We now use APM and
console logging to understand how long flamegraph construction takes.

* Remove unnecessary Set usage

* Remove superfluous clone

This was likely added when we needed to avoid infinite recursion when
serializing to JSON. This no longer has a useful function.

* Pass in pre-calculated frame group info

I noticed that we were creating frame group info multiple times so I
added it as a parameter for the intermediate node.

* Sort callees in one place

Callees should be sorted first by samples decreasing and then by frame
groups. Combining the two sorts makes the post-processing clearer to
future readers and/or maintainers.

* Capitalize fields in preparation of merging

* Align both node data structures

* Pass metadata instead of copying fields

* Refactor frame label method

* Use pre-calculated array length

* Use pre-allocated array

* Refactor intermediate node

* Remove intermediate node structure

* Move if statement out of for loop

* Fix comments

* Sort sibling nodes by frame group ID

* Calculate graph size during creation

* Add missing groupStackFrameMetadataByStackTrace

* Fix formatting

* Fix generated callee source

* Fix creation of frame group

* Fix test

* Remove filter for relevant traces

* Stop passing frame group

* Create root node inside createCallerCalleeGraph

* Fix timestamps

* Remove frame group comparator

* Add instrumentation for topN functions

* Allow for missing stacktraces

* Use Date.now instead
This commit is contained in:
Joseph Crail 2022-09-22 09:37:45 -07:00 committed by GitHub
parent 0fcfaec2dd
commit d8901857aa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 413 additions and 809 deletions

View file

@ -6,60 +6,18 @@
*/
import { sum } from 'lodash';
import {
createCallerCalleeDiagram,
createCallerCalleeIntermediateNode,
fromCallerCalleeIntermediateNode,
} from './callercallee';
import { createFrameGroupID } from './frame_group';
import { createStackFrameMetadata } from './profiling';
import { createCallerCalleeGraph } from './callercallee';
import { events, stackTraces, stackFrames, executables } from './__fixtures__/stacktraces';
describe('Caller-callee operations', () => {
test('1', () => {
const parentFrame = createStackFrameMetadata({
FileID: '6bc50d345244d5956f93a1b88f41874d',
FrameType: 3,
AddressOrLine: 971740,
FunctionName: 'epoll_wait',
SourceID: 'd670b496cafcaea431a23710fb5e4f58',
SourceLine: 30,
ExeFileName: 'libc-2.26.so',
});
const parent = createCallerCalleeIntermediateNode(parentFrame, 10, 'parent');
const childFrame = createStackFrameMetadata({
FileID: '8d8696a4fd51fa88da70d3fde138247d',
FrameType: 3,
AddressOrLine: 67000,
FunctionName: 'epoll_poll',
SourceID: 'f0a7901dcefed6cc8992a324b9df733c',
SourceLine: 150,
ExeFileName: 'auditd',
});
const child = createCallerCalleeIntermediateNode(childFrame, 10, 'child');
const root = createCallerCalleeIntermediateNode(createStackFrameMetadata(), 10, 'root');
root.callees.set(createFrameGroupID(child.frameGroup), child);
root.callees.set(createFrameGroupID(parent.frameGroup), parent);
const graph = fromCallerCalleeIntermediateNode(root);
// Modify original frames to verify graph does not contain references
parent.samples = 30;
child.samples = 20;
expect(graph.Callees[0].Samples).toEqual(10);
expect(graph.Callees[1].Samples).toEqual(10);
});
test('2', () => {
const totalSamples = sum([...events.values()]);
const root = createCallerCalleeDiagram(events, stackTraces, stackFrames, executables);
expect(root.Samples).toEqual(totalSamples);
expect(root.CountInclusive).toEqual(totalSamples);
expect(root.CountExclusive).toEqual(0);
const graph = createCallerCalleeGraph(events, stackTraces, stackFrames, executables);
expect(graph.root.Samples).toEqual(totalSamples);
expect(graph.root.CountInclusive).toEqual(totalSamples);
expect(graph.root.CountExclusive).toEqual(0);
});
});

View file

@ -5,19 +5,12 @@
* 2.0.
*/
import { clone } from 'lodash';
import {
compareFrameGroup,
createFrameGroup,
createFrameGroupID,
FrameGroup,
FrameGroupID,
} from './frame_group';
import { createFrameGroup, createFrameGroupID, FrameGroupID } from './frame_group';
import {
createStackFrameMetadata,
emptyStackTrace,
Executable,
FileID,
groupStackFrameMetadataByStackTrace,
StackFrame,
StackFrameID,
StackFrameMetadata,
@ -25,298 +18,160 @@ import {
StackTraceID,
} from './profiling';
export interface CallerCalleeIntermediateNode {
frameGroup: FrameGroup;
frameGroupID: string;
callers: Map<FrameGroupID, CallerCalleeIntermediateNode>;
callees: Map<FrameGroupID, CallerCalleeIntermediateNode>;
frameMetadata: Set<StackFrameMetadata>;
samples: number;
countInclusive: number;
countExclusive: number;
export interface CallerCalleeNode {
Callers: Map<FrameGroupID, CallerCalleeNode>;
Callees: Map<FrameGroupID, CallerCalleeNode>;
FrameMetadata: StackFrameMetadata;
FrameGroupID: FrameGroupID;
Samples: number;
CountInclusive: number;
CountExclusive: number;
}
export function createCallerCalleeIntermediateNode(
export function createCallerCalleeNode(
frameMetadata: StackFrameMetadata,
samples: number,
frameGroupID: string
): CallerCalleeIntermediateNode {
frameGroupID: FrameGroupID,
samples: number
): CallerCalleeNode {
return {
frameGroup: createFrameGroup(frameMetadata),
callers: new Map<FrameGroupID, CallerCalleeIntermediateNode>(),
callees: new Map<FrameGroupID, CallerCalleeIntermediateNode>(),
frameMetadata: new Set<StackFrameMetadata>([frameMetadata]),
samples,
countInclusive: 0,
countExclusive: 0,
frameGroupID,
Callers: new Map<FrameGroupID, CallerCalleeNode>(),
Callees: new Map<FrameGroupID, CallerCalleeNode>(),
FrameMetadata: frameMetadata,
FrameGroupID: frameGroupID,
Samples: samples,
CountInclusive: 0,
CountExclusive: 0,
};
}
interface RelevantTrace {
frames: StackFrameMetadata[];
index: number;
export interface CallerCalleeGraph {
root: CallerCalleeNode;
size: number;
}
// selectRelevantTraces searches through a map that maps trace hashes to their
// frames and only returns those traces that have a frame that are equivalent
// to the rootFrame provided. It also sets the "index" in the sequence of
// traces at which the rootFrame is found.
//
// If the rootFrame is "empty" (e.g. fileID is empty and line number is 0), all
// traces in the given time frame are deemed relevant, and the "index" is set
// to the length of the trace -- since there is no root frame, the frame should
// be considered "calls-to" only going.
function selectRelevantTraces(
rootFrame: StackFrameMetadata,
frames: Map<StackTraceID, StackFrameMetadata[]>
): Map<StackTraceID, RelevantTrace> {
const result = new Map<StackTraceID, RelevantTrace>();
const rootString = createFrameGroupID(createFrameGroup(rootFrame));
for (const [stackTraceID, frameMetadata] of frames) {
if (rootFrame.FileID === '' && rootFrame.AddressOrLine === 0) {
// If the root frame is empty, every trace is relevant, and all elements
// of the trace are relevant. This means that the index is set to the
// length of the frameMetadata, implying that in the absence of a root
// frame the "topmost" frame is the root frame.
result.set(stackTraceID, {
frames: frameMetadata,
index: frameMetadata.length,
} as RelevantTrace);
} else {
// Search for the right index of the root frame in the frameMetadata, and
// set it in the result.
for (let i = 0; i < frameMetadata.length; i++) {
if (rootString === createFrameGroupID(createFrameGroup(frameMetadata[i]))) {
result.set(stackTraceID, {
frames: frameMetadata,
index: i,
} as RelevantTrace);
}
}
}
}
return result;
}
function sortRelevantTraces(relevantTraces: Map<StackTraceID, RelevantTrace>): StackTraceID[] {
const sortedRelevantTraces = new Array<StackTraceID>();
for (const trace of relevantTraces.keys()) {
sortedRelevantTraces.push(trace);
}
return sortedRelevantTraces.sort((t1, t2) => {
if (t1 < t2) return -1;
if (t1 > t2) return 1;
return 0;
});
}
// createCallerCalleeIntermediateRoot creates a graph in the internal
// representation from a StackFrameMetadata that identifies the "centered"
// function and the trace results that provide traces and the number of times
// that the trace has been seen.
// createCallerCalleeGraph creates a graph in the internal representation
// from a StackFrameMetadata that identifies the "centered" function and
// the trace results that provide traces and the number of times that the
// trace has been seen.
//
// The resulting data structure contains all of the data, but is not yet in the
// form most easily digestible by others.
export function createCallerCalleeIntermediateRoot(
rootFrame: StackFrameMetadata,
traces: Map<StackTraceID, number>,
frames: Map<StackTraceID, StackFrameMetadata[]>
): CallerCalleeIntermediateNode {
// Create a node for the centered frame
const root = createCallerCalleeIntermediateNode(rootFrame, 0, 'root');
export function createCallerCalleeGraph(
events: Map<StackTraceID, number>,
stackTraces: Map<StackTraceID, StackTrace>,
stackFrames: Map<StackFrameID, StackFrame>,
executables: Map<FileID, Executable>
): CallerCalleeGraph {
// Create a root node for the graph
const rootFrame = createStackFrameMetadata();
const rootFrameGroup = createFrameGroup(
rootFrame.FileID,
rootFrame.AddressOrLine,
rootFrame.ExeFileName,
rootFrame.SourceFilename,
rootFrame.FunctionName
);
const rootFrameGroupID = createFrameGroupID(rootFrameGroup);
const root = createCallerCalleeNode(rootFrame, rootFrameGroupID, 0);
const graph: CallerCalleeGraph = { root, size: 1 };
// Obtain only the relevant frames (e.g. frames that contain the root frame
// somewhere). If the root frame is "empty" (e.g. fileID is zero and line
// number is zero), all frames are deemed relevant.
const relevantTraces = selectRelevantTraces(rootFrame, frames);
// For a deterministic result we have to walk the traces in a deterministic
// order. A deterministic result allows for deterministic UI views, something
// that users expect.
const relevantTracesSorted = sortRelevantTraces(relevantTraces);
const sortedStackTraceIDs = new Array<StackTraceID>();
for (const trace of stackTraces.keys()) {
sortedStackTraceIDs.push(trace);
}
sortedStackTraceIDs.sort((t1, t2) => {
return t1.localeCompare(t2);
});
// Walk through all traces that contain the root. Increment the count of the
// root by the count of that trace. Walk "up" the trace (through the callers)
// and add the count of the trace to each caller. Then walk "down" the trace
// (through the callees) and add the count of the trace to each callee.
for (const traceHash of relevantTracesSorted) {
const trace = relevantTraces.get(traceHash)!;
// The slice of frames is ordered so that the leaf function is at index 0.
// This means that the "second part" of the slice are the callers, and the
// "first part" are the callees.
for (const stackTraceID of sortedStackTraceIDs) {
// The slice of frames is ordered so that the leaf function is at the
// highest index. This means that the "first part" of the slice are the
// callers, and the "second part" are the callees.
//
// We currently assume there are no callers.
const callees = trace.frames;
const samples = traces.get(traceHash)!;
// Go through the callees, reverse iteration
let currentNode = clone(root);
root.samples += samples;
// It is possible that we do not have a stacktrace for an event,
// e.g. when stopping the host agent or on network errors.
const stackTrace = stackTraces.get(stackTraceID) ?? emptyStackTrace;
const lenStackTrace = stackTrace.FrameIDs.length;
const samples = events.get(stackTraceID)!;
let currentNode = root;
root.Samples += samples;
for (let i = 0; i < lenStackTrace; i++) {
const frameID = stackTrace.FrameIDs[i];
const fileID = stackTrace.FileIDs[i];
const addressOrLine = stackTrace.AddressOrLines[i];
const frame = stackFrames.get(frameID)!;
const executable = executables.get(fileID)!;
const frameGroup = createFrameGroup(
fileID,
addressOrLine,
executable.FileName,
frame.FileName,
frame.FunctionName
);
const frameGroupID = createFrameGroupID(frameGroup);
let node = currentNode.Callees.get(frameGroupID);
for (let i = 0; i < callees.length; i++) {
const callee = callees[i];
const calleeName = createFrameGroupID(createFrameGroup(callee));
let node = currentNode.callees.get(calleeName);
if (node === undefined) {
node = createCallerCalleeIntermediateNode(callee, samples, calleeName);
currentNode.callees.set(calleeName, node);
const callee = 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 = createCallerCalleeNode(callee, frameGroupID, samples);
currentNode.Callees.set(frameGroupID, node);
graph.size++;
} else {
node.samples += samples;
node.Samples += samples;
}
node.countInclusive += samples;
node.CountInclusive += samples;
if (i === callees.length - 1) {
if (i === lenStackTrace - 1) {
// Leaf frame: sum up counts for exclusive CPU.
node.countExclusive += samples;
node.CountExclusive += samples;
}
currentNode = node;
}
}
root.countExclusive = 0;
root.countInclusive = root.samples;
root.CountExclusive = 0;
root.CountInclusive = root.Samples;
return root;
return graph;
}
export interface CallerCalleeNode {
FrameID: string;
FrameGroupID: string;
Callers: CallerCalleeNode[];
Callees: CallerCalleeNode[];
FileID: string;
FrameType: number;
ExeFileName: string;
FunctionID: string;
FunctionName: string;
AddressOrLine: number;
FunctionSourceLine: number;
FunctionSourceID: string;
FunctionSourceURL: string;
SourceFilename: string;
SourceLine: number;
Samples: number;
CountInclusive: number;
CountExclusive: number;
}
export function createCallerCalleeNode(options: Partial<CallerCalleeNode> = {}): CallerCalleeNode {
const node = {} as CallerCalleeNode;
node.FrameID = options.FrameID ?? '';
node.FrameGroupID = options.FrameGroupID ?? '';
node.Callers = clone(options.Callers ?? []);
node.Callees = clone(options.Callees ?? []);
node.FileID = options.FileID ?? '';
node.FrameType = options.FrameType ?? 0;
node.ExeFileName = options.ExeFileName ?? '';
node.FunctionID = options.FunctionID ?? '';
node.FunctionName = options.FunctionName ?? '';
node.AddressOrLine = options.AddressOrLine ?? 0;
node.FunctionSourceLine = options.FunctionSourceLine ?? 0;
node.FunctionSourceID = options.FunctionSourceID ?? '';
node.FunctionSourceURL = options.FunctionSourceURL ?? '';
node.SourceFilename = options.SourceFilename ?? '';
node.SourceLine = options.SourceLine ?? 0;
node.Samples = options.Samples ?? 0;
node.CountInclusive = options.CountInclusive ?? 0;
node.CountExclusive = options.CountExclusive ?? 0;
return node;
}
// selectCallerCalleeData is the "standard" way of merging multiple frames into
// one node. It simply takes the data from the first frame.
function selectCallerCalleeData(frameMetadata: Set<StackFrameMetadata>, node: CallerCalleeNode) {
for (const metadata of frameMetadata) {
node.FileID = metadata.FileID;
node.FrameType = metadata.FrameType;
node.ExeFileName = metadata.ExeFileName;
node.FunctionID = metadata.FunctionName;
node.FunctionName = metadata.FunctionName;
node.AddressOrLine = metadata.AddressOrLine;
node.FrameID = metadata.FrameID;
// Unknown/invalid offsets are currently set to 0.
//
// In this case we leave FunctionSourceLine=0 as a flag for the UI that the
// FunctionSourceLine should not be displayed.
//
// As FunctionOffset=0 could also be a legit value, this work-around needs
// a real fix. The idea for after GA is to change FunctionOffset=-1 to
// indicate unknown/invalid.
if (metadata.FunctionOffset > 0) {
node.FunctionSourceLine = metadata.SourceLine - metadata.FunctionOffset;
} else {
node.FunctionSourceLine = 0;
}
node.FunctionSourceID = metadata.SourceID;
node.FunctionSourceURL = metadata.SourceCodeURL;
node.SourceFilename = metadata.SourceFilename;
node.SourceLine = metadata.SourceLine;
break;
}
}
function sortNodes(
nodes: Map<FrameGroupID, CallerCalleeIntermediateNode>
): CallerCalleeIntermediateNode[] {
const sortedNodes = new Array<CallerCalleeIntermediateNode>();
for (const node of nodes.values()) {
export function sortCallerCalleeNodes(
nodes: Map<FrameGroupID, CallerCalleeNode>
): CallerCalleeNode[] {
const sortedNodes = new Array<CallerCalleeNode>();
for (const [_, node] of nodes) {
sortedNodes.push(node);
}
return sortedNodes.sort((n1, n2) => {
return compareFrameGroup(n1.frameGroup, n2.frameGroup);
if (n1.Samples > n2.Samples) {
return -1;
}
if (n1.Samples < n2.Samples) {
return 1;
}
return n1.FrameGroupID.localeCompare(n2.FrameGroupID);
});
}
// fromCallerCalleeIntermediateNode is used to convert the intermediate representation
// of the diagram into the format that is easily JSONified and more easily consumed by
// others.
export function fromCallerCalleeIntermediateNode(
root: CallerCalleeIntermediateNode
): CallerCalleeNode {
const node = createCallerCalleeNode({
FrameGroupID: root.frameGroupID,
Samples: root.samples,
CountInclusive: root.countInclusive,
CountExclusive: root.countExclusive,
});
// Populate the other fields with data from the root node. Selectors are not supposed
// to be able to fail.
selectCallerCalleeData(root.frameMetadata, node);
// Now fill the caller and callee arrays.
// For a deterministic result we have to walk the callers / callees in a deterministic
// order. A deterministic result allows deterministic UI views, something that users expect.
for (const caller of sortNodes(root.callers)) {
node.Callers.push(fromCallerCalleeIntermediateNode(caller));
}
for (const callee of sortNodes(root.callees)) {
node.Callees.push(fromCallerCalleeIntermediateNode(callee));
}
return node;
}
export function createCallerCalleeDiagram(
events: Map<StackTraceID, number>,
stackTraces: Map<StackTraceID, StackTrace>,
stackFrames: Map<StackFrameID, StackFrame>,
executables: Map<FileID, Executable>
): CallerCalleeNode {
const rootFrame = createStackFrameMetadata();
const frameMetadataForTraces = groupStackFrameMetadataByStackTrace(
stackTraces,
stackFrames,
executables
);
const root = createCallerCalleeIntermediateRoot(rootFrame, events, frameMetadataForTraces);
return fromCallerCalleeIntermediateNode(root);
}

View file

@ -6,16 +6,8 @@
*/
import fnv from 'fnv-plus';
import { CallerCalleeNode, createCallerCalleeDiagram } from './callercallee';
import {
describeFrameType,
Executable,
FileID,
StackFrame,
StackFrameID,
StackTrace,
StackTraceID,
} from './profiling';
import { CallerCalleeGraph, sortCallerCalleeNodes } from './callercallee';
import { getCalleeLabel } from './profiling';
interface ColumnarCallerCallee {
Label: string[];
@ -30,7 +22,7 @@ interface ColumnarCallerCallee {
ExecutableID: string[];
}
export interface ElasticFlameGraph {
export interface FlameGraph {
Label: string[];
Value: number[];
Position: number[];
@ -41,6 +33,9 @@ export interface ElasticFlameGraph {
ID: string[];
FrameID: string[];
ExecutableID: string[];
}
export interface ElasticFlameGraph extends FlameGraph {
TotalSeconds: number;
TotalTraces: number;
SampledTraces: number;
@ -99,205 +94,110 @@ function normalize(n: number, lower: number, upper: number): number {
return (n - lower) / (upper - lower);
}
function checkIfStringHasParentheses(s: string) {
return /\(|\)/.test(s);
}
// createColumnarCallerCallee flattens the intermediate representation of the diagram
// into a columnar format that is more compact than JSON. This representation will later
// need to be normalized into the response ultimately consumed by the flamegraph.
export function createColumnarCallerCallee(graph: CallerCalleeGraph): ColumnarCallerCallee {
const numCallees = graph.size;
const columnar: ColumnarCallerCallee = {
Label: new Array<string>(numCallees),
Value: new Array<number>(numCallees),
X: new Array<number>(numCallees),
Y: new Array<number>(numCallees),
Color: new Array<number>(numCallees * 4),
CountInclusive: new Array<number>(numCallees),
CountExclusive: new Array<number>(numCallees),
ID: new Array<string>(numCallees),
FrameID: new Array<string>(numCallees),
ExecutableID: new Array<string>(numCallees),
};
function getFunctionName(node: CallerCalleeNode) {
return node.FunctionName !== '' && !checkIfStringHasParentheses(node.FunctionName)
? `${node.FunctionName}()`
: node.FunctionName;
}
const queue = [{ x: 0, depth: 1, node: graph.root, parentID: 'root' }];
function getExeFileName(node: CallerCalleeNode) {
if (node?.ExeFileName === undefined) {
return '';
}
if (node.ExeFileName !== '') {
return node.ExeFileName;
}
return describeFrameType(node.FrameType);
}
let idx = 0;
while (queue.length > 0) {
const { x, depth, node, parentID } = queue.pop()!;
function getLabel(node: CallerCalleeNode) {
if (node.FunctionName !== '') {
const sourceFilename = node.SourceFilename;
const sourceURL = sourceFilename ? sourceFilename.split('/').pop() : '';
return `${getExeFileName(node)}: ${getFunctionName(node)} in ${sourceURL} #${node.SourceLine}`;
}
return getExeFileName(node);
}
export class FlameGraph {
// 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)
// ...
sampleRate: number;
// 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.
totalCount: number;
totalSeconds: number;
events: Map<StackTraceID, number>;
stacktraces: Map<StackTraceID, StackTrace>;
stackframes: Map<StackFrameID, StackFrame>;
executables: Map<FileID, Executable>;
constructor({
sampleRate,
totalCount,
events,
stackTraces,
stackFrames,
executables,
totalSeconds,
}: {
sampleRate: number;
totalCount: number;
events: Map<StackTraceID, number>;
stackTraces: Map<StackTraceID, StackTrace>;
stackFrames: Map<StackFrameID, StackFrame>;
executables: Map<FileID, Executable>;
totalSeconds: number;
}) {
this.sampleRate = sampleRate;
this.totalCount = totalCount;
this.events = events;
this.stacktraces = stackTraces;
this.stackframes = stackFrames;
this.executables = executables;
this.totalSeconds = totalSeconds;
}
private countCallees(root: CallerCalleeNode): number {
let numCallees = 1;
for (const callee of root.Callees) {
numCallees += this.countCallees(callee);
if (x === 0 && depth === 1) {
columnar.Label[idx] = 'root: Represents 100% of CPU time.';
} else {
columnar.Label[idx] = getCalleeLabel(node.FrameMetadata);
}
return numCallees;
}
columnar.Value[idx] = node.Samples;
columnar.X[idx] = x;
columnar.Y[idx] = depth;
// createColumnarCallerCallee flattens the intermediate representation of the diagram
// into a columnar format that is more compact than JSON. This representation will later
// need to be normalized into the response ultimately consumed by the flamegraph.
private createColumnarCallerCallee(root: CallerCalleeNode): ColumnarCallerCallee {
const numCallees = this.countCallees(root);
const columnar: ColumnarCallerCallee = {
Label: new Array<string>(numCallees),
Value: new Array<number>(numCallees),
X: new Array<number>(numCallees),
Y: new Array<number>(numCallees),
Color: new Array<number>(numCallees * 4),
CountInclusive: new Array<number>(numCallees),
CountExclusive: new Array<number>(numCallees),
ID: new Array<string>(numCallees),
FrameID: new Array<string>(numCallees),
ExecutableID: new Array<string>(numCallees),
};
const [red, green, blue, alpha] = rgbToRGBA(frameTypeToRGB(node.FrameMetadata.FrameType, x));
const j = 4 * idx;
columnar.Color[j] = red;
columnar.Color[j + 1] = green;
columnar.Color[j + 2] = blue;
columnar.Color[j + 3] = alpha;
const queue = [{ x: 0, depth: 1, node: root, parentID: 'root' }];
columnar.CountInclusive[idx] = node.CountInclusive;
columnar.CountExclusive[idx] = node.CountExclusive;
let idx = 0;
while (queue.length > 0) {
const { x, depth, node, parentID } = queue.pop()!;
const id = fnv.fast1a64utf(`${parentID}${node.FrameGroupID}`).toString();
if (x === 0 && depth === 1) {
columnar.Label[idx] = 'root: Represents 100% of CPU time.';
} else {
columnar.Label[idx] = getLabel(node);
}
columnar.Value[idx] = node.Samples;
columnar.X[idx] = x;
columnar.Y[idx] = depth;
columnar.ID[idx] = id;
columnar.FrameID[idx] = node.FrameMetadata.FrameID;
columnar.ExecutableID[idx] = node.FrameMetadata.FileID;
const [red, green, blue, alpha] = rgbToRGBA(frameTypeToRGB(node.FrameType, x));
const j = 4 * idx;
columnar.Color[j] = red;
columnar.Color[j + 1] = green;
columnar.Color[j + 2] = blue;
columnar.Color[j + 3] = alpha;
// For a deterministic result we have to walk the callers / callees in a deterministic
// order. A deterministic result allows deterministic UI views, something that users expect.
const callees = sortCallerCalleeNodes(node.Callees);
columnar.CountInclusive[idx] = node.CountInclusive;
columnar.CountExclusive[idx] = node.CountExclusive;
const id = fnv.fast1a64utf(`${parentID}${node.FrameGroupID}`).toString();
columnar.ID[idx] = id;
columnar.FrameID[idx] = node.FrameID;
columnar.ExecutableID[idx] = node.FileID;
node.Callees.sort((a: CallerCalleeNode, b: CallerCalleeNode) => b.Samples - a.Samples);
let delta = 0;
for (const callee of node.Callees) {
delta += callee.Samples;
}
for (let i = node.Callees.length - 1; i >= 0; i--) {
delta -= node.Callees[i].Samples;
queue.push({ x: x + delta, depth: depth + 1, node: node.Callees[i], parentID: id });
}
idx++;
let delta = 0;
for (const callee of callees) {
delta += callee.Samples;
}
return columnar;
}
// createElasticFlameGraph normalizes the intermediate columnar representation into the
// response ultimately consumed by the flamegraph.
private createElasticFlameGraph(columnar: ColumnarCallerCallee): ElasticFlameGraph {
const graph: ElasticFlameGraph = {
Label: [],
Value: [],
Position: [],
Size: [],
Color: [],
CountInclusive: [],
CountExclusive: [],
ID: [],
FrameID: [],
ExecutableID: [],
TotalSeconds: this.totalSeconds,
TotalTraces: Math.floor(this.totalCount / this.sampleRate),
SampledTraces: this.totalCount,
};
graph.Label = columnar.Label;
graph.Value = columnar.Value;
graph.Color = columnar.Color;
graph.CountInclusive = columnar.CountInclusive;
graph.CountExclusive = columnar.CountExclusive;
graph.ID = columnar.ID;
graph.FrameID = columnar.FrameID;
graph.ExecutableID = columnar.ExecutableID;
const maxX = columnar.Value[0];
const maxY = columnar.Y.reduce((max, n) => (n > max ? n : max), 0);
for (let i = 0; i < columnar.X.length; i++) {
const x = normalize(columnar.X[i], 0, maxX);
const y = normalize(maxY - columnar.Y[i], 0, maxY);
graph.Position.push(x, y);
for (let i = callees.length - 1; i >= 0; i--) {
delta -= callees[i].Samples;
queue.push({ x: x + delta, depth: depth + 1, node: callees[i], parentID: id });
}
graph.Size = graph.Value.map((n) => normalize(n, 0, maxX));
return graph;
idx++;
}
toElastic(): ElasticFlameGraph {
const root = createCallerCalleeDiagram(
this.events,
this.stacktraces,
this.stackframes,
this.executables
);
return this.createElasticFlameGraph(this.createColumnarCallerCallee(root));
}
return columnar;
}
// createFlameGraph normalizes the intermediate columnar representation into the
// response ultimately consumed by the flamegraph in the UI.
export function createFlameGraph(columnar: ColumnarCallerCallee): FlameGraph {
const graph: FlameGraph = {
Label: [],
Value: [],
Position: [],
Size: [],
Color: [],
CountInclusive: [],
CountExclusive: [],
ID: [],
FrameID: [],
ExecutableID: [],
};
graph.Label = columnar.Label;
graph.Value = columnar.Value;
graph.Color = columnar.Color;
graph.CountInclusive = columnar.CountInclusive;
graph.CountExclusive = columnar.CountExclusive;
graph.ID = columnar.ID;
graph.FrameID = columnar.FrameID;
graph.ExecutableID = columnar.ExecutableID;
const maxX = columnar.Value[0];
const maxY = columnar.Y.reduce((max, n) => (n > max ? n : max), 0);
for (let i = 0; i < columnar.X.length; i++) {
const x = normalize(columnar.X[i], 0, maxX);
const y = normalize(maxY - columnar.Y[i], 0, maxY);
graph.Position.push(x, y);
}
graph.Size = graph.Value.map((n) => normalize(n, 0, maxX));
return graph;
}

View file

@ -5,161 +5,27 @@
* 2.0.
*/
import { compareFrameGroup, createFrameGroup, createFrameGroupID } from './frame_group';
import { createStackFrameMetadata } from './profiling';
import { createFrameGroup, createFrameGroupID } from './frame_group';
const nonSymbolizedFrameGroups = [
createFrameGroup(
createStackFrameMetadata({
FileID: '0x0123456789ABCDEF',
AddressOrLine: 102938,
})
),
createFrameGroup(
createStackFrameMetadata({
FileID: '0x0123456789ABCDEF',
AddressOrLine: 1234,
})
),
createFrameGroup(
createStackFrameMetadata({
FileID: '0x0102030405060708',
AddressOrLine: 1234,
})
),
createFrameGroup('0x0123456789ABCDEF', 102938, '', '', ''),
createFrameGroup('0x0123456789ABCDEF', 1234, '', '', ''),
createFrameGroup('0x0102030405060708', 1234, '', '', ''),
];
const elfSymbolizedFrameGroups = [
createFrameGroup(
createStackFrameMetadata({
FileID: '0x0123456789ABCDEF',
ExeFileName: 'libc',
FunctionName: 'strlen()',
})
),
createFrameGroup(
createStackFrameMetadata({
FileID: '0xFEDCBA9876543210',
ExeFileName: 'libc',
FunctionName: 'strtok()',
})
),
createFrameGroup(
createStackFrameMetadata({
FileID: '0xFEDCBA9876543210',
ExeFileName: 'myapp',
FunctionName: 'main()',
})
),
createFrameGroup('0x0123456789ABCDEF', 0, 'libc', '', 'strlen()'),
createFrameGroup('0xFEDCBA9876543210', 0, 'libc', '', 'strtok()'),
createFrameGroup('0xFEDCBA9876543210', 0, 'myapp', '', 'main()'),
];
const symbolizedFrameGroups = [
createFrameGroup(
createStackFrameMetadata({
ExeFileName: 'chrome',
SourceFilename: 'strlen()',
FunctionName: 'strlen()',
})
),
createFrameGroup(
createStackFrameMetadata({
ExeFileName: 'dockerd',
SourceFilename: 'main()',
FunctionName: 'createTask()',
})
),
createFrameGroup(
createStackFrameMetadata({
ExeFileName: 'oom_reaper',
SourceFilename: 'main()',
FunctionName: 'crash()',
})
),
createFrameGroup('', 0, 'chrome', 'strlen()', 'strlen()'),
createFrameGroup('', 0, 'dockerd', 'main()', 'createTask()'),
createFrameGroup('', 0, 'oom_reaper', 'main()', 'crash()'),
];
describe('Frame group operations', () => {
describe('check if a non-symbolized frame group is', () => {
test('less than another non-symbolized frame group', () => {
expect(compareFrameGroup(nonSymbolizedFrameGroups[1], nonSymbolizedFrameGroups[0])).toEqual(
-1
);
});
test('equal to another non-symbolized frame group', () => {
expect(compareFrameGroup(nonSymbolizedFrameGroups[0], nonSymbolizedFrameGroups[0])).toEqual(
0
);
});
test('greater than another non-symbolized frame group', () => {
expect(compareFrameGroup(nonSymbolizedFrameGroups[1], nonSymbolizedFrameGroups[2])).toEqual(
1
);
});
test('less than an ELF-symbolized frame group', () => {
expect(compareFrameGroup(nonSymbolizedFrameGroups[1], elfSymbolizedFrameGroups[0])).toEqual(
-1
);
});
test('less than a symbolized frame group', () => {
expect(compareFrameGroup(nonSymbolizedFrameGroups[1], symbolizedFrameGroups[0])).toEqual(-1);
});
});
describe('check if an ELF-symbolized frame group is', () => {
test('less than another ELF-symbolized frame group', () => {
expect(compareFrameGroup(elfSymbolizedFrameGroups[0], elfSymbolizedFrameGroups[1])).toEqual(
-1
);
});
test('equal to another ELF-symbolized frame group', () => {
expect(compareFrameGroup(elfSymbolizedFrameGroups[0], elfSymbolizedFrameGroups[0])).toEqual(
0
);
});
test('greater than another ELF-symbolized frame group', () => {
expect(compareFrameGroup(elfSymbolizedFrameGroups[1], elfSymbolizedFrameGroups[0])).toEqual(
1
);
});
test('greater than a non-symbolized frame group', () => {
expect(compareFrameGroup(elfSymbolizedFrameGroups[0], nonSymbolizedFrameGroups[0])).toEqual(
1
);
});
test('less than a symbolized frame group', () => {
expect(compareFrameGroup(elfSymbolizedFrameGroups[2], symbolizedFrameGroups[0])).toEqual(-1);
});
});
describe('check if a symbolized frame group is', () => {
test('less than another symbolized frame group', () => {
expect(compareFrameGroup(symbolizedFrameGroups[0], symbolizedFrameGroups[1])).toEqual(-1);
});
test('equal to another symbolized frame group', () => {
expect(compareFrameGroup(symbolizedFrameGroups[0], symbolizedFrameGroups[0])).toEqual(0);
});
test('greater than another symbolized frame group', () => {
expect(compareFrameGroup(symbolizedFrameGroups[1], symbolizedFrameGroups[0])).toEqual(1);
});
test('greater than a non-symbolized frame group', () => {
expect(compareFrameGroup(symbolizedFrameGroups[0], nonSymbolizedFrameGroups[0])).toEqual(1);
});
test('greater than an ELF-symbolized frame group', () => {
expect(compareFrameGroup(symbolizedFrameGroups[0], elfSymbolizedFrameGroups[2])).toEqual(1);
});
});
describe('check serialization for', () => {
test('non-symbolized frame', () => {
expect(createFrameGroupID(nonSymbolizedFrameGroups[0])).toEqual(

View file

@ -47,113 +47,38 @@ export type FrameGroup = EmptyFrameGroup | ElfFrameGroup | FullFrameGroup;
// For ELF-symbolized frames, group by FunctionName, ExeFileName and FileID.
// For non-symbolized frames, group by FileID and AddressOrLine.
// otherwise group by ExeFileName, SourceFilename and FunctionName.
export function createFrameGroup(frame: StackFrameMetadata): FrameGroup {
if (frame.FunctionName === '') {
export function createFrameGroup(
fileID: StackFrameMetadata['FileID'],
addressOrLine: StackFrameMetadata['AddressOrLine'],
exeFilename: StackFrameMetadata['ExeFileName'],
sourceFilename: StackFrameMetadata['SourceFilename'],
functionName: StackFrameMetadata['FunctionName']
): FrameGroup {
if (functionName === '') {
return {
name: FrameGroupName.EMPTY,
fileID: frame.FileID,
addressOrLine: frame.AddressOrLine,
fileID,
addressOrLine,
} as EmptyFrameGroup;
}
if (frame.SourceFilename === '') {
if (sourceFilename === '') {
return {
name: FrameGroupName.ELF,
fileID: frame.FileID,
exeFilename: frame.ExeFileName,
functionName: frame.FunctionName,
fileID,
exeFilename,
functionName,
} as ElfFrameGroup;
}
return {
name: FrameGroupName.FULL,
exeFilename: frame.ExeFileName,
functionName: frame.FunctionName,
sourceFilename: frame.SourceFilename,
exeFilename,
functionName,
sourceFilename,
} as FullFrameGroup;
}
// compareFrameGroup compares any two frame groups
//
// In general, frame groups are ordered using the following steps:
//
// * If frame groups are the same type, then we compare using their same
// properties
// * If frame groups have different types, then we compare using overlapping
// properties
// * If frame groups do not share properties, then we compare using the frame
// group type
//
// The union of the properties across all frame group types are ordered below
// from highest to lowest. For instance, given any two frame groups, shared
// properties are compared in the given order:
//
// * exeFilename
// * sourceFilename
// * functionName
// * fileID
// * addressOrLine
//
// Frame group types are ordered according to how much symbolization metadata
// is available, starting from most to least:
//
// * Symbolized frame group
// * ELF-symbolized frame group
// * Unsymbolized frame group
export function compareFrameGroup(a: FrameGroup, b: FrameGroup): number {
if (a.name === FrameGroupName.EMPTY) {
if (b.name === FrameGroupName.EMPTY) {
if (a.fileID < b.fileID) return -1;
if (a.fileID > b.fileID) return 1;
if (a.addressOrLine < b.addressOrLine) return -1;
if (a.addressOrLine > b.addressOrLine) return 1;
return 0;
}
if (b.name === FrameGroupName.ELF) {
if (a.fileID < b.fileID) return -1;
if (a.fileID > b.fileID) return 1;
}
return -1;
}
if (a.name === FrameGroupName.ELF) {
if (b.name === FrameGroupName.EMPTY) {
if (a.fileID < b.fileID) return -1;
if (a.fileID > b.fileID) return 1;
return 1;
}
if (b.name === FrameGroupName.ELF) {
if (a.functionName < b.functionName) return -1;
if (a.functionName > b.functionName) return 1;
if (a.exeFilename < b.exeFilename) return -1;
if (a.exeFilename > b.exeFilename) return 1;
return 0;
}
if (a.functionName < b.functionName) return -1;
if (a.functionName > b.functionName) return 1;
if (a.exeFilename < b.exeFilename) return -1;
if (a.exeFilename > b.exeFilename) return 1;
return -1;
}
if (b.name === FrameGroupName.FULL) {
if (a.exeFilename < b.exeFilename) return -1;
if (a.exeFilename > b.exeFilename) return 1;
if (a.sourceFilename < b.sourceFilename) return -1;
if (a.sourceFilename > b.sourceFilename) return 1;
if (a.functionName < b.functionName) return -1;
if (a.functionName > b.functionName) return 1;
return 0;
}
if (b.name === FrameGroupName.ELF) {
if (a.functionName < b.functionName) return -1;
if (a.functionName > b.functionName) return 1;
if (a.exeFilename < b.exeFilename) return -1;
if (a.exeFilename > b.exeFilename) return 1;
}
return 1;
}
export function createFrameGroupID(frameGroup: FrameGroup): FrameGroupID {
switch (frameGroup.name) {
case FrameGroupName.EMPTY:

View file

@ -5,17 +5,12 @@
* 2.0.
*/
import * as t from 'io-ts';
import { createFrameGroup, createFrameGroupID, FrameGroupID } from './frame_group';
import {
compareFrameGroup,
createFrameGroup,
createFrameGroupID,
FrameGroup,
FrameGroupID,
} from './frame_group';
import {
createStackFrameMetadata,
emptyStackTrace,
Executable,
FileID,
groupStackFrameMetadataByStackTrace,
StackFrame,
StackFrameID,
StackFrameMetadata,
@ -25,7 +20,7 @@ import {
interface TopNFunctionAndFrameGroup {
Frame: StackFrameMetadata;
FrameGroup: FrameGroup;
FrameGroupID: FrameGroupID;
CountExclusive: number;
CountInclusive: number;
}
@ -48,8 +43,6 @@ export function createTopNFunctions(
startIndex: number,
endIndex: number
): TopNFunctions {
const metadata = groupStackFrameMetadataByStackTrace(stackTraces, stackFrames, executables);
// The `count` associated with a frame provides the total number of
// traces in which that node has appeared at least once. However, a
// frame may appear multiple times in a trace, and thus to avoid
@ -59,35 +52,63 @@ export function createTopNFunctions(
const topNFunctions = new Map<FrameGroupID, TopNFunctionAndFrameGroup>();
// Collect metadata and inclusive + exclusive counts for each distinct frame.
for (const [traceHash, count] of events) {
for (const [stackTraceID, count] of events) {
const uniqueFrameGroupsPerEvent = new Set<FrameGroupID>();
totalCount += count;
// It is possible that we do not have a stacktrace for an event,
// e.g. when stopping the host agent or on network errors.
const frames = metadata.get(traceHash) ?? [];
for (let i = 0; i < frames.length; i++) {
const frameGroup = createFrameGroup(frames[i]);
const stackTrace = stackTraces.get(stackTraceID) ?? emptyStackTrace;
const lenStackTrace = stackTrace.FrameIDs.length;
for (let i = 0; i < lenStackTrace; i++) {
const frameID = stackTrace.FrameIDs[i];
const fileID = stackTrace.FileIDs[i];
const addressOrLine = stackTrace.AddressOrLines[i];
const frame = stackFrames.get(frameID)!;
const executable = executables.get(fileID)!;
const frameGroup = createFrameGroup(
fileID,
addressOrLine,
executable.FileName,
frame.FileName,
frame.FunctionName
);
const frameGroupID = createFrameGroupID(frameGroup);
if (!topNFunctions.has(frameGroupID)) {
topNFunctions.set(frameGroupID, {
Frame: frames[i],
FrameGroup: frameGroup,
let topNFunction = topNFunctions.get(frameGroupID);
if (topNFunction === 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,
});
topNFunction = {
Frame: metadata,
FrameGroupID: frameGroupID,
CountExclusive: 0,
CountInclusive: 0,
});
}
};
const topNFunction = topNFunctions.get(frameGroupID)!;
topNFunctions.set(frameGroupID, topNFunction);
}
if (!uniqueFrameGroupsPerEvent.has(frameGroupID)) {
uniqueFrameGroupsPerEvent.add(frameGroupID);
topNFunction.CountInclusive += count;
}
if (i === frames.length - 1) {
if (i === lenStackTrace - 1) {
// Leaf frame: sum up counts for exclusive CPU.
topNFunction.CountExclusive += count;
}
@ -105,7 +126,7 @@ export function createTopNFunctions(
if (a.CountExclusive < b.CountExclusive) {
return -1;
}
return compareFrameGroup(a.FrameGroup, b.FrameGroup);
return a.FrameGroupID.localeCompare(b.FrameGroupID);
})
.reverse();
@ -121,7 +142,7 @@ export function createTopNFunctions(
Frame: frameAndCount.Frame,
CountExclusive: frameAndCount.CountExclusive,
CountInclusive: frameAndCount.CountInclusive,
Id: createFrameGroupID(frameAndCount.FrameGroup),
Id: frameAndCount.FrameGroupID,
}));
return {

View file

@ -50,7 +50,7 @@ describe('Stack frame metadata operations', () => {
SourceFilename: 'dockerd',
FunctionOffset: 0x183a5b0,
});
expect(getCalleeSource(metadata)).toEqual('dockerd+0x0');
expect(getCalleeSource(metadata)).toEqual('dockerd');
});
test('metadata has source name and function offset', () => {

View file

@ -54,6 +54,13 @@ export interface StackTrace {
Types: number[];
}
export const emptyStackTrace: StackTrace = {
FrameIDs: [],
FileIDs: [],
AddressOrLines: [],
Types: [],
};
export interface StackFrame {
FileName: string;
FunctionName: string;
@ -86,6 +93,8 @@ export interface StackFrameMetadata {
SourceFilename: string;
// StackFrame.LineNumber
SourceLine: number;
// auto-generated - see createStackFrameMetadata
FunctionSourceLine: number;
// Executable.FileName
ExeFileName: string;
@ -123,9 +132,54 @@ export function createStackFrameMetadata(
metadata.SourcePackageURL = options.SourcePackageURL ?? '';
metadata.SourceType = options.SourceType ?? 0;
// Unknown/invalid offsets are currently set to 0.
//
// In this case we leave FunctionSourceLine=0 as a flag for the UI that the
// FunctionSourceLine should not be displayed.
//
// As FunctionOffset=0 could also be a legit value, this work-around needs
// a real fix. The idea for after GA is to change FunctionOffset=-1 to
// indicate unknown/invalid.
if (metadata.FunctionOffset > 0) {
metadata.FunctionSourceLine = metadata.SourceLine - metadata.FunctionOffset;
} else {
metadata.FunctionSourceLine = 0;
}
return metadata;
}
function checkIfStringHasParentheses(s: string) {
return /\(|\)/.test(s);
}
function getFunctionName(metadata: StackFrameMetadata) {
return metadata.FunctionName !== '' && !checkIfStringHasParentheses(metadata.FunctionName)
? `${metadata.FunctionName}()`
: metadata.FunctionName;
}
function getExeFileName(metadata: StackFrameMetadata) {
if (metadata?.ExeFileName === undefined) {
return '';
}
if (metadata.ExeFileName !== '') {
return metadata.ExeFileName;
}
return describeFrameType(metadata.FrameType);
}
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} #${
metadata.SourceLine
}`;
}
return getExeFileName(metadata);
}
export function getCalleeFunction(frame: StackFrameMetadata): string {
// In the best case scenario, we have the file names, source lines,
// and function names. However we need to deal with missing function or
@ -137,7 +191,7 @@ export function getCalleeFunction(frame: StackFrameMetadata): string {
}
export function getCalleeSource(frame: StackFrameMetadata): string {
if (frame.FunctionName === '' && frame.SourceLine === 0) {
if (frame.SourceFilename === '' && frame.SourceLine === 0) {
if (frame.ExeFileName) {
// If no source line or filename available, display the executable offset
return frame.ExeFileName + '+0x' + frame.AddressOrLine.toString(16);
@ -154,26 +208,23 @@ export function getCalleeSource(frame: StackFrameMetadata): string {
return frame.SourceFilename + (frame.SourceLine !== 0 ? `#${frame.SourceLine}` : '');
}
// groupStackFrameMetadataByStackTrace collects all of the per-stack-frame
// metadata for a given set of trace IDs and their respective stack frames.
//
// This is similar to GetTraceMetaData in pf-storage-backend/storagebackend/storagebackendv1/reads_webservice.go
export function groupStackFrameMetadataByStackTrace(
stackTraces: Map<StackTraceID, StackTrace>,
stackFrames: Map<StackFrameID, StackFrame>,
executables: Map<FileID, Executable>
): Map<StackTraceID, StackFrameMetadata[]> {
const frameMetadataForTraces = new Map<StackTraceID, StackFrameMetadata[]>();
): Record<string, StackFrameMetadata[]> {
const stackTraceMap: Record<string, StackFrameMetadata[]> = {};
for (const [stackTraceID, trace] of stackTraces) {
const frameMetadata = new Array<StackFrameMetadata>();
for (let i = 0; i < trace.FrameIDs.length; i++) {
const numFramesPerTrace = trace.FrameIDs.length;
const frameMetadata = new Array<StackFrameMetadata>(numFramesPerTrace);
for (let i = 0; i < numFramesPerTrace; i++) {
const frameID = trace.FrameIDs[i];
const fileID = trace.FileIDs[i];
const addressOrLine = trace.AddressOrLines[i];
const frame = stackFrames.get(frameID)!;
const executable = executables.get(fileID)!;
const metadata = createStackFrameMetadata({
frameMetadata[i] = createStackFrameMetadata({
FrameID: frameID,
FileID: fileID,
AddressOrLine: addressOrLine,
@ -184,10 +235,8 @@ export function groupStackFrameMetadataByStackTrace(
SourceFilename: frame.FileName,
ExeFileName: executable.FileName,
});
frameMetadata.push(metadata);
}
frameMetadataForTraces.set(stackTraceID, frameMetadata);
stackTraceMap[stackTraceID] = frameMetadata;
}
return frameMetadataForTraces;
return stackTraceMap;
}

View file

@ -8,7 +8,12 @@
import { schema } from '@kbn/config-schema';
import { RouteRegisterParameters } from '.';
import { getRoutePaths } from '../../common';
import { FlameGraph } from '../../common/flamegraph';
import { createCallerCalleeGraph } from '../../common/callercallee';
import {
createColumnarCallerCallee,
createFlameGraph,
ElasticFlameGraph,
} from '../../common/flamegraph';
import { createProfilingEsClient } from '../utils/create_profiling_es_client';
import { withProfilingSpan } from '../utils/with_profiling_span';
import { getClient } from './compat';
@ -39,6 +44,7 @@ export function registerFlameChartSearchRoute({ router, logger }: RouteRegisterP
timeTo,
kuery,
});
const totalSeconds = timeTo - timeFrom;
const { stackTraces, executables, stackFrames, eventsIndex, totalCount, stackTraceEvents } =
await getExecutablesAndStackTraces({
@ -48,23 +54,47 @@ export function registerFlameChartSearchRoute({ router, logger }: RouteRegisterP
sampleSize: targetSampleSize,
});
const flamegraph = await withProfilingSpan('collect_flamegraph', async () => {
return new FlameGraph({
sampleRate: eventsIndex.sampleRate,
totalCount,
events: stackTraceEvents,
const flamegraph = await withProfilingSpan('create_flamegraph', async () => {
const t0 = Date.now();
const graph = createCallerCalleeGraph(
stackTraceEvents,
stackTraces,
stackFrames,
executables,
totalSeconds: timeTo - timeFrom,
}).toElastic();
executables
);
logger.info(`creating caller-callee graph took ${Date.now() - t0} ms`);
const t1 = Date.now();
const columnar = createColumnarCallerCallee(graph);
logger.info(`creating columnar caller-callee graph took ${Date.now() - t1} ms`);
const t2 = Date.now();
const fg = createFlameGraph(columnar);
logger.info(`creating flamegraph took ${Date.now() - t2} ms`);
return fg;
});
// 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 body: ElasticFlameGraph = {
...flamegraph,
TotalSeconds: totalSeconds,
TotalTraces: Math.floor(totalCount / eventsIndex.sampleRate),
SampledTraces: totalCount,
};
logger.info('returning payload response to client');
return response.ok({
body: flamegraph,
});
return response.ok({ body });
} catch (e) {
logger.error(e);
return response.customError({

View file

@ -54,7 +54,8 @@ export function registerTopNFunctionsSearchRoute({ router, logger }: RouteRegist
sampleSize: targetSampleSize,
});
const topNFunctions = await withProfilingSpan('collect_topn_functions', async () => {
const t0 = Date.now();
const topNFunctions = await withProfilingSpan('create_topn_functions', async () => {
return createTopNFunctions(
stackTraceEvents,
stackTraces,
@ -64,6 +65,7 @@ export function registerTopNFunctionsSearchRoute({ router, logger }: RouteRegist
endIndex
);
});
logger.info(`creating topN functions took ${Date.now() - t0} ms`);
logger.info('returning payload response to client');

View file

@ -50,13 +50,13 @@ export async function getExecutablesAndStackTraces({
if (totalCount > sampleSize * 1.1) {
p = sampleSize / totalCount;
logger.info('downsampling events with p=' + p);
const t0 = new Date().getTime();
const t0 = Date.now();
const downsampledTotalCount = downsampleEventsRandomly(
stackTraceEvents,
p,
filter.toString()
);
logger.info(`downsampling events took ${new Date().getTime() - t0} ms`);
logger.info(`downsampling events took ${Date.now() - t0} ms`);
logger.info('downsampled total count: ' + downsampledTotalCount);
logger.info('unique downsampled stacktraces: ' + stackTraceEvents.size);
}

View file

@ -12,9 +12,9 @@ export async function logExecutionLatency<T>(
activity: string,
func: () => Promise<T>
): Promise<T> {
const start = new Date().getTime();
const start = Date.now();
return await func().then((res) => {
logger.info(activity + ' took ' + (new Date().getTime() - start) + 'ms');
logger.info(activity + ' took ' + (Date.now() - start) + 'ms');
return res;
});
}

View file

@ -269,7 +269,7 @@ export async function mgetStackTraces({
const stackFrameDocIDs = new Set<string>();
const executableDocIDs = new Set<string>();
const t0 = new Date().getTime();
const t0 = Date.now();
// flatMap() is significantly slower than an explicit for loop
for (const res of stackResponses) {
for (const trace of res.docs) {
@ -298,7 +298,7 @@ export async function mgetStackTraces({
}
}
}
logger.info(`processing data took ${new Date().getTime() - t0} ms`);
logger.info(`processing data took ${Date.now() - t0} ms`);
if (stackTraces.size !== 0) {
logger.info('Average size of stacktrace: ' + totalFrames / stackTraces.size);
@ -336,7 +336,7 @@ export async function mgetStackFrames({
// Create a lookup map StackFrameID -> StackFrame.
let framesFound = 0;
const t0 = new Date().getTime();
const t0 = Date.now();
const docs = resStackFrames.docs;
for (const frame of docs) {
if ('error' in frame) {
@ -361,7 +361,7 @@ export async function mgetStackFrames({
});
}
}
logger.info(`processing data took ${new Date().getTime() - t0} ms`);
logger.info(`processing data took ${Date.now() - t0} ms`);
logger.info('found ' + framesFound + ' / ' + stackFrameIDs.size + ' frames');
@ -391,7 +391,7 @@ export async function mgetExecutables({
// Create a lookup map StackFrameID -> StackFrame.
let exeFound = 0;
const t0 = new Date().getTime();
const t0 = Date.now();
const docs = resExecutables.docs;
for (const exe of docs) {
if ('error' in exe) {
@ -408,7 +408,7 @@ export async function mgetExecutables({
});
}
}
logger.info(`processing data took ${new Date().getTime() - t0} ms`);
logger.info(`processing data took ${Date.now() - t0} ms`);
logger.info('found ' + exeFound + ' / ' + executableIDs.size + ' executables');

View file

@ -8,7 +8,7 @@
import { schema } from '@kbn/config-schema';
import type { IRouter, Logger } from '@kbn/core/server';
import { RouteRegisterParameters } from '.';
import { fromMapToRecord, getRoutePaths, INDEX_EVENTS } from '../../common';
import { getRoutePaths, INDEX_EVENTS } from '../../common';
import { ProfilingESField } from '../../common/elasticsearch';
import { computeBucketWidthFromTimeRangeAndBucketCount } from '../../common/histogram';
import { groupStackFrameMetadataByStackTrace, StackTraceID } from '../../common/profiling';
@ -142,9 +142,7 @@ export async function topNElasticSearchQuery({
);
const metadata = await withProfilingSpan('collect_stackframe_metadata', async () => {
return fromMapToRecord(
groupStackFrameMetadataByStackTrace(stackTraces, stackFrames, executables)
);
return groupStackFrameMetadataByStackTrace(stackTraces, stackFrames, executables);
});
logger.info('returning payload response to client');